-
-
[原创]一个内存映像校验anti-debug程序的破解之路
-
发表于: 2013-3-5 21:08 3237
-
新手F7步入,老鸟F8步过,第一次写文章,有什么不妥当的地方请大家指正,谢谢。
最近在做一些破解类的题目,前面遇到的都是比较简单的crackme程序,即使遇些anti-debug手段还没有意识到的时候都被OllyDbg的插件 anti anti-debug了。这个程序是我意识到的第一个anti-debug程序,一直不知道如何下手,纠结了将近一个星期直到今天终于将其破解,特别欣慰,特此记录一下破解过程。
首先说明一下我遇到的问题:OD载入这个crackme程序,和往常一样F8一步一步的执行指令,想看一下是哪个Call指令创建了窗口。但是F8了几遍,程序都不创建窗口,或是窗口一闪而逝,接着就终止运行了,直接F9程序运行没有任何问题。这个时候尝试在任意指令处下F2断点,F9运行,程序运行效果和F8单步一样,但下内存访问断点或硬件执行断点程序都能正常运行。以前破解都没有遇到过这种问题,所以这次比较困惑,到底程序使用了什么方式来进行anti-debug的呢?后来分析发现程序运行时创建了一个线程以5000ms为周期,不停的对.text段进行CRC校验,若是与事先计算好的校验值不同则调用ExitProcess函数退出。具体原因由于OD的断点机制,会将断点处的指令替换成CC即INT3指令,所以只要有F2断点或者F8单步时都会造成.text的CRC校验值的不同。
程序创建线程的代码段如下:
004011F9 56 PUSH ESI
004011FA A3 24304000 MOV DWORD PTR DS:[403024],EAX
004011FF FFD7 CALL EDI
00401201 6A 00 PUSH 0 ; pThreadId
00401203 6A 00 PUSH 0 ; CreationFlags
00401205 6A 00 PUSH 0 ; pThreadParm
00401207 68 B0104000 PUSH CrackMe.004010B0 ; ThreadFunction
0040120C 6A 00 PUSH 0 ; StackSize
0040120E 6A 00 PUSH 0 ; pSecurity
00401210 A3 20304000 MOV DWORD PTR DS:[403020],EAX
00401215 FF15 08204000 CALL DWORD PTR DS:[<&KERNEL32.CreateThre>; kernel32.CreateThread
0040121B 5F POP EDI
0040121C B8 01000000 MOV EAX,1
00401221 5E POP ESI
00401222 C3 RETN
线程创建后会调用004010B0处的代码执行。我们观察一下004010B0的代码,代码完成的功能见注释:
004010B0 53 PUSH EBX
004010B1 8B1D 10204000 MOV EBX,DWORD PTR DS:[<&KERNEL32.Sleep>] ; kernel32.Sleep
004010B7 55 PUSH EBP
004010B8 8B2D 14204000 MOV EBP,DWORD PTR DS:[<&KERNEL32.ExitPro>; kernel32.ExitProcess
004010BE 56 PUSH ESI
004010BF 57 PUSH EDI
004010C0 8B3D 18204000 MOV EDI,DWORD PTR DS:[<&KERNEL32.GetModu>; kernel32.GetModuleHandleA
004010C6 6A 00 PUSH 0
004010C8 FFD7 CALL EDI
004010CA 8B48 3C MOV ECX,DWORD PTR DS:[EAX+3C]
004010CD 33D2 XOR EDX,EDX
004010CF 03C8 ADD ECX,EAX ; ecx 指向pe标志
004010D1 66:8B51 14 MOV DX,WORD PTR DS:[ECX+14] ; dx指向扩展头结构的长度
004010D5 8B71 FC MOV ESI,DWORD PTR DS:[ECX-4] ; esi保存已经事先计算好的待对比的校验和
004010D8 8D540A 18 LEA EDX,DWORD PTR DS:[EDX+ECX+18] ; edx指向.text段
004010DC 8B4A 08 MOV ECX,DWORD PTR DS:[EDX+8] ; ecx存储.text段的尺寸
004010DF 8B52 0C MOV EDX,DWORD PTR DS:[EDX+C] ; .text节区RVA地址
004010E2 03D0 ADD EDX,EAX ; .text的绝对地址
004010E4 51 PUSH ECX
004010E5 52 PUSH EDX
004010E6 E8 45FFFFFF CALL CrackMe.00401030 ; CRC校验
004010EB 83C4 08 ADD ESP,8
004010EE 3BF0 CMP ESI,EAX ; 将现在计算的CRC值(eax中存放)与事先计算好的校验和对比
004010F0 75 09 JNZ SHORT CrackMe.004010FB ; 不等则退出,相等则sleep 5000ms,然后继续校验
004010F2 68 88130000 PUSH 1388
004010F7 FFD3 CALL EBX ; kernel32.Sleep
004010F9 ^ EB CB JMP SHORT CrackMe.004010C6
004010FB 6A 01 PUSH 1
004010FD FFD5 CALL EBP ; kernel32.ExitProcess
004010FF ^ EB C5 JMP SHORT CrackMe.004010C6
00401101 90 NOP
00401102 90 NOP
因此,为了能够F2下断点或F8单步调试,我们可以将004010F0处的JNZ SHORT CrackMe.004010FB直接nop掉,或者将00401201到00401215处的CreateThread参数入栈和函数调用全部nop掉。保存可执行文件为一个新的文件,使用OD加载这个破解后的文件,然后按照常规的方法下bp GetWindowTextA或bp EnableWindow断点进行就可以调试了。
还有一个方法更为便捷,不需要更改任何指令,只需要一个硬件执行断点。方法是:原程序载入后,在反汇编窗口右键,选择“查找”,“当前模块中的名称(标签)”快捷键CTRL+N,找到GetWindowTextA函数,右键选择“反汇编窗口中跟随输入函数”,这是反汇编窗口跳转到了77D3216B处,在这里下一个“硬件执行断点”。F9运行,ALT+F9执行到用户空间。程序中断到00401270行。
00401266 6A 0A PUSH 0A
00401268 50 PUSH EAX
00401269 51 PUSH ECX
0040126A FF15 64204000 CALL DWORD PTR DS:[<&USER32.GetWindowTex>; USER32.GetWindowTextA
00401270 68 10304000 PUSH CrackMe.00403010 ; ASCII "Iceberg"
00401275 E8 96FEFFFF CALL CrackMe.00401110 ; 对ASCII "Iceberg"进行变换
0040127A 8D5424 08 LEA EDX,DWORD PTR SS:[ESP+8]
0040127E 8BF0 MOV ESI,EAX ; 将变换后的值放入esi存储
00401280 52 PUSH EDX ; 将我们输入的字符串入栈
00401281 E8 BAFEFFFF CALL CrackMe.00401140 ; 对于我们输入的字符串进行变换
00401286 83C4 08 ADD ESP,8
00401289 3BF0 CMP ESI,EAX ; 比较转化后的值是否相同
0040128B 5E POP ESI
0040128C 75 0E JNZ SHORT CrackMe.0040129C ; 不同则跳转,相同则调用EnableWindow使按钮可用
0040128E A1 20304000 MOV EAX,DWORD PTR DS:[403020]
00401293 6A 01 PUSH 1
00401295 50 PUSH EAX
00401296 FF15 5C204000 CALL DWORD PTR DS:[<&USER32.EnableWindow>; USER32.EnableWindow
0040129C 81C4 00010000 ADD ESP,100
004012A2 C3 RETN
004012A3 90 NOP
004012A4 90 NOP
行00401275的call对字符串"Iceberg"进行变换运算的,F7步入观察变换步骤
00401110 8B5424 04 MOV EDX,DWORD PTR SS:[ESP+4] ; CrackMe.00403010
00401114 33C0 XOR EAX,EAX
00401116 8A0A MOV CL,BYTE PTR DS:[EDX]
00401118 84C9 TEST CL,CL
0040111A 74 1A JE SHORT CrackMe.00401136
0040111C 80F9 41 CMP CL,41
0040111F 7C 15 JL SHORT CrackMe.00401136
00401121 80F9 5A CMP CL,5A
00401124 0FBEC9 MOVSX ECX,CL
00401127 7E 03 JLE SHORT CrackMe.0040112C
00401129 83E9 20 SUB ECX,20
0040112C 03C1 ADD EAX,ECX
0040112E 8A4A 01 MOV CL,BYTE PTR DS:[EDX+1]
00401131 42 INC EDX
00401132 84C9 TEST CL,CL
00401134 ^ 75 E6 JNZ SHORT CrackMe.0040111C
00401136 35 78560000 XOR EAX,5678
0040113B C3 RETN
具体运算我们并不关心,只关心它的运算结果,即执行完00401136后EAX的值,F4直接是程序运行到0040113B行,观察右侧寄存器EAX的值,可以看到EAX的值为00005789。
行00401281的CALL命令对我们输入的字符串进行变换,F7步入,观察变换方法
00401140 8B5424 04 MOV EDX,DWORD PTR SS:[ESP+4]
00401144 33C0 XOR EAX,EAX
00401146 8A0A MOV CL,BYTE PTR DS:[EDX]
00401148 84C9 TEST CL,CL
0040114A 74 11 JE SHORT CrackMe.0040115D
0040114C 0FBEC9 MOVSX ECX,CL
0040114F 8D0480 LEA EAX,DWORD PTR DS:[EAX+EAX*4]
00401152 42 INC EDX
00401153 8D4441 D0 LEA EAX,DWORD PTR DS:[ECX+EAX*2-30]
00401157 8A0A MOV CL,BYTE PTR DS:[EDX]
00401159 84C9 TEST CL,CL
0040115B ^\75 EF JNZ SHORT CrackMe.0040114C
0040115D 35 34120000 XOR EAX,1234 ; 将我们输入的字符串转化成十六进制数存放在EAX中并与1234进行异或
00401162 C3 RETN
F4直接运行到0040115D行,可以观察到eax的值为000045BD,即我们输入的密码17853的十六进制表示形式。F8使函数返回,到00401286,F8一步,到00401289。由行0040115D的注释可以知道程序对我们输入的字符串转化成十六进制数并与1234进行异或再和"Iceberg"进行变换运算后的值00005789比较,不等则跳转,相等则调用EnableWindow使“注册成功”按钮可用,因此只要将5789与1234进行异或再转换成十进制就能得到正确的密码了。转换使用的是OD带的破解辅助计算工具,很容易就能计算出密码为17853.到此对这个crackme的分析全部结束。
在此要特别感谢scusword对我的帮助,谢了曾哥!
本文使用的crackme在附件中。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)