160 个 CrackMe 之 087 - n0p3x.9 手动脱壳及修复、注册分析和去NAG。
本帖最后由 solly 于 2019-6-2 18:00 编辑160 个 CrackMe 之 087 - n0p3x.9 是一个比较老的 CrackMe,由于其加壳的原因,不能直接在目前的系统上运行,但脱壳后是可以在最新系统上运行的。
该 CrackMe 需要一个 borland 公司的运行库支持,库文件是 CW3220.DLL ,在网络上有下载。如果没有这个库,壳代码可以正常解码,但是所有库函数入口地址填充的是0,这个时候可以脱壳,但没有导入函数,也运行不了。
也由于这个库的原因,导致没有脱壳前也不能正常运行,这是因为没有脱壳前,壳代码会动态加载CW3220.DLL,并取得各个API入口,填充到原CrackMe代码中。但是这个时候,原有代码并没有解码完成,还有一些全局变量的地址没有解析成可用地址,这个时候调用该库,该库的 DllMain() 初始化时会反过来调用 CrackMe 的一个导出函数“__GetExceptDLLinfo”,导致内存访问非法的异常,系统会关闭 CrackMe,具体解决办法有几个,后面我们采用跳过执行该段代码的方法来避免异常。
先看看文件信息:
信息不对,按“Scan /t”再次扫描:
这个信息也不对,也脱不了壳,只有手动脱壳了,载入OD中,如下:
第一条指令就是一个 PUSHAD,第二条指令是一个CALL (E8 00000000),用来取得第三条指令的地址。于是往下拖,寻找 POPAD,一路向下,来到这里(0x00409197处):
一条 POPAD 指令,加上一个 JMP 00401000,这个 00401000 就是 OEP 了,于是在 POPAD 下一个断点,如下图:
然后,直接一个 F9 ,看看能不能脱壳,但是,没有达成愿望,代码来到下面位置(0x004016F3),停了下来,出现异常:
上图对应代码如下:
004016E0 55 push ebp
004016E1 8BEC mov ebp, esp
004016E3 8B45 08 mov eax, dword ptr
004016E6 64:8B15 04000000 mov edx, dword ptr fs: ; StackBase
004016ED 8B4A F8 mov ecx, dword ptr
004016F0 83C1 30 add ecx, 30
004016F3 890D 000013A8 mov dword ptr , ecx
004016F9 C705 0000138C 00001478 mov dword ptr , 78140000
00401703 C705 00001390 00001468 mov dword ptr , 68140000
0040170D C700 49737282 mov dword ptr , 82727349
00401713 C740 04 00001380 mov dword ptr , 80130000
0040171A 5D pop ebp
0040171B C3 retn
这里的指令地址在OEP(00401000)的节范围内,属于CrackMe 的代码,并且,变量地址还没有修正,指向了一个非法地址,被 OD 断下来了。
到这里后,程序已经无法再运行了。只有重新来了,但指令地址是正确的了,请记住,后面要用到。
重新加载到OD,我们一路F8+F4组合使用,来到这里:
具体代码如下:
00409135 . |50 push eax
00409136 . |83C7 08 add edi, 8
00409139 . |FF96 64700000 call dword ptr ;KERNEL32.LoadLibraryA
这里就是我在最前面説的,壳代码通过LoadLibraryA()动态加载库文件"CW3220.DLL"的地方,而且这个 CW3220.DLL 也不会出现在 CrackMe 的导入表中,脱壳后需要修正导入表才可以正常运行。
壳代码加载完 CW3220.DLL 后,再通过 GetProcAddress() 取得导入函数入口并填充到代码中的地址变量中,如下:
可以看到,这里并没有对两个函数的调用结果进行检查,而是直接保存填充,因此,如果没有 CW3220.DLL 时,程序也不会报错,就算脱壳后也不能运行,就是这个没有填充或填充不对的原因。如果填充不对,则API入口代码如下:
00401776 FF25 B8304000 jmp dword ptr
0040177C FF25 BC304000 jmp dword ptr
00401782 FF25 C0304000 jmp dword ptr
00401788 FF25 C4304000 jmp dword ptr
0040178E FF25 C8304000 jmp dword ptr
00401794 FF25 CC304000 jmp dword ptr
0040179A FF25 D0304000 jmp dword ptr
004017A0 FF25 D4304000 jmp dword ptr
004017A6 FF25 D8304000 jmp dword ptr
004017AC FF25 DC304000 jmp dword ptr
004017B2 FF25 E0304000 jmp dword ptr
004017B8 FF25 E4304000 jmp dword ptr
004017BE- FF25 EC304000 jmp dword ptr ; KERNEL32.lstrlenA
004017C4- FF25 F0304000 jmp dword ptr ; KERNEL32.lstrcmpA
004017CA- FF25 F4304000 jmp dword ptr ; KERNEL32.GetModuleHandleA
004017D0- FF25 F8304000 jmp dword ptr ; KERNEL32.lstrcatA
004017D6- FF25 FC304000 jmp dword ptr ; KERNEL32.CreateFileA
004017DC- FF25 00314000 jmp dword ptr ; KERNEL32.GetProcAddress
004017E2- FF25 04314000 jmp dword ptr ; KERNEL32.CloseHandle
004017E8- FF25 0C314000 jmp dword ptr ; USER32.MessageBoxA
004017EE- FF25 10314000 jmp dword ptr ; USER32.GetDlgItemTextA
004017F4- FF25 14314000 jmp dword ptr ; USER32.EndDialog
004017FA- FF25 18314000 jmp dword ptr ; USER32.DialogBoxParamA
可以看到,前面有10多个函数地址是没有的。正确的结果如下:
00401776- FF25 B8304000 jmp dword ptr ; cw3220.__startup
0040177C- FF25 BC304000 jmp dword ptr ; cw3220.__unlockDebuggerData
00401782- FF25 C0304000 jmp dword ptr ; cw3220._close
00401788- FF25 C4304000 jmp dword ptr ; cw3220._abort
0040178E- FF25 C8304000 jmp dword ptr ; cw3220.___debuggerDisableTerminateCallback
00401794- FF25 CC304000 jmp dword ptr ; cw3220.__ExceptionHandler
0040179A- FF25 D0304000 jmp dword ptr ; cw3220._itoa
004017A0- FF25 D4304000 jmp dword ptr ; cw3220._flushall
004017A6- FF25 D8304000 jmp dword ptr ; cw3220._open
004017AC- FF25 DC304000 jmp dword ptr ; cw3220._read
004017B2- FF25 E0304000 jmp dword ptr ; cw3220._CatchCleanup
004017B8- FF25 E4304000 jmp dword ptr ; cw3220.__lockDebuggerData
004017BE- FF25 EC304000 jmp dword ptr ; KERNEL32.lstrlenA
004017C4- FF25 F0304000 jmp dword ptr ; KERNEL32.lstrcmpA
004017CA- FF25 F4304000 jmp dword ptr ; KERNEL32.GetModuleHandleA
004017D0- FF25 F8304000 jmp dword ptr ; KERNEL32.lstrcatA
004017D6- FF25 FC304000 jmp dword ptr ; KERNEL32.CreateFileA
004017DC- FF25 00314000 jmp dword ptr ; KERNEL32.GetProcAddress
004017E2- FF25 04314000 jmp dword ptr ; KERNEL32.CloseHandle
004017E8- FF25 0C314000 jmp dword ptr ; USER32.MessageBoxA
004017EE- FF25 10314000 jmp dword ptr ; USER32.GetDlgItemTextA
004017F4- FF25 14314000 jmp dword ptr ; USER32.EndDialog
004017FA- FF25 18314000 jmp dword ptr ; USER32.DialogBoxParamA
我们先不要执行这个 LoadLibraryA()调用,先在该指令下面设置一个断点,便于后面快速返回。
还要再去我们前面出错的代码处设置另外一个断点,用到代码中右键“转到”功能,如下图:
输入我们前面记住的过程地址,来到这里:
我们在 0x004016E1 处下一个断点,阻止代码对几个寄存器的值的修改。
然后,按F9 执行,程序断在我们的断点上,然后我们修改 EIP 的值,跳过几个指令的执行,如下图:
重新设置EIP 为 0x0040171A 。如果出现下面提示,按“是”:
下图就是EIP变动后的值,跳过了中间9条指令:
然后,直接 F9 运行,返回到壳代码,回到了我们前面设置的断点处:
这个时候,EAX中才是正确的 DLL 的 hInstance 值了。
后面的 GetProcAddress() 也可以取到正确的API地址入口了。
这里正常后,脱壳也就没有问题了。
直接按 F9 来到我们前面在 POPAD 处设置的断点处,然后几个 F8 来到 OEP(00401000) 处,就可以开始脱壳了。如下图:
选择“Dump debugged process"即可:
全部默认设置,直接点”Dump"并保存好 Dump 后的文件,这时候还有一个要处理的问题,导入表的问题,dump出来的 crackme 的导入表是没有 CW3220.DLL 的。需要我们手动修正。
以管理员身份启动“ ImportREC v1.6F",如下图:
在"Attach to an Active Process"列表中选择 crackme.exe 进程。
OEP 显示的是 00009000,这个是不对的,我们改成正确的 00001000,如下图:
改好后,点击按钮”IAT AutoSearch",出现下面的提示:
提示我们试一下“Get Import”,我们按“确定”关闭提示,然后点击最下面那个“Get Imports" 按钮,取得当前进程的导入数据:
中间的文本框显示有3个导入库,这时,包括了我们需要的 cw3220.dll 在内。
这个时候,可以点一下右边的”Show Invalid",看看有没有无效的导入,如果有,我们还要删除这些无效导入数据。这里我们没有发现无效数据。
然后直接点击最下面的按钮“Fix Dump",选择我们前面在 OD 中Dump 出来的那个文件,如下图:
选择好文件后,按“打开”即可。回到ImportREC的主界面,显示修正成功:
并且,生成一个新文件,就是在原文件名后加了一个下划线“_",这个就是修正导入表后的文件,也就是可以正常运行的脱壳文件了。至此,脱壳才算完成。
下一步就是对 crackme 的自有代码进行处理了。
这个 CrackMe 带有一个 NAG,运行显示如下:
就是一个消息框,通过 IDA 就可以看到,在 WinMain() 最前面就是显示这个消息框的代码。如下:
00401378 68 00100000 push 1000
0040137D 8D83 29020000 lea eax, dword ptr
00401383 50 push eax
00401384 8D93 EC010000 lea edx, dword ptr
0040138A 52 push edx
0040138B 6A 00 push 0
0040138D E8 56040000 call 004017E8 ; MessageBox(), 显示未注册NAG
将上面的代码全部"NOP"后,就可以去除 NAG 的显示。
这个Crackme还有对注册文件(keyfile)的检查,注册文件验证通过后,才会显示主界面,否则会退出 CrackMe。如果没有注册文件或文件内容不正确,则提示:
在 WinMain() 最后一段代码,就有对注册文件校验的调用:
004013DC E8 1BFDFFFF call 004010FC ; 注册文件校验
004013E1 85C0 test eax, eax ; eax 不等于 0, 则校验成功!
004013E3 74 15 je short 004013FA ; eax == 0 就转去显示文件校验失败,并退出CrackMe
004013E5 6A 00 push 0
004013E7 68 01134000 push 00401301 ; DialogProc,回调过程
004013EC 6A 00 push 0
004013EE 6A 01 push 1
004013F0 FF75 08 push dword ptr
004013F3 E8 02040000 call 004017FA ; jmp 到 USER32.DialogBoxParamA
004013F8 EB 1A jmp short 00401414
004013FA 68 00100000 push 1000 ; 这里开始提示注册文件不成确
004013FF 8D83 04030000 lea eax, dword ptr
00401405 50 push eax
00401406 8D93 AF020000 lea edx, dword ptr
0040140C 52 push edx
0040140D 6A 00 push 0
0040140F E8 D4030000 call 004017E8 ; MessageBoxA(),显示注册文件不正确
00401414 33C0 xor eax, eax
00401416 5B pop ebx
00401417 5D pop ebp
00401418 C2 1000 retn 10
其中的 ” call 004010FC " 就是调用keyfile 校验的子过程,keyfile 的文件名为:Register.dat。
keyfile 的文件内容为:
Why didn't n0p3x use a more difficult keyfile method?
keyfile 校验的子过程就是将文件读入局部量后与一条明文常量字符串进行比较。而且,其并没有初始化局部变量的内存空间,所以读入后有可能字符串不是“null”结尾,会导致比较不成功!(跟踪过程中碰到过一次,大多情况下好象没有问题)
如果 keyfile 校验通过,则会显示主界面:
看看 About:
输入一组假码,测试一下:
点击OK,得到如下错误提示:
因为是基于对话框的程序,通过 WinMain()中以下代码,可以找到DlgProc:
004013E5 6A 00 push 0
004013E7 68 01134000 push 00401301 ; DialogProc,回调过程
004013EC 6A 00 push 0
004013EE 6A 01 push 1
004013F0 FF75 08 push dword ptr
004013F3 E8 02040000 call 004017FA ; jmp 到 USER32.DialogBoxParamA
004013F8 EB 1A jmp short 00401414
其 DlgProc 地址为 0x00401301,通过DlgProc中的事件处理过程,很快就可以定位到注册码校验过程,主要代码如下:
00401194 6A 14 push 14
00401196 8D45 EC lea eax, dword ptr
00401199 50 push eax
0040119A 6A 67 push 67 ; 用户名
0040119C 53 push ebx
0040119D E8 4C060000 call 004017EE ; jmp 到 USER32.GetDlgItemTextA
004011A2 6A 14 push 14
004011A4 8D55 D8 lea edx, dword ptr
004011A7 52 push edx
004011A8 6A 66 push 66 ; 公司名
004011AA 53 push ebx
004011AB E8 3E060000 call 004017EE ; jmp 到 USER32.GetDlgItemTextA
004011B0 6A 14 push 14
004011B2 8D4D C4 lea ecx, dword ptr
004011B5 51 push ecx
004011B6 6A 65 push 65 ; 注册码
004011B8 53 push ebx
004011B9 E8 30060000 call 004017EE ; jmp 到 USER32.GetDlgItemTextA
004011BE 8D46 56 lea eax, dword ptr ; eax ==> null, 空串
004011C1 50 push eax
004011C2 8D55 EC lea edx, dword ptr ; edx ===> 用户名:"solly"
004011C5 52 push edx
004011C6 E8 F9050000 call 004017C4 ; jmp 到 KERNEL32.lstrcmpA
004011CB 85C0 test eax, eax ; 检查用户名是否为空
004011CD 75 19 jnz short 004011E8
004011CF 68 00100000 push 1000
004011D4 8D4E 6F lea ecx, dword ptr
004011D7 51 push ecx
004011D8 8D46 57 lea eax, dword ptr
004011DB 50 push eax
004011DC 6A 00 push 0
004011DE E8 05060000 call 004017E8 ; jmp 到 USER32.MessageBoxA
004011E3 E9 13010000 jmp 004012FB
004011E8 8D56 78 lea edx, dword ptr ; edx ===> null,空串
004011EB 52 push edx
004011EC 8D4D D8 lea ecx, dword ptr ; ecx ===> 公司名:"who am i"
004011EF 51 push ecx
004011F0 E8 CF050000 call 004017C4 ; jmp 到 KERNEL32.lstrcmpA
004011F5 85C0 test eax, eax ; 检查公司名是否为空
004011F7 75 1C jnz short 00401215
004011F9 68 00100000 push 1000
004011FE 8D86 90000000 lea eax, dword ptr
00401204 50 push eax
00401205 8D56 79 lea edx, dword ptr
00401208 52 push edx
00401209 6A 00 push 0
0040120B E8 D8050000 call 004017E8 ; jmp 到 USER32.MessageBoxA
00401210 E9 E6000000 jmp 004012FB
00401215 8D8E 99000000 lea ecx, dword ptr ; ecx ===> null, 空串
0040121B 51 push ecx
0040121C 8D45 C4 lea eax, dword ptr ; eax ===> 注册码:"787878787878"
0040121F 50 push eax
00401220 E8 9F050000 call 004017C4 ; jmp 到 KERNEL32.lstrcmpA
00401225 85C0 test eax, eax ; 检查注册码是否为空
00401227 75 1F jnz short 00401248
00401229 68 00100000 push 1000
0040122E 8D96 B0000000 lea edx, dword ptr
00401234 52 push edx
00401235 8D8E 9A000000 lea ecx, dword ptr
0040123B 51 push ecx
0040123C 6A 00 push 0
0040123E E8 A5050000 call 004017E8 ; jmp 到 USER32.MessageBoxA
00401243 E9 B3000000 jmp 004012FB
00401248 8D45 C4 lea eax, dword ptr ; eax ===> 注册码:"787878787878"
0040124B 50 push eax
0040124C E8 6D050000 call 004017BE ; __lstrlen(),计算注册码的长度
00401251 83F8 09 cmp eax, 9
00401254 7D 1F jge short 00401275 ; 注册码是否大于或等9个字符
00401256 68 00100000 push 1000
0040125B 8D96 F9000000 lea edx, dword ptr
00401261 52 push edx
00401262 8D8E B9000000 lea ecx, dword ptr
00401268 51 push ecx
00401269 6A 00 push 0
0040126B E8 78050000 call 004017E8 ; jmp 到 USER32.MessageBoxA
00401270 E9 86000000 jmp 004012FB
00401275 53 push ebx ; hDlg
00401276 E8 D9FEFFFF call 00401154 ; 取注册码长度
0040127B 59 pop ecx ; hDlg
0040127C 8D45 EC lea eax, dword ptr ; eax ===> 用户名
0040127F 50 push eax
00401280 E8 39050000 call 004017BE ; __lstrlen(),计算用户名的长度
00401285 8BD8 mov ebx, eax ; ebx == 5, 用户名的长度
00401287 8D45 D8 lea eax, dword ptr ; eax ===> 公司名
0040128A 50 push eax
0040128B E8 2E050000 call 004017BE ; __lstrlen(),计算公司名的长度
00401290 0FAFD8 imul ebx, eax ; ebx = 用户名长度 * 公司名长度 = 0x28 = 40
00401293 81C3 A93E0F00 add ebx, 0F3EA9 ; ebx = ebx(40) + 0x000F3EA9(999081)
00401299 8BC3 mov eax, ebx ; eax == ebx = 999121
0040129B 6A 0A push 0A ; 转换成10进制字符串
0040129D 8D55 90 lea edx, dword ptr ; 转换结果缓冲区, 结果为"999121"
004012A0 52 push edx
004012A1 50 push eax
004012A2 E8 F3040000 call 0040179A ; _itoa(eax)
004012A7 83C4 0C add esp, 0C
004012AA 8D8E 02010000 lea ecx, dword ptr ; ecx ===> ":-)"
004012B0 51 push ecx
004012B1 8D45 90 lea eax, dword ptr ; eax ===> "999121"
004012B4 50 push eax
004012B5 E8 16050000 call 004017D0 ; __Lstrcat(),在上面转换结果"999121"后加上":-)"
004012BA 50 push eax ; eax ==> "999121:-)"
004012BB 8D55 C4 lea edx, dword ptr ; edx ===> "787878787878",输入的假码
004012BE 52 push edx
004012BF E8 00050000 call 004017C4 ; __lstrcmp(str1, str2), 注册码比较
004012C4 85C0 test eax, eax
004012C6 75 19 jnz short 004012E1
004012C8 6A 40 push 40
004012CA 8D8E 2C010000 lea ecx, dword ptr
004012D0 51 push ecx
004012D1 8D86 06010000 lea eax, dword ptr
004012D7 50 push eax
004012D8 6A 00 push 0
004012DA E8 09050000 call 004017E8 ; jmp 到 USER32.MessageBoxA
004012DF EB 1A jmp short 004012FB
004012E1 68 00100000 push 1000
004012E6 8D96 8D010000 lea edx, dword ptr
004012EC 52 push edx
004012ED 8D8E 36010000 lea ecx, dword ptr
004012F3 51 push ecx
004012F4 6A 00 push 0
004012F6 E8 ED040000 call 004017E8 ; jmp 到 USER32.MessageBoxA
004012FB 5E pop esi
004012FC 5B pop ebx
004012FD 8BE5 mov esp, ebp
004012FF 5D pop ebp
00401300 C3 retn
从上面代码分析可以看出注册码规则如下:
1、注册码的长度必须大于或等于 9。
2、计算用户名的长度,记为 int a = strlen(username);
3、计算公司名的长度,记为 int b = strlen(companyname);
4、计算 a 和 b 的 积, 记为 int c = a * b;
5、将 c 加上 999081,记为 int d = c + 999081;
6、将上面的 d 转换成10进制字符串,然后在后面加上一个笑脸“:-)”,就是最后的注册码。
根据我前面输入的注册信息,注册码计算如下:
d = 5*8 + 999081 = 999121
注册码则是 "999121:-)"
因此,口算就可以了,也就不需要写注册机了。
在界面输入上面计算的注册码,得到提示如下:
注册成功!!!!!!
脱壳后的文件信息如下,”scan /t“后:
分析完毕!!! 888066yjg 发表于 2019-6-11 01:51
加我2176847672 帮我破解一个 价格可以谈
你好,这个只是一点爱好,而且年纪大了,近来只是发点水贴,刷刷存在感。不接破解需求。 帖子写的真详细,可以跟着操作,是我们学习的最佳教程。 感谢分享 楼主加油 感谢楼主分享,支持一下! 感谢了,学习案例。 学习喽,谢谢楼主分享 感谢楼主写的这么清晰!!!
页:
[1]
2