一个简单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 10h
test 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内容时应该觉得不难了吧
[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。