做CTF的逆向题时苦于对大佬们的wp各种看不懂,只因自己太菜跟不上大佬的节奏,所以才写了这一篇最最最基础的,保姆级的,手把手教的,看完包会的调试器使用教程。其中重点讲OllyDbg,参考看雪的书《加密与解密》,参考小甲鱼的视频《OD使用教程》做的笔记,其次做了一些x64dbg与OD有区别的笔记。希望这篇文章不仅能帮助你们看懂大佬们的wp,而且可以自己实现逆向破解,一起加油吧!
一、OllyDbg 1. OD界面
汇编代码对应的地址窗口:存放虚拟地址,一般情况下同一程序的同一条指令在不同系统环境下此值相同
汇编代码对应的十六进制机器码窗口
反汇编窗口
反汇编代码对应的注释信息窗口
寄存器信息窗口(32位4个字节00000000-FFFFFFFF)
EAX扩展累加寄存器
EBX扩展基址寄存器
ECX扩展计数寄存器
EDX扩展数据寄存器
ESI扩展来源寄存器
EDI扩展目标寄存器
EBP扩展基址指针寄存器:主要用于栈和栈桢
ESP扩展堆栈指针寄存器:指向当前进程的栈空间地址
EIP扩展指令指针寄存器:指向下一条要被执行的指令
当前执行到反汇编代码的信息窗口
~9. 数据所在的内存地址,十六进制,ASCII码
~12. 栈地址,存放的数据,对应说明信息
2. OD常用的快捷键
快捷键
功能
F2
下断点,也就是指定断点的地址
Alt+B
打开断点管理
空格
快速切换断点状态(开启/禁止)
F3
加载一个可执行程序,进行调试分析
F4
程序执行到光标处
F5
缩小、还原当前窗口
F7
单步步入
F8
单步步过
Ctrl+F8
自动单步步过
F9
直接运行程序,遇到断点处,程序暂停
Ctrl+F2
重新运行程序到起始处,一般用于重新调试程序
Ctrl+F9
执行到函数返回处,用于跳出函数实现
Shift+F9
与F9相同,但是如果被调试程序发生异常而中止,调试器会首先尝试执行被调试程序指定的异常处理
Alt+F9
执行到用户代码处,用于快速跳出函数系统
Ctrl+G
输入十六进制地址,快速定位到该地址处
;
添加注释
3. 实验一 改变内容
目标:将标题改为“v5le0n9”,内容改为“这是我修改的第一个程序”
F3加载程序进OD,F8单步步过,在弹出弹窗的地址F2下断点。Ctrl+F2重新加载程序,F9到达断点处,F7单步步入,F8单步步过,在弹出弹窗的地址按F2下断点,Alt+B查看所有断点,将光标放置在要取消断点的元组,按空格取消。重复以上,直到在注释窗口看到弹窗的内容。
从汇编角度看,“hello”被压入了00937b30地址,“这是我写的第一个程序”被压入了00937b68地址。
在数据窗口按Ctrl+G快速定位到00937b30
双击00937b30的第一个十六进制68,取消keep size,在unicode框中直接修改为v5le0n9(如果原内容用Unicode就用Unicode,ASCII就用ASCII),如果后面没有00要在末尾加上00,因为字符串默认以00结尾。我们这里有很多个00,所以不用管它。
接下来修改内容,Ctrl+G定位到00937b68,发现直接在Unicode改中文不可行,那只能改下面的16进制表示。将我们要输入的字符串改为16进制。
由于PE结构用的是小端存储,所以8fd9要写成d98f表示“这”,以此类推。
再按F9,由于后面没有断点直接运行到程序结束,程序成功修改。
选中我们在数据窗口所有修改过的痕迹,右键->copy to executable file,会弹出一个窗口,图标为D,在窗口内右键->backup->save data to file,另存为exe文件,这就是“破解版”。双击新的exe直接显示我们修改好的内容。
4. 容易犯错的调试方法 一路F8,碰到问题下断点,直到把问题解决。就是我们实验一的做法,但在实验一是可行的,因为它不需要用户输入任何东西。如果需要用户输入,实验一的调试会陷入死循环,因为用户没有接触到窗口,它一直在等用户发过来的消息。
4.1 正确的破解方式 Windows程序是以API函数为基础的程序,所有程序都要调用API函数。
程序读取文本框内容的字符串通常用的是以下两个函数:
1 2 3 Ctrl+G跟踪表达式 GetDlgItemTextA(GetDlgItemTextW) GetWindowTextA(GetWindowTextW)
汇编代码的返回值约定是存放在eax里面的,如果32位的eax不够存放返回值,系统会将返回值放在内存某个位置并把该位置的地址放在eax返回。
5. 实验二 TraceMe
目标:无论输入什么都成功。
在IDA查看一下源代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 int __stdcall WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) { ::hInstance = hInstance; DialogBoxParamA(hInstance, (LPCSTR)0x65 , 0 , DialogFunc, 0 ); return 0 ; } BOOL __stdcall DialogFunc (HWND hWnd, UINT a2, WPARAM a3, LPARAM a4) { int v5; HWND v6; HWND v7; HWND v8; HICON v9; CHAR String2[4 ]; int v11; int v12; __int16 v13; char v14; CHAR v15; char v16; CHAR v17; __int16 v18; char v19; CHAR String; CHAR String1; qmemcpy(&v15, byte_405060, 0x16 u); v11 = dword_405054; v16 = byte_405060[22 ]; v14 = byte_40505E; qmemcpy(&v17, &unk_405038, 0x14 u); *(_DWORD *)String2 = dword_405050; v18 = *((_WORD *)&unk_405038 + 10 ); v13 = word_40505C; v12 = dword_405058; v19 = *((_BYTE *)&unk_405038 + 22 ); if ( a2 == 16 ) { DestroyWindow(hWnd); return 1 ; } if ( a2 == 272 ) { v9 = LoadIconA(hInstance, (LPCSTR)0x70 ); SendMessageA(hWnd, 0x80 u, 1u , (LPARAM)v9); SendDlgItemMessageA(hWnd, 110 , 0xC5 u, 0x50 u, 0 ); return 1 ; } if ( a2 != 273 ) return 0 ; if ( (signed int )(unsigned __int16)a3 > 1013 ) { if ( (unsigned __int16)a3 == 1014 || (unsigned __int16)a3 == 40002 ) DialogBoxParamA(hInstance, (LPCSTR)0x67 , hWnd, sub_401020, 0 ); return 0 ; } if ( (unsigned __int16)a3 != 1013 ) { if ( (unsigned __int16)a3 == 2 || (unsigned __int16)a3 == 1002 ) { SendMessageA(hWnd, 0x10 u, 0 , 0 ); return 0 ; } return 0 ; } v5 = GetDlgItemTextA(hWnd, 110 , &String, 81 ); GetDlgItemTextA(hWnd, 1000 , &String1, 101 ); if ( String && v5 >= 5 ) { if ( sub_401340(&String1, &String, v5) ) { lstrcpyA(::String1, String2); v6 = GetDlgItem(hWnd, 110 ); EnableWindow(v6, 0 ); v7 = GetDlgItem(hWnd, 1000 ); EnableWindow(v7, 0 ); v8 = GetDlgItem(hWnd, 1000 ); } else { lstrcpyA(::String1, &v17); v8 = GetDlgItem(hWnd, 1000 ); } } else { lstrcpyA(::String1, &v15); v8 = GetDlgItem(hWnd, 110 ); } SetFocus(v8); MessageBeep(0 ); DialogBoxParamA(hInstance, (LPCSTR)0x79 , hWnd, sub_401060, 0 ); return 0 ; }
可知程序用的是GetDlgItemTextA
函数,Ctrl+G查找函数,F2下断点。按F9,会弹出输入框,输入后一路F8,直到看到我们刚才输入的内容,再仔细分析接下来的汇编。
1 2 3 4 5 6 7 8 9 10 004011D7 . 8D5424 4C lea edx,dword ptr ss:[esp+0x4C];v5le0n9 004011DB . 53 push ebx;ebx=7,用户名长度 004011DC . 8D8424 A00000>lea eax,dword ptr ss:[esp+0xA0];l30n9ry0n 004011E3 . 52 push edx;将用户名push进去 004011E4 . 50 push eax;将序列号push进去 004011E5 . E8 56010000 call TraceMe.00401340;验证用户名和序列号的函数 004011EA . 8B3D BC404000 mov edi,dword ptr ds:[<&USER32.GetDlgIte>; user32.GetDlgItem 004011F0 . 83C4 0C add esp,0xC 004011F3 . 85C0 test eax,eax;TraceMe.00401340函数的返回值 004011F5 . /74 37 je short TraceMe.0040122E;跳转
因为我们现在是暴力破解,所以可以不用进验证函数查看具体验证操作,直接改函数的返回值即可。在test eax,eax
执行完后,也就是在je short TraceMe.0040122E
指令将寄存器中Z标志位改变。F9走到程序结束,弹窗成功了。
如果想保存破解版的话,直接改标志位是不可行的。另一种方法是将je short TraceMe.0040122E
改为nop
或改为jne short TraceMe.0040122E
,右键->保存到可执行文件->选择,在D图标中右键->备份->保存数据到文件。破解版无论输入什么都是成功的。
6. 加载目标文件调试 设置OD中断在程序的入口点:Options->Debugging options->Events
1 2 3 system breakpoint 系统断点(int 13) entry point of main module 主模块的入口点,即文件的入口点 winmain 程序的WinMain()函数入口点
6.1 call指令 call xxx
等同于push eip;jmp xxx
1 2 3 4 5 call 404000h 直接跳到函数或过程的地址 call eax 函数或过程地址存放在eax call dword ptr [eax] call dword ptr [eax+5] call dword ptr [<&API>] 执行一个系统API:系统提供给我们的接口
6.2 mov指令 mov指令格式:mov dest, src
将src的内容拷贝到dest
6.2.1 mov扩展 movs/movsb/movsw/movsd edi, esi:按串/字节/字/双字节为单位将esi寄存器指向的数据复制到edi寄存器指向的空间。
movsx:符号位扩展,byte->word, word->dword(扩展后高位全用符号位填充),然后实现mov。
1 2 3 MOV BL,80H MOVSX AX,BL 运行完以上汇编语句之后,AX的值为FF80H。由于BL为80H=1000 0000,最高位也即符号位为1,在进行带符号扩展时,其扩展的高8位均为1,故赋值AX为1111 1111 1000 0000,即AX=FF80H。
movzx:零扩展,byte->word, word->dword(扩展后高位全用0填充),然后实现mov。
6.3 cmp指令 cmp指令格式:cmp dest, src
比较两个操作数,并通过比较结果设置C/O/Z标志位
1 2 3 cmp eax, ebx 相等Z标志位置1,否则置0 cmp eax, [404000] 取404000h地址的dword型数据与eax相比较 cmp [404000], eax
Z标志位:Zero 零标志位 运算结果为0时置1,否则置0
O标志位:Overflow 溢出标志位 溢出置1,否则置0
C标志位:Carry flag 进位标志位 进位置1,否则置0
6.4 test指令 test指令格式:test dest, src
和and指令一样,对两个操作数进行按位与运算,但test指令不将结果保存到test中,仅对标志位重新置位
1 test eax, eax 如果eax为的值0,则Z标志位置1
eax存放所有函数的返回值。
6.5 条件跳转指令 想让它跳转就设置无条件跳转jmp指令
不想让它跳转设置nop指令
调试的时候注重跳转指令和cmp指令还有test指令。
破解版(免注册机):全选修改过的程序右键->复制到可执行文件->选择,会生成一个文件,文件右键->备份->保存数据到文件。
跳过指令的方法:
改为无条件跳转指令jmp
将需要跳过的地方全用nop填充(右键->二进制->用NOP填充)
6.6 patch(补丁) OD可用/
(有的OD是P
)查看修改的内容
7. 实验三 ReverseMe
目标:让它绕过过期指令。
F3加载进OD,可以看到这个程序很小,汇编代码量不多。右键->中文搜索引擎->智能搜索,查看字符串。
1 2 3 4 5 6 7 8 9 10 中文搜索引擎 地址 反汇编 文本字符串 00401000 push 0x0 (Initial CPU selection) 0040106E push reverseM.00402079 Keyfile.dat 0040107F push reverseM.00402000 Key File ReverseMe 00401084 push reverseM.00402017 Evaluation period out of date.Purchase new license 004010F9 push reverseM.00402000 Key File ReverseMe 004010FE push reverseM.00402086 Keyfile is not valid. Sorry. 00401207 push reverseM.00402000 Key File ReverseMe 0040120C push reverseM.004020DE You really did it! Congratz !!!
再结合注释窗口,我们可以知道,Keyfile.dat
是存储许可证的地方,有三个MessageBox
,但运行程序只会弹出一个,可能是if语句。我们现在要让它弹出You really did it! Congratz !!!
。
因为这个程序很小,从头开始就可以认真看汇编,关键是跳转指令,千万不要让它跳转到其它弹窗。
1 2 3 4 jnz short 0040109A => jz short 0040109A或jmp short 0040109A jnz short 004010B4 => jz short 004010B4或jmp short 004010B4 jl short 004010F7 => jnl short 004010F7或nop jl short 004010F7 => jnl short 004010F7或nop
将修改过的痕迹全选,按照上次实验1,2保存破解版。
还可以做一个补丁版,这个程序的补丁就是要将Keyfile.dat
中的许可证变为有效的,需要仔细分析程序汇编代码,要很强的阅读汇编代码的能力。(在OD的/
图标可查看所有打过的补丁。)
修改第一个让它进入Readfile
1 jnz short 0040109A => jz short 0040109A或jmp short 0040109A
修改第二个避免让它跳出Readfile
1 jnz short 004010B4 => jz short 004010B4或jmp short 004010B4
下面是Readfile
函数实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 0040109A > \6A 00 push 0x0 ; /pOverlapped = NULL 0040109C . 68 73214000 push reverseM.00402173 ; |pBytesRead = reverseM.00402173 004010A1 . 6A 46 push 0x46 ; |BytesToRead = 46 (70.) 004010A3 . 68 1A214000 push reverseM.0040211A ; |Buffer = reverseM.0040211A 004010A8 . 50 push eax ; |hFile = NULL 004010A9 . E8 2F020000 call <jmp.&KERNEL32.ReadFile> ; \ReadFile 004010AE . 85C0 test eax,eax 004010B0 74 02 je short reverseM.004010B4 004010B2 . EB 43 jmp short reverseM.004010F7 004010B4 > \33DB xor ebx,ebx;ebx=0 004010B6 . 33F6 xor esi,esi;esi=0 ; reverseM.<ModuleEntryPoint> 004010B8 . 833D 73214000>cmp dword ptr ds:[0x402173],0x10;ds:[0402173]要16个byte 004010BF 7C 36 jl short reverseM.004010F7;否则跳出函数 004010C1 > 8A83 1A214000 mov al,byte ptr ds:[ebx+0x40211A];将Buffer[0]的内容赋值到al,第二次将Buffer[1]的内容赋值到al... 004010C7 . 3C 00 cmp al,0x0;比较al是否为0 004010C9 . 74 08 je short reverseM.004010D3;是的话就跳转实现,到D3位置 004010CB . 3C 47 cmp al,0x47;比较al是否是大写字母G 004010CD . 75 01 jnz short reverseM.004010D0;不是的话就跳出函数 004010CF . 46 inc esi;esi+=1 ; reverseM.<ModuleEntryPoint> 004010D0 > 43 inc ebx;ebx+=1 004010D1 .^ EB EE jmp short reverseM.004010C1;回到C1位置,循环 004010D3 > 83FE 08 cmp esi,0x8;比较esi是否为8,也就是循环8次 004010D6 7C 1F jl short reverseM.004010F7 004010D8 . E9 28010000 jmp reverseM.00401205
所以Keyfile.dat
中,前8个是G,长度为16,后8个字符任意。
在与ReveseMe.exe
同一目录下创建一个Keyfile.dat
文件
有了这个dat文件后,将原文件直接跑,也是成功的。
8. 去除NAG窗口 nag窗口是软件设计者用来时不时提醒用户购买正版的警告窗口。
父句柄消灭,子进程也会被消灭。
8.1 PE结构 PE(PortableExecutable):可执行文件
AOE(AddressOfEntryPoint):程序入口地址
VA(VirtualAddress):虚拟地址
RVA(RelativeVirtualAddress):相对虚拟地址
EP(EntryPoint):程序入口点
PE文件中,最下面的节区(区块)是运行一个程序真正需要的内容。上面的“头”仅仅是帮助Windows加载器定位和寻找下面区块的内容。
GetModuleHandle这个API函数用于获取程序的ImageBase(基址)。
MessageBox的OwnerHandle为0,可以将这个值改为一个不存在的值,例如1,这样它就找不到父句柄,就不会被显示出来。
9. 实验四 RegisterMe
目标:去除NAG窗口。
OD载入程序就看到注释区有NAG窗口的字符串。看不到的话就右键->查找->所有参考文本字串,找到第一个NAG窗口。
第一种方法:
1 je short 00401024 => jmp short 00401024或jne short 00401024
第二种方法:
1 2 3 4 5 6 00401011 |. |6A 00 push 0x0 ; /Style = MB_OK|MB_APPLMODAL 00401013 |. |68 7D304000 push Register.0040307D ; |Title = "Register Me" 00401018 |. |68 34304000 push Register.00403034 ; |Text = "Remove the nags to register This will make program fully registered :))" 0040101D |. |6A 00 push 0x0 ; |hOwner = NULL 0040101F |. |E8 C6010000 call <jmp.&USER32.MessageBoxA> ; \MessageBoxA
将MessageBoxA函数选中->右键->Binary->Fill with NOPs
第三种方法:
将MessageBoxA函数的父句柄改为1,因为我们没有一个父进程是1所以MessageBoxA不实现 。
1 0040101D |. |6A 00 push 0x0 ; |hOwner = NULL 将push 0x0 => push 1
第四种方法:
修改文件入口处绕过第一个NAG窗口,第二个NAG窗口再用以上三种方法解决。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 00401000 >/$ 6A 00 push 0x0 ; /pModule = NULL 00401002 |. E8 0D020000 call <jmp.&KERNEL32.GetModuleHandleA> ; \GetModuleHandleA 00401007 |. A3 1C314000 mov dword ptr ds:[0x40311C],eax 0040100C |. 83F8 00 cmp eax,0x0 0040100F 74 13 je short Register.00401024 00401011 6A 00 push 0x0 ; /Style = MB_OK|MB_APPLMODAL 00401013 68 7D304000 push Register.0040307D ; |Title = "Register Me" 00401018 68 34304000 push Register.00403034 ; ASCII "Remove the nags to register\r\nThis will make program fully registered :))" 0040101D 6A 00 push 0x0 0040101F E8 C6010000 call <jmp.&USER32.MessageBoxA> ; \MessageBoxA 00401024 |> \6A 0A push 0xA ; /Arg4 = 0000000A 00401026 |. FF35 20314000 push dword ptr ds:[0x403120] ; |Arg3 = 00000000 0040102C |. 6A 00 push 0x0 ; |Arg2 = 00000000 0040102E |. FF35 1C314000 push dword ptr ds:[0x40311C] ; |Arg1 = 00000000 00401034 |. E8 19000000 call Register.00401052 ; \Register.00401052 00401039 |. 6A 00 push 0x0 ; /Style = MB_OK|MB_APPLMODAL 0040103B |. 68 7D304000 push Register.0040307D ; |Title = "Register Me" 00401040 |. 68 89304000 push Register.00403089 ; |Text = "Oops! I am not registered !!" 00401045 |. 6A 00 push 0x0 ; |hOwner = NULL 00401047 |. E8 9E010000 call <jmp.&USER32.MessageBoxA> ; \MessageBoxA 0040104C |. 50 push eax ; /ExitCode = 0x19FFCC 0040104D \. E8 BC010000 call <jmp.&KERNEL32.ExitProcess> ; \ExitProcess
程序原本的入口点在401000地址,绕过MessageBoxA,那就修改程序入口点为401024。
点击图标M,M是内存分布表,找到401000地址的PE头:
1 2 3 4 5 6 7 8 9 Memory map, item 14 Address=00400000 Size=00001000 (4096.) Owner=Register 00400000 (itself) Section= Contains=PE header Type=Imag 01001002 Access=R Initial access=RWE
双击这一行,会弹出D图标,找到“Offset to PE signature”,PE头开始的地方。
1 2 VA 内存RVA 人类易读RVA 注释 0040003C C0000000 DD 000000C0 ; Offset to PE signature
再去找RVA为000000C0也就是VA为4000C0地址的地方。找到AOE,它的VA是4000E8。
1 004000E8 00100000 DD 00001000 ; AddressOfEntryPoint = 0x1000
返回图标C的数据窗口,Ctrl+G快速定位到4000E8,存的是00 10 00 00,由于是小端序存储,我们要改成24 10 00 00,就能绕过第一个NAG窗口。
选中24,即我们修改过的地方,右键->复制到可执行文件->右键->备份->复制数据到文件。如果修改第二个NAG要从半破解版程序的基础上改(因为数据窗口和反汇编窗口都要选中再复制到可执行文件我不会)。
9.1 RegisterMe.Oops 双击竟然打不开。OD运行也打不开。奇奇怪怪,是我这个程序有问题吧!!但还是可以看汇编代码的。
查看PE头,也是400000-401000正常值,但是点进PE头会发现某些数值大得离谱。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 004000C0 50 45 00 00>ASCII "PE" ; PE signature (PE) 004000C4 4C01 DW 014C ; Machine = IMAGE_FILE_MACHINE_I386 004000C6 0400 DW 0004 ; NumberOfSections = 0x4 004000C8 1E29D138 DD 38D1291E ; TimeDateStamp = 0x38D1291E 004000CC 00000000 DD 00000000 ; PointerToSymbolTable = 0x0 004000D0 00000000 DD 00000000 ; NumberOfSymbols = 0x0 004000D4 E000 DW 00E0 ; SizeOfOptionalHeader = E0 (224.) 004000D6 0F01 DW 010F ; Characteristics = EXECUTABLE_IMAGE|32BIT_MACHINE|RELOCS_STRIPPED|LINE_NUMS_STRIPPED|LOCAL_SYMS_STRIPPED 004000D8 0B01 DW 010B ; MagicNumber = PE32 004000DA 05 DB 05 ; MajorLinkerVersion = 0x5 004000DB 0C DB 0C ; MinorLinkerVersion = C (12.) 004000DC 00040040 DD 40000400 ; SizeOfCode = 40000400 (1073742848.)改为400 004000E0 000A0040 DD 40000A00 ; SizeOfInitializedData = 40000A00 (1073744384.)改为A00 004000E4 00000000 DD 00000000 ; SizeOfUninitializedData = 0x0 004000E8 00100000 DD 00001000 ; AddressOfEntryPoint = 0x1000 004000EC 00100040 DD 40001000 ; BaseOfCode = 0x40001000改为1000 004000F0 00200040 DD 40002000 ; BaseOfData = 0x40002000改为2000 004000F4 00004000 DD 00400000 ; ImageBase = 0x400000 004000F8 00100000 DD 00001000 ; SectionAlignment = 0x1000 004000FC 00020000 DD 00000200 ; FileAlignment = 0x200 00400100 0400 DW 0004 ; MajorOSVersion = 0x4 00400102 0000 DW 0000 ; MinorOSVersion = 0x0 00400104 0000 DW 0000 ; MajorImageVersion = 0x0 00400106 0000 DW 0000 ; MinorImageVersion = 0x0 00400108 0400 DW 0004 ; MajorSubsystemVersion = 0x4 0040010A 0000 DW 0000 ; MinorSubsystemVersion = 0x0 0040010C 00000000 DD 00000000 ; Reserved 00400110 00500000 DD 00005000 ; SizeOfImage = 5000 (20480.) 00400114 00040000 DD 00000400 ; SizeOfHeaders = 400 (1024.) 00400118 99B40000 DD 0000B499 ; CheckSum = 0xB499 0040011C 0200 DW 0002 ; Subsystem = IMAGE_SUBSYSTEM_WINDOWS_GUI 0040011E 0000 DW 0000 ; DLLCharacteristics = 0x0 00400120 00001000 DD 00100000 ; SizeOfStackReserve = 100000 (1048576.) 00400124 00100000 DD 00001000 ; SizeOfStackCommit = 1000 (4096.) 00400128 00001000 DD 00100000 ; SizeOfHeapReserve = 100000 (1048576.) 0040012C 00100000 DD 00001000 ; SizeOfHeapCommit = 1000 (4096.) 00400130 00000000 DD 00000000 ; LoaderFlags = 0x0 00400134 04000040 DD 40000004 ; NumberOfRvaAndSizes = 40000004 (1073741828.)改为10 00400138 00005000 DD 00500000 ; Export Table address = 0x500000改为0 0040013C 00000500 DD 00050000 ; Export Table size = 50000 (327680.)改为0 00400140 50200000 DD 00002050 ; Import Table address = 0x2050 00400144 3C000000 DD 0000003C ; Import Table size = 3C (60.) 00400148 00400000 DD 00004000 ; Resource Table address = 0x4000 0040014C 9C030000 DD 0000039C ; Resource Table size = 39C (924.) 00400150 00000000 DD 00000000 ; Exception Table address = 0x0 00400154 00000000 DD 00000000 ; Exception Table size = 0x0
右键->modify integer修改以上数值。修改完后返回C图标的数据窗口,Ctrl+G定位到我们修改过的地方,选中保存。保存后的程序就跟RegisterMe一样了。
10. SEH 10.1 Windows程序异常 SEH(Structured Exception Handling):结构化异常处理
SEH是Windows操作系统提供的功能,跟开发工具无关。
Windows程序设计中最重要就是消息传递,事件驱动。当GUI应用程序触发一个消息时,系统将把该消息放入消息队列,然后去查找并调用窗体的消息处理函数(CALLBACK),传递的参数就是这个消息。
异常也当作一种消息,应用程序发生异常时就触发了该消息并告知系统。系统接收后同样会找它的“回调函数”,也就是我们的异常处理例程。
如果在程序中没有做异常处理的话,系统也不会置之不理,它将弹出常见的应用程序错误框,然后结束该程序。
在OD中,Shift+F7/F8/F9来忽略程序异常。或者在Options->Debugging options->Exceptions->Add range->00000000~FFFFFFFF(32位整个内存段)
11. 实验五 pixtopianbook
目标:突破每组只能有4个联系人,只能创建3个组,去除未注册信息,注册版本爆破。
OD载入程序,F9运行程序,弹出界面。
(如果没有弹出界面,可能是在异常处中断了。在OD中,Shift+F7/F8/F9来忽略程序异常。或者在Options->Debugging options->Exceptions->Add range->设置00000000~FFFFFFFF(32位整个内存段))
突破4个联系人
为什么不能右键->查找->所有参考文本字串?我猜可能是因为触发错误异常的弹窗文本不计入内?
但可以右键->中文搜索引擎->智能搜索,查看字符串。一下子就找到了下面的汇编代码。
另一种方法:
已知我们准备添加第5个联系人时程序会弹窗,在Command窗口设置软断点bp MessageBoxA
,意味着当程序执行到有 MessageBoxA 的地方就会停止程序运行。回到界面添加联系人,程序没有反应,但回到OD看已经来到了MessageBoxA处(进到MessageBoxA函数里)。一路F8单步单过,到达call User32.MessageBoxA,程序弹出错误框,按确定(“确定”按不了可能是因为显示弹框的线程在调试的时候被暂停了,没恢复过来就会卡住。这时按F12暂停,再F9运行,就可以按“确定”了;或者到T图标,在界面处右键->Resume All Threads。这两种方法原理差不多),F8到ret指令,返回到上一级函数位置,查看上下代码没什么有用的信息。再一路F8返回上一级,查看上下汇编代码。
1 2 3 4 5 6 7 8 9 10 11 00412DD0 . 83F8 04 cmp eax,0x4;eax与4对比 00412DD3 . 7C 1A jl short Pixtopia.00412DEF;跳到EF,可以跳过E5的弹窗 00412DD5 . 8B4C24 10 mov ecx,dword ptr ss:[esp+0x10] 00412DD9 . 6A 10 push 0x10 00412DDB . 68 00F74800 push Pixtopia.0048F700 ; ASCII "Please register PixtopianBook" 00412DE0 . 68 68FC4800 push Pixtopia.0048FC68 ; ASCII "You've reached the limit of 4 entries per group.\n\nPlease register PixtopianBook today!" 00412DE5 . E8 03350400 call Pixtopia.004562ED;间接调用MessageBox函数 00412DEA . E9 DD000000 jmp Pixtopia.00412ECC 00412DEF > \8D4C24 14 lea ecx,dword ptr ss:[esp+0x14] 00412DF3 . E8 38610100 call Pixtopia.00428F30 00412DF8 . 68 D8784900 push Pixtopia.004978D8
1 jl short Pixtopia.00412DEF => jmp short Pixtopia.00412DEF
大程序的话建议修改一处就保存一次,因为会遇到很多分岔路,很多陷阱。然后在修改过的程序做第二次修改,以此类推。
突破3个组
按上述方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 00408B05 . 83F8 03 cmp eax,0x3;跟3比较 00408B08 . 7C 2A jl short Pixtopia.00408B34;跳到34,可以跳过18的弹窗 00408B0A . 6A 10 push 0x10 00408B0C . 68 00F74800 push Pixtopia.0048F700 ; ASCII "Please register PixtopianBook" 00408B11 . 68 B4F64800 push Pixtopia.0048F6B4 ; ASCII "You've reached the limit of 3 groups.\n\nPlease register PixtopianBook today!" 00408B16 . 8BCB mov ecx,ebx 00408B18 . E8 D0D70400 call Pixtopia.004562ED;间接调用MessageBox函数 00408B1D . 5E pop esi ; Pixtopia.004761E8 00408B1E . 5B pop ebx ; Pixtopia.004761E8 00408B1F . 8B8C24 340100>mov ecx,dword ptr ss:[esp+0x134] ; Pixtopia.00456A5E 00408B26 . 64:890D 00000>mov dword ptr fs:[0],ecx 00408B2D . 81C4 40010000 add esp,0x140 00408B33 . C3 retn 00408B34 > \6A 00 push 0x0 00408B36 . 8D4C24 24 lea ecx,dword ptr ss:[esp+0x24] 00408B3A . E8 21E20100 call Pixtopia.00426D60
1 jl short Pixtopia.00408B34 => jmp short Pixtopia.00408B34
如果是两次一起保存,右键->复制到可执行文件->所有修改。
去除未注册信息
查找UNREGISTERED
字符串。
到M图标查找字符串,Ctrl+B或右键->查找,在ASCII找一下。
1 2 3 4 5 6 7 0048F97F 75 6E 72 65 67 69 73 74 65 72 65 64 20 76 65 72 unregistered ver 0048F98F 73 69 6F 6E 20 6F 66 20 50 69 78 74 6F 70 69 61 sion of Pixtopia 0048F99F 6E 42 6F 6F 6B 2E 20 50 6C 65 61 73 65 20 72 65 nBook. Please re 0048F9AF 67 69 73 74 65 72 20 74 6F 64 61 79 21 00 00 00 gister today!... 0048F9BF 00 43 49 6E 69 74 44 69 61 6C 6F 67 42 61 72 00 .CInitDialogBar. 0048F9CF 00 43 4C 65 66 74 46 6F 72 6D 56 69 65 77 00 00 .CLeftFormView.. 0048F9DF 00 4E 6F 74 65 20 20 20 .Note
选中要修改的地方,右键->二进制->编辑,选中保持大小,因为可能会有对齐方面的问题,意味着我们写的长度不能比它长。由于不能直接在M图标中保存,所以要返回到C图标保存。记住地址起始处48F97F,返回到C图标的数据窗口快速定位Ctrl+G,右键->复制到可执行文件->右键->保存文件。
再次查找ASCII还有没有UNREGISTERED字符串,重复上述步骤,查完ASCII再查UNICODE,重复上述步骤。
去除上面图片的注册信息
一样在M图标查找字符串,回到C的数据窗口查找。选中开始字节右键->查找参考->双击,就会去到字符串的反汇编窗口。查看上下汇编代码
1 2 3 4 5 6 7 8 9 10 11 12 13 0040C22F > \81FD 07090000 cmp ebp,0x907 0040C235 . 75 1A jnz short Pixtopia.0040C251;让它跳,绕过37地址 0040C237 . 68 74F94800 push Pixtopia.0048F974 ; ASCII "This is an registered version for v5leon9 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" 0040C23C . 8BCE mov ecx,esi 0040C23E . E8 BDFEFFFF call Pixtopia.0040C100 0040C243 . 8B56 1C mov edx,dword ptr ds:[esi+0x1C] 0040C246 . 6A 01 push 0x1 ; /Erase = TRUE 0040C248 . 6A 00 push 0x0 ; |pRect = NULL 0040C24A . 52 push edx ; |hWnd = NULL 0040C24B . FF15 E4564700 call dword ptr ds:[<&USER32.InvalidateRe>; \InvalidateRect 0040C251 > 55 push ebp 0040C252 . 8BCE mov ecx,esi 0040C254 . E8 7F230500 call Pixtopia.0045E5D8
1 jnz short Pixtopia.0040C251 => jz short Pixtopia.0040C251
11.1 查找字符串的几种方法
右键->查找->所有参考文本字串
右键->中文搜索引擎->智能搜索
M图标右键查找字符串
12. OD两种断点 OD从原理上来区分,有两种不同的断点:软件断点和硬件断点。
内存断点属于一种特殊的软件断点。
内存断点每次只能设置一个,如果设置了另一个,上一个会被自动删除。
设置一个内存断点,会改变整块(4KB)内存的属性。
内存断点明显降低OD性能,因为OD经常校对内存。
软件断点:
F2设置的断点就是软件断点。
设置该断点的原理是在断点处重写代码,插入一个int 3中断指令,当CPU执行到int 3指令的时候,OD就可以获得控制权。
硬件断点:
可行性依赖于CPU的物理支持。
调试寄存器Dr0~Dr7,其中Dr0~Dr3寄存器用来存放中断地址,Dr4、Dr5保留不使用,Dr6、Dr7用来记录Dr0~Dr3的属性(如读、写还是执行,单位是字节、字还是双字),所以硬件断点最多只能有4个。
软件断点只能在OD的CPU界面下,在数据段下不了,在一条指令的中间也下不了。
硬件断点可以下在Windows的动态链接库里,因为软件断点下在DLL文件中不会被保存,重启程序后将丢失断点。
13. 实验六 VisualSite Designer
目标:破除只能打开10次的限制,去除软件关闭后弹出的广告。
破除打开10次的限制
小技巧:用杀毒软件的沙盘模式运行不会减少次数,因为在沙盘里面只是虚拟出一个环境,将用户模式与底层之间再加一层分隔开,次数是通过写入文件或写入注册表的形式来计算的。当关闭沙盘时,沙盘的文件清空,记录的数据也不复存在。
Ctrl+F8自动步过遇到问题下断点,F7步入。重复以上。
程序跳出循环的办法:在循环的下一条指令下断点,按F9运行。
仔细看下面这条断点指令,jmp到MFC42里面,mfc42.dll是windows系统的一个动态链接文件,不可或缺。
1 004BD520 |. E8 43000000 call <jmp.&MFC42.#AfxWinMain_1576>
所以接下来我们会去到DLL的段,下断点的方式与之前不同,右键->断点->硬件执行。可在调试->硬件断点处查看。
1 6E344F38 FFD6 call esi ; VisualSi.0058B264
一般地址数值较大的是存放DLL的领空。
综上,在用户领空就下软件断点,在动态链接库领空就下硬件断点。
走到下面这条指令还需要继续往下走吗?我们试试。
1 00489912 . E8 132B0300 call <jmp.&MFC42.#CDialog::DoModal_2514>
又走到了一个DLL领空,下硬件断点,继续步入。
1 6E370AD8 E8 73D6FEFF call mfc42.#CWnd::RunModalLoop_5718
发现我们一直在DLL领空死循环,NAG窗口出来了。但是我们在DLL领空操作不了,所以找最近跳转的用户领空就可以了。回到用户断点处。
1 00489912 . E8 132B0300 call <jmp.&MFC42.#CDialog::DoModal_2514>
F8步过,弹出窗口,在窗口处点一下进入程序,OD跳到下一条指令,窗口消失,call指令完成。即call指令执行的是打开NAG窗口,让次数-1,返回值存到eax里去。下一条指令是eax与1对比,eax是1就进入到程序里去,非1退出程序。可以在call指令后面添加注释返回值要为1。
1 2 00489912 . E8 132B0300 call <jmp.&MFC42.#CDialog::DoModal_2514>;NAG,return 1 00489917 . 83F8 01 cmp eax,0x1
那怎么才能让返回值为1呢?简单粗暴的方法就是直接改call指令。
1 call <jmp.&MFC42.#CDialog::DoModal_2514> => mov eax,1
将修改的保存起来。OD载入新程序,F9运行,会发现一开始NAG窗口已经不见了。
去除软件关闭后弹出的广告
OD载入新程序,F9运行,进入程序,点关闭按钮,会弹出广告,这时回到OD按F12暂停,停在了win32u模块,dll领空。查看K图标,K的作用是:截止到暂停处,通过调用堆栈判断出哪些函数被调用过。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 地址 堆栈 函数过程 / 参数 调用来自 结构 0019EAF0 75A45E55 win32u.NtUserGetMessage user32.75A45E4F 0019EB2C 0019EB30 6E344425 user32.GetMessageA mfc42.6E34441F 0019EB2C 0019EB34 0058B264 pMsg = VisualSi.0058B264 0019EB38 00000000 hWnd = NULL 0019EB3C 00000000 MsgFilterMin = 0x0 0019EB40 00000000 MsgFilterMax = 0x0 0019EB4C 6E35E253 包含mfc42.6E344425 mfc42.6E35E251 0019EB70 0019EB74 6E370ADD mfc42.#CWnd::RunModalLoop_5718 mfc42.6E370AD8 0019EB70 0019EBBC 00480C29 ? <jmp.&MFC42.#CDialog::DoModal_2514 VisualSi.00480C24 0019EBB8 0019EC2C 6E3478D5 包含VisualSi.00480C29 mfc42.6E3478D3 0019EC30 0019EC34 6E33C51B mfc42.6E3478C0 mfc42.6E33C516 0019EC30 0019ECCC 6E343B84 包含mfc42.6E33C51B mfc42.6E343B82 0019ECC8 0019ECF4 6E33BD7B 包含mfc42.6E343B84 mfc42.6E33BD79 0019ECF0 0019ED74 6E33BC2F mfc42.#AfxCallWndProc_1109 mfc42.6E33BC2A 0019ED70 0019EDBC 75A636DB 包含mfc42.6E33BC2F user32.75A636D9 0019EDB8 0019EDE8 75A5A66A user32.75A636B0 user32.75A5A665 0019EDE4 0019EECC 75A5A26A user32.75A5A330 user32.75A5A265 0019EEC8 0019EF30 75A5DE3F 包含user32.75A5A26A user32.75A5DE3D 0019EF2C 0019EF6C 77624E9D 包含user32.75A5DE3F ntdll.77624E9B 0019EF68
发现几乎大部分的函数都在操作系统领空,除了一个是在用户领空的,双击这行的“调用来自”,进入用户领空。
1 0019EBBC 00480C29 ? <jmp.&MFC42.#CDialog::DoModal_2514 VisualSi.00480C24 0019EBB8
在00480C24处下个断点。重新载入程序运行,关闭,没有弹出广告,OD停在了断点处。
1 2 00480C24 . E8 01B80300 call <jmp.&MFC42.#CDialog::DoModal_2514> 00480C29 . 8D4C24 00 lea ecx,dword ptr ss:[esp]
如果按F8,广告弹出,关闭广告,跳到下一条指令。即call <jmp.&MFC42.#CDialog::DoModal_2514>
的作用就是用来弹出广告的。那我们不想要它弹广告,就将这条指令用nop
填充。
1 call <jmp.&MFC42.#CDialog::DoModal_2514> => nop
保存到新程序。OD载入查看,没有倒数,关闭也没有广告了。
13.1 破解的绝对方案 就VisualSite Designer来说,程序的行为发生改变,在于注册前和注册后的行为发生改变。如果运行完10次后,程序就不允许我们打开,代码走的路线与之前的不同。用什么来实现代码走的路线不同呢?一种是条件判断(低级),另一种是jmp+变量(高级)。
13.1.1 条件判断 VisualSite Designer就是用条件判断来选择代码实现。
从程序入口点开始,一路F8,遇到条件跳转指令特别注意,跳转已实现用“Y”注释,跳转未实现用“N”注释。一直注释下去,直到弹出倒数窗口,下断点,重载,运行,删断点,进去注释,弹出倒数窗口,下断点,以此类推,直到弹出最后一个NAG窗口(也就是之前我们走到的用户地址9912处)。
以下情况可以不用注释:
如果跳转指令在循环里面可以不用理会,因为循环里面的跳转只是为了实现循环。
如果跳转指令在动态链接库的领空也可以不用理会,因为我们暂时不能修改动态链接库的东西(如果跳转进到动态链接库可以按Alt+F9返回到用户领空)。
如果遇到无条件跳转指令jmp也不用理会,因为这个程序没有jmp+变量的高级跳转。
注释完后,在OD里将原来的程序次数用到0。再载入OD,F8对着注释查看条件跳转指令是否改变,如果发生改变根据我们之前学过的绕过跳转指令来实现程序原本功能。(将程序次数用到0,我们之前修改的1,2版本也不能用了,因为它们用的都是同一个文件或注册表)
1 2 004898EF . 85C0 test eax,eax;在信息窗口可以看到eax=0 004898F1 . /0F8E A1000000 jle VisualSi.00489998 ; n
1 test eax,eax => mov eax,1
现在是0也可以进入程序了,再结合去除NAG窗口和关闭后弹出的广告就完美了!(这个程序也避免了原来程序用完次数,导致1、2也用不了的情况)
这种方法破解程序屡试不爽,同时需要大量的耐心。
14. 实验七 PC Surgen
(这个程序在Win10一直报错,界面呈现也不对,所以拿我的Windows 2003靶机来调试吧)
目标:去除NAG窗口,去除未注册信息,解除天数限制。
这个程序弹出NAG窗口后等几秒后会自动进入到界面。NAG窗口很大可能是破解的切入点,因为只要找到了NAG的触发点,也就是找到了注册和未注册的跳转、判断位置。
14.1 基本的打补丁方式(暴力破解) OD载入程序,运行到界面处。
看到图2标题,它说未注册,还剩5天时间。那可不可以像实验五那样直接改字符串呢?肯定不可以啊,因为那个倒数是动态的,应该是有内置函数用来算倒数的,这样的话覆盖到函数部分会发生错误,很有可能程序都打不开。
在C图标里右键->查找->所有参考文本字符串,弹窗R图标,右键->查找文本,查找<unregistered
,将“区分大小写”与“整个范围”勾上。如果光标处不是想要找的字符串,可以右键->查找下一个(Ctrl+L)。找到你所需要的字符串,双击去到汇编代码处。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 005CC836 . /0F85 F9000000 jnz pcsurgeo.005CC935 005CC83C . |68 70CB5C00 push pcsurgeo.005CCB70 ; ASCII 50,"C Surgeon <unregistered - " 005CC841 . |D905 3CCA5C00 fld dword ptr ds:[0x5CCA3C] 005CC847 . |A1 8CEC6000 mov eax,dword ptr ds:[0x60EC8C] 005CC84C . |DC20 fsub qword ptr ds:[eax] 005CC84E . |83C4 F4 add esp,-0xC 005CC851 . |DB3C24 fstp tbyte ptr ss:[esp] 005CC854 . |9B wait 005CC855 . |8D55 E4 lea edx,dword ptr ss:[ebp-0x1C] 005CC858 . |B8 94CB5C00 mov eax,pcsurgeo.005CCB94 ; UNICODE "0" 005CC85D . |E8 42FAE3FF call pcsurgeo.0040C2A4 005CC862 . |FF75 E4 push dword ptr ss:[ebp-0x1C] 005CC865 . |68 A0CB5C00 push pcsurgeo.005CCBA0 ; ASCII 20,"days remaining..>" 005CC86A . |8D45 E8 lea eax,dword ptr ss:[ebp-0x18] 005CC86D . |BA 03000000 mov edx,0x3 005CC872 . |E8 A98AE3FF call pcsurgeo.00405320 005CC877 . |8B55 E8 mov edx,dword ptr ss:[ebp-0x18] ; gdi32.77C11307 005CC87A . |8B45 FC mov eax,dword ptr ss:[ebp-0x4] 005CC87D . |E8 22D5EAFF call pcsurgeo.00479DA4 005CC882 . |8B45 FC mov eax,dword ptr ss:[ebp-0x4] 005CC885 . |8B80 20120000 mov eax,dword ptr ds:[eax+0x1220] 005CC88B . |C680 20030000>mov byte ptr ds:[eax+0x320],0x1 005CC892 . |8B10 mov edx,dword ptr ds:[eax] 005CC894 . |FF92 EC000000 call dword ptr ds:[edx+0xEC] 005CC89A . |8D45 F4 lea eax,dword ptr ss:[ebp-0xC] 005CC89D . |BA BCCB5C00 mov edx,pcsurgeo.005CCBBC 005CC8A2 . |E8 7587E3FF call pcsurgeo.0040501C 005CC8A7 . |68 02000080 push 0x80000002 005CC8AC . |6A 00 push 0x0 005CC8AE . |6A 00 push 0x0 005CC8B0 . |B9 D4CB5C00 mov ecx,pcsurgeo.005CCBD4 ; ASCII 53,"oftware\Dean Software\PC Surgeon" 005CC8B5 . |B2 01 mov dl,0x1 005CC8B7 . |A1 1CE94900 mov eax,dword ptr ds:[0x49E91C] 005CC8BC . |E8 BB2BEDFF call pcsurgeo.0049F47C 005CC8C1 . |8945 EC mov dword ptr ss:[ebp-0x14],eax 005CC8C4 . |33C0 xor eax,eax 005CC8C6 . |55 push ebp 005CC8C7 . |68 2EC95C00 push pcsurgeo.005CC92E 005CC8CC . |64:FF30 push dword ptr fs:[eax] 005CC8CF . |64:8920 mov dword ptr fs:[eax],esp 005CC8D2 . |6A 00 push 0x0 005CC8D4 . |8D45 E0 lea eax,dword ptr ss:[ebp-0x20] 005CC8D7 . |50 push eax 005CC8D8 . |B9 00CC5C00 mov ecx,pcsurgeo.005CCC00 005CC8DD . |BA 1CCC5C00 mov edx,pcsurgeo.005CCC1C 005CC8E2 . |8B45 EC mov eax,dword ptr ss:[ebp-0x14] ; gdi32.77BF820F 005CC8E5 . |E8 122DEDFF call pcsurgeo.0049F5FC 005CC8EA . |8B45 E0 mov eax,dword ptr ss:[ebp-0x20] ; usp10.74B0F772 005CC8ED . |8B55 F4 mov edx,dword ptr ss:[ebp-0xC] 005CC8F0 . |E8 B78AE3FF call pcsurgeo.004053AC 005CC8F5 . |74 21 je short pcsurgeo.005CC918 005CC8F7 . |8B55 FC mov edx,dword ptr ss:[ebp-0x4] 005CC8FA . |8B45 FC mov eax,dword ptr ss:[ebp-0x4] 005CC8FD . |E8 A2200300 call pcsurgeo.005FE9A4 005CC902 . |8B45 F4 mov eax,dword ptr ss:[ebp-0xC] 005CC905 . |50 push eax 005CC906 . |BA 1CCC5C00 mov edx,pcsurgeo.005CCC1C 005CC90B . |B9 00CC5C00 mov ecx,pcsurgeo.005CCC00 005CC910 . |8B45 EC mov eax,dword ptr ss:[ebp-0x14] ; gdi32.77BF820F 005CC913 . |E8 6430EDFF call pcsurgeo.0049F97C 005CC918 > |33C0 xor eax,eax 005CC91A . |5A pop edx ; usp10.74B0F772 005CC91B . |59 pop ecx ; usp10.74B0F772 005CC91C . |59 pop ecx ; usp10.74B0F772 005CC91D . |64:8910 mov dword ptr fs:[eax],edx 005CC920 . |68 42C95C00 push pcsurgeo.005CC942 005CC925 > |8B45 EC mov eax,dword ptr ss:[ebp-0x14] ; gdi32.77BF820F 005CC928 . |E8 4377E3FF call pcsurgeo.00404070 005CC92D . |C3 retn 005CC92E .^|E9 D17EE3FF jmp pcsurgeo.00404804 005CC933 .^|EB F0 jmp short pcsurgeo.005CC925 005CC935 > \BA 30CA5C00 mov edx,pcsurgeo.005CCA30 ; ASCII "PC Surgeon"
看到上面指令的第2条就是要找的字符串,下面还有“days remaining..”的字样,不难猜测两条注释中间的指令就是计算倒数的指令。往上看可以看到条件跳转指令。经过这么多次实验,条件跳转指令的重要性不必再说了吧。如果跳转实现,就会跳到CC935地址,绕过计算倒数指令。
先尝试一下,在跳转指令下个断点,重载程序,运行,将Z标志位改为0,跳转,F9运行到程序,发现图1的NAG窗口绕过了,直接进到界面,界面的标题也没有了“<unregistered … days remaining..”字样。说明跳过的指令还包含了NAG窗口。现在来打补丁,打完保存数据到文件。(也可以跟下面步骤一起打完再保存,但我太傻了还是修改一个保存一个吧)
1 jnz pcsurgeo.005CC935 => jz pcsurgeo.005CC935
载入新程序,没问题。但是图3还是显示未注册,说明破解还不完全。同样的方法查找字符串,不能用实验五查找字符串的方法的原因是就算改了字符串,但Use Reg Key
还在,也还是未完全破解。
1 2 3 4 5 6 7 8 9 00562466 . /75 22 jnz short pcsurgeo.0056248A 00562468 . |8B83 08030000 mov eax,dword ptr ds:[ebx+0x308] 0056246E . |B2 01 mov dl,0x1 00562470 . |E8 1F78F1FF call pcsurgeo.00479C94 00562475 . |8B83 24030000 mov eax,dword ptr ds:[ebx+0x324] 0056247B . |BA B8265600 mov edx,pcsurgeo.005626B8 ; ASCII "<Unregistered Version>" 00562480 . |E8 1F79F1FF call pcsurgeo.00479DA4 00562485 . |E9 E0010000 jmp pcsurgeo.0056266A 0056248A > \8B83 08030000 mov eax,dword ptr ds:[ebx+0x308]
字符串的上面也有一个跳转指令,如果跳转实现跳到248A地址,绕过字符串。同样的方法尝试一下。跳转指令下断点,重载运行,界面help->about,没有弹出窗口,OD停到断点处,将Z标志位改为0,运行程序,弹出已注册窗口。
说明我们改对了,现在去打补丁。
1 jnz short pcsurgeo.0056248A => jz short pcsurgeo.0056248A
保存数据到文件,破解实现。
14.2 根据代码逻辑破解 这个程序比较简单,所以修改几条跳转指令足以实现破解。但很多程序都是比较大型的,未注册到注册可能经过层层验证,可能需要修改很多条跳转指令。如果不想修改跳转指令,那就需要了解代码的内部逻辑。
同样,通过查找字符串去到<unregistered
汇编代码处。往上找最近的call指令,下断点,重载运行,分析代码逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 005CC81B . E8 9C5FEDFF call pcsurgeo.004A27BC 005CC820 . A1 8CEB6000 mov eax,dword ptr ds:[0x60EB8C] ;函数返回值存到eax里,在信息窗口可以看到eax=ds:[0x60EB8C]=610c4a 005CC825 . 8038 00 cmp byte ptr ds:[eax],0x0 ;函数返回值与0对比(相减),ds:[eax]=00 005CC828 . 0F85 07010000 jnz pcsurgeo.005CC935;相减为0,不跳过 005CC82E . A1 DCF16000 mov eax,dword ptr ds:[0x60F1DC] ;eax=ds:[0x60F1DC]=610c5e 005CC833 . 8038 00 cmp byte ptr ds:[eax],0x0;ds:[eax]=00 005CC836 0F85 F9000000 jnz pcsurgeo.005CC935;相减为0,不跳过 ;为什么有两个判断?判断是否注册,未注册的话判断还剩几天 005CC83C . 68 70CB5C00 push pcsurgeo.005CCB70 ; ASCII 50,"C Surgeon <unregistered - " 005CC841 . D905 3CCA5C00 fld dword ptr ds:[0x5CCA3C] ; fld浮点指令,相当于push,ds:[0x5CCA3C]=15.00000 005CC847 . A1 8CEC6000 mov eax,dword ptr ds:[0x60EC8C] ;eax=ds:[0x60EC8C]=610c54 005CC84C . DC20 fsub qword ptr ds:[eax] ;fsub相当于sub,st=15.00000,ds:[eax]=10.00000 ;未注册的用户可以使用15天,ds:[eax]=ds:[0x610c54]=10存的是经过的天数 005CC84E . 83C4 F4 add esp,-0xC 005CC851 . DB3C24 fstp tbyte ptr ss:[esp];st=5,剩余的天数 005CC854 . 9B wait 005CC855 . 8D55 E4 lea edx,dword ptr ss:[ebp-0x1C] 005CC858 . B8 94CB5C00 mov eax,pcsurgeo.005CCB94 ; UNICODE "0" 005CC85D . E8 42FAE3FF call pcsurgeo.0040C2A4 005CC862 . FF75 E4 push dword ptr ss:[ebp-0x1C];将剩余的天数入栈 005CC865 . 68 A0CB5C00 push pcsurgeo.005CCBA0 ; ASCII 20,"days remaining..>"
分析得很清晰了,我们要绕过的话就饶过call函数后面的第一次跳转,等等!这不是跟暴力破解一样吗?!当然我们不是这么做。(虚晃一枪)
题外话:选中汇编代码如mov eax,dword ptr ds:[0x60EB8C]
,信息窗口会显示ds:[0x60EB8C]=00610c4a
,在信息窗口中选中,右键->数据窗口中跟随数值,就可在数据窗口中看到610c4a的值。
已知call指令结束,60eb8c地址存的东西要赋值给eax,eax通常存的是返回值或返回值的指针,所以不管怎样,死抓60eb8c就对了!选中汇编代码右键->查找参考->地址常量,弹出R图标,里面是整个程序所有用到60eb8c地址的汇编代码。我们现在要找初始化60eb8c地址的地址,因为那个地址是判断注册与未注册的分叉点。右键->在每个命令上设置断点。重载,运行,停在了第一个断点处,也就是程序初始化60eb8c地址处。
1 2 3 4 5 6 7 8 9 10 11 12 13 005C2BF6 . 8B15 8CEB6000 mov edx,dword ptr ds:[0x60EB8C] ; pcsurgeo.00610C4A ;ecx=ds:[0x60EB8C]=610c4a 005C2BFC . 8802 mov byte ptr ds:[edx],al ;将al赋值给610c4a,al=00,ds:[610c4a]=00 005C2BFE . A1 8CEB6000 mov eax,dword ptr ds:[0x60EB8C] ;ds:[0060eb8c]=00610c4a 005C2C03 . 8038 00 cmp byte ptr ds:[eax],0x0;将ds:[eax]与0比较 005C2C06 . 75 0D jnz short pcsurgeo.005C2C15;相减为0不跳转 005C2C08 . E8 6307EEFF call pcsurgeo.004A3370 005C2C0D . A1 8CEC6000 mov eax,dword ptr ds:[0x60EC8C] 005C2C12 . DD18 fstp qword ptr ds:[eax] 005C2C14 . 9B wait 005C2C15 > 8D45 9C lea eax,dword ptr ss:[ebp-0x64]
其中两种改法:
1 mov byte ptr ds:[edx],al => mov byte ptr ds:[edx],1
上面这种改法需要重定位,可能不太安全,那可以用下面这种改法。
1 2 cmp byte ptr ds:[eax],0x0 => mov byte ptr ds:[eax],1 jnz short pcsurgeo.005C2C15 => jmp short pcsurgeo.005C2C15
保存后重载运行,整个程序就处于注册阶段了,不需要一次次修改跳转指令。
15. 实验八 MrBills
目标:破解注册。
想要定位到You have entered
字符串,右键->中文搜索引擎->智能搜索,找到双击去到反汇编窗口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 004299B9 . /75 36 jnz short MrBills.004299F1 004299BB . |6A 30 push 0x30 004299BD . |68 70134C00 push MrBills.004C1370 ; You have entered an invalid email address or license number. Please try again. 004299C2 . |E8 74270800 call MrBills.004AC13B 004299C7 . |8D8E 20010000 lea ecx,dword ptr ds:[esi+0x120] 004299CD . |E8 567CFDFF call MrBills.00401628 004299D2 . |8BCF mov ecx,edi ; MrBills.<ModuleEntryPoint> 004299D4 . |E8 4F7CFDFF call MrBills.00401628 004299D9 . |53 push ebx 004299DA . |8BCE mov ecx,esi ; MrBills.<ModuleEntryPoint> 004299DC . |E8 D5A60700 call MrBills.004A40B6 004299E1 . |8D8E 7C010000 lea ecx,dword ptr ds:[esi+0x17C] 004299E7 . |E8 83D00700 call MrBills.004A6A6F 004299EC . |E9 29010000 jmp MrBills.00429B1A 004299F1 > \6A 40 push 0x40 004299F3 . 68 50134C00 push MrBills.004C1350 ; Thank you for registering!
找到上面最近的跳转指令,跳转指令未实现,若实现则跳过未注册弹窗,去到Thank you for registering!
,不知道自己的推测对不对的话就先改标志位,因为标志位在重载程序不会被保存,错了重载程序即可。
貌似没有问题,现在就来打补丁吧。
1 jnz short MrBills.004299F1 => jmp short MrBills.004299F1
运行程序,注册后界面标题的未注册提示也已经没了。但每次启动都要注册显然很麻烦,而且真正的注册成功后Register...
是变灰色的,而且会弹窗提示,所以这个并没有注册成功。因为如果这样打补丁的话,相当于注册与其他功能分隔开了。
我们找是找对了,但要进一步追踪实现跳转的条件。jnz的意思是如果Z标志位非0则跳转,为0则不跳转。哪条指令改变了Z标志位?往上找,看到test指令,test指令是进行按位与运算,值为0在Z标志位置1,非0置0。
1 2 3 4 004299B5 . 84C0 test al,al ;非0注册 004299B7 . 59 pop ecx ; kernel32.7716FA29 004299B8 . 53 push ebx 004299B9 75 36 jnz short MrBills.004299F1 ; 要实现跳转
是什么改变了al的值?再往上找。call指令的返回值存放在eax中,所以call指令会影响al的值。要进入这个call函数了解内部逻辑。注释标志1为第一个进入的函数,因为可能接下来要进入很多个函数,很乱,像走迷宫一样,所以标志数字说明你走过,走的是哪个函数。
1 2 3 4 5 6 7 004299AD . E8 9AD7FDFF call MrBills.0040714C ;需进入1 004299B2 . 59 pop ecx ; kernel32.7716FA29 004299B3 . 33DB xor ebx,ebx 004299B5 . 84C0 test al,al ;非0注册 004299B7 . 59 pop ecx ; kernel32.7716FA29 004299B8 . 53 push ebx 004299B9 75 36 jnz short MrBills.004299F1 ; 要实现跳转
在call指令下断点,重载运行。输入邮箱和证书运行到断点位置。进入函数1,里面还有很多个函数,先看第1个把它注释为“需进入2”,下面函数还需不需要进入要看第1个函数下面的跳转指令会不会跳过下面的函数。先不要进入第1个函数,F8往下走走。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 0040714C $ 55 push ebp 0040714D . 8BEC mov ebp,esp 0040714F . FF75 0C push dword ptr ss:[ebp+0xC] 00407152 . FF75 08 push dword ptr ss:[ebp+0x8] ; MrBills.004C1014 00407155 . E8 77FEFFFF call MrBills.00406FD1 ;需进入2 0040715A . 84C0 test al,al ;al=00,Z=1 0040715C . 59 pop ecx ; MrBills.004299B2 0040715D . 59 pop ecx ; MrBills.004299B2 0040715E . A2 A0765000 mov byte ptr ds:[0x5076A0],al 00407163 . 75 1B jnz short MrBills.00407180 ;跳转未实现 00407165 . FF75 0C push dword ptr ss:[ebp+0xC] 00407168 . FF75 08 push dword ptr ss:[ebp+0x8] ; MrBills.004C1014 0040716B . E8 ADFEFFFF call MrBills.0040701D ;需进入3 00407170 . 84C0 test al,al ;al=00,Z=1 00407172 . 59 pop ecx ; MrBills.004299B2 00407173 . 59 pop ecx ; MrBills.004299B2 00407174 . A2 A0765000 mov byte ptr ds:[0x5076A0],al 00407179 . A2 A2765000 mov byte ptr ds:[0x5076A2],al 0040717E . 74 0D je short MrBills.0040718D ;跳转实现 00407180 > FF75 0C push dword ptr ss:[ebp+0xC] 00407183 . FF75 08 push dword ptr ss:[ebp+0x8] 00407186 . E8 45F8FFFF call MrBills.004069D0 ;被跳过,无需理会 0040718B . 59 pop ecx ; 00199DEC 0040718C . 59 pop ecx ; 00199DEC 0040718D > 5D pop ebp ; 00199DEC 0040718E .^ E9 D6FEFFFF jmp MrBills.00407069 ;跳到上面执行,eax存的是地址,不管,最后返回到函数1
函数1的整体逻辑弄懂了,现在继续看函数2。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 00406FD1 /$ B8 AB374B00 mov eax,MrBills.004B37AB 00406FD6 |. E8 EDF00700 call MrBills.004860C8;经过这条指令,eax存的是地址,不是返回值 00406FDB |. 51 push ecx 00406FDC |. 53 push ebx 00406FDD |. FF35 A4415000 push dword ptr ds:[0x5041A4] ; ORUWOZ3FOI 00406FE3 |. 8D4D F0 lea ecx,[local.4] 00406FE6 |. E8 84B1FFFF call MrBills.0040216F;同上不是 00406FEB |. FF75 0C push [arg.2] 00406FEE |. 8365 FC 00 and [local.1],0x0 00406FF2 |. FF75 08 push [arg.1] 00406FF5 |. 8D45 F0 lea eax,[local.4] 00406FF8 |. 50 push eax 00406FF9 |. E8 4DFFFFFF call MrBills.00406F4B;经过这条指令,al=00,需进入4 00406FFE |. 8B4D F0 mov ecx,[local.4] 00407001 |. 83C4 0C add esp,0xC 00407004 |. 83C1 F0 add ecx,-0x10 00407007 |. 8AD8 mov bl,al;用bl存放al的值 00407009 |. E8 3AA1FFFF call MrBills.00401148 0040700E |. 8B4D F4 mov ecx,[local.3] 00407011 |. 8AC3 mov al,bl;用al存放bl的值 ;al给bl,bl又给回al,中间有call指令,涉及两种情况:①保护现场,②传参。在这里猜测bl暂存al的值,中间的call指令要用到al,等call指令用完al,bl把值还给al。起到保护现场作用。也可以推测在call指令前al存的就很有可能是注册或未注册的返回值,所以al=00那个call指令是一定要进入的(需进入4) 00407013 |. 5B pop ebx ; 00199D98 00407014 |. 64:890D 00000>mov dword ptr fs:[0],ecx 0040701B |. C9 leave 0040701C \. C3 retn;退出函数2
如果不确定要不要进入某个函数,可以按回车查看函数内部代码,但并不执行。想进去就按F7,不想进去就按F8。
进入函数4:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 00406F4B /$ B8 E9374B00 mov eax,MrBills.004B37E9 00406F50 |. E8 73F10700 call MrBills.004860C8 00406F55 |. 51 push ecx 00406F56 |. 8B45 08 mov eax,[arg.1] 00406F59 |. 53 push ebx 00406F5A |. 56 push esi ; MrBills.00507738 00406F5B |. FF30 push dword ptr ds:[eax] 00406F5D |. 8D45 08 lea eax,[arg.1] 00406F60 |. 50 push eax 00406F61 |. E8 38FBFFFF call MrBills.00406A9E 00406F66 |. 8B45 0C mov eax,[arg.2] 00406F69 |. FF30 push dword ptr ds:[eax] 00406F6B |. 8365 FC 00 and [local.1],0x0 00406F6F |. 8D45 F0 lea eax,[local.4] 00406F72 |. 50 push eax 00406F73 |. E8 26FBFFFF call MrBills.00406A9E 00406F78 |. FF75 10 push [arg.3] 00406F7B |. C645 FC 01 mov byte ptr ss:[ebp-0x4],0x1 00406F7F |. 50 push eax 00406F80 |. 8D45 08 lea eax,[arg.1] 00406F83 |. 50 push eax 00406F84 |. 8D45 0C lea eax,[arg.2] 00406F87 |. 50 push eax 00406F88 |. E8 89FDFFFF call MrBills.00406D16 00406F8D |. FF30 push dword ptr ds:[eax] 00406F8F |. 8B75 08 mov esi,[arg.1] 00406F92 |. 56 push esi ; MrBills.00507738 00406F93 |. E8 4FF10700 call MrBills.004860E7 00406F98 |. 8B4D 0C mov ecx,[arg.2];al=01,bl=00 00406F9B |. 83C4 28 add esp,0x28 00406F9E |. 8BD8 mov ebx,eax 00406FA0 |. F7DB neg ebx 00406FA2 |. 1ADB sbb bl,bl 00406FA4 |. 83C1 F0 add ecx,-0x10 00406FA7 |. FEC3 inc bl 00406FA9 |. E8 9AA1FFFF call MrBills.00401148 00406FAE |. 8B4D F0 mov ecx,[local.4] 00406FB1 |. 83C1 F0 add ecx,-0x10 00406FB4 |. E8 8FA1FFFF call MrBills.00401148 00406FB9 |. 8D4E F0 lea ecx,dword ptr ds:[esi-0x10] 00406FBC |. E8 87A1FFFF call MrBills.00401148 00406FC1 |. 8B4D F4 mov ecx,[local.3] 00406FC4 |. 5E pop esi ; MrBills.00406FFE 00406FC5 |. 8AC3 mov al,bl 00406FC7 |. 5B pop ebx ;al=00,bl=00 ; MrBills.00406FFE 00406FC8 |. 64:890D 00000>mov dword ptr fs:[0],ecx 00406FCF |. C9 leave 00406FD0 \. C3 retn
所以在这个决定al的值的函数4,是通过改变bl间接赋给al返回值。
函数4和函数2执行完,接下来执行的是函数3。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 0040701D /$ B8 AB374B00 mov eax,MrBills.004B37AB 00407022 |. E8 A1F00700 call MrBills.004860C8 00407027 |. 51 push ecx 00407028 |. 53 push ebx 00407029 |. FF35 A0415000 push dword ptr ds:[0x5041A0] ; OBZG65DJM4 0040702F |. 8D4D F0 lea ecx,[local.4] 00407032 |. E8 38B1FFFF call MrBills.0040216F 00407037 |. FF75 0C push [arg.2] 0040703A |. 8365 FC 00 and [local.1],0x0 0040703E |. FF75 08 push [arg.1] 00407041 |. 8D45 F0 lea eax,[local.4] 00407044 |. 50 push eax 00407045 |. E8 01FFFFFF call MrBills.00406F4B;这个也是函数4 ;有没有觉得似曾相识?接下来的代码跟函数2的代码一模一样,表明它们用的算法思路是一样的,或者加强检验 0040704A |. 8B4D F0 mov ecx,[local.4] 0040704D |. 83C4 0C add esp,0xC 00407050 |. 83C1 F0 add ecx,-0x10 00407053 |. 8AD8 mov bl,al 00407055 |. E8 EEA0FFFF call MrBills.00401148 0040705A |. 8B4D F4 mov ecx,[local.3] ; MrBills.00407170 0040705D |. 8AC3 mov al,bl 0040705F |. 5B pop ebx ; MrBills.00407170 00407060 |. 64:890D 00000>mov dword ptr fs:[0],ecx 00407067 |. C9 leave 00407068 \. C3 retn
弄明白逻辑,现在来修改函数。先修改函数2,不行再修改函数3(如果是加强检验)。
进入函数1->进入函数2->进入函数4,找到关键点,也就是bl赋值给al的指令
1 2 3 00406FC4 |. 5E pop esi ; MrBills.00406FFE 00406FC5 |. 8AC3 mov al,bl 00406FC7 |. 5B pop ebx ;al=00,bl=00 ; MrBills.00406FFE
运行,这样才算是成功了。
16. 针对性逆向 掌握不同编译器编译出来的程序的特点,然后才能有针对性地进行逆向。
查壳工具:exeinfope.exe
16.1 Visual BASIC(实验九) VB是由早期DOS时代的BASIC语言发展而来的可视化编程语言。VB是由事件驱动的编程语言。VB语言用的是Unicode编码。
所有的VB程序几乎都是依赖于一个外部的动态链接库MSVBVM60.dll
。正是因为VB中所有的API函数都在dll中去实现,所以导致在跟踪程序的执行代码时频繁地在dll领空和程序领空横跳。正因为如此,VB程序很难在函数内修改返回值,而且没有MessageBox
函数,它的弹窗是tcMsgBox
函数。
16.1.1 VB破解的关键
针对变量
针对字符串
__vbaVarTstEq
__vbaStrCmp
__vbaVarTstNe
__vbaStrComp
__vbaVarCompEq
__vbaStrCompVar
__vbaVarCompLe
__vbaStrLike
__vbaVarCompLt
__vbaStrTextComp
__vbaVarCompGe
__vbaStrTextLike
__vbaVarCompGt
__vbaVarCompNe
加粗都是比较常用的函数
在一个VB写成的程序中需要注册码,如何破解?VB程序在装载时会结合硬件信息生成一个独一无二的Key(注册码)。在OD中载入程序,Ctrl+N打开输入输出表(N图标),直接输入需要查找的函数,在标题处会显示输入的字母,选中函数右键->在每个参考上设置断点。重载运行,停到断点处后F8单步步过,如果整个程序走完了还没看到Key,那就换一个函数继续找。它的Key就在注释窗口处,很容易被找到。
就算把程序卸载后重装,它还是已经注册好了。
16.2 Delphi(实验十) 非常多的call指令。
1 2 3 push 004A52F4 …… retn ;相当于jmp 004A52F4
-
返回上一条指令查看,但程序已经执行到下面一条指令了。
17. 实验九 Flash Producer
目标:破解程序。
既然认识了查壳工具,那就用用它吧。可以知道它是32位exe文件,用C++编译,无壳。
载入OD,在注册时弹窗“Invalid Password”,试试搜索字符串。右键->中文搜索引擎->智能搜索,查找字符串,定位到反汇编窗口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 00404851 . 68 34034300 push fjproduc.00430334 ; v5le0n9 ;push用户输入的password,接下来的函数就是判断password对错 00404856 . E8 E5FDFFFF call fjproduc.00404640 0040485B . 83C4 04 add esp,0x4;al=00 0040485E . 84C0 test al,al;按位与为0时Z置1,非0置0 00404860 . 74 35 je short fjproduc.00404897;相等跳转,与jz用法相同。Z为1跳转 00404862 . 8B0D ACE34200 mov ecx,dword ptr ds:[0x42E3AC] ; Password 00404868 . 68 60034300 push fjproduc.00430360 ; /FileName = "" 0040486D . 68 34034300 push fjproduc.00430334 ; |String = "" 00404872 . 51 push ecx ; |Key = "j`h犬B" 00404873 . 68 149D4200 push fjproduc.00429D14 ; |Registration 00404878 . FF15 28904200 call dword ptr ds:[<&KERNEL32.WritePriva>; \WritePrivateProfileStringA 0040487E . 6A 01 push 0x1 00404880 . E8 5BFEFFFF call fjproduc.004046E0 00404885 . 83C4 04 add esp,0x4 00404888 > 6A 01 push 0x1 ; /Result = 0x1 0040488A . 56 push esi ; |hWnd = 004203BB 0040488B . FF15 2C924200 call dword ptr ds:[<&USER32.EndDialog>] ; \EndDialog 00404891 . 33C0 xor eax,eax 00404893 . 5E pop esi ; kernel32.74F60419 00404894 . C2 1000 retn 0x10 00404897 > 6A 30 push 0x30 ; /Style = MB_OK|MB_ICONEXCLAMATION|MB_APPLMODAL 00404899 . 68 A49B4200 push fjproduc.00429BA4 ; |Error 0040489E . 68 409D4200 push fjproduc.00429D40 ; |Invalid Password 004048A3 . 56 push esi ; |hOwner = 004203BB 004048A4 . FF15 48924200 call dword ptr ds:[<&USER32.MessageBoxA>>; \MessageBoxA
可以看到4897地址有一个被指向符号,是从4860地址的跳转指令跳过来的。跳转指令上方有个调用指令,有个改变Z标志位的test指令。在call指令处下断点,进入到被调用函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 00404640 /$ 83EC 2C sub esp,0x2C 00404643 |. 56 push esi ; fjproduc.00430334 00404644 |. 8B7424 34 mov esi,dword ptr ss:[esp+0x34] ; fjproduc.00430334 00404648 |. 8BC6 mov eax,esi ; fjproduc.00430334 eax=v5le0n9 0040464A |. 8D50 01 lea edx,dword ptr ds:[eax+0x1];edx=5le0n9 ;lea指令用于把源操作数的地址偏移量传送目的操作数 0040464D |. 8D49 00 lea ecx,dword ptr ds:[ecx] 00404650 |> 8A08 /mov cl,byte ptr ds:[eax];将v赋值到cl 00404652 |. 40 |inc eax ; fjproduc.00430334 eax指向5 00404653 |. 84C9 |test cl,cl;按位与cl 00404655 |.^ 75 F9 \jnz short fjproduc.00404650;eax不为空则跳转 00404657 |. 2BC2 sub eax,edx 00404659 |. 83F8 04 cmp eax,0x4;eax长度与4对比 0040465C |. 73 07 jnb short fjproduc.00404665 ;jump if not below 如果eax长度不小于4则跳转(password要4个字符以上) 0040465E |> 32C0 xor al,al;al=00 ;从下面的jnz跳到这里,往下执行退出函数,一种改法:xor al,al => mov al,1 ;类似于if(xxx) return 1;else return 1; 00404660 |. 5E pop esi ; fjproduc.00430334 00404661 |. 83C4 2C add esp,0x2C 00404664 |. C3 retn 00404665 |> 6A 03 push 0x3 00404667 |. 68 C49C4200 push fjproduc.00429CC4 ; fjr 0040466C |. 56 push esi ; fjproduc.00430334 esi指向v5le0n9 0040466D |. E8 6EAA0100 call fjproduc.0041F0E0 ;猜测上面操作只是为了要求password在4个字符以上,这个函数才是判断password是否有效的一部分 ;上面3个push是为调用函数传参 ;结合下面468B地址中esi=e0n9,可知这个函数用了前三个v5l与fjr比较,也就是说password必须以fjr为前缀 00404672 |. 83C4 0C add esp,0xC;al=01 00404675 |. 85C0 test eax,eax ; fjproduc.00430334 00404677 |.^ 75 E5 jnz short fjproduc.0040465E ;jump if not zero 按位与不为0则跳转退出函数;先修改这里让它往下走 00404679 |. 68 B8E04200 push fjproduc.0042E0B8 ; 0123456789abcdefghijklmnopqrstuvwxyz ;这个奇奇怪怪又有规律的字符串一定有猫腻 0040467E |. 8D4424 08 lea eax,dword ptr ss:[esp+0x8] 00404682 |. 50 push eax ; fjproduc.00430334 00404683 |. 8D4C24 10 lea ecx,dword ptr ss:[esp+0x10] 00404687 |. 51 push ecx ; user32.76DE97B6 00404688 |. 83C6 03 add esi,0x3 0040468B |. 56 push esi ; fjproduc.00430334 esi=e0n9 0040468C |. E8 7FE40000 call fjproduc.00412B10 ;传了4个参数给2B10函数,其中包括用户输入的password,所以这个函数也是判断password的一部分 00404691 |. 8B4424 14 mov eax,dword ptr ss:[esp+0x14];eax=03 00404695 |. 83C4 10 add esp,0x10 00404698 |. 83F8 08 cmp eax,0x8 0040469B |.^ 7C C1 jl short fjproduc.0040465E;eax小于8跳转,退出函数 ;ZF=0,且SF⊕OF=1,这里不让它跳,所以修改SF或OF 0040469D |. 68 0C034300 push fjproduc.0043030C ; ASCII "-1568272844" 004046A2 |. E8 F9E20000 call fjproduc.004129A0 004046A7 |. 83C4 04 add esp,0x4;eax=0b esp存的是v5le0n9(长度为7) 004046AA |. 33F6 xor esi,esi ; fjproduc.00430334 004046AC |. 8D6424 00 lea esp,dword ptr ss:[esp] ;edx=1568272844 004046B0 |> 33D2 /xor edx,edx;edx=0 004046B2 |. 8A5434 08 |mov dl,byte ptr ss:[esp+esi+0x8] 004046B6 |. 52 |push edx;edx=c5 004046B7 |. E8 84E30000 |call fjproduc.00412A40 004046BC |. 83C4 04 |add esp,0x4 004046BF |. 884434 08 |mov byte ptr ss:[esp+esi+0x8],al 004046C3 |. 46 |inc esi ; fjproduc.00430334 004046C4 |. 83FE 04 |cmp esi,0x4 004046C7 |.^ 7C E7 \jl short fjproduc.004046B0;循环4次 004046C9 |. 807C24 0B 5F cmp byte ptr ss:[esp+0xB],0x5F 004046CE |. 0f94c0 sete al;al=ZF=0 004046D1 |. 5E pop esi ; fjproduc.00430334 004046D2 |. 83C4 2C add esp,0x2C 004046D5 \. C3 retn
另一种改法:
1 2 3 jnz short fjproduc.0040465E => jz short fjproduc.0040465E jl short fjproduc.0040465E => jnl short fjproduc.0040465E sete al => mov al,1
应该还有第三种改法,按照它的算法前缀为frj开头之类的…好难不会
18. 实验十 TechScheduler
目标:破解注册。
是32位下的Delphi可执行文件,意味着有很多的函数调用。
右键->中文搜索引擎->智能搜索,搜索first name
可搜到“Enter a First Name value now…”,往下看看还有“Registration Key accepted!”字样,双击定位到接受注册密钥的汇编代码处。
往上找找到当前的函数入口处,期间可以看到注释窗口很多“first name”, “last name”, “error”之类的,尝试在函数的某个地方下断点,看是否会停到断点处。停到断点处后F8往下执行,直到运行到错误弹框出现,用nop填充。(为什么要用nop填充而不是跟进去?可能是最后是去到了dll领空,我们改不了)保存文件。
重载运行,跟上面实验一样关注修改跳转语句,修改运行到“Registration Key accepted!”即可。
这里我讲得有点空,看多几遍视频就可理解了。小甲鱼OD使用教程12
19. 实验十一 XoftSpy
目标:实现注册。
通常从程序的对话框里面获取字符串,一般都是调用API函数GetWindowText
。
方法一:OD载入程序,右键->查找->所有模块间的调用,直接键盘输入查找GetWindowText
。选中右键->在每个调用到GetWindowTextA上设置断点。可以Alt+B查看在哪里设置了断点,有几个断点。
方法二:Ctrl+N,直接键盘输入查找GetWindowText
。选中右键->在每个参考上设置断点。
运行程序,发现程序还没完全打开时就停在了断点处,肯定不是我们想去到的地方。我们需要的是在输入Key点OK后的那个断点。
F8走走,再F9运行重复几次,发现回到那个断点处3次,应该是每次获取一个框的内容。之后一路F8,直到在信息窗口或在寄存器窗口或在堆栈窗口看到刚才我们输入的东西时要格外留心,再看到条件跳转指令要注意跳到哪里。注释窗口出现关键字符串“Congratulations!”,在004174E3地址有个箭头,可以在信息窗口看到跳转来自00417465和0041749C。选中信息窗口的“跳转来自”右键,分别去到两个地址处。发现它们调用的是同一个函数,也就是算法实现是一样的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 0041745E . E8 2D020000 call XoftSpy.00417690 ; 很有可能是关键函数 00417463 . 84C0 test al,al 00417465 . 75 7C jnz short XoftSpy.004174E3 ; 实现跳转 00417467 . 51 push ecx 00417468 . 8D5424 14 lea edx,dword ptr ss:[esp+0x14] 0041746C . 8BCC mov ecx,esp 0041746E . 896424 20 mov dword ptr ss:[esp+0x20],esp 00417472 . 52 push edx 00417473 . E8 9D6A0400 call XoftSpy.0045DF15 00417478 . 51 push ecx 00417479 . 8D4424 1C lea eax,dword ptr ss:[esp+0x1C] 0041747D . 8BCC mov ecx,esp 0041747F . 896424 20 mov dword ptr ss:[esp+0x20],esp 00417483 . 50 push eax 00417484 . C64424 34 03 mov byte ptr ss:[esp+0x34],0x3 00417489 . E8 876A0400 call XoftSpy.0045DF15 0041748E . 8BCE mov ecx,esi 00417490 . C64424 30 01 mov byte ptr ss:[esp+0x30],0x1 00417495 . E8 F6010000 call XoftSpy.00417690 ; 很有可能是关键函数 0041749A . 84C0 test al,al 0041749C 75 45 jnz short XoftSpy.004174E3 ; 实现跳转 0041749E . 6A 00 push 0x0 004174A0 . 68 04544800 push XoftSpy.00485404 ; ASCII "XoftSpy" 004174A5 . 68 C4684800 push XoftSpy.004868C4 ; ASCII "Invalid code." 004174AA . 8BCE mov ecx,esi 004174AC . E8 664F0400 call XoftSpy.0045C417 004174B1 . 68 48FA4800 push XoftSpy.0048FA48 004174B6 . 8BCD mov ecx,ebp 004174B8 . E8 206E0400 call XoftSpy.0045E2DD 004174BD . 68 48FA4800 push XoftSpy.0048FA48 004174C2 . 8BCF mov ecx,edi 004174C4 . E8 146E0400 call XoftSpy.0045E2DD 004174C9 . 68 48FA4800 push XoftSpy.0048FA48 004174CE . 8BCB mov ecx,ebx 004174D0 . E8 086E0400 call XoftSpy.0045E2DD 004174D5 . 6A 00 push 0x0 004174D7 . 8BCE mov ecx,esi 004174D9 . E8 03590400 call XoftSpy.0045CDE1 004174DE . E9 9D000000 jmp XoftSpy.00417580 004174E3 > 57 push edi 004174E4 . 55 push ebp 004174E5 . E8 769D0100 call XoftSpy.00431260 004174EA . 83C4 08 add esp,0x8 004174ED . 8BCE mov ecx,esi 004174EF . 6A 00 push 0x0 004174F1 . 68 04544800 push XoftSpy.00485404 ; ASCII "XoftSpy" 004174F6 . 68 98684800 push XoftSpy.00486898 ; ASCII "Congratulations! successfully registered" 004174FB . E8 174F0400 call XoftSpy.0045C417
在00417465地址处下断点跟进。粗略看一下,发现函数有两个返回。一个是要al=1,另一个是要al=0,通常程序都是1表示已注册,0表示未注册。
1 2 3 4 5 6 7 8 9 00417894 |. E8 07690400 call XoftSpy.0045E1A0 00417899 |. 5F pop edi ; XoftSpy.00417463 0041789A |. 5E pop esi ; XoftSpy.00417463 0041789B |. B0 01 mov al,0x1 0041789D |. 5B pop ebx ; XoftSpy.00417463 0041789E |. 8B4C24 1C mov ecx,dword ptr ss:[esp+0x1C] 004178A2 |. 64:890D 00000>mov dword ptr fs:[0],ecx 004178A9 |. 83C4 28 add esp,0x28 004178AC |. C2 0800 retn 0x8
1 2 3 4 5 6 7 8 9 0041790F |. E8 8C680400 call XoftSpy.0045E1A0 00417914 |. 8B4C24 28 mov ecx,dword ptr ss:[esp+0x28] 00417918 |. 5F pop edi ; XoftSpy.00417463 00417919 |. 5E pop esi ; XoftSpy.00417463 0041791A |. 32C0 xor al,al 0041791C |. 5B pop ebx ; XoftSpy.00417463 0041791D |. 64:890D 00000>mov dword ptr fs:[0],ecx 00417924 |. 83C4 28 add esp,0x28 00417927 \. C2 0800 retn 0x8
往上找找跳转指令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 00417800 |. /0F85 A9000000 jnz XoftSpy.004178AF 00417806 |. |3BFA cmp edi,edx 00417808 |. |0F85 A1000000 jnz XoftSpy.004178AF 0041780E |. |3BD8 cmp ebx,eax 00417810 |. |0F85 99000000 jnz XoftSpy.004178AF 00417816 |. |8B4424 20 mov eax,dword ptr ss:[esp+0x20] 0041781A |. |B9 0A000000 mov ecx,0xA 0041781F |. |99 cdq 00417820 |. |F7F9 idiv ecx 00417822 |. |8B4424 24 mov eax,dword ptr ss:[esp+0x24] 00417826 |. |BE 0A000000 mov esi,0xA 0041782B |. |8BCA mov ecx,edx 0041782D |. |99 cdq 0041782E |. |F7FE idiv esi 00417830 |. |3BD1 cmp edx,ecx 00417832 |. |75 7B jnz short XoftSpy.004178AF ; 不能实现
将这一连串的跳转指令全都nop掉试试,弹窗注册成功。
但它还是说还没有注册。
那查找一下字符串,右键->中文搜索引擎->智能搜索,找到字符串所在的汇编代码处。在所在函数的入口点下断点。重载运行,在界面按下“About”时停在了断点处,F8往下执行。
1 2 3 4 5 6 7 8 9 10 11 00401492 . E8 093C0300 call XoftSpy1.004350A0 00401497 . 84C0 test al,al 00401499 . 74 12 je short XoftSpy1.004014AD;不能跳,即al不能等于0 0040149B . 68 C4514800 push XoftSpy1.004851C4 ; This license of XoftSpy has been registered 004014A0 . 8D4C24 08 lea ecx,dword ptr ss:[esp+0x8] 004014A4 . E8 34CE0500 call XoftSpy1.0045E2DD 004014A9 . 6A 00 push 0x0 004014AB . EB 10 jmp short XoftSpy1.004014BD 004014AD > 68 94514800 push XoftSpy1.00485194 ; This XoftSpy license has not been registered 004014B2 . 8D4C24 08 lea ecx,dword ptr ss:[esp+0x8] 004014B6 . E8 22CE0500 call XoftSpy1.0045E2DD
在00401492处下断点跟进,粗略看一下发现只有一个返回点。在返回点附近格外关注al的值。由于al的值是bl赋予的,所以再往上看就要关注bl的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 00435219 |. /75 3A jnz short XoftSpy1.00435255 ; 跳转 0043521B |. |51 push ecx 0043521C |. |8D5424 14 lea edx,dword ptr ss:[esp+0x14] 00435220 |. |8BCC mov ecx,esp 00435222 |. |896424 24 mov dword ptr ss:[esp+0x24],esp 00435226 |. |52 push edx 00435227 |. |E8 E98C0200 call XoftSpy1.0045DF15 0043522C |. |51 push ecx 0043522D |. |8D4424 1C lea eax,dword ptr ss:[esp+0x1C] 00435231 |. |8BCC mov ecx,esp 00435233 |. |896424 24 mov dword ptr ss:[esp+0x24],esp 00435237 |. |50 push eax 00435238 |. |C64424 38 07 mov byte ptr ss:[esp+0x38],0x7 0043523D |. |E8 D38C0200 call XoftSpy1.0045DF15 00435242 |. |8BCE mov ecx,esi 00435244 |. |885C24 34 mov byte ptr ss:[esp+0x34],bl 00435248 |. |E8 63000000 call XoftSpy1.004352B0 0043524D |. |84C0 test al,al 0043524F |. |75 04 jnz short XoftSpy1.00435255 00435251 |. |32DB xor bl,bl;bl=0 00435253 |. |EB 02 jmp short XoftSpy1.00435257 00435255 |> \B3 01 mov bl,0x1;bl=1 00435257 |> 8D4C24 10 lea ecx,dword ptr ss:[esp+0x10] 0043525B |. C64424 2C 04 mov byte ptr ss:[esp+0x2C],0x4 00435260 |. E8 3B8F0200 call XoftSpy1.0045E1A0 00435265 |. 8D4C24 14 lea ecx,dword ptr ss:[esp+0x14] 00435269 |. C64424 2C 01 mov byte ptr ss:[esp+0x2C],0x1 0043526E |. E8 2D8F0200 call XoftSpy1.0045E1A0 00435273 |. 8D4C24 08 lea ecx,dword ptr ss:[esp+0x8] 00435277 |. C64424 2C 00 mov byte ptr ss:[esp+0x2C],0x0 0043527C |. E8 1F8F0200 call XoftSpy1.0045E1A0 00435281 |. 8D4C24 0C lea ecx,dword ptr ss:[esp+0xC] 00435285 |. C74424 2C FFF>mov dword ptr ss:[esp+0x2C],-0x1 0043528D |. E8 0E8F0200 call XoftSpy1.0045E1A0 00435292 |. 8B4C24 24 mov ecx,dword ptr ss:[esp+0x24] 00435296 |. 8AC3 mov al,bl 00435298 |. 5E pop esi ; 0019BFFC 00435299 |. 64:890D 00000>mov dword ptr fs:[0],ecx 004352A0 |. 5B pop ebx ; 0019BFFC 004352A1 |. 83C4 28 add esp,0x28 004352A4 \. C3 retn
这个程序有很多修改的方法
1 2 3 4 5 6 7 8 方法一 00435296 |. 8AC3 mov al,bl => mov al,1 方法二 00435219 |. /75 3A jnz short XoftSpy1.00435255 => jmp short XoftSpy1.00435255 方法三 0043524F |. |75 04 jnz short XoftSpy1.00435255 => jmp short XoftSpy1.00435255
以上三种方法都能成功注册,也没有了注册按钮,说明程序成功破解。
所以在这个程序中,搜索API函数的方法是错误的,是程序发明者挖的坑,故意让我们跳进去。而搜索字符串才能破解成功。当程序发明者有了逆向意识,不想让我们破解程序,就会制造很多坑让我们踩,这时搜索API函数和字符串其实都会让我们踩坑,最好的办法就是查看调用堆栈。
19.1 常用的定位方法
查找字符串(11.1)
查找API函数(破解常用API函数大全.chm)
20. Inline patch(内嵌补丁) 内嵌补丁在加壳软件的破解上用得比较多,但不是加壳的也可以用。内嵌补丁指在程序文件中把补丁代码写入文件里达到破解的目的。
目标:破解过期程序。
由于这个程序还有30天可用,把时间调到未来再打开就可以过期了。
把时间调回去却还是过期了,说明可能在某个注册表或某个文件记录它是否过期的信息。
查了一下壳,是32位下由C++编译的可执行文件。
F3载入OD运行,在程序界面点击Enter Reg Code
注册。随便输入弹窗错误提示,点确定后,回到OD看还是没有去到retn指令。(之前的程序都会去到retn指令退出函数)
这是因为进入了循环,就是用户可以在这注册窗口输入无数次,不用再次点击Enter Reg Code
再输入注册序列号。这个程序用的是SetTimer
和KillTimer
设置类似于C语言switch语句的循环。
输入好序列号后,先别按ok。回到OD按下Ctrl+N查找API函数KillTimer
,选中右键->在每个参考上设置断点。再按ok,发现停在了某个KillTimer
断点处。(严格按照这个步骤来,不然会停在别的KillTimer
处)
我们停在了4DC1A7地址,在注释窗口看到了很多有用的信息。想要注册成功就必须走到4DC1D4地址处。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 004DC1A2 . 8B57 1C mov edx,dword ptr ds:[edi+0x1C] ; Case 3 of switch 004DBDA4 004DC1A5 . 50 push eax ; /TimerID = 0x3 004DC1A6 . 52 push edx ; |hWnd = 000406E8 ('DVD Menu Studio - [Untitled]',class='Afx:400000:8:10003:0:2e00151') 004DC1A7 . FF15 B4575E00 call dword ptr ds:[<&USER32.KillTimer>] ; \KillTimer 004DC1AD . 6A 00 push 0x0 004DC1AF . 6A 00 push 0x0 004DC1B1 . 68 5C666400 push DVDMenuS.0064665C ; The registration code seems to be not valid.\nPlease check if you didn't made any mistake. 004DC1B6 . E8 919D0D00 call DVDMenuS.005B5F4C 004DC1BB . E9 B7000000 jmp DVDMenuS.004DC277 004DC1C0 > 83F8 04 cmp eax,0x4 ;从信息窗口可知跳转来自004DC1A0,右键->转到jnz来自004DC1A0就会跳到004DC1A0地址 004DC1C3 . 75 1E jnz short DVDMenuS.004DC1E3;不能跳,即eax=4 004DC1C5 . 50 push eax ; /TimerID = 0x3; Case 4 of switch 004DBDA4 004DC1C6 . 8B47 1C mov eax,dword ptr ds:[edi+0x1C] ; | 004DC1C9 . 50 push eax ; |hWnd = 00000003 004DC1CA . FF15 B4575E00 call dword ptr ds:[<&USER32.KillTimer>] ; \KillTimer 004DC1D0 . 6A 00 push 0x0 004DC1D2 . 6A 00 push 0x0 004DC1D4 . 68 FC656400 push DVDMenuS.006465FC ; Thank you for your support!\nPlease Exit the Software and start it again to validate the code. 004DC1D9 . E8 6E9D0D00 call DVDMenuS.005B5F4C 004DC1DE . E9 94000000 jmp DVDMenuS.004DC277 004DC1E3 > 83F8 05 cmp eax,0x5 004DC1E6 . 75 15 jnz short DVDMenuS.004DC1FD
现在就往上翻查看哪个跳转可以跳过case3去到case4。一直跟踪cmp和jnz语句去到switch语句开头。
1 004DBDA4 . 83F8 01 cmp eax,0x1 ; Switch (cases 1..B)
在4DBD80和4DBDA4各下断点,运行一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 004DBD80 . 55 push ebp 004DBD81 . 8BEC mov ebp,esp 004DBD83 . 6A FF push -0x1 004DBD85 . 68 17B85D00 push DVDMenuS.005DB817 ; SE 处理程序安装 004DBD8A . 64:A1 0000000>mov eax,dword ptr fs:[0] 004DBD90 . 50 push eax 004DBD91 . 64:8925 00000>mov dword ptr fs:[0],esp 004DBD98 . 81EC 68010000 sub esp,0x168 004DBD9E . 8B45 08 mov eax,dword ptr ss:[ebp+0x8];在判断前最后一个给eax赋值的指令 004DBDA1 . 53 push ebx ; DVDMenuS.004DBD80 004DBDA2 . 56 push esi 004DBDA3 . 57 push edi 004DBDA4 . 83F8 01 cmp eax,0x1 ; Switch (cases 1..B)
那么能改成如下指令吗?不行。
1 mov eax,dword ptr ss:[ebp+0x8] => mov eax,4
因为mov eax,dword ptr ss:[ebp+0x8]
指令占3个字节,而mov eax,4
占5个字节,会将后面的指令覆盖,可能会出现奇奇怪怪的问题。
方法一:因为4只需一个字节(8bit)就可存放,所以修改al即可。这条指令占2个字节,所以不用担心后面指令被覆盖的问题。
1 mov eax,dword ptr ss:[ebp+0x8] => mov al,4
保存文件,就可正常运行,也没有了NAG窗口。为了好看,再将未注册字样用11.1的第2、3种方法(第1种不行)修改成注册即可。
方法二:内嵌补丁
一个程序在内存中需要对齐,所以会出现很多空白处。一般在汇编代码的末尾。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 005E47C0 00 db 00 005E47C1 00 db 00 005E47C2 00 db 00 005E47C3 00 db 00 005E47C4 00 db 00 005E47C5 00 db 00 005E47C6 00 db 00 005E47C7 00 db 00 005E47C8 00 db 00 005E47C9 00 db 00 005E47CA 00 db 00 005E47CB 00 db 00 005E47CC 00 db 00 005E47CD 00 db 00 005E47CE 00 db 00 005E47CF 00 db 00
我们可以利用这些空白处写入修改指令,在需要这些修改指令的地方修改成无条件跳转指令即可。
这个程序要在004DBD9E地址处修改成跳转指令,去到005E47C0处。由于jmp指令需要5个字节,所以先去空白代码处放置好我们要修改的指令(要修改的mov指令和复制004DBD9E地址后面两条指令)。
1 005E47C0 B8 04000000 mov eax,0x4
选中004DBD9E地址后面两条指令右键->二进制->二进制复制,复制到5E47C0后面。需要两个字节就选中两个字节粘贴。
1 2 004DBDA1 . 53 push ebx ; DVDMenuS.004DBD80 004DBDA2 . 56 push esi
回到004DBD9E处修改成无条件跳转指令,下一条指令的地址为004DBDA3。
1 mov eax,dword ptr ss:[ebp+0x8] => jmp 005E47C0
1 2 004DBD9E /E9 1D8A1000 jmp DVDMenuS.005E47C0 004DBDA3 . |57 push edi ; DVDMenuS.<ModuleEntryPoint>
去到空白代码处再加一条jmp指令回到004DBDA3地址处。
1 2 3 4 005E47C0 B8 04000000 mov eax,0x4 005E47C5 53 push ebx 005E47C6 56 push esi ; DVDMenuS.<ModuleEntryPoint> 005E47C7 ^ E9 D775EFFF jmp DVDMenuS.004DBDA3
保存所有修改,试运行,发现一直弹出注册成功弹窗。
将新程序载入OD,在005E47C0处下个断点。F9运行,F8一步步查看代码运行情况,发现一直在switch里循环。
如果每个case都跳转(到case4处将ZF改变),直到4DC241处停下。看到注释窗口的“ResetToolbars”和“Settings”——循环结束的标志,我们要经过那里,所以4DC244的跳转不让它实现(将ZF改变),也就是eax要等于0xB,才能结束循环。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 004DC241 > \83F8 0B cmp eax,0xB 004DC244 . 75 31 jnz short DVDMenuS.004DC277 004DC246 . 50 push eax ; /TimerID = 0x4; Case B of switch 004DBDA4 004DC247 . 8B47 1C mov eax,dword ptr ds:[edi+0x1C] ; | 004DC24A . 50 push eax ; |hWnd = 00000004 004DC24B . FF15 B4575E00 call dword ptr ds:[<&USER32.KillTimer>] ; \KillTimer 004DC251 . E8 F3570E00 call DVDMenuS.005C1A49 004DC256 . 8B40 04 mov eax,dword ptr ds:[eax+0x4] 004DC259 . 6A 00 push 0x0 004DC25B . 68 B03C6400 push DVDMenuS.00643CB0 ; ASCII "ResetToolbars" 004DC260 . 68 1CF16300 push DVDMenuS.0063F11C ; ASCII "Settings" 004DC265 . 8BC8 mov ecx,eax 004DC267 . E8 255C0E00 call DVDMenuS.005C1E91 004DC26C . 85C0 test eax,eax 004DC26E . 74 07 je short DVDMenuS.004DC277 004DC270 . 8BCF mov ecx,edi 004DC272 . E8 99190000 call DVDMenuS.004DDC10 004DC277 > 8BCF mov ecx,edi ; Default case of switch 004DBDA4 004DC279 . E8 51040D00 call DVDMenuS.005AC6CF 004DC27E . 8B4D F4 mov ecx,dword ptr ss:[ebp-0xC] 004DC281 . 5F pop edi ; 024F8688 004DC282 . 5E pop esi ; 024F8688 004DC283 . 5B pop ebx ; 024F8688 004DC284 . 64:890D 00000>mov dword ptr fs:[0],ecx 004DC28B . 8BE5 mov esp,ebp 004DC28D . 5D pop ebp ; 024F8688 004DC28E . C2 0400 retn 0x4
又要等于4又要0xB怎么可能呢?从4到0xB之间又没有别的函数能改变eax的值,那干脆直接让eax=0xB。因为这是个验证序列号是否正确的函数,如果不正确会一直循环让你填写序列号,就算正确也会一直提示你序列号正确,不如直接退出函数,跳出验证过程。
1 2 3 4 005E47C0 B8 04000000 mov eax,0xB 005E47C5 53 push ebx 005E47C6 56 push esi ; DVDMenuS.<ModuleEntryPoint> 005E47C7 ^ E9 D775EFFF jmp DVDMenuS.004DBDA3
保存文件,运行成功。
那方法一那样的修改为什么就不会执行循环呢?载入用方法一修改的程序,发现它在case等于4时也跳转,但它也绕过了“ResetToolbars”和“Settings”,直接跳到4DC277地址,最后返回。因为没有循环所以也可以不用循环结束标志。
1 2 004DBD9E . 8B45 08 mov al,4;eax=0019fc04 004DC279 . E8 51040D00 call DVDMenuS.005AC6CF;eax=0
而方法二
1 2 3 4 005E47C0 B8 04000000 mov eax,0x4;eax=00000004 004DC1C9 . 50 push eax;eax=00160842 004DC1D0 . 6A 00 push 0x0;eax=00000000 004DC1D9 . E8 6E9D0D00 call DVDMenuS.005B5F4C;按F8还是会步入函数
F8:单步步过到下一条指令,如果当前命令是一个函数,则一次执行完这个函数(除非这个函数内部包含断点或发生了异常)。
4dc1d9->5e47c0->进入switch语句->4dc1d9一直循环,所以其实是不能进入case4里面的,因为一进去就不停循环,并且去不了循环结束的标志处。哪里能去到循环结束处?当eax=0xB时。
方法三:调用堆栈
我们发现程序是打开界面再弹出NAG窗口,那直接去掉NAG窗口是不是可以用了呢?
F9运行程序,程序弹出过期NAG窗口,F12暂停,点击K图标查看堆栈。
1 2 3 4 5 6 7 8 9 10 地址 堆栈 函数过程 / 参数 调用来自 结构 0019F9B0 74EF8085 win32u.NtUserGetMessage user32.74EF807F 0019F9EC 0019F9F0 005B044F user32.GetMessageA DVDMenuS.005B0449 0019F9EC 0019F9F4 00662858 pMsg = DVDMenuS.00662858 0019F9F8 00000000 hWnd = NULL 0019F9FC 00000000 MsgFilterMin = 0x0 0019FA00 00000000 MsgFilterMax = 0x0 0019FA0C 005AF0A2 包含DVDMenuS.005B044F DVDMenuS.005AF09F 0019FA30 005ABF2E DVDMenuS.005AEFC8 DVDMenuS.005ABF29 0019FA6C 004DC0D6 ? DVDMenuS.005ABE4D DVDMenuS.004DC0D1
因为进入到界面没有进行任何操作,很有可能最后一个就是调用NAG弹窗(不是再倒着往上找)。
双击去到汇编代码处,给它下个断点,重载运行。主界面出来了,但NAG没有出现。所以005ABE4D地址很有可能就是我们要去除的NAG弹窗。F8单步步过断点,发现走不了,原来是NAG窗口出现了,在NAG窗口点exit后,OD执行到下一条指令。可以确定005ABE4D就是调用了NAG窗口。再点运行就退出程序了。
在调用了这个函数的父函数入口处设置断点,也就是004DBD80处下断点。Ctrl+F2重载,F9运行,程序停在4DBD80处,一路F8,遇到第一个jnz跳转未实现,第二个jnz跳转实现,再F9运行,发现又去到了4DBD80处而不是到达NAG窗口,一路F8,这时遇到第一个jnz跳转实现,跳过了刚才第二个jnz,F8去到了NAG窗口处。后面的事情我们都知道啦,不知道给我看上一段话!所以在NAG窗口创建过程中,调用NAG函数的父函数被执行了两次。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 004DBD80 . 55 push ebp 004DBD81 . 8BEC mov ebp,esp 004DBD83 . 6A FF push -0x1 004DBD85 . 68 17B85D00 push DVDMenuS.005DB817 ; SE 处理程序安装 004DBD8A . 64:A1 0000000>mov eax,dword ptr fs:[0] 004DBD90 . 50 push eax 004DBD91 . 64:8925 00000>mov dword ptr fs:[0],esp 004DBD98 . 81EC 68010000 sub esp,0x168 004DBD9E 8B45 08 mov eax,dword ptr ss:[ebp+0x8] 004DBDA1 53 push ebx ; DVDMenuS.004DBD80 004DBDA2 56 push esi 004DBDA3 . 57 push edi 004DBDA4 . 83F8 01 cmp eax,0x1 ; Switch (cases 1..B) 004DBDA7 . 8BF9 mov edi,ecx 004DBDA9 . 75 53 jnz short DVDMenuS.004DBDFE;第一次不跳转,第二次跳转 004DBDAB . 50 push eax ; /TimerID = 0x1; Case 1 of switch 004DBDA4 004DBDAC . 8B47 1C mov eax,dword ptr ds:[edi+0x1C] ; |DVDMenuS.00400564 004DBDAF . 50 push eax ; |hWnd = 00000001 004DBDB0 . FF15 B4575E00 call dword ptr ds:[<&USER32.KillTimer>] ; \KillTimer 004DBDB6 . E8 8E5C0E00 call DVDMenuS.005C1A49 004DBDBB . 8B40 04 mov eax,dword ptr ds:[eax+0x4] 004DBDBE . 83B8 64010000>cmp dword ptr ds:[eax+0x164],0x1 004DBDC5 . 75 1F jnz short DVDMenuS.004DBDE6;第一次跳转,第二次被跳过
第二次运行状态
1 2 004DBDFE > \83F8 07 cmp eax,0x7 004DBE01 . 0F85 42020000 jnz DVDMenuS.004DC049;跳转,eax=2
1 2 3 4 004DC049 > \83F8 02 cmp eax,0x2 004DC04C . 0F85 4B010000 jnz DVDMenuS.004DC19D;不跳转 ... 004DC0D1 . E8 77FD0C00 call DVDMenuS.005ABE4D;NAG窗口
那怎么修改才能去除NAG窗口呢?因为调用NAG窗口需要执行两次父函数,而第二个jnz跳转实现后经过一系列指令又回到了父函数。那可不可以修改第一个jnz跳转实现,绕过第二个jnz,避免第二次回到父函数呢?
第一次状态,将第一个jnz的ZF改变
1 2 004DBDFE > \83F8 07 cmp eax,0x7 004DBE01 . 0F85 42020000 jnz DVDMenuS.004DC049;跳转,eax=1
1 2 004DC049 > \83F8 02 cmp eax,0x2 004DC04C . 0F85 4B010000 jnz DVDMenuS.004DC19D;跳转,eax=1
1 2 004DC19D > \83F8 03 cmp eax,0x3 004DC1A0 . 75 1E jnz short DVDMenuS.004DC1C0;跳转
1 2 004DC1C0 > \83F8 04 cmp eax,0x4 004DC1C3 . 75 1E jnz short DVDMenuS.004DC1E3;跳转
1 2 004DC1E3 > \83F8 05 cmp eax,0x5 004DC1E6 . 75 15 jnz short DVDMenuS.004DC1FD;跳转
1 2 004DC1FD > \83F8 06 cmp eax,0x6 004DC200 . 75 0D jnz short DVDMenuS.004DC20F;跳转
1 2 004DC20F > \83F8 0A cmp eax,0xA 004DC212 . 75 2D jnz short DVDMenuS.004DC241;跳转
1 2 004DC241 > \83F8 0B cmp eax,0xB 004DC244 . 75 31 jnz short DVDMenuS.004DC277;跳转
1 2 3 4 5 6 7 8 9 10 004DC277 > \8BCF mov ecx,edi ; Default case of switch 004DBDA4 004DC279 . E8 51040D00 call DVDMenuS.005AC6CF 004DC27E . 8B4D F4 mov ecx,dword ptr ss:[ebp-0xC] 004DC281 . 5F pop edi ; 025D8688 004DC282 . 5E pop esi ; 025D8688 004DC283 . 5B pop ebx ; 025D8688 004DC284 . 64:890D 00000>mov dword ptr fs:[0],ecx 004DC28B . 8BE5 mov esp,ebp 004DC28D . 5D pop ebp ; 025D8688 004DC28E . C2 0400 retn 0x4
一直循环到父函数开头。即如果修改第一个jnz跳转实现,父函数会形成死循环,不仅不能避免第二次回到父函数还有第三次第N次回到父函数。
那修改第二个jnz为跳转未实现,又会怎样呢?
第一次状态
1 2 3 4 5 6 7 8 9 10 004DBDC5 . /75 1F jnz short DVDMenuS.004DBDE6;第二个jnz 004DBDC7 . |8BCF mov ecx,edi 004DBDC9 . |E8 621C0000 call DVDMenuS.004DDA30 004DBDCE . |8B4F 1C mov ecx,dword ptr ds:[edi+0x1C] 004DBDD1 . |6A 00 push 0x0 ; /Timerproc = NULL 004DBDD3 . |68 C8000000 push 0xC8 ; |Timeout = 200. ms 004DBDD8 . |6A 07 push 0x7 ; |TimerID = 0x7 004DBDDA . |51 push ecx ; |hWnd = 0019FBE4 004DBDDB . |FF15 B8575E00 call dword ptr ds:[<&USER32.SetTimer>] ; \SetTimer 004DBDE1 . |E9 91040000 jmp DVDMenuS.004DC277
1 2 3 4 5 6 7 8 9 10 004DC277 > \8BCF mov ecx,edi ; Default case of switch 004DBDA4 004DC279 . E8 51040D00 call DVDMenuS.005AC6CF 004DC27E . 8B4D F4 mov ecx,dword ptr ss:[ebp-0xC] 004DC281 . 5F pop edi ; 024C8688 004DC282 . 5E pop esi ; 024C8688 004DC283 . 5B pop ebx ; 024C8688 004DC284 . 64:890D 00000>mov dword ptr fs:[0],ecx 004DC28B . 8BE5 mov esp,ebp 004DC28D . 5D pop ebp ; 024C8688 004DC28E . C2 0400 retn 0x4
第二次去到父函数
1 004DBDA9 . 75 53 jnz short DVDMenuS.004DBDFE;跳转,eax=7
1 2 3 4 5 004DBDFE > \83F8 07 cmp eax,0x7 004DBE01 . 0F85 42020000 jnz DVDMenuS.004DC049;不跳转 ... 004DBE4A . 83F8 01 cmp eax,0x1 004DBE4D . /0F85 54010000 jnz DVDMenuS.004DBFA7;跳转,eax=0
1 2 3 004DBFA7 > \8B45 EC mov eax,dword ptr ss:[ebp-0x14] ; DVDMenuS.006508B0 ... 004DC015 . E8 16CFFAFF call DVDMenuS.00488F30;运行到这里卡住,原来是有弹窗
关闭弹窗,执行到下一条指令。F9运行,程序正常使用。
从而我们可知,到NAG窗口不是第二次跳不跳到父函数的问题,关键在于eax的值。eax=2去到NAG窗口,eax=1一直循环,eax=7注册成功。
调用堆栈对程序进行破解和观察,比搜索字符串方式来得高级,养成良好习惯,首先考虑堆栈,迫不得已再搜索字符串。
22. 实验十三 ReverseMe.NAGs
目标:去掉开始和结束的NAG窗口。
它说点击注册按钮就完成程序的注册,就可以避免结束时弹出的NAG窗口,但开始前的NAG窗口还是在的。所以点击注册也不能达到我们的目标。在help->about要求我们在不使用注册按钮的情况下去掉前后的NAG窗口。
查壳,发现它是由C++编译的32位的可执行文件。
F3载入OD,F9运行,在它弹出第一个NAG窗口后迅速点暂停,查看堆栈。发现有一个“Dialog”字样的函数,对话框,很有可能就是NAG窗口。选中双击“调用来自”那一列,去到汇编代码处下断点。重载运行,指令停在断点处,F8步过,弹出NAG窗口,再F8,NAG窗口消失。可以确定这就是调用第一个NAG窗口处。
F9运行,竟然又回到断点处,F8步过,发现程序界面也是调用这个函数,点击exit后OD执行下条指令。再F9再次回到断点处,F8步过发现结束后的NAG窗口也是调用这个函数。这时再按F9,程序就结束了。
综上所述这个程序共有3个窗口,第1个和第3个是NAG,第2个是程序界面窗口。肯定不能用nop填充,因为主程序也在里面。往上看发现有一个条件跳转,通过验证发现只要在第1次和第3次让它跳转实现就可以绕过NAG窗口。(改ZF验证猜想对错)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 00420379 |. /74 3F je short ReverseM.004203BA 0042037B |. |8D4C24 4C lea ecx,dword ptr ss:[esp+0x4C] 0042037F |. |89B424 CC3700>mov dword ptr ss:[esp+0x37CC],esi 00420386 |. |890D F4694400 mov dword ptr ds:[0x4469F4],ecx 0042038C |. |8B56 08 mov edx,dword ptr ds:[esi+0x8] 0042038F |. |8D4C24 4C lea ecx,dword ptr ss:[esp+0x4C] 00420393 |. |899424 A03700>mov dword ptr ss:[esp+0x37A0],edx 0042039A |. |E8 C1280100 call <jmp.&MFC42.#CDialog::DoModal_2514>;断点处 0042039F |. |8D8424 C83700>lea eax,dword ptr ss:[esp+0x37C8] 004203A6 |. |8D4C24 20 lea ecx,dword ptr ss:[esp+0x20] 004203AA |. |50 push eax 004203AB |. |E8 2A270100 call <jmp.&MFC42.#CString::operator=_858> 004203B0 |. |8B4C24 1C mov ecx,dword ptr ss:[esp+0x1C] 004203B4 |. |890D F4694400 mov dword ptr ds:[0x4469F4],ecx 004203BA |> \55 push ebp ; /hMem = 00A00014 004203BB |. FF15 7C804300 call dword ptr ds:[<&KERNEL32.GlobalUnlo>; \GlobalUnlock
怎么操作?类似于C语言的if语句,如果i==2的话就不跳转,i!=2的话就跳转。只不过是转换成汇编语言来写这个语句。由于我们写入的语句要占很多个字节,不可能直接在je语句里修改,这就要用到inline patch。而jmp语句占5个字节,会把je指令的下一条指令覆盖掉,所以je指令的下一条指令也要写在inline patch里。
那“i”要存到哪里去呢?存到PE结构的数据段里去,因为代码段不允许我们写入。点击M图标,找到00400000地址的那个PE结构,找到.data段双击,在里面找到一大片00的地方,随便找一个字节作为i的存储地址,比如00445ec0。(i的取值为1到3,所以一个字节足够了)
找好了后应该对这个地址进行测试,确定这个地址确实在程序运行过程中没有被使用到。选中00右键->断点->硬件写入->Byte。重载运行,没有停在我们选中的地址处就证实没被使用到。
把反编译窗口的滚动条拉到最下方,在一大片00的地方随便找一处,比如00437d6b,写入if语句的汇编代码。
1 2 3 4 5 00437D6B FE05 C05E4400 inc byte ptr ds:[0x445EC0];i+=1 00437D71 803D C05E4400 02 cmp byte ptr ds:[0x445EC0],0x2;i-2==0?不跳转:跳转 00437D78 ^ 0F85 3C86FEFF jnz ReverseM.004203BA 00437D7E 8D4C24 4C lea ecx,dword ptr ss:[esp+0x4C];被jmp指令覆盖的指令 00437D82 ^ E9 F885FEFF jmp ReverseM.0042037F
1 2 3 00420379 |. /74 3F je short ReverseM.004203BA 修改为 00420379 /E9 F27B0100 jmp ReverseM.00437D6B
将所有修改保存,若保存不了试试换写汇编代码的地址。这样就去除完两个NAG窗口了。
23. 模态对话框与非模态对话框 对话框就是完成“人机对话”的功能,是程序与用户进行交互的中介,如输入参数、输入文本、退出确认等。
对话框从类型上分为两类:modal对话框和modeless对话框。
它们之间的区别在于是否允许用户在不同窗口间进行切换,模态对话框不允许,而非模态对话框允许。模态对话框由Windows为它内建一个消息循环,而非模态对话框的消息则是通过用户程序中的消息循环派送的。
创建模态对话框是由调用DialogBoxParam函数实现,而非模态对话框调用CreateDialogParam函数实现。
23.1 DialogBoxParam函数 1 2 3 4 5 6 7 HWND DialogBoxParam ( HINSTANCE hInstance, LPCTSTR IpTemplateName, HWND hWndParent, DLGPROC IPDialogFunc, LPARAM dwlnitParam ) ;
23.2 CreateDialogParam函数 1 2 3 4 5 6 7 HWND CreateDialogParam ( HINSTANCE hlnstancem, LPCTSTR IpTemplateName, HWND hWndParent, DLGPROCIpDialogFunc, LPARAM dwlniParam ) ;
24. 实验十四 Urlegal
目标:完成注册,去除NAG窗口。
我们知道,只要我们成功注册,NAG窗口也会随之消失。就用以前的方法也可以破解这个软件。这次用另一种方法破解。
NAG其实是个对话框,而且是模态对话框。将程序拖进exescope查看。
在这个程序可以修改对话框的内容,但这个不是实验的重点。可以知道这个对话框的编号是103,也就是DialogBoxParam
函数的第一个参数,换成16进制是0x67。将Urlegal载入OD,右键->查找->所有命令,输入命令push 0x67
弹出R图标,显示这个命令的地址。双击进入反汇编窗口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 00401220 /$ 55 push ebp 00401221 |. 8BEC mov ebp,esp 00401223 |. 6A FF push -0x1 00401225 |. 68 05744100 push Urlegal.00417405 ; SE 处理程序安装 0040122A |. 64:A1 0000000>mov eax,dword ptr fs:[0] 00401230 |. 50 push eax 00401231 |. 64:8925 00000>mov dword ptr fs:[0],esp 00401238 |. 51 push ecx ; Urlegal.<ModuleEntryPoint> 00401239 |. 894D F0 mov [local.4],ecx ; Urlegal.<ModuleEntryPoint> 0040123C |. 8B45 08 mov eax,[arg.1] 0040123F |. 50 push eax 00401240 |. 6A 67 push 0x67;句柄103 00401242 |. 8B4D F0 mov ecx,[local.4] 00401245 |. E8 6A570100 call <jmp.&MFC42.#CDialog::CDialog_324>;可知这个就是NAG窗口函数 ;因为函数的参数是从后往前进栈,所以第一个参数是最后进栈的,然后才调用函数
这个函数是不希望进去的,但从入口处到函数执行前却没有跳转指令,那要返回到上一层函数看看。
在00401220地址处下断点,重载运行,在主程序窗口点击关闭按钮(因为NAG窗口是在关闭时出现的),OD停在断点处。在堆栈窗口可以看到
1 0019ED24 004023C0 返回到 Urlegal.004023C0 来自 Urlegal.00401220
选中右键->反汇编窗口中跟随
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 004023AA |. E8 691A0000 call Urlegal.00403E18 004023AF |. 85C0 test eax,eax 004023B1 |. /75 43 jnz short Urlegal.004023F6 004023B3 |. |6A 00 push 0x0 004023B5 |. |8D8D 98FEFFFF lea ecx,[local.90] 004023BB |. |E8 60EEFFFF call Urlegal.00401220 004023C0 |. |C745 FC 00000>mov [local.1],0x0 004023C7 |. |8D8D 98FEFFFF lea ecx,[local.90] 004023CD |. |E8 3A450100 call <jmp.&MFC42.#CDialog::DoModal_2514> 004023D2 |. |83F8 01 cmp eax,0x1 004023D5 |. |75 0B jnz short Urlegal.004023E2 004023D7 |. |8B8D 94FEFFFF mov ecx,[local.91] 004023DD |. |E8 82470100 call <jmp.&MFC42.#CFrameWnd::OnClose_4413> 004023E2 |> |C745 FC FFFFF>mov [local.1],-0x1 004023E9 |. |8D8D 98FEFFFF lea ecx,[local.90] 004023EF |. |E8 3CF0FFFF call Urlegal.00401430 004023F4 |. |EB 0B jmp short Urlegal.00402401 004023F6 |> \8B8D 94FEFFFF mov ecx,[local.91]
004023BB的call指令执行的就是那个没有跳转指令的函数。往上找有一个jnz指令,如果不想NAG窗口出现那就必须要让它跳转实现。jnz上面有test指令和call指令,call指令应该执行的是判断程序是否注册的函数,返回值存给eax。
在004023AA处下断点重载运行,点关闭,OD停在断点处,F7进入函数
1 2 3 4 5 6 7 8 9 00403E18 /$ 55 push ebp 00403E19 |. 8BEC mov ebp,esp 00403E1B |. 51 push ecx ; Urlegal.00422428 ;以上不能修改 00403E1C |. 894D FC mov [local.1],ecx ; Urlegal.00422428 00403E1F |. 8B45 FC mov eax,[local.1] 00403E22 |. 8B40 2C mov eax,dword ptr ds:[eax+0x2C] 00403E25 |. 8BE5 mov esp,ebp ;以下不能修改 00403E27 |. 5D pop ebp ; Urlegal.004023AF 00403E28 \. C3 retn
这个判断过程非常短,很容易看明白。只要将eax修改为1即可。由于mov eax,1
占5个字节,能修改的指令一行最多占3个字节,可以用inline patch方式修改,也可以选中mov eax
的那两条进行修改。
1 2 mov eax,[local.1] => mov eax,1 mov eax,dword ptr ds:[eax+0x2C]
选中两条右键->汇编,修改指令,保存。
发现“Register”已经变成灰色,关闭也没有NAG窗口。
接下来尝试用搜索字符串方式破解。
右键->中文搜索引擎->智能搜索图上字符串,看到关键字符串“Thank you for registering!”,双击进入反汇编窗口。往上找跳转指令
1 2 3 4 5 6 7 8 9 10 11 00402648 |. E8 C3050000 call Urlegal.00402C10 ; 很有可能是关键函数 0040264D |. 85C0 test eax,eax ; msctf.779D27BC 0040264F |. 75 76 jnz short Urlegal.004026C7 00402651 |. 8D55 EC lea edx,[local.5] 00402654 |. 52 push edx 00402655 |. 8D45 88 lea eax,[local.30] 00402658 |. 50 push eax ; msctf.779D27BC 00402659 |. E8 02060000 call Urlegal.00402C60 ; 很有可能是关键函数 0040265E |. 25 FF000000 and eax,0xFF 00402663 |. 85C0 test eax,eax ; msctf.779D27BC 00402665 |. 74 60 je short Urlegal.004026C7
004026C7地址是注册失败的地址,这两条跳转指令都会去到注册失败的地方,先看第一个函数,当我们输入注册序列号后,F8往下,发现jnz不跳转,说明第一个函数其实不是关键函数。再F8,发现je跳转,修改ZF,弹窗注册成功,注册也变成了灰色,说明破解成功,也就说明第二个函数是关键函数。尝试用nop修改je指令,可以正常执行。
用这种方法不好的就是每次打开软件都要进行注册。再仔细想想,如果我们是已注册用户,程序就不会进到注册窗口,所以我们要找到这个函数的父函数,让它跳过注册函数。呃啊…没找到…
25. 实验十五 movgear
目标:去除关闭后的NAG窗口。
与实验十四的实验步骤一样,Resource Hacker跟eyescope实现的功能差不多。
知道对话框的编号为100,16进制为0x64,寻找push 0x64
时却发现不止一个地址,右键->在每个命令上设置断点。重载运行,点关闭时停下的断点才是我们要找的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 00406707 > \6A 00 push 0x0 00406709 . 6A 00 push 0x0 0040670B . E8 30AF0200 call movgear.00431640 00406710 . 83C4 08 add esp,0x8 00406713 . 83F8 01 cmp eax,0x1 00406716 . 74 16 je short movgear.0040672E 00406718 . A1 68854600 mov eax,dword ptr ds:[0x468568] 0040671D . 6A 01 push 0x1 ; /lParam = 00000001 0040671F . 68 D0E84000 push movgear.0040E8D0 ; |DlgProc = movgear.0040E8D0 00406724 . 56 push esi ; |hOwner = 00110402 ('GIF Movie Gear',class='GIF Movie Gear') 00406725 . 6A 64 push 0x64 ; |pTemplate = 0x64 00406727 . 50 push eax ; |hInst = 00400000 00406728 . FF15 0C834400 call dword ptr ds:[<&USER32.DialogBoxPar>; \DialogBoxParamA 0040672E > 8B8C24 B80000>mov ecx,dword ptr ss:[esp+0xB8]
往上看跳转和call指令,在0040670B处下断点,步入。粗略看下函数,只有一个返回的地方。
1 2 3 4 5 0043173E |. 8BC3 mov eax,ebx ; movgear.00406170 00431740 |. 5E pop esi ; movgear.00406710 00431741 |. 5B pop ebx ; movgear.00406710 00431742 |. 81C4 D0000000 add esp,0xD0 00431748 \. C3 retn
可知eax的值是ebx赋给它的,往上找ebx只找到一处。
1 2 3 4 5 6 7 8 004316C7 |. 51 push ecx ; movgear.00406170 004316C8 |. 52 push edx 004316C9 |. E8 B2FEFFFF call movgear.00431580;验证函数 004316CE |. 83C4 08 add esp,0x8 004316D1 |. 85C0 test eax,eax 004316D3 |. 74 5D je short movgear.00431732 004316D5 |. 8B9424 E00000>mov edx,dword ptr ss:[esp+0xE0] ; user32.758C67DC 004316DC |. BB 01000000 mov ebx,0x1
je不能跳转,一跳转就不能让ebx=1了,所以可以猜测je上面的call指令就是调用验证是否注册的函数。在004316C9下断点,发现没经过就直接弹窗了。从函数开头下断点,F8一步步走。
1 2 00431697 |. 85C0 test eax,eax 00431699 |. 0F85 93000000 jnz movgear.00431732
发现00431699跳转直接跳过了验证函数。走到mov ebx,0x1
好像挺多要改的,那就不去了,直接在退出函数前改eax的值。因为只有两个字节可以修改,而mov eax,1
占5个字节,那就修改al的值,因为返回值准确来说是存放在al中。
1 0043173E |. 8BC3 mov eax,ebx => mov al,1
也可以从几个常用的函数推测004316C9是验证函数地址。
25.1 RegOpenKeyEx 函数功能:用于打开一个指定的注册表键。
注册表可以理解为Windows数据库,Windows通过注册表写入软件的版本信息,运行状态等。
1 2 3 4 5 6 7 LONG RegOpenKeyEx ( HKEY hKey, LPCTSTR lpSubKey, DWORD ulOptions, REGSAM samDesired, PHKEY phkResult ) ;
25.2 RegQueryValueEx 函数功能:检索一个已打开的注册表句柄中,指定的注册表键的类型和设置值。
这个函数是从注册表里取出数据,传到lpdata里去。
1 2 3 4 5 6 7 8 LONG WINAPI RegQueryValueEx ( HKEY hKey, LPCTSTR lpValueName, LPDWORD lpReserved, LPDWORD lpType, LPBYTE lpData, LPDWORD lpcbData ) ;
25.3 RegCloseKey 函数功能:释放指定注册键的句柄。
1 2 3 LONG RegCloseKey ( HKEY hKey ) ;
在0040670B处进入函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 0043164D |. 50 push eax ; /pHandle = NULL 0043164E |. 68 19000200 push 0x20019 ; |Access = KEY_READ 00431653 |. 6A 00 push 0x0 ; |Reserved = 0x0 00431655 |. 68 F8B34400 push movgear.0044B3F8 ; |Subkey = "Software\gamani\GIFMovieGear\2.0" 0043165A |. 68 01000080 push 0x80000001 ; |hKey = HKEY_CURRENT_USER 0043165F |. 83CB FF or ebx,-0x1 ; | 00431662 |. FF15 00804400 call dword ptr ds:[<&ADVAPI32.RegOpenKey>; \RegOpenKeyExA 00431668 |. 85C0 test eax,eax 0043166A |. 0F85 C2000000 jnz movgear.00431732 00431670 |. 8D4C24 10 lea ecx,dword ptr ss:[esp+0x10] 00431674 |. 8B35 04804400 mov esi,dword ptr ds:[<&ADVAPI32.RegQuer>; apphelp.6CB30350 0043167A |. 8D5424 14 lea edx,dword ptr ss:[esp+0x14] 0043167E |. 51 push ecx ; /pBufSize = movgear.00406170 0043167F |. 52 push edx ; |Buffer = NULL ;lpData,edx=esp+0x14,存的是注册名 00431680 |. 50 push eax ; |pValueType = NULL 00431681 |. 50 push eax ; |Reserved = NULL 00431682 |. 8B4424 1C mov eax,dword ptr ss:[esp+0x1C] ; | 00431686 |. BF 64000000 mov edi,0x64 ; | 0043168B |. 68 98D44400 push movgear.0044D498 ; |ValueName = "RegName3" 00431690 |. 50 push eax ; |hKey = 0x0 ;6个push传入API函数的6个参数 00431691 |. 897C24 28 mov dword ptr ss:[esp+0x28],edi ; | 00431695 |. FFD6 call esi ; \RegQueryValueExA 00431697 |. 85C0 test eax,eax 00431699 0F85 93000000 jnz movgear.00431732 0043169F |. 8D4C24 10 lea ecx,dword ptr ss:[esp+0x10] 004316A3 |. 8D5424 78 lea edx,dword ptr ss:[esp+0x78] 004316A7 |. 51 push ecx ; /pBufSize = movgear.00406170 004316A8 |. 52 push edx ; |Buffer = NULL ;lpData,edx=esp+0x78,存的是密码 004316A9 |. 50 push eax ; |pValueType = NULL 004316AA |. 50 push eax ; |Reserved = NULL 004316AB |. 8B4424 1C mov eax,dword ptr ss:[esp+0x1C] ; | 004316AF |. 68 A4D44400 push movgear.0044D4A4 ; |ValueName = "RegCode3" 004316B4 |. 50 push eax ; |hKey = 0x0 004316B5 |. 897C24 28 mov dword ptr ss:[esp+0x28],edi ; | 004316B9 |. FFD6 call esi ; \RegQueryValueExA 004316BB |. 85C0 test eax,eax 004316BD |. 75 73 jnz short movgear.00431732 004316BF |. 8D4C24 78 lea ecx,dword ptr ss:[esp+0x78];密码 004316C3 |. 8D5424 14 lea edx,dword ptr ss:[esp+0x14];注册名 004316C7 |. 51 push ecx ; movgear.00406170 004316C8 |. 52 push edx 004316C9 |. E8 B2FEFFFF call movgear.00431580 ;将注册名和密码进栈,所以这个函数肯定是验证函数
26. Keygen(注册机) Key Generator,是软件注册生成所需的注册码或序列号的程序。Keygen可独立作为一个可执行程序存在,也可以作为程序的一个模块实现。主要功能是利用已经安排好的算法计算出独一无二的序列号(注册码)等。
27. 实验十六 KeygenMe
目标:逆向算法破解注册机。
程序是一个对话框获取字符串,可以在command里输入bp GetDlgItemTextA
或Ctrl+G查找API函数下断点,断在了DLL领空。F9运行,输入字符串,停在断点处,因为在DLL领空,我们不能做任何修改。一路F8走到返回指令,或Alt+F9返回到用户代码,再或在堆栈窗口右键->反汇编窗口中跟随,都可以到达父函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 004012B1 |. 6A 1A push 0x1A ; /Count = 1A (26.) 004012B3 |. 68 38304000 push KeygenMe.00403038 ; |Buffer = KeygenMe.00403038 004012B8 |. 6A 6A push 0x6A ; |ControlID = 6A (106.) 004012BA |. FF75 08 push [arg.1] ; |hWnd = 000905F0 (' Tut selfkeygenMe ',class='lena151') 004012BD |. E8 08010000 call <jmp.&USER32.GetDlgItemTextA> ; \GetDlgItemTextA 004012C2 |. 83F8 00 cmp eax,0x0 004012C5 |. 74 18 je short KeygenMe.004012DF 004012C7 |. 6A 1A push 0x1A ; /Count = 1A (26.) 004012C9 |. 68 38314000 push KeygenMe.00403138 ; |Buffer = KeygenMe.00403138 004012CE |. 6A 6B push 0x6B ; |ControlID = 6B (107.) 004012D0 |. FF75 08 push [arg.1] ; |hWnd = 000905F0 (' Tut selfkeygenMe ',class='lena151') 004012D3 |. E8 F2000000 call <jmp.&USER32.GetDlgItemTextA> ; \GetDlgItemTextA 004012D8 |. 83F8 00 cmp eax,0x0 004012DB |. 74 02 je short KeygenMe.004012DF 004012DD |. EB 17 jmp short KeygenMe.004012F6 004012DF |> 6A 00 push 0x0 ; /Style = MB_OK|MB_APPLMODAL 004012E1 |. 68 62344000 push KeygenMe.00403462 ; |Title = "KeyGen lena151 " 004012E6 |. 68 00304000 push KeygenMe.00403000 ; |Text = " Give me more material hehe!!" 004012EB |. 6A 00 push 0x0 ; |hOwner = NULL 004012ED |. E8 FC000000 call <jmp.&USER32.MessageBoxA> ; \MessageBoxA 004012F2 |. C9 leave 004012F3 |. C2 1000 retn 0x10
刚才是进入了004012BD地址的函数,看到下面还有一个GetDlgItemTextA
,说明还要进入一次刚才的断点处。结合上面整段代码来看,第一次存入第一个框的内容,第二次存入第二个框的内容,两个GetDlgItemTextA
下面的je跳转是判断用户是否输入字符串,没有就跳到MessageBoxA
处。如果两个框都有输入,则跳到下面的代码处。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 004012F6 |> \68 38304000 push KeygenMe.00403038 ; /String = "" 将第一个框的内容压栈 004012FB |. E8 30010000 call <jmp.&kernel32.lstrlen> ; \lstrlenA 获取字符串长度 00401300 |. 33F6 xor esi,esi;esi清零 00401302 |. 8BC8 mov ecx,eax;字符串长度作为循环次数,i 00401304 |. B8 01000000 mov eax,0x1;eax=1 00401309 |> 8B15 38304000 /mov edx,dword ptr ds:[0x403038] ;第一个字符传给edx 0040130F |. 8A90 37304000 |mov dl,byte ptr ds:[eax+0x403037] ;第一个字符传给dl 00401315 |. 81E2 FF000000 |and edx,0xFF 0040131B |. 8BDA |mov ebx,edx 0040131D |. 0FAFDA |imul ebx,edx 00401320 |. 03F3 |add esi,ebx ; KeygenMe.004011E2 00401322 |. 8BDA |mov ebx,edx 00401324 |. D1FB |sar ebx,1 00401326 |. 83C3 03 |add ebx,0x3 00401329 |. 0FAFDA |imul ebx,edx 0040132C |. 2BDA |sub ebx,edx 0040132E |. 03F3 |add esi,ebx ; KeygenMe.004011E2 00401330 |. 03F6 |add esi,esi 00401332 |. 40 |inc eax ;eax++ 00401333 |. 49 |dec ecx ;i-- ; KeygenMe.004011E2 00401334 |.^ 75 D3 \jnz short KeygenMe.00401309 00401336 |. 3B35 38314000 cmp esi,dword ptr ds:[0x403138] ;匹配第二个框的内容,esi从上面的算法得出,所以第一个框和第二个框其实是有关联的 ;这个是注册机,对它进行爆破没有意义,我们想要的是它得出的序列号 0040133C |. 75 15 jnz short KeygenMe.00401353 0040133E |. 6A 00 push 0x0 ; /Style = MB_OK|MB_APPLMODAL 00401340 |. 68 62344000 push KeygenMe.00403462 ; |Title = "KeyGen lena151 " 00401345 |. 68 B8344000 push KeygenMe.004034B8 ; |Text = " That's right. (Self)keygen me now!" 0040134A |. 6A 00 push 0x0 ; |hOwner = NULL 0040134C |. E8 9D000000 call <jmp.&USER32.MessageBoxA> ; \MessageBoxA 00401351 |. /EB 13 jmp short KeygenMe.00401366 00401353 |> |6A 00 push 0x0 ; /Style = MB_OK|MB_APPLMODAL 00401355 |. |68 62344000 push KeygenMe.00403462 ; |Title = "KeyGen lena151 " 0040135A |. |68 86344000 push KeygenMe.00403486 ; |Text = " Error detected! Remove debugger from Hard Drive " 0040135F |. |6A 00 push 0x0 ; |hOwner = NULL 00401361 |. |E8 88000000 call <jmp.&USER32.MessageBoxA> ; \MessageBoxA 00401366 |> \EB 15 jmp short KeygenMe.0040137D 00401368 |> FF75 14 push [arg.4] ; /lParam = 0x20582 0040136B |. FF75 10 push [arg.3] ; |wParam = 6C (108.) 0040136E |. FF75 0C push [arg.2] ; |Message = WM_COMMAND 00401371 |. FF75 08 push [arg.1] ; |hWnd = 000D0262 (' Tut selfkeygenMe ',class='lena151') 00401374 |. E8 3F000000 call <jmp.&USER32.DefWindowProcA> ; \DefWindowProcA 00401379 |. C9 leave 0040137A |. C2 1000 retn 0x10 0040137D |> 33C0 xor eax,eax 0040137F |. C9 leave 00401380 \. C2 1000 retn 0x10
方法一:用Inline patch将算出来的序列号存入数据段,把数据压入提示错误的MessageBoxA,因为我们输入的序列号肯定是错误的,弹框出来肯定是提示错误的弹窗,这样我们压入的数据就可以显示出来。
在数据窗口看到一堆的00,选中一个右键->断点->硬件写入->Dword,比如439000地址。也可以点击M图标在数据段找空白处。重载运行一下确保这个地址没有被使用。
在反汇编窗口找空白代码处,比如40138D。编写汇编代码
1 2 mov dword ptr [439000],esi jmp 401353
在0040133C处修改代码
1 jnz short KeygenMe.00401353 => jmp 40138D
在0040135A处修改代码
1 push KeygenMe.00403486 => push 439000
保存文件,运行,序列号竟然是乱码。
载入OD查看439000地址的HEX数据是3b92d6,也就是上面乱码字符的ASCII码的16进制形式,也可以看esi也是3b92d6。
弹窗复制不了,只能复制OD里面的了!回到原始程序,注册成功。
方法二:逆向算法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 004012F6 |> \68 38304000 push KeygenMe.00403038 ; /String = "v5le0n9" 将第一个框的内容压栈 004012FB |. E8 30010000 call <jmp.&kernel32.lstrlen> ; \lstrlenA 获取字符串长度 00401300 |. 33F6 xor esi,esi;esi清零 00401302 |. 8BC8 mov ecx,eax;字符串长度作为循环次数,i=7 00401304 |. B8 01000000 mov eax,0x1;eax=1 00401309 |> 8B15 38304000 /mov edx,dword ptr ds:[0x403038] ;edx=656c3576,0x76=v,0x35=5,0x6c=l,0x65=e 0040130F |. 8A90 37304000 |mov dl,byte ptr ds:[eax+0x403037] ;dl=0x76=v 00401315 |. 81E2 FF000000 |and edx,0xFF ;给edx的高24位清零,edx=00000076 0040131B |. 8BDA |mov ebx,edx ;ebx=edx=00000076 0040131D |. 0FAFDA |imul ebx,edx ;ebx*=edx='v'*'v' 00401320 |. 03F3 |add esi,ebx ; KeygenMe.004011E2 esi+=ebx,即esi='v'*'v' 00401322 |. 8BDA |mov ebx,edx ;ebx=edx=0x76 00401324 |. D1FB |sar ebx,1 ;右移一位,相当于除以2,ebx=ebx/2='v'/2 00401326 |. 83C3 03 |add ebx,0x3 ;ebx+=3,ebx='v'/2+3 00401329 |. 0FAFDA |imul ebx,edx ;ebx=ebx*edx=('v'/2+3)*'v' 0040132C |. 2BDA |sub ebx,edx ;ebx=ebx-edx=('v'/2+3)*'v'-'v' 0040132E |. 03F3 |add esi,ebx; KeygenMe.004011E2 ;esi+=('v'/2+3)*'v'-'v',esi='v'*'v'+('v'/2+3)*'v'-'v' 00401330 |. 03F6 |add esi,esi;esi=esi*2 00401332 |. 40 |inc eax ;eax++ 00401333 |. 49 |dec ecx ;i-- ; KeygenMe.004011E2 00401334 |.^ 75 D3 \jnz short KeygenMe.00401309
exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <stdio.h> int main () { char name[]="v5le0n9" ; int i; int result=0 ; for (i=0 ; i<7 ; i++) { result += name[i]*name[i]+(name[i]/2 +3 )*name[i]-name[i]; result += result; } printf ("%d" ,result); }
用工具转换不了字符串的话,就直接在数据窗口一段00处打16进制,ASCII码窗口就会显示你所要的字符串。
28. 多态和变形 28.1 多态 多态是第一种对杀毒软件造成严重威胁的技术。一个多态病毒在触发时由解密模块进行解密,在感染时由加密模块进行加密并感染,但其加密模块在每一次的感染中会有所修改。因此,一个仔细设计的多态病毒在每一次感染中没有一部分是相同的。这使得使用病毒特征码进行侦测变得困难。杀毒软件必须在模拟器上对该病毒解密进而侦测该病毒,或是利用加密病毒其统计样板上的分析。
28.2 变形 变形病毒为了避免被杀毒软件通过模拟环境或“蜜罐”系统查杀,在每一次感染都完全将其自身改写。有些变形病毒可以感染多个操作系统,如Simile病毒可以感染Windows的PE文件和Linux的ELF文件。变形病毒要达到可变形,一个变形引擎是必需的。一个变形病毒通常非常庞大且复杂,如Simile病毒包含了14000行汇编语言,其中90%都是变形引擎。
28.3 XOR指令 加密最基础的指令
1 2 如果A xor B = C 则 C xor B = A
29. 实验十七 ReverseMe Tutorial
目标:
Win32汇编编写的程序,32位可执行文件。(用汇编写代码nb)
载入OD,在反汇编窗口就可看到NAG窗口的字符串,看不到的话就搜索字符串定位。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 00401288 > $ 6A 00 push 0x0 0040128A E8 db E8 0040128B EF db EF 0040128C FF db FF 0040128D FF db FF 0040128E FF db FF 0040128F A3 db A3 00401290 30314000 dd ReverseM.00403130 00401294 . BF 11104000 mov edi,ReverseM.00401011 00401299 . E8 71000000 call ReverseM.0040130F 0040129E > E8 6EFDFFFF call ReverseM.00401011 004012A3 . 33C0 xor eax,eax 004012A5 . 50 push eax 004012A6 . 57 push edi ; ReverseM.<ModuleEntryPoint> 004012A7 .^ 7C F5 jl short ReverseM.0040129E 004012A9 .^ 0F84 51FDFFFF je ReverseM.00401000 004012AF . 6A 00 push 0x0 ; /Style = MB_OK|MB_APPLMODAL 004012B1 . 68 7D314000 push ReverseM.0040317D ; |Title = "TutorialNag" 004012B6 . 68 34314000 push ReverseM.00403134 ; |Text = "You need to remove the nag Try to do it in a two byte patch. Regards!" 004012BB . 6A 00 push 0x0 ; |hOwner = NULL 004012BD . E8 92FFFFFF call <jmp.&USER32.MessageBoxA> ; \MessageBoxA
在004012AF处下断点,运行试试,发现还没跑到断点处就弹出NAG,说明这个MessageBoxA
是作者误导人的。观察一下函数的入口处,我们之前的程序大多是55作为函数的开头,这个是6A00开头。而且接下来的不像是指令,更像是数据段存放的数据,F8步过还会跳转(128A跳转到128F),这是因为OD自动帮我们分析这个程序,右键->分析->从模块中删除分析,就可看到汇编指令。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 00401288 > 6A 00 push 0x0 0040128A E8 EFFFFFFF call <jmp.&KERNEL32.GetModuleHandleA> 0040128F A3 30314000 mov dword ptr ds:[0x403130],eax ; ReverseM.00400000 00401294 BF 11104000 mov edi,ReverseM.00401011 00401299 E8 71000000 call ReverseM.0040130F 0040129E E8 6EFDFFFF call ReverseM.00401011 004012A3 33C0 xor eax,eax ; ReverseM.00400000 004012A5 50 push eax ; ReverseM.00400000 004012A6 57 push edi ; ReverseM.<ModuleEntryPoint> 004012A7 ^ 7C F5 jl short ReverseM.0040129E 004012A9 ^ 0F84 51FDFFFF je ReverseM.00401000 004012AF 6A 00 push 0x0 004012B1 68 7D314000 push ReverseM.0040317D ; ASCII "TutorialNag" 004012B6 68 34314000 push ReverseM.00403134 ; ASCII "You need to remove the nag\r\nTry to do it in a two byte patch. \r\nRegards!" 004012BB 6A 00 push 0x0 004012BD E8 92FFFFFF call <jmp.&USER32.MessageBoxA>
从头开始F8步过,发现在0040129E地址处就弹出NAG。下断点进入函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 00401011 $ 33C0 xor eax,eax ; ReverseM.00401218 ;edi=401011 00401013 . 66:C707 6A00 mov word ptr ds:[edi],0x6A ;将0x6A赋值给edi,这个401011地址处的机器码改为6A00 00401018 . 83C7 02 add edi,0x2 0040101B . C707 687D3040 mov dword ptr ds:[edi],0x40307D68 00401021 . 83C7 04 add edi,0x4 00401024 . C607 00 mov byte ptr ds:[edi],0x0 00401027 . 47 inc edi ; ReverseM.00401011 00401028 . C707 68343040 mov dword ptr ds:[edi],0x40303468 0040102E . 83C7 04 add edi,0x4 00401031 . C607 00 mov byte ptr ds:[edi],0x0 00401034 . 47 inc edi ; ReverseM.00401011 00401035 . 66:C707 6A00 mov word ptr ds:[edi],0x6A 0040103A . 83C7 02 add edi,0x2 0040103D . C707 E8300200 mov dword ptr ds:[edi],0x230E8 00401043 . 83C7 04 add edi,0x4 00401046 . C607 00 mov byte ptr ds:[edi],0x0 00401049 . 47 inc edi ; ReverseM.00401011 0040104A . 66:C707 EB44 mov word ptr ds:[edi],0x44EB 0040104F . 83EF 24 sub edi,0x24 00401052 . FFD7 call edi ; ReverseM.00401011 00401054 . E8 C7020000 call ReverseM.00401320 00401059 . E8 64020000 call ReverseM.004012C2 0040105E . EB 15 jmp short ReverseM.00401075
往后走下的每一步,都会修改上一步的代码,光标到40104F地址代码变化如下(因为如果过了0040104F就会弹出NAG了,40104F变成了40104B和401051):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 00401011 . 6A 00 push 0x0 00401013 ? 68 7D304000 push ReverseM.0040307D 00401018 ? 68 34304000 push ReverseM.00403034 0040101D . 6A 00 push 0x0 0040101F . E8 30020000 call <jmp.&USER32.MessageBoxA> 00401024 ? EB 44 jmp short ReverseM.0040106A 00401026 ? 0047 C7 add byte ptr ds:[edi-0x39],al 00401029 ? 07 pop es 0040102A ? 68 34304083 push 0x83403034 0040102F ? C704C6 070047>mov dword ptr ds:[esi+eax*8],0x66470007 00401036 ? C707 6A0083C7 mov dword ptr ds:[edi],0xC783006A 0040103C ? 02C7 add al,bh 0040103E ? 07 pop es 0040103F ? E8 30020083 call 83401274 00401044 ? C704C6 070047>mov dword ptr ds:[esi+eax*8],0x66470007 0040104B ? C707 EB4483EF mov dword ptr ds:[edi],0xEF8344EB 00401051 ? 24 FF and al,0xFF 00401053 ? D7 xlat byte ptr ds:[ebx+al] 00401054 ? E8 C7020000 call ReverseM.00401320 00401059 . E8 64020000 call ReverseM.004012C2 0040105E . EB 15 jmp short ReverseM.00401075
右键->分析->分析代码(快捷键Ctrl+A),发现401011处竟然出现了MessageBoxA
,40104F又回来了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 00401011 $ 6A 00 push 0x0 ; /Style = MB_OK|MB_APPLMODAL 00401013 . 68 7D304000 push ReverseM.0040307D ; |Title = "缙擒邻疫猿竟嶂捃胖撉壑撦以撜淋迵芹謸嶂胖晾筑譂摼国菗幸輷阎撟茌謸谳撘撉能撗是掷撁仪雄潛竟衿菗峙至蕮芮壑翐儡咂勤茌撢罁遮葜澗咕圭圳罁嶂胖晾筑謸诶撁伊菗苷撘撉魄芰谝邼擌葙苁? 00401018 . 68 34304000 push ReverseM.00403034 ; |Text = "贶茡葜肿撉軗林捃胖撉壑撦以竟缌蕮擒撟軗谇撢輷覔悄軗咽侵撁仪雄潛竟嶂砸磷罀崇魄芰谝啐以尘贯洲芘謸芹謸菀該樟苻撉壑撫峙至乐潛竟撔逸撗謸总葜撢輷覔悄軗咽侵罁靡切蹪摼柜魄撝胖潦撥芹至摾苓魄谲輷诶撜谳譂竟竟幺诶撫峙至乐撢罁靡燎撥論覔瞧擒邻?... 0040101D . 6A 00 push 0x0 ; |hOwner = NULL 0040101F . E8 30020000 call <jmp.&USER32.MessageBoxA> ; \MessageBoxA 00401024 . EB 44 jmp short ReverseM.0040106A 00401026 00 db 00 00401027 . 47 inc edi ; ReverseM.00401024 00401028 . C707 68343040 mov dword ptr ds:[edi],0x40303468 0040102E . 83C7 04 add edi,0x4 00401031 . C607 00 mov byte ptr ds:[edi],0x0 00401034 . 47 inc edi ; ReverseM.00401024 00401035 . 66:C707 6A00 mov word ptr ds:[edi],0x6A 0040103A . 83C7 02 add edi,0x2 0040103D . C707 E8300200 mov dword ptr ds:[edi],0x230E8 00401043 . 83C7 04 add edi,0x4 00401046 . C607 00 mov byte ptr ds:[edi],0x0 00401049 . 47 inc edi ; ReverseM.00401024 0040104A . 66:C707 EB44 mov word ptr ds:[edi],0x44EB 0040104F . 83EF 24 sub edi,0x24 00401052 . FFD7 call edi ; ReverseM.00401024 edi=401000 00401054 . E8 C7020000 call ReverseM.00401320 00401059 . E8 64020000 call ReverseM.004012C2 0040105E . EB 15 jmp short ReverseM.00401075
步入call edi
指令,去到401000处。F8走走发现第3行到第8行在执行一个循环。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 00401000 > /B8 00304000 mov eax,ReverseM.00403000 ;eax=403000,存的是代码段的地址 00401005 > |8030 B3 xor byte ptr ds:[eax],0xB3 ;代码段的地址与0xb3异或,即这条指令在修改自身的代码,可以在数据窗口定位到403000观察数据变化 00401008 . |40 inc eax 00401009 . |3D 28314000 cmp eax,ReverseM.00403128 ;403000到403128这段代码分别与0xb3按字节进行异或(解密过程) 0040100E .^|7C F5 jl short ReverseM.00401005 00401010 . |40 inc eax 00401011 $ |6A 00 push 0x0 ; /Style = MB_OK|MB_APPLMODAL 00401013 . |68 7D304000 push ReverseM.0040307D ; |Title = "缙擒邻疫猿竟嶂捃胖撉壑撦以撜淋迵芹謸嶂胖晾筑譂摼国菗幸輷阎撟茌謸谳撘撉能撗是掷撁仪雄潛竟衿菗峙至蕮芮壑翐儡咂勤茌撢罁遮葜澗咕圭圳罁嶂胖晾筑謸诶撁伊菗苷撘撉魄芰谝邼擌葙苁? 00401018 . |68 34304000 push ReverseM.00403034 ; |Text = "贶茡葜肿撉軗林捃胖撉壑撦以竟缌蕮擒撟軗谇撢輷覔悄軗咽侵撁仪雄潛竟嶂砸磷罀崇魄芰谝啐以尘贯洲芘謸芹謸菀該樟苻撉壑撫峙至乐潛竟撔逸撗謸总葜撢輷覔悄軗咽侵罁靡切蹪摼柜魄撝胖潦撥芹至摾苓魄谲輷诶撜谳譂竟竟幺诶撫峙至乐撢罁靡燎撥論覔瞧擒邻?... 0040101D . |6A 00 push 0x0 ; |hOwner = NULL 0040101F . |E8 30020000 call <jmp.&USER32.MessageBoxA> ; \MessageBoxA
而乱码的Title和Text的地址就在403000到403128之中,等循环走完,乱码的Title和Text解密解出来有意义的字符串。
1 2 3 4 5 6 7 00401011 $ 6A 00 push 0x0 ; /Style = MB_OK|MB_APPLMODAL 00401013 . 68 7D304000 push ReverseM.0040307D ; |Title = "TutorialNag" 00401018 . 68 34304000 push ReverseM.00403034 ; |Text = "You need to remove the nag Try to do it in a two byte patch. Regards!" 0040101D . 6A 00 push 0x0 ; |hOwner = NULL 0040101F . E8 30020000 call <jmp.&USER32.MessageBoxA> ; \MessageBoxA
可见作者是不想我们通过字符串搜索查找出破解的线索,通过异或的方式进行加密。我们不能用nop方式消除NAG,因为这是解密之后的代码,如果全都nop掉,解密前的代码会受到影响,程序不能正常执行。并且由于它改的范围有点大,逆向回去不太好逆。我们之前还学过一种方法去除NAG:将它的父句柄修改,让它找不到父句柄,即push 0x1
。
直接修改肯定不行,那从头开始走一遍,不放过经过的每个call指令(API函数的call不跟进),了解它的加解密过程。
00401299地址的call指令:解密401000到401218,xor 0x5A
1 2 3 4 5 6 0040130F /$ B8 00104000 mov eax,ReverseM.00401000 00401314 |> 8030 5A xor byte ptr ds:[eax],0x5A 00401317 |. 40 inc eax ; ReverseM.00401001 00401318 |. 3D 18124000 cmp eax,<jmp.&USER32.BeginPaint> ; 入口地址 401218 0040131D |.^ 7C F5 jl short ReverseM.00401314 0040131F \. C3 retn
0040129E地址的call指令:修改自身代码,401011到401025
1 2 0040104A . 66:C707 EB44 mov word ptr ds:[edi],0x44EB ;在401024位置写入两个数据,也就是修改到401025位置
00401052地址的call指令:对NAG窗口加密的标题和文本进行解密,403000到403128,xor 0xB3
修改自身代码,401011到401027
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 0040106A > \83C7 11 add edi,0x11 ;edi=401011 0040106D . 66:C707 6A0A mov word ptr ds:[edi],0xA6A 00401072 . 83C7 02 add edi,0x2 00401075 > C707 FF353431 mov dword ptr ds:[edi],0x313435FF 0040107B . 83C7 04 add edi,0x4 0040107E . 66:C707 4000 mov word ptr ds:[edi],0x40 00401083 . 83C7 02 add edi,0x2 00401086 . 66:C707 6A00 mov word ptr ds:[edi],0x6A 0040108B . 83C7 02 add edi,0x2 0040108E . C707 FF353031 mov dword ptr ds:[edi],0x313035FF 00401094 . 83C7 04 add edi,0x4 00401097 . 66:C707 4000 mov word ptr ds:[edi],0x40 0040109C . 83C7 02 add edi,0x2 0040109F . C707 E8900000 mov dword ptr ds:[edi],0x90E8 004010A5 . 83C7 04 add edi,0x4 004010A8 . C607 00 mov byte ptr ds:[edi],0x0 004010AB . 47 inc edi ; ReverseM.00401000 004010AC . 66:C707 EB2C mov word ptr ds:[edi],0x2CEB ;edi=401026,写入两个字节,也就是到401027 004010B1 . 83EF 15 sub edi,0x15 004010B4 . FFD7 call edi ; ReverseM.00401000
004010B4地址的call指令->00401021地址的call指令:显示主程序
00401054地址的call指令:重新对NAG窗口加密,403000到403128,xor 0x8D
1 2 3 4 5 6 00401320 /$ B8 00304000 mov eax,ReverseM.00403000 ; ASCII "ReverseMeTutorial" 00401325 |> 8030 8D /xor byte ptr ds:[eax],0x8D 00401328 |. 40 |inc eax 00401329 |. 3D 28314000 |cmp eax,ReverseM.00403128 0040132E |.^ 7C F5 \jl short ReverseM.00401325 00401330 \. C3 retn
00401059地址的call指令:退出程序
现在如果想要修改为push 0x1
,则0040101D地址处的机器码要为6A01。
走进0040129E地址的函数,盯着寄存器窗口的edi值变为1D时,光标走到下面这个位置。
1 2 3 00401035 . 66:C707 6A00 mov word ptr ds:[edi],0x6A 要让它变为 00401035 . 66:C707 6A01 mov word ptr ds:[edi],0x016A
不能直接修改,因为前面还有个解密过程xor 0x5A。1^5A=5B
66:C707 6A00中的00是00401039,在数据窗口Ctrl+G查找地址,修改为5B即可。
还有一种方法绕过MessageBoxA。这个函数的下面有个无条件跳转指令,不如让它提前执行,绕过MessageBoxA。
1 2 3 4 5 6 7 8 00401011 $ 6A 00 push 0x0 ; /Style = MB_OK|MB_APPLMODAL 00401013 . 68 7D304000 push ReverseM.0040307D ; |Title = "TutorialNag" 00401018 . 68 34304000 push ReverseM.00403034 ; |Text = "You need to remove the nag Try to do it in a two byte patch. Regards!" 0040101D 6A 00 push 0x0 0040101F . E8 30020000 call <jmp.&USER32.MessageBoxA> ; \MessageBoxA 00401024 . EB 44 jmp short ReverseM.0040106A
1 2 3 00401011 $ 6A 00 push 0x0 变为 00401011 /EB 57 jmp short ReverseM.0040106A
所以就是让401011地址的机器码变为EB57。继续盯着edi变为1011时
1 2 3 00401013 . 66:C707 6A00 mov word ptr ds:[edi],0x6A ; |Title 要让它变为 00401013 66:C707 EB57 mov word ptr ds:[edi],0x57EB
EB\^5A=B1,57\^5A=0D。在数据窗口的401016地址处修改为B10D。(检查401016地址处是否为6A00,是的话再改,不是的话程序先执行到401016地址是6A00为止)
30. 反调试 好的软件及病毒、木马等都具备反调试功能,要研究它们,必须先掌握它们的反调试技术,才能提升出反反调试的方案。(禁止套娃!)
30.1 Windows API 方法 Win32 提供了两个 API ,IsDebuggerPresent
和 CheckRemoteDebuggerPresent
可以用来检测当前进程是否正在被调试,以IsDebuggerPresent
函数为例,例子如下:
1 2 3 4 5 6 7 8 9 10 #include <stdio.h> #include <Windows.h> int main () { BOOL ret = IsDebuggerPresent (); printf ("ret = %d\n" , ret); }
破解方法很简单,就是在系统里将这两个函数 hook 掉,让这两个函数一直返回 false 就可以了,网上有很多做 hook API 工作的工具,也有很多工具源代码是开放的,所以这里就不细谈了。
30.2 查询进程 PEB 的 BeingDebugged 标志位 当进程被调试器所附加的时候,操作系统会自动设置这个标志位,因此在程序里定期查询这个标志位就可以了,例子如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 bool PebIsDebuggedApproach () { char result = 0 ; __asm { mov eax, fs:[30 h] mov al, BYTE PTR [eax + 2 ] mov result, al } return result != 0 ; }
30.3 查询进程 PEB 的 NtGlobal 标志位 跟第二个方法一样,当进程被调试的时候,操作系统除了修改 BeingDebugged 这个标志位以外,还会修改其他几个地方,其中NtDll 中一些控制堆(Heap)操作的函数的标志位就会被修改,因此也可以查询这个标志位,例子如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 bool PebNtGlobalFlagsApproach () { int result = 0 ; __asm { mov eax, fs:[30 h] mov eax, [eax + 68 h] and eax, 0x70 mov result, eax } return result != 0 ; }
30.4 查询进程堆的一些标志位 这个方法是第三个方法的变种,只要进程被调试,进程在堆上分配的内存,在分配的堆的头信息里,ForceFlags 这个标志位会被修改,因此可以通过判断这个标志位的方式来反调试。因为进程可以有很多的堆,因此只要检查任意一个堆的头信息就可以了,所以这个方法貌似很强大,例子如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 bool HeapFlagsApproach () { int result = 0 ; __asm { mov eax, fs:[30 h] mov eax, [eax + 18 h] mov eax, [eax + 10 h] mov result, eax } return result != 0 ; }
NtQueryInformationProcess
函数是一个未公开的 API,它的第二个参数可以用来查询进程的调试端口。如果进程被调试,那么返回的端口值会是 -1,否则就是其他的值。由于这个函数是一个未公开的函数,因此需要使用 LoadLibrary 和 GetProceAddress的方法获取调用地址,示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 typedef NTSTATUS (WINAPI *NtQueryInformationProcessPtr) ( HANDLE processHandle, PROCESSINFOCLASS processInformationClass, PVOID processInformation, ULONG processInformationLength, PULONG returnLength) ; bool NtQueryInformationProcessApproach () { int debugPort = 0 ; HMODULE hModule = LoadLibrary(TEXT("Ntdll.dll " )); NtQueryInformationProcessPtr NtQueryInformationProcess = (NtQueryInformationProcessPtr)GetProcAddress(hModule, "NtQueryInformationProcess" ); if ( NtQueryInformationProcess(GetCurrentProcess(), (PROCESSINFOCLASS)7 , &debugPort, sizeof (debugPort), NULL ) ) printf ("[ERROR NtQueryInformationProcessApproach] NtQueryInformationProcess failed\n" ); else return debugPort == -1 ; return false ; }
这个也是使用 Windows 的一个未公开函数的方法,你可以在当前线程里调用 NtSetInformationThread,调用这个函数时,如果在第二个参数里指定 0x11 这个值(意思是 ThreadHideFromDebugger ),等于告诉操作系统,将所有附加的调试器统统取消掉。示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 typedef NTSTATUS (*NtSetInformationThreadPtr) (HANDLE threadHandle, THREADINFOCLASS threadInformationClass, PVOID threadInformation, ULONG threadInformationLength) ; void NtSetInformationThreadApproach () { HMODULE hModule = LoadLibrary(TEXT("ntdll.dll" )); NtSetInformationThreadPtr NtSetInformationThread = (NtSetInformationThreadPtr)GetProcAddress(hModule, "NtSetInformationThread" ); NtSetInformationThread(GetCurrentThread(), (THREADINFOCLASS)0x11 , 0 , 0 ); }
30.7 触发异常的方法 这个技术的原理是,首先,进程使用 SetUnhandledExceptionFilter 函数注册一个未处理异常处理函数A,如果进程没有被调试的话,那么触发一个未处理异常,会导致操作系统将控制权交给先前注册的函数A;而如果进程被调试的话,那么这个未处理异常会被调试器捕捉,这样我们的 函数A 就没有机会运行了。
这里有一个技巧,就是触发未处理异常的时候,如果跳转回原来代码继续执行,而不是让操作系统关闭进程。方案是在函数A里修改eip的值,因为在函数A的参数_EXCEPTION_POINTERS
里,会保存当时触发异常的指令地址,所以在 函数A 里根据这个指令地址修改寄存器eip的值就可以了,示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 LONG WINAPI MyUnhandledExceptionFilter (struct _EXCEPTION_POINTERS *pei) { SetUnhandledExceptionFilter ((LPTOP_LEVEL_EXCEPTION_FILTER) pei->ContextRecord->Eax); pei->ContextRecord->Eip += 2 ; return EXCEPTION_CONTINUE_EXECUTION; } bool UnhandledExceptionFilterApproach () { SetUnhandledExceptionFilter (MyUnhandledExceptionFilter); __asm { xor eax, eax div eax } return false ; }
30.8 调用 DeleteFiber 函数 如果给 DeleteFiber 函数传递一个无效的参数的话,DeleteFiber函数除了会抛出一个异常以外,还是将进程的LastError值设置为具体出错原因的代号。然而,如果进程正在被调试的话,这个LastError值会被修改,因此如果调试器绕过了第七步里讲的反调试技术的话,我们还可以通过验证LastError值是不是被修改过来检测调试器的存在,示例代码:
1 2 3 4 5 6 7 8 bool DeleteFiberApproach () { char fib[1024 ] = {0 }; DeleteFiber(fib); return (GetLastError() != 0x57 ); }
31. 实验十八 ReverseMe反调试
这四个都可以利用Keyfile.dat正确打开,但它们利用了不同的反调试技术。
31.1 ReverseMe.A 这个要用原版OD,吾爱太强大了,直接给通过。
F8一路走,弹窗下断点,跟进去。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 004010D3 > E8 23000000 CALL ReverseM.004010FB;弹窗,跟进去 004010D8 . 83FE 08 CMP ESI,8 004010DB . 7C 05 JL SHORT ReverseM.004010E2 004010DD . E8 2C000000 CALL ReverseM.0040110E 004010E2 > 6A 00 PUSH 0 ; |/Style = MB_OK|MB_APPLMODAL 004010E4 . 68 00204000 PUSH ReverseM.00402000 ; ||Title = " Key File ReverseMe" 004010E9 . 68 86204000 PUSH ReverseM.00402086 ; ||Text = "Keyfile is not valid. Sorry." 004010EE . 6A 00 PUSH 0 ; ||hOwner = NULL 004010F0 . E8 72020000 CALL <JMP.&user32.MessageBoxA> ; |\MessageBoxA 004010F5 . E8 BF010000 CALL <JMP.&kernel32.ExitProcess> ; \ExitProcess 004010FA . C3 RETN 004010FB $ E8 D7010000 CALL <JMP.&kernel32.IsDebuggerPresent> ; [IsDebuggerPresent ;调用IsDebuggerPresent函数 00401100 . 83F8 01 CMP EAX,1 ;返回值1表示在调试,0表示无调试 00401103 .^74 DD JE SHORT ReverseM.004010E2 00401105 . C3 RETN 00401106 . 6A 00 PUSH 0 ; /ExitCode = 0 00401108 . E8 AC010000 CALL <JMP.&kernel32.ExitProcess> ; \ExitProcess 0040110D . C3 RETN 0040110E /$ 6A 00 PUSH 0 ; |/Style = MB_OK|MB_APPLMODAL 00401110 |. 68 00204000 PUSH ReverseM.00402000 ; ||Title = " Key File ReverseMe" 00401115 |. 68 51204000 PUSH ReverseM.00402051 ; ||Text = " You really did it! Congratz !!!" 0040111A |. 6A 00 PUSH 0 ; ||hOwner = NULL 0040111C |. E8 46020000 CALL <JMP.&user32.MessageBoxA> ; |\MessageBoxA 00401121 \. E8 93010000 CALL <JMP.&kernel32.ExitProcess> ; \ExitProcess 00401126 . C3 RETN
1 2 3 4 00401103 .^74 DD JE SHORT ReverseM.004010E2 修改为 00401103 90 NOP 00401104 90 NOP
在OD运行也成功了。
31.2 ReverseMe.B 点击运行,OD停在了401106处,执行不下去。
重载,一路F8步过,是从4010D3地址处的call指令去到10FB地址再去到1106卡住。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 004010D3 > E8 23000000 CALL ReverseM.004010FB 004010D8 . 83FE 08 CMP ESI,8 004010DB . 7C 05 JL SHORT ReverseM.004010E2 004010DD . E8 2C000000 CALL ReverseM.0040110E 004010E2 > 6A 00 PUSH 0 ; |/Style = MB_OK|MB_APPLMODAL 004010E4 . 68 00204000 PUSH ReverseM.00402000 ; ||Title = " Key File ReverseMe" 004010E9 . 68 86204000 PUSH ReverseM.00402086 ; ||Text = "Keyfile is not valid. Sorry." 004010EE . 6A 00 PUSH 0 ; ||hOwner = NULL 004010F0 . E8 72020000 CALL <JMP.&user32.MessageBoxA> ; |\MessageBoxA 004010F5 . E8 BF010000 CALL <JMP.&kernel32.ExitProcess> ; \ExitProcess 004010FA . C3 RETN 004010FB /$ E8 D7010000 CALL <JMP.&kernel32.IsDebuggerPresent> ; [IsDebuggerPresent 00401100 |. 83F8 01 CMP EAX,1 00401103 |. 74 01 JE SHORT ReverseM.00401106 00401105 |. C3 RETN 00401106 |> 8925 21114000 MOV DWORD PTR DS:[401121],ESP ;跳到一个错误位置 0040110C \. C3 RETN
同样nop掉关键跳转指令不让它跳转
1 2 3 4 00401103 |. 74 01 JE SHORT ReverseM.00401106 修改为 00401103 90 NOP 00401104 90 NOP
31.3 ReverseMe.C
这一次是直接退出程序了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 004010D3 > E8 23000000 CALL ReverseM.004010FB 004010D8 . 83FE 08 CMP ESI,8 004010DB . 7C 05 JL SHORT ReverseM.004010E2 004010DD . E8 2C000000 CALL ReverseM.0040110E 004010E2 > 6A 00 PUSH 0 ; |/Style = MB_OK|MB_APPLMODAL 004010E4 . 68 00204000 PUSH ReverseM.00402000 ; ||Title = " Key File ReverseMe" 004010E9 . 68 86204000 PUSH ReverseM.00402086 ; ||Text = "Keyfile is not valid. Sorry." 004010EE . 6A 00 PUSH 0 ; ||hOwner = NULL 004010F0 . E8 72020000 CALL <JMP.&user32.MessageBoxA> ; |\MessageBoxA 004010F5 . E8 BF010000 CALL <JMP.&kernel32.ExitProcess> ; \ExitProcess 004010FA . C3 RETN 004010FB /$ E8 D7010000 CALL <JMP.&kernel32.IsDebuggerPresent> ; [IsDebuggerPresent 00401100 |. 83F8 01 CMP EAX,1 00401103 |. 74 01 JE SHORT ReverseM.00401106 00401105 |. C3 RETN 00401106 |> 6A 00 PUSH 0 ; /ExitCode = 0 00401108 \. E8 AC010000 CALL <JMP.&kernel32.ExitProcess> ; \ExitProcess 0040110D . C3 RETN
又是改那个je跳转。
31.4 ReverseMe.D
这次直接变空白了,OD要傻掉了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 004010D3 > E8 23000000 CALL ReverseM.004010FB 004010D8 . 83FE 08 CMP ESI,8 004010DB . 7C 05 JL SHORT ReverseM.004010E2 004010DD . E8 2C000000 CALL ReverseM.0040110E 004010E2 > 6A 00 PUSH 0 ; |/Style = MB_OK|MB_APPLMODAL 004010E4 . 68 00204000 PUSH ReverseM.00402000 ; ||Title = " Key File ReverseMe" 004010E9 . 68 86204000 PUSH ReverseM.00402086 ; ||Text = "Keyfile is not valid. Sorry." 004010EE . 6A 00 PUSH 0 ; ||hOwner = NULL 004010F0 . E8 72020000 CALL <JMP.&user32.MessageBoxA> ; |\MessageBoxA 004010F5 . E8 BF010000 CALL <JMP.&kernel32.ExitProcess> ; \ExitProcess 004010FA . C3 RETN 004010FB /$ E8 D7010000 CALL <JMP.&kernel32.IsDebuggerPresent> ; [IsDebuggerPresent 00401100 |. 83F8 01 CMP EAX,1 00401103 |. 74 01 JE SHORT ReverseM.00401106 00401105 |. C3 RETN 00401106 \> FFE0 JMP EAX ;跳到内存00000001位置,不是程序领空 00401108 . E8 01000000 CALL ReverseM.0040110E 0040110D . C3 RETN
又是改je就可。
32. 实验十九 Debugger Detected
这个连英文原版OD都可以逃过检测,那还是用回熟悉的OD来调试它吧。
这个程序在没有调试器开启的情况下就会显示上图,但它如果探测到调试器启动时,它会弹窗“Your debugger is detected!”
将它载入OD,遇到问题下断点跟进,但一进去是DLL领空,Alt+F9出不来,一直在DLL领空循环。搜索一下我们要进去的API函数DialogBoxParamA
(其实API函数可以不跟进,因为调用的全都是dll领空里面的内容)
32.1 DialogBoxParam 1 2 3 4 5 6 7 int DialogBoxParam ( HINSTANCE hInstance, LPCTSTR IpTemplateName, HWND hWndParent, DLGPROC IPDialogFunc, LPARAM dwlnitParam ) ;
也就是说IPDialogFunc
指向的地址就是实现函数的过程。
1 2 3 4 5 6 0040106C |. 6A 00 push 0x0 ; /lParam = NULL 0040106E |. 68 8C104000 push Debugger.0040108C ; |DlgProc = Debugger.0040108C 00401073 |. 6A 00 push 0x0 ; |hOwner = NULL 00401075 |. 68 04304000 push Debugger.00403004 ; |pTemplate = "KeyGenDialog" 0040107A |. FF35 CC344000 push dword ptr ds:[0x4034CC] ; |hInst = 00400000 00401080 |. E8 C9030000 call <jmp.&user32.DialogBoxParamA> ; \DialogBoxParamA
指向0040108C,所以这个地址就是我们要找的地方。在函数入口处下断点,试运行,又回到了这个断点处,说明这个函数被调用了两次。第一次jnz跳过,第二次jnz不跳过。
1 2 3 4 5 6 7 8 9 0040108C /. 55 push ebp 0040108D |. 8BEC mov ebp,esp 0040108F |. 53 push ebx 00401090 |. 817D 0C 10010>cmp [arg.2],0x110 00401097 |. 75 3B jnz short Debugger.004010D4 00401099 |. 74 01 je short Debugger.0040109C 0040109B |. CC int3 0040109C |> 8BC0 mov eax,eax ; Debugger.0040108C 0040109E |. E8 28010000 call Debugger.004011CB
去到0040109E处弹出错误,跟进函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 004011CB /$ 55 push ebp 004011CC |. 8BEC mov ebp,esp 004011CE |. 81C4 D4FEFFFF add esp,-0x12C 004011D4 |. 53 push ebx 004011D5 |. 56 push esi 004011D6 |. 57 push edi ; Debugger.0040304C 004011D7 |. 6A 00 push 0x0 ; /ProcessID = 0x0 004011D9 |. 6A 0F push 0xF ; |Flags = TH32CS_SNAPALL 004011DB |. E8 3E020000 call <jmp.&kernel32.CreateToolhelp32Snap>; \CreateToolhelp32Snapshot 创建进程快照 004011E0 |. 8945 FC mov [local.1],eax 004011E3 |. 8DB5 D4FEFFFF lea esi,[local.75] 004011E9 |. 8D3D 4C304000 lea edi,dword ptr ds:[0x40304C] 004011EF |. 56 push esi ; /lppe 004011F0 |. FF75 FC push [local.1] ; |hSnapshot = 00000258 004011F3 |. E8 38020000 call <jmp.&kernel32.Process32First> ; \Process32First 004011F8 |. 85C0 test eax,eax 004011FA |. 74 2B je short Debugger.00401227 004011FC |. 8D46 24 lea eax,dword ptr ds:[esi+0x24] 004011FF |. 50 push eax ; /String2 = 00000258 ??? 获取第一个进程名字 00401200 |. 57 push edi ; |String1 = "OLLYDBG.EXE" 00401201 |. E8 3C020000 call <jmp.&kernel32.lstrcmpiA> ; \lstrcmpiA 与OLLYDBG.EXE对比 00401206 |. 85C0 test eax,eax 00401208 |. 74 2A je short Debugger.00401234 ;如果进程名字有OLLYDBG.EXE则弹框说Your debugger is detected !!! 0040120A |> 56 /push esi ; /lppe 0040120B |. FF75 FC |push [local.1] ; |hSnapshot = 00000258 0040120E |. E8 23020000 |call <jmp.&kernel32.Process32Next> ; \Process32Next 00401213 |. 85C0 |test eax,eax 00401215 |. 74 10 |je short Debugger.00401227 00401217 |. 8D46 24 |lea eax,dword ptr ds:[esi+0x24] 0040121A |. 50 |push eax ; /String2 = 00000258 ??? 获取除第一个进程的名字 0040121B |. 57 |push edi ; |String1 = "OLLYDBG.EXE" 0040121C |. E8 21020000 |call <jmp.&kernel32.lstrcmpiA> ; \lstrcmpiA 与OLLYDBG.EXE对比 00401221 |. 85C0 |test eax,eax 00401223 |. 74 0F |je short Debugger.00401234 ;如果进程名字有OLLYDBG.EXE则弹框说Your debugger is detected !!! 00401225 |.^ EB E3 \jmp short Debugger.0040120A 00401227 |> FF75 FC push [local.1] ; /hObject = 00000258 0040122A |. E8 E9010000 call <jmp.&kernel32.CloseHandle> ; \CloseHandle 0040122F |. 5F pop edi ; 0019F9B0 00401230 |. 5E pop esi ; 0019F9B0 00401231 |. 5B pop ebx ; 0019F9B0 00401232 |. C9 leave 00401233 |. C3 retn 00401234 |> 6A 10 push 0x10 ; /Style = MB_OK|MB_ICONHAND|MB_APPLMODAL 00401236 |. 68 11304000 push Debugger.00403011 ; |Title = "Debugger Detected tutorial " 0040123B |. 68 58304000 push Debugger.00403058 ; |Text = "Your debugger is detected !!!" 00401240 |. FF75 08 push [arg.1] ; |hOwner = 00000001 00401243 |. E8 24020000 call <jmp.&user32.MessageBoxA> ; \MessageBoxA 00401248 |. 50 push eax ; /ExitCode = 0x258 00401249 \. E8 D6010000 call <jmp.&kernel32.ExitProcess> ; \ExitProcess
代码分析得很清楚了,在调用CreateToolhelp32Snapshot
函数的时候会拍一张快照,记录本机所有的进程,再将所有的进程名字与OLLYDBG.EXE
对比,如果相同则弹窗有调试器。
破解也很简单,直接绕过0040109E处,改上面的跳转指令即可。
1 2 3 00401097 |. 75 3B jnz short Debugger.004010D4 修改为 00401097 /EB 3B jmp short Debugger.004010D4
33. 万能断点特征码 如果字符串,API函数都不管用,可试试万能断点特征码。
1 F3 A5 8B C8 83 E1 03 F3 A4 E8
在需要操作前的那一刻下断,比如想要输入注册码后点击按钮后可以断下,就先输入好注册码,再下断,再点击按钮。如果先下断再输入注册码,在输入注册码时,就会断下。
XP系统可用,Win10未知是否可行。
34. 不算总结的总结 这些总结都是看Shark恒的逆向教程扫盲扫出来的,我手上也没那么多程序一个个过,所以不懂的话就去看Shark恒的视频,一个视频就10分钟左右,花不了多长时间。
34.1 BC++和Delphi BC++和Delphi都可以使用DeDe(DelphiDecompiler)查看按钮事件(点击按钮后运行的程序地址)。
BC++需要手动修复IAT,在OEP最近的一个call进去,发现全是jmp系统函数,找到第一个jmp系统函数右键,数据窗口跟随,取第一个系统函数的偏移地址作为IAT首地址,大小取1000,将无效指针剪切掉即可。
Shark恒逆向教程 BC++假自校验
Shark恒逆向教程 DephiDecompiler用法
34.2 易语言 用易语言编写的软件可以用push窗体法,易语言按钮事件FF 55 FC 5F 5E,窗体事件FF 25。Ctrl+G去到易语言窗体事件处,附近有个push xxxx,这个就是第一个展示在屏幕的窗口的指令。程序有多个窗体事件,我们的目的就是将我们需要的窗体事件的地址粘贴到那个push里,让它执行我们想要的窗体。Ctrl+F查找命令 push 10001,往下数第二行就是push 窗体事件的地址,将指令复制粘贴到FF25处附近的push xxxx。保存文件。如果不是我们想要的窗体,继续Ctrl+L查找下一个,重复上面操作,直至找到我们想要的窗口。但这种方法容易触发暗桩,比如蓝屏,关机等,所以慎用!
Shark恒逆向教程 push窗体法
34.3 VB VB写的程序,如果这个程序很大,手动下按钮事件可能非常多,所以可以用脚本帮我们下按钮事件断点。如果是重启验证类的软件,要注意程序还没打开就停在断点处的断点,找到是否有cmp ax,0xFFFF,这个直接影响下面的关键跳,所以一定要注意。也就是为什么不能修改返回值为1,因为它是与0xFFFF比较的。如果有些地方还显示未注册,说明有其它跳转到未注册处,搜索命令cmp ax,0xFFFF下断一个个判断,直至全显示已注册。
Shark恒逆向教程 VB通用技巧
如果VB程序打开后自动退出,可以查找所有模块间的调用,找退出程序的函数。VB的退出程序函数是__vbaEnd
。如果程序含有暗桩,可以看 Shark恒逆向教程 取消关机暗桩 。
34.4 重启验证类程序 重启验证类型的软件改关键跳不管用,因为每次重启都要检验注册码,如果注册码不对还是要重新注册,所以要进入到关键call找到注册码或破解。
34.5 网络验证类程序 如果运行到某处卡住,可能在进行网络验证,将这个call指令nop掉,再将紧接着的跳转指令也nop掉。
34.6 快速定位的小技巧 载入OD运行,输入假码后弹出提示框,此时回到OD暂停,Alt+F9返回到用户代码,回到程序点击提示框按钮,OD成功回到用户代码。点击暂停后也可以用调用堆栈的方法回到用户代码。
Shark恒逆向教程 暂停法找注册码
34.7 关于OD的琐碎小知识 如果在OD鼠标与选中行不一致,可以Ctrl + A分析,就正常了。
去除花指令:插件 -> E Junk Code -> MainDlg,VA是执行到花指令的地方,大小改999就行。
二、x64dbg
字符串搜索:CPU(反汇编)窗口->鼠标右键->搜索->选择模块(根据个人需求选择,一般选择当前模块,前提是得先执行到主模块)->字符串
用space改汇编语言
保存修改到文件:反汇编窗口右键->补丁->修补文件