一个简单ReverseMe的完全分析
这个ReverseMe是<<加密与解密>>第2版P89提到的ReverseMe01.exe, 但是书上并没有给出完全分析, 在本文中给出.
阅读本文之前读者应当熟悉窗口程序的基本结构和基本API函数
为了方便新手阅读, 代码尽量保持良好的可读性, 这样做同时也是因为本文是采用纯静态反汇编分析的方法
文章很菜, 还望各位不吝赐教
一. 工欲善其事, 必先利其器
首先准备好工具, 这里我们需要3样工具, 他们是:
1. Stud_PE或PEiD...............获取欲逆向文件的第一手信息, 包括编写语言, 是否加壳等
2. IDA.........................强大的逆向工程工具, 把它用在单纯的破解上可以说是杀鸡用牛刀了
3. MSDN........................我们是新手, 有不懂的地方它永远是最好的老师
二. 知彼知己, 百战不殆
工具准备好了, 可以正式开始了, 不过在开始之前, 我们要问自己以下几个问题:
1.这个程序行为如何?
运行之, 出现一小窗口, 上有一按钮和一文本框, 按钮上有"not reversed" 几个字, 单击按钮会弹出对话框"Ok, for now, mission failed" .
2.这个程序是否加壳?是何语言编写?该语言编写的程序有什么特点?
Peid显示为无壳, 汇编语言编写, 汇编语言编写的程序反汇编结果基本与源码一致, 可读性很好.
3.这个程序用到了哪些API?各是干什么的?
用Peid查看其输入表, 结果如下, 我一一作简要的解释供参考, 详细解释请查阅Msdn
基本上都是很基本的窗口程序API
USER32.dll
DestroyWindow ord:141 rva: 00002010
清除一个窗口并导致一个WM_DESTROY消息
GetWindowTextA ord:347 rva: 00002014
获取窗口标题(或者按钮, 文本框)文本
LoadCursorA ord:407 rva: 00002018
装载光标
LoadIconA ord:411 rva: 0000201C
装载图标
MessageBoxA ord:443 rva: 00002020
不用多说了, 大家都知道
PostQuitMessage ord:477 rva: 00002024
在消息队列里加入一条WM_QUIT消息
DispatchMessageA ord:148 rva: 00002028
向窗口过程分发消息, 窗口程序基本API之一
GetMessageA ord:296 rva: 0000202C
取消息队列消息, 基本函数
SetFocus ord:555 rva: 00002030
设置窗口焦点
SetWindowTextA ord:601 rva: 00002034
设置窗口文本
ShowWindow ord:613 rva: 00002038
设置窗口显隐状态
TranslateMessage ord:637 rva: 0000203C
翻译消息
UpdateWindow ord:651 rva: 00002040
刷新窗口
DefWindowProcA ord:131 rva: 00002044
消息默认处理函数, 基本函数之一
CreateWindowExA ord:88 rva: 00002048
建立窗口
RegisterClassExA ord:495 rva: 0000204C
注册类
SendMessageA ord:528 rva: 00002050
向窗口过程发送消息, 阻塞型函数
KERNEL32.dll
GetModuleHandleA ord:273 rva: 00002000
获取模块句柄, 基本函数
GetCommandLineA ord:182 rva: 00002004
获取命令行
ExitProcess ord:117 rva: 00002008
退出进程
三. 实战
下面我们开始逆向之旅
我们目的是完全逆向这个程序, 由于程序很简单, 从入口来分析是合理的
start proc near
.text:00401000 push NULL ; lpModuleName
.text:00401002 call GetModuleHandleA
.text:00401007 mov hInstance, eax
.text:0040100C call GetCommandLineA
.text:00401011 push SW_SHOWDEFAULT ; nCmdShow
.text:00401013 push lpCmdLine ; lpCmdLine
.text:00401019 push NULL ; hPrevInstance
.text:0040101B push hInstance ; hInstance
.text:00401021 call WinMain
.text:00401026 push eax ; uExitCode
.text:00401027 call ExitProcess
.text:00401027 start endp
很标准的入口初始化过程: 首先获取本模块句柄, 然后获取命令行指针, 再启动WinMain主函数, 最后ExitProcess退出.
不知大家注意到没, 这个入口过程有一个小错误, 看出来了吗?就是那个lpCmdLine参数不正确, 应当push eax才对
我们看看WinMain里有什么
; int __stdcall WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPCSTR lpCmdLine,int nCmdShow)
.text:0040102C WinMain proc near ; CODE XREF: start+21p
.text:0040102C
.text:0040102C hWnd = dword ptr -50h
.text:0040102C stMsg = MSG ptr -4Ch
.text:0040102C stWndClassex = WNDCLASSEXA ptr -30h
.text:0040102C hInstance = dword ptr 8
.text:0040102C hPrevInstance = dword ptr 0Ch
.text:0040102C lpCmdLine = dword ptr 10h
.text:0040102C nCmdShow = dword ptr 14h
.text:0040102C
.text:0040102C push ebp
.text:0040102D mov ebp , esp
.text:0040102F add esp , -50h
.text:00401032 mov [ebp +stWndClassex.cbSize], SIZEOF WNDCLASSEX
.text:00401039 mov [ebp +stWndClassex.style], 3
.text:00401040 mov [ebp +stWndClassex.lpfnWndProc], offset WndProc
.text:00401047 mov [ebp +stWndClassex.cbClsExtra], 0
.text:0040104E mov [ebp +stWndClassex.cbWndExtra], 0
.text:00401055 push [ebp +hInstance]
.text:00401058 pop [ebp +stWndClassex.hInstance]
.text:0040105B mov [ebp +stWndClassex.hbrBackground], 0Fh + 1h
.text:00401062 mov [ebp +stWndClassex.lpszMenuName], offset aWhatmenu ; "WhatMenu"
.text:00401069 mov [ebp +stWndClassex.lpszClassName], offset ClassName ; "SupremeDickHead"
.text:00401070 push IDI_APPLICATION ; lpIconName
.text:00401075 push NULL ; hInstance
.text:00401077 call LoadIconA
.text:0040107C mov [ebp +stWndClassex.hIcon], eax
.text:0040107F mov [ebp +stWndClassex.hIconSm], eax
.text:00401082 push IDC_ARROW ; lpCursorName
.text:00401087 push NULL ; hInstance
.text:00401089 call LoadCursorA
.text:0040108E mov [ebp +stWndClassex.hCursor], eax
以上一段代码初始化窗口类, 类名为"SupremeDickHead" , 窗口程序为WndProc, 由于反汇编代码已经最大程度的整理过了, 所以如果读者熟悉窗口程序的话应该很容易看明白, 我就不作解释了
.text:00401091 lea eax , [ebp +stWndClassex]
.text:00401094 push eax ; WNDCLASSEXA *
.text:00401095 call RegisterClassExA
注册窗口类
.text:0040109A push NULL ; lpParam
.text:0040109C push [ebp +hInstance] ; hInstance
.text:0040109F push NULL ; hMenu
.text:004010A1 push NULL ; hWndParent
.text:004010A3 push 0C8h ; nHeight
.text:004010A8 push 12Ch ; nWidth
.text:004010AD push 0C8h ; Y
.text:004010B2 push 0C8h ; X
.text:004010B7 push 80C80000h ; dwStyle
.text:004010BC push offset Caption ; "ReverseMe #1"
.text:004010C1 push offset ClassName ; "SupremeDickHead"
.text:004010C6 push 0 ; dwExStyle
.text:004010C8 call CreateWindowExA
.text:004010CD mov [ebp +hWnd], eax
创建窗口, 窗口标题为 "ReverseMe #1" , 没有菜单
.text:004010D0 push SW_SHOWNORMAL ; nCmdShow
.text:004010D2 push [ebp +hWnd] ; hWnd
.text:004010D5 call ShowWindow
显示窗口
.text:004010DA push [ebp +hWnd] ; hWnd
.text:004010DD call UpdateWindow
.text:004010E2
刷新窗口
.text:004010E2 MessageLoop: ; CODE XREF: WinMain+DBj
.text:004010E2 push 0 ; wMsgFilterMax
.text:004010E4 push 0 ; wMsgFilterMin
.text:004010E6 push NULL ; hWnd
.text:004010E8 lea eax , [ebp +stMsg]
.text:004010EB push eax ; lpMsg
.text:004010EC call GetMessageA
.text:004010F1 or eax , eax ;收到WM_QUIT消息就退出
.text:004010F3 jz short Exit
.text:004010F5 lea eax , [ebp +stMsg]
.text:004010F8 push eax ; lpMsg
.text:004010F9 call TranslateMessage
.text:004010FE lea eax , [ebp +stMsg]
.text:00401101 push eax ; lpMsg
.text:00401102 call DispatchMessageA
.text:00401107 jmp short MessageLoop
消息循环
.text:00401109
.text:00401109 Exit: ; CODE XREF: WinMain+C7j
.text:00401109 mov eax , [ebp +stMsg.wParam]
.text:0040110C leave
.text:0040110D retn 10h
.text:0040110D WinMain endp
这个WinMain是非常标准的窗口程序主函数, 很可惜, 没有什么特别之处, 我就不多说了, 如果读者还是不理解原理请参考Windows程序设计相关书籍, 论坛置顶帖有介绍的
重点在WndProc函数处, 下面我们一起来看看这个函数; int __stdcall WndProc(HWND hWnd,UINT Msg,WPARAM wParam,LPARAM lParam)
.text:00401110 WndProc proc near ; DATA XREF: WinMain+14o
.text:00401110
.text:00401110 hWnd = dword ptr 8
.text:00401110 Msg = dword ptr 0Ch
.text:00401110 wParam = dword ptr 10h
.text:00401110 lParam = dword ptr 14h
.text:00401110
.text:00401110 push ebp
.text:00401111 mov ebp , esp
.text:00401113 cmp [ebp +Msg], WM_DESTROY
.text:00401117 jnz short loc_401125
;if (Msg == WM_DESTROY)
.text:00401119 push 0 ; nExitCode
.text:0040111B call PostQuitMessage
.text:00401120 jmp return
如果窗口关闭则发送退出消息使主程序退出消息循环
.text:00401125 loc_401125: ; CODE XREF: WndProc+7j
.text:00401125 cmp [ebp +Msg], WM_CREATE
.text:00401129 jnz short loc_4011A1
else if (Msg == WM_CREATE)
.text:0040112B push 0 ; lpParam
.text:0040112D push hInstance ; hInstance
.text:00401133 push ID_EDIT ; nControlID
.text:00401135 push [ebp +hWnd] ; hWndParent
.text:00401138 push 19h ; nHeight
.text:0040113A push 0C8h ; nWidth
.text:0040113F push 23h ; Y
.text:00401141 push 32h ; X
.text:00401143 push 50800080h ; dwStyle
.text:00401148 push NULL ; lpWindowName
.text:0040114A push offset aEdit ; "edit"
.text:0040114F push WS_EX_CLIENTEDGE ; dwExStyle
.text:00401154 call CreateWindowExA
.text:00401159 mov hEdit, eax
在窗口中创建一个Edit控件
.text:0040115E push hEdit ; hWnd
.text:00401164 call SetFocus
并设置焦点
.text:00401169 push NULL ; lpParam
.text:0040116B push hInstance ; hInstance
.text:00401171 push ID_BUTTON ; nControlID
.text:00401173 push [ebp +hWnd] ; hWndParent
.text:00401176 push 19h ; nHeight
.text:00401178 push 8Ch ; nWidth
.text:0040117D push 46h ; Y
.text:0040117F push 4Bh ; X
.text:00401181 push 50000001h ; dwStyle
.text:00401186 push offset aNotReversed ; "Not Reversed"
.text:0040118B push offset aButton ; "button"
.text:00401190 push 0 ; dwExStyle
.text:00401192 call CreateWindowExA
.text:00401197 mov hButton, eax
在文本编辑控件的下面创建一个按钮
.text:0040119C jmp return
以上一段是处理WM_CREATE消息的代码, 亦即窗口初始化代码. 这段代码创建了一个文本编辑控件和一个按钮, 就是主程序上所看到的两个控件
.text:004011A1
.text:004011A1 loc_4011A1: ; CODE XREF: WndProc+19j
.text:004011A1 cmp [ebp +Msg], WM_COMMAND
.text:004011A8 jnz loc_401317
else if (Msg == WM_COMMAND)
以下是处理WM_COMMAND消息的代码, 冗长复杂, 后面我还会详细分析
.text:004011AE mov eax , [ebp +wParam]
.text:004011B1 cmp [ebp +lParam], 0 ; Is this message from a control?
.text:004011B5 jnz loc_4012F6
.text:004011BB cmp ax , ID_BUTTON ; Yes, it is from a control
.text:004011BF jnz short loc_4011EA
.text:004011C1 push offset String ; "Edit Box Is Good Mmmmkay ?"
.text:004011C6 push hEdit ; hWnd
.text:004011CC call SetWindowTextA
.text:004011D1 push 0 ; lParam
.text:004011D3 push '#' ; wParam
.text:004011D5 push WM_KEYDOWN ; Msg
.text:004011DA push hEdit ; hWnd
.text:004011E0 call SendMessageA
.text:004011E5 jmp return
.text:004011EA
.text:004011EA loc_4011EA: ; CODE XREF: WndProc+AFj
.text:004011EA cmp ax , ID_EDIT
.text:004011EE jnz short loc_401202
.text:004011F0 push NULL ; lpString
.text:004011F2 push hEdit ; hWnd
.text:004011F8 call SetWindowTextA
.text:004011FD jmp return
这一段很有趣, 程序首先判断消息是否来自一个子窗口控件(什么?你问为什么?那么我问你, WM_COMMND消息的lParam是什么含义? 对, 发送消息的控件句柄, 如果不是来自控件这个参数就是0啦), 如果不是则分别处理消息来自按钮,文本控件和一个未知ID的情况, 如果消息来自按钮则把Edit控件的文本设置为:"Edit Box Is Good Mmmmkay ?" , 然后向Edit控件发送一个WM_KEYDOWN消息. 如果消息来自Edit, 程序会清除Edit控件的内容
问题是, 明知消息不是来自控件却要处理消息来自控件的情况, 这是干什么? 还有....
.text:00401202
.text:00401202 loc_401202: ; CODE XREF: WndProc+DEj
.text:00401202 cmp ax , ID_WHATID
.text:00401206 jnz loc_4012EC
.text:0040120C mov eax , offset loc_40123B
.text:00401211 jmp eax
ID_WHATID(=3)是什么控件的ID? 不得而知. 如果这里跳走的话就会结束程序(下面会调用DestroyWindow函数)
到现在已经有很多疑问了, 我们不管, 继续向下看,
.text:00401213 push 200h
.text:00401218 push offset szTextBuffer ; lpString
.text:0040121D push hEdit ; hWnd
.text:00401223 call GetWindowTextA
.text:00401228 push MB_OK ; uType
.text:0040122A push offset Caption ; "ReverseMe #1"
.text:0040122F push offset szTextBuffer ; lpText
.text:00401234 push NULL ; hWnd
.text:00401236 call MessageBoxA
这一段代码是永远也不可能执行了(jmp eax ), 它作用是获取Edit的内容并用一个MessageBox把获取的内容显示出来
.text:0040123B
.text:0040123B loc_40123B: ; DATA XREF: WndProc+FCo
.text:0040123B push MB_OK ; uType
.text:0040123D push offset Caption ; "ReverseMe #1"
.text:00401242 push offset aOkayForNowMiss ; "Okay, for now, mission failed !"
.text:00401247 push NULL ; hWnd
.text:00401249 call MessageBoxA
显示一个"Okay, for now, mission failed !" 对话框, 就是我们运行程序时看到的
.text:0040124E push 200h ; nMaxCount
.text:00401253 push offset szTextBuffer ; lpString
.text:00401258 push hEdit ; hWnd
.text:0040125E call GetWindowTextA
.text:00401263 mov ecx , eax
这时ecx存放有字符个数, 因为GetWindowTextA返回值(eax )为字符个数
.text:00401265 xor edx , edx ; edx 清零
.text:00401267 or ebx , 0FFFFFFFFh
.text:0040126A inc ebx ; ebx 清零
.text:0040126B mov eax , offset szTextBuffer
让eax指向字符串
怎么?要做字符串处理了呀, 此时eax指向字符串, ebx , edx为0
.text:00401270
.text:00401270 loc_401270: ; CODE XREF: WndProc+183j
.text:00401270 mov bl , [eax ]
.text:00401272 cmp byte ptr [eax ], 0
.text:00401275 jz short loc_401295
.text:00401277 cmp byte ptr [eax ], '0'
.text:0040127A jl short loc_401295
.text:0040127C cmp byte ptr [eax ], '9'
.text:0040127F ja short loc_401295
验证字符串合法性, 如果发现了空字符串或者有除了0~9之外的字符就提示错误
.text:00401281 add edx , ebx ;ebx和edx相加
.text:00401283 shl edx , 14h ;相加的结果左移20位
.text:00401286 add edx , ecx ;再和字符个数相加
.text:00401288 inc eax
.text:00401289 add ecx , -1 ;处理下一个字符
.text:0040128C test edx , edx
.text:0040128E jnz short loc_401291
.text:00401290 inc edx ;edx如果是0的话则设置为1
.text:00401291
.text:00401291 loc_401291: ; CODE XREF: WndProc+17Ej
.text:00401291 test ecx , ecx ;结束否?
.text:00401293 jnz short loc_401270
以上一段算法可以用C语言描述为:int i = NumberOfChars;
char* str = StringFromEditControl; int tmp = 0;
do {
tmp += (int ) *str ;
tmp <<= 20;
tmp += i;
str ++;
i--;
if (tmp == 0) tmp++;
}
while (i != 0); .text:00401295
.text:00401295 loc_401295: ; CODE XREF: WndProc+165j
.text:00401295 ; WndProc+16Aj ...
.text:00401295 test edx , 0
.text:0040129B jg short loc_4012B6
.text:0040129D jz short loc_4012CB
.text:0040129F push MB_OK ; uType
.text:004012A1 push offset Caption ; "ReverseMe #1"
.text:004012A6 push offset aThatNotANumber ; "That not a number ! CHEATHER !"
.text:004012AB push NULL ; hWnd
.text:004012AD call MessageBoxA
.text:004012B2 leave
.text:004012B3 retn 10htest edx , 0 ??!! 这意味着jg永远不能执行而jz会执行, 不过实际上这两个跳转之后都有一句jmp到ret处, 实际上都一样
.text:004012B6
.text:004012B6 loc_4012B6: ; CODE XREF: WndProc+18Bj
.text:004012B6 jmp short locret_4012E0
.text:004012B8 push MB_OK
.text:004012BA push offset Caption ; "ReverseMe #1"
.text:004012BF push offset aGoodNumber ; "Good Number"
.text:004012C4 push NULL ; hWnd
.text:004012C6 call MessageBoxA
.text:004012CB
.text:004012CB loc_4012CB: ; CODE XREF: WndProc+18Dj
.text:004012CB jmp short locret_4012E0
.text:004012CD push MB_OK
.text:004012CF push offset Caption ; "ReverseMe #1"
.text:004012D4 push offset aBadNumber ; "Bad Number"
.text:004012D9 push NULL ; hWnd
.text:004012DB call MessageBoxA
.text:004012E0
.text:004012E0 locret_4012E0: ; CODE XREF: WndProc:loc_4012B6j
.text:004012E0 ; WndProc:loc_4012CBj
.text:004012E0 leave
.text:004012E1 retn 10h
.text:004012E4 pop ebp
.text:004012E5 push eax
.text:004012E6 leave
.text:004012E7 retn 10h
.text:004012EA jmp short return
.text:004012EC
.text:004012EC loc_4012EC: ; CODE XREF: WndProc+F6j
.text:004012EC push [ebp +hWnd] ; hWnd
.text:004012EF call DestroyWindow
.text:004012F4 jmp short return
刚才提到过了
.text:004012F6
.text:004012F6 loc_4012F6: ; CODE XREF: WndProc+A5j
.text:004012F6 cmp ax , ID_BUTTON
.text:004012FA jnz short loc_401315
.text:004012FC shr eax , 10h
.text:004012FF or ax , ax
.text:00401302 jnz short loc_401315
.text:00401304 push 0 ; lParam
.text:00401306 push ID_WHATID ; wParam
.text:00401308 push WM_COMMAND ; Msg
.text:0040130D push [ebp +hWnd] ; hWnd
.text:00401310 call SendMessageA
.text:00401315
.text:00401315 loc_401315: ; CODE XREF: WndProc+1EAj
.text:00401315 ; WndProc+1F2j
.text:00401315 jmp short return
这里终于知道ID_WHATID的来源了, 原来是这个SendMessage函数!
.text:00401317
.text:00401317 loc_401317: ; CODE XREF: WndProc+98j
.text:00401317 push [ebp +lParam] ; lParam
.text:0040131A push [ebp +wParam] ; wParam
.text:0040131D push [ebp +Msg] ; Msg
.text:00401320 push [ebp +hWnd] ; hWnd
.text:00401323 call DefWindowProcA
.text:00401328 leave
.text:00401329 retn 10h
.text:0040132C
.text:0040132C return: ; CODE XREF: WndProc+10j
.text:0040132C ; WndProc+8Cj ...
.text:0040132C xor eax , eax
.text:0040132E leave
.text:0040132F retn 10h
.text:0040132F WndProc endp
四. 回顾
程序结束了, 现在回过头来看看程序中的微妙之处.
程序首先用标准的方法建立了一个窗口, 在窗口函数中, 程序处理了WM_CREATE, WM_DESTROY和WM_COMMAND这3个消息. 在处理WM_CREATE时, 程序分别建立了一个Edit控件和一个按钮; 在WM_COMMAND消息的处理中, 如果程序侦测到消息是按钮发出的, 则以ID_WHATID(等于3)为WPARAM, 0为LPARAM向自身发送WM_COMMAND消息. 由于在WM_COMMAND消息处理的开头, 程序会判断消息的来源, 仅当消息不是从子窗口控件发出时才处理(004011AE和004011FD之间的代码实际上不会执行), 所以00401213到004012EA的这段代码(就是那段算法代码)名义上是处理ID为ID_WHATID的"控件"发出的消息, 实质上是按钮消息处理过程的延续而已
关于那段算法代码, 由于不管结果是多少最后都要和0做and运算, 结果毫无疑问都是0, 亦即那句jz将肯定会执行. 所以这段算法是没有多大意义的. 但是这个程序毕竟是ReverseMe, 就是要求大家把不正确的内容修改正确. 至于到底怎么修改就看大家自由发挥了.
明白了这些, 相信大家再看<<加密与解密>>第2版P89~P91内容时应该觉得不难了吧
[注意]APP应用上架合规检测服务,协助应用顺利上架!