前不久破解一个软件的时候遇到了各种反调试,折腾的自己各种难受,最终爆破了之后感觉心情大快就顺手写下了这篇文章
十六进制分析工具:winhex
查壳工具:PEID
脱壳工具:ollydump插件或者LordPE
脱壳修复工具:ImportREC
逆向工具:OllyDbg
打开源程序所在文件夹,发现有一个crackme,双机运行程序发现有这个提示:
应该是文件的PE结构被修改了,winhex载入分析发现:
果然是PE结构的问题,在DOS头后面的PE头的16进制应该为50 45,将上述52修改为45,保存文件之后发现仍然运行不了,证明PE结构仍然存在问题。
分析PE头后面的IMAGE_FILE_HEADER(映像文件头,NT头),对比结构:
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; //运行平台
WORD NumberOfSections; //文件的区块数目
DWORD TimeDateStamp; //文件创建的日期和时间
DWORD PointerToSymbolTable; //指向符号表
DWORD NumberOfSymbols; //符号表中符号个数
WORD SizeOfOptionalHeader; //IMAGE_OPTIONAL_HEADER32结构大小
WORD Characteristics; //文件属性
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
上述地址分别相对于50450000之后偏移04h(4C01),06h(0300),08h,0Ch,10h,14h,16h对比之后发现在运行平台上为1F0h。
而我们熟知运行平台如下图:
故将其修改为14Ch,保存文件。修复PE结构之后可以正常运行了
双机运行之后如下图
输入用户名和注册码之后提示不正确,而且在过了一段时间之后程序自动退出,应该加了时间控制(此时主窗口已经退出,只有错误提示框)
用PEID分析载入分析之后发现:
程序加了WinUpack的壳好在只是一个普通的压缩弱壳,用PEID自带的插件Krypto ANAlzer扫了一遍程序
了解到该程序并没有使用什么知名的加密算法。
因为这里加了壳,不便于静态分析,故笔者在这里并未使用IDA。用OD载入程序,OEP被壳修改,要先脱壳。因为WinUpack为弱壳,所以根据OEP定律,单步运行至OEP改变时右键数据窗口中跟随,然后下硬件访问断点,运行之后程序停在OEP,因为加了壳使OD并没有完全正常解析指令,:
开始脱壳,用ollydump记录下程序OEP,lordpe转存,脱壳之后因为IAT被破坏所以无法正常运行软件。
使用ImportREC修复脱壳后程序,将 OEP改为上述14EC,获取输入表,发现全部有效,然后修复上面脱壳的转存文件:
能正确运行。OD再次载入,停在正确的OEP,正式开始破解:(这里因为重建了输入表,所以程序的大小会比之前的源程序要大一些,属于正常情况)
看到程序的入口点应该想到程序使用了较为高级的花指令,伪装了一些API调用,然后通过call eax致使无法查到这些API的调用。
根据最开始的提示Error(标题栏)和“注册码错误”,使用字符串查找如下:
Ctrl+G(转到上述地址),到这些地址处发现:
退回到OEP一步步分析:运行到这一步时程序会自动终止:
因为前面有一个捕获异常函数SetUnhandledExceptionFilter,在程序被调试时,ptr ds[eax]此处地址为0是不可读写的,而这里向一块不可写的内存中写入0x1,自然触发异常,终止程序。
Nop掉这个函数和异常触发的mov。
重新载入程序至:
这个是窗口主函数了,这个API的第4个参数为00401340,就是窗口主程序所在地址了,转到在00401340下int 3断点,运行至后得:
先不着急单步,浏览一遍代码之后发现程序在此段中多次调用了0040101E处的函数,enter进去之后发现第二层反调试:
00401027 |. 68 28010000 push 0x128 ; /Length = 128 (296.)
0040102C |. 8D85 D8FEFFFF lea eax,[local.74] ; |
00401032 |. 50 push eax ; |Destination
00401033 |. E8 FA050000 call <jmp.&kernel32.RtlZeroMemory> ; \RtlZeroMemory
00401038 |. C785 D8FEFFFF>mov [local.74],0x128
00401042 |. 6A 00 push 0x0 ; /ProcessID = 0
00401044 |. 6A 02 push 0x2 ; |Flags = TH32CS_SNAPPROCESS
00401046 |. E8 AB050000 call <jmp.&kernel32.CreateToolhelp32Snap>; \CreateToolhelp32Snapshot
0040104B |. 8985 D4FEFFFF mov [local.75],eax
00401051 |. 8D85 D8FEFFFF lea eax,[local.74]
00401057 |. 50 push eax ; /lppe
00401058 |. FFB5 D4FEFFFF push [local.75] ; |hSnapshot
0040105E |. E8 C3050000 call <jmp.&kernel32.Process32First> ; \Process32First
00401063 |. EB 1F jmp Xdump1.00401084
00401065 |> E8 98050000 /call <jmp.&kernel32.GetCurrentProcessId>; [GetCurrentProcessId
0040106A |. 3B85 E0FEFFFF |cmp eax,[local.72]
00401070 |. 74 26 |je Xdump1.00401098
00401072 |. 8D85 D8FEFFFF |lea eax,[local.74]
00401078 |. 50 |push eax ; /lppe
00401079 |. FFB5 D4FEFFFF |push [local.75] ; |hSnapshot
0040107F |. E8 A8050000 |call <jmp.&kernel32.Process32Next> ; \Process32Next
00401084 |> 0BC0 or eax,eax
00401086 |.^ 75 DD \jnz Xdump1.00401065
上面主要是通过调用系统快照函数(红色字体标注部分),然后遍历这个系统当前的进程ID,直到找到当前dump的进程ID后跳走,数据窗口中跟踪[local.72]地址,发现确实在遍历进程名和ID。证实了上面我的想法。
而在以下代码中发现了第二层反调试的真面目:
将当前进程的父进程与系统下Explorer.exe进行对比。
继续单步运行:第二次判断父进程是否为CMD.exe
这里因为一般运行在windows系统下进程调度时,大部分进程都是有父进程Explorer.exe或者cmd.exe创建的,而当程序处于调试状态时父进程肯定是调试进程,所以这一层反调试能针对很多调试软件起到很好的反调试作用。
在程序中多次调用了这一层反调试,所以单纯的nop需要靠IDC脚本实现多次,这里我们让
00401114 |. /74 68 je Xdump1.0040117E
改为jmp 00401117E,让它恒跳走,让程序误以为父进程校验正确。
保存文件之后想到刚开始注册时会有成功或者失败提示,那么是调用了MessageBox这个函数。根据这个信息,我们想到了查找函数。于是查找api调用如下
发现这里并没有messagebox,这里应该是到了宏,调用api之前将api名字做了隐藏,之后直接call eax,程序刚开始时代码说明了这一点:
在调用的API中发现了GetDlgItem,这是一个破绽。直接下int 3断点,运行之后发现自己还没来得及输入用户名和注册码程序自动退出,这让我想到刚刚主窗口中的两个可疑的函数SetTimer
0040138E |. 6A 00 push 0x0 ; /Timerproc = NULL
00401390 |. 68 E8030000 push 0x3E8 ; |Timeout = 1000. ms
00401395 |. 6A 06 push 0x6 ; |TimerID = 6
00401397 |. FF75 08 push [arg.1] ; |hWnd
0040139A |. E8 45020000 call <jmp.&user32.SetTimer> ; \SetTimer
和
004013C4 |. 6A 00 push 0x0 ; /Timerproc = NULL
004013C6 |. 68 10270000 push 0x2710 ; |Timeout = 10000. ms
004013CB |. 6A 05 push 0x5 ; |TimerID = 5
004013CD |. FF75 08 push [arg.1] ; |hWnd
004013D0 |. E8 0F020000 call <jmp.&user32.SetTimer> ; \SetTimer
两个都是SetTimer,这个就能解释之前程序会自动退出的原因了,SetTmier函数即为每隔固定的一个时间向所在窗口发送消息。上面这段应该发送的是WM_CLOSE而销毁了窗口。,分析代码知道一个是1000ms一个是10000ms,而我们在反调试分析代码过程中所需要的时间远远大于这些时间,所以自然会退出。这也是利用调试时间差起到反调试的思路。
将timeout参数值改成FFFF,时间应该我们足够逆向分析用了。保存文件之后载入文件,进一步分析
在getdlgitem上下int 3断点,成功断下:
0040143B |. 6A 03 push 0x3 ; /ControlID = 3
0040143D |. FF75 08 push [arg.1] ; |hWnd
00401440 |. E8 8D010000 call <jmp.&user32.GetDlgItem> ; \GetDlgItem
00401445 |. A3 60304000 mov dword ptr ds:[0x403060],eax
0040144A |. 6A 04 push 0x4 ; /ControlID = 4
0040144C |. FF75 08 push [arg.1] ; |hWnd
0040144F |. E8 7E010000 call <jmp.&user32.GetDlgItem> ; \GetDlgItem
00401454 |. A3 64304000 mov dword ptr ds:[0x403064],eax
00401459 |. 68 74304000 push dump3.00403074 ; /lParam = 403074
0040145E |. 6A 32 push 0x32 ; |wParam = 32
00401460 |. 6A 0D push 0xD ; |Message = WM_GETTEXT
00401462 |. FF35 60304000 push dword ptr ds:[0x403060] ; |hWnd = C0B2C
00401468 |. E8 71010000 call <jmp.&user32.SendMessageA> ; \SendMessageA
0040146D |. 68 F4304000 push dump3.004030F4 ; /lParam = 4030F4
00401472 |. 6A 32 push 0x32 ; |wParam = 32
00401474 |. 6A 0D push 0xD ; |Message = WM_GETTEXT
00401476 |. FF35 64304000 push dword ptr ds:[0x403064] ; |hWnd = A07EC
0040147C |. E8 5D010000 call <jmp.&user32.SendMessageA> ; \SendMessageA
程序使用SendMessageA,将字符串的内容送至00403074和004030F4两处,避免使用GetDlgItemTextA函数直接能获取明文。
单步跟踪
发现算法在call eax之后来到如下代码。跟进之后算法分析见代码中注释:
004011D9 > \A1 56304000 mov eax,dword ptr ds:[0x403056] ; 核心算法,此地址处存放用户名的长度
004011DE . 83F8 06 cmp eax,0x6 ; 用户名长度>=6
004011E1 . 0F8C 97000000 jl dump3.0040127E
004011E7 . 50 push eax
004011E8 . 59 pop ecx
004011E9 . 8D35 00304000 lea esi,dword ptr ds:[0x403000] ; 预定字符串S1
004011EF . 8D3D 74304000 lea edi,dword ptr ds:[0x403074] ; 用户名
004011F5 > 33C0 xor eax,eax
004011F7 . 33DB xor ebx,ebx
004011F9 . 8B07 mov eax,dword ptr ds:[edi] ; 将4位用户名给eax
004011FB . 8B1E mov ebx,dword ptr ds:[esi] ; 将4位s1给ebx
004011FD . 25 FF000000 and eax,0xFF ; 去掉高位保留第一位,即取一位用户名
00401202 . 81E3 FF000000 and ebx,0xFF ; 去掉高位保留第一位,取S一位
00401208 . 33C3 xor eax,ebx ; 二者异或
0040120A . 0305 4E304000 add eax,dword ptr ds:[0x40304E] ; 然后累加
00401210 . A3 4E304000 mov dword ptr ds:[0x40304E],eax
00401215 . 46 inc esi
00401216 . 47 inc edi ; 循环向后读取
00401217 .^ E2 DC loopd Xdump3.004011F5
00401219 . 33C9 xor ecx,ecx
0040121B . 8B0D 5A304000 mov ecx,dword ptr ds:[0x40305A] ; 注册码长度
00401221 . 8D35 25304000 lea esi,dword ptr ds:[0x403025] ; 预定字符串S2
00401227 . 8D3D F4304000 lea edi,dword ptr ds:[0x4030F4] ; 注册码
0040122D > 33C0 xor eax,eax
0040122F . 33DB xor ebx,ebx
00401231 . 8B07 mov eax,dword ptr ds:[edi] ; 算法同上
00401233 . 8B1E mov ebx,dword ptr ds:[esi]
00401235 . 25 FF000000 and eax,0xFF
0040123A . 81E3 FF000000 and ebx,0xFF
00401240 . 33C3 xor eax,ebx
00401242 . 0305 52304000 add eax,dword ptr ds:[0x403052]
00401248 . A3 52304000 mov dword ptr ds:[0x403052],eax
0040124D . 46 inc esi
0040124E . 47 inc edi
0040124F .^ E2 DC loopd Xdump3.0040122D ; 循环读取
00401251 . A1 52304000 mov eax,dword ptr ds:[0x403052]
00401256 . 8B1D 4A304000 mov ebx,dword ptr ds:[0x40304A]
0040125C . 85DB test ebx,ebx
0040125E . 75 3A jnz Xdump3.0040129A ; 不等跳至失败
00401260 . 8505 4E304000 test dword ptr ds:[0x40304E],eax ; 比较用户名与S1异或和是否等于注册码与S2的异或和
00401266 . 75 32 jnz Xdump3.0040129A ; 不等则跳向失败
00401268 . 6A 00 push 0x0
0040126A . 68 98114000 push dump3.00401198 ; ASCII "Success"
在jnz时将标志位Z改为1之后看到程序注册成功了,
然后直接将jnz修改为nop
保存之后按照理论上来讲,应该爆破成功,此时无论输入任何用户名和注册码都应该会提示成功,但是当我们再次运行程序的时候发现
我们在到反汇编窗口发现代码刚刚爆破的jnz被还原了
看来是有代码SMC防爆自校验,防止爆破技术,回溯之前代码,发现在程序载入时又一个这样的call
跟进之后发现:
004012F3 /$ B8 66124000 mov eax,dump4.00401266
004012F8 |. A3 90384000 mov dword ptr ds:[0x403890],eax
004012FD |. 8B18 mov ebx,dword ptr ds:[eax]
004012FF |. 66:81FB 753A cmp bx,0x3A75
00401304 |. 74 41 je Xdump4.00401347
00401306 |. 68 94384000 push dump4.00403894 ; /pOldProtect = dump4.00403894
0040130B |. 6A 40 push 0x40 ; |NewProtect = PAGE_EXECUTE_READWRITE
0040130D |. 6A 10 push 0x10 ; |Size = 10 (16.)
0040130F |. FF35 90384000 push dword ptr ds:[0x403890] ; |Address = NULL
00401315 |. E8 2C030000 call <jmp.&kernel32.VirtualProtect> ; \VirtualProtect
0040131A |. A1 90384000 mov eax,dword ptr ds:[0x403890]
0040131F |. BB 753A0000 mov ebx,0x3A75
00401324 |. 66:8918 mov word ptr ds:[eax],bx
00401327 |. B8 6E124000 mov eax,dump4.0040126E
0040132C |. A3 90384000 mov dword ptr ds:[0x403890],eax
00401331 |. 8B18 mov ebx,dword ptr ds:[eax]
00401333 |. 66:81FB 7532 cmp bx,0x3275
00401338 |. 74 0D je Xdump4.00401347
0040133A |. A1 90384000 mov eax,dword ptr ds:[0x403890]
0040133F |. BB 75320000 mov ebx,0x3275
00401344 |. 66:8918 mov word ptr ds:[eax],bx
00401347 \> C3 retn
原来在程序载入的时候会对上面两个关键的jnz进行检验,因为jnz机器码为7532如果发现被修改了就再次修改回去,这样就无法简单的nop成功了。
于是我们将这个函数也nop掉保存文件,再次注册时成功了
总算分析的差不多了,贴上一张图
原文地址:http://blog.csdn.net/nightsay/article/details/45034787