-
-
暑假前最后一笔:例析SDK程序的逆向(简单的例子,新手可以看看)
-
发表于:
2006-7-15 19:56
8929
-
暑假前最后一笔:例析SDK程序的逆向(简单的例子,新手可以看看)
所谓SDK程序,通常意义上的理解就是用Windows提供的API函数集来开发的程序。这种
程序是最容易逆向的,原因是其中除了一些核心算法外,大部分功能实现都是通过API调用
来完成的,只要识别出这些API,程序的基本结构也就差不多分析出来了。而现在各种反汇
编工具如IDA,W32Dasm以及OllyDbg都能够识别基本的API。当然,这里首先得假定分析者
对Windows程序的架构有着基本的了解,如窗口建立,消息机制,过程回调等等。否则就算
把源代码摆你面前你也看不懂程序在干什么,更不用说逆向了。
写SDK程序常用两种语言,C语言和汇编。开发者喜欢用C,毕竟在操作系统的“母语”
中对API调用实现起来最为简单明了。但从逆向分析者的角度看,用C写成的SDK程序还要通
过C编译器的编译和优化等工作,如果要把它变回源代码,还必须对编译原理有所了解。而
如果是汇编写的,由于汇编指令直接对应着CPU的机器指令,当exe文件被反汇编程序加载
后,出来的可以说基本上就是源代码了。当然事物总有两面性,在汇编级别搞代码混淆也
是最为容易的事,如果作者除了实现正常程序功能以外,还有心在编码上耍一些花招,那
么逆向的时候也需要费些力气才能分析出程序的流程,理解它的意图。对于这样的程序片
段该如何处理,没有一般的规律,只能具体问题具体对待,并且也不是本文的重点。
下面我们通过一个例子来演示如何对SDK程序进行逆向。这是一个汇编写的Crackme。
这个程序被创建的初衷只是让别人分析它的注册算法。不过我们现在打算做得更彻底些,
索性把它的源代码重建出来。
我们选用的工具是OllyDbg。这是一个可以免费使用的反汇编及调试工具。如果你有
IDA,分析起来会更加轻松,可以说易如反掌。但IDA的注册费用不菲。另外一个辅助分析
的工具是Resource Hacker,用它可以提取PE文件的资源,我们主要用它来识别代码中出现
的资源ID。
用OllyDbg载入程序完毕后停在入口点处:
====================以下是代码==================
00401000 >/$ 6A 00 push 0 ; /pModule = NULL
00401002 |. E8 E3000000 call <jmp.&kernel32.GetModuleHandleA> ; \GetModuleHandleA
00401007 |. 6A 00 push 0 ; /lParam = NULL
00401009 |. 68 22104000 push 00401022 ; |DlgProc = v5.00401022
0040100E |. 6A 00 push 0 ; |hOwner = NULL
00401010 |. 68 E9030000 push 3E9 ; |pTemplate = 3E9
00401015 |. 50 push eax ; |hInst
00401016 |. E8 B7000000 call <jmp.&user32.DialogBoxParamA> ; \DialogBoxParamA
0040101B |. 6A 00 push 0 ; /ExitCode = 0
0040101D \. E8 C2000000 call <jmp.&kernel32.ExitProcess> ; \ExitProcess
====================以上是代码==================
这就是主程序的全部内容,包含三个API调用:GetModuleHandle取得本模块句柄后作为
DialogBoxParam的参数,让其建立一个对话框,对话框退出后用ExitProcess结束程序。如
前所述,这里假定你对Windows编程有着基本的了解,如果你还要问诸如一大堆push指令是
干什么用的,GetModuleHandle与GetModuleHandleA有何区别这类问题,那只能说,你应该
先去看一下介绍Windows编程的相关资料。说实话,鄙人也是通过看这些书(与Windows汇
编相关的当然首推罗云彬先生的那一本),从当初的什么都不会,到现在好歹还能一知半
解。
n个push加一个call这种格式的API调用可以方便地转化为MASM中的invoke语句,只是
需要注意参数的分析,因为反汇编出来的代码中立即数参数都是数值形式表示的,但数
值方式不是很直观,象上面的push 3E9这一句,如果我们直接把3E9写到源程序中,谁也
看不出它是代表对话框的资源ID,所以源程序头部需要加上一句“DLG_MAIN equ 3E9h”
(当然最好是用十进制形式的“DLG_MAIN equ 1001”,因为资源脚本中用的是十进制),
在调用此句时代替3E9h把符号常量DLG_MAIN写到源程序中,才能显得比较直观。与此类似
的,GetModuleHandle的参数0可以改成NULL,而ExitProcess的参数0则不需要改,因为这
个参数并没有什么特别的意义。
对话框响应用户动作主要是靠回调过程,每当有消息到达时,此过程被Windows调用以
作出对此消息的处理,DialogBoxParam中的第四个参数提供了该过程的入口地址,按上图
看是在地址00401022处,跟随这个地址来到:
====================以下是代码==================
00401022 /. 55 push ebp
00401023 |. 8BEC mov ebp, esp
00401025 |. 53 push ebx
00401026 |. 56 push esi
00401027 |. 57 push edi
00401028 |. 817D 0C 11010>cmp dword ptr [ebp+C], 111
0040102F |. 75 56 jnz short 00401087
00401031 |. 817D 10 EB030>cmp dword ptr [ebp+10], 3EB
00401038 |. 75 4B jnz short 00401085
0040103A |. 6A 00 push 0 ; /IsSigned = FALSE
0040103C |. 6A 00 push 0 ; |pSuccess = NULL
0040103E |. 68 EA030000 push 3EA ; |ControlID = 3EA (1002.)
00401043 |. FF75 08 push dword ptr [ebp+8] ; |hWnd
00401046 |. E8 8D000000 call <jmp.&user32.GetDlgItemInt> ; \GetDlgItemInt
0040104B |. 50 push eax
0040104C |. 68 BF104000 push 004010BF
00401051 |. E8 48000000 call 0040109E
00401056 |. 83F8 01 cmp eax, 1
00401059 |. 74 16 je short 00401071
0040105B |. 6A 10 push 10 ; /Style = MB_OK|MB_ICONHAND|MB_APPLMODAL
0040105D |. 68 00304000 push 00403000 ; |Title = "Fishing with DiLA v0.5"
00401062 |. 68 17304000 push 00403017 ; |Text = "Sorry, wrong code!"
00401067 |. FF75 08 push dword ptr [ebp+8] ; |hOwner
0040106A |. E8 6F000000 call <jmp.&user32.MessageBoxA> ; \MessageBoxA
0040106F |. EB 14 jmp short 00401085
00401071 |> 6A 40 push 40 ; /Style = MB_OK|MB_ICONASTERISK|MB_APPLMODAL
00401073 |. 68 00304000 push 00403000 ; |Title = "Fishing with DiLA v0.5"
00401078 |. 68 2A304000 push 0040302A ; |Text = "Success! Thank you for playing ;)"
0040107D |. FF75 08 push dword ptr [ebp+8] ; |hOwner
00401080 |. E8 59000000 call <jmp.&user32.MessageBoxA> ; \MessageBoxA
00401085 |> EB 0E jmp short 00401095
00401087 |> 837D 0C 10 cmp dword ptr [ebp+C], 10
0040108B |. 75 08 jnz short 00401095
0040108D |. FF75 08 push dword ptr [ebp+8] ; /hWnd
00401090 |. E8 37000000 call <jmp.&user32.DestroyWindow> ; \DestroyWindow
00401095 |> 33C0 xor eax, eax
00401097 |. 5F pop edi
00401098 |. 5E pop esi
00401099 |. 5B pop ebx
0040109A |. C9 leave
0040109B \. C2 1000 retn 10
0040109E /$ 83C4 10 add esp, 10
004010A1 |. 83EC 0C sub esp, 0C
004010A4 |. 66:35 AFDE xor ax, 0DEAF
004010A8 |. C1C0 10 rol eax, 10
004010AB |. BB CFFFDA3A mov ebx, 3ADAFFCF
004010B0 |. 3BC3 cmp eax, ebx
004010B2 |. 75 08 jnz short 004010BC
004010B4 |. B8 01000000 mov eax, 1
004010B9 |. 33DB xor ebx, ebx
004010BB |. C3 retn
004010BC |> 33DB xor ebx, ebx
004010BE \. C3 retn
004010BF . 58 pop eax
004010C0 . 80C4 20 add ah, 20
004010C3 . F7D8 neg eax
004010C5 . 68 A1104000 push 004010A1
004010CA . C3 retn
====================以上是代码==================
标准子过程入口处都会由编译器加上push ebp和mov ebp, esp建立堆栈框架,并且用
sub esp, xxxx(MASM通常用add esp, -xxxx)预留局部变量空间的指令,在返回时用相反
的指令废除堆栈框架。这里没有sub esp, xxxx说明这个过程没有局部变量。接下来的几个
push属于在源程序中使用了uses伪指令而插入的保护有关寄存器的指令。真正有用的第一
句是cmp dword ptr [ebp+C], 111,为了说明这句是干什么的,注意到建立堆栈框架时,
ebp等于原esp,那么[ebp]中就是刚保存的ebp,[ebp+4]是call该过程时推入的主调程序返
回地址,[ebp+8]以下就是参数了,而对话框回调过程的原型是:
BOOL __stdcall DialogFunc(HWND hWnd,UINT uMsg,WPARAM wParam,LPARAM lParam)
stdcall方式参数入栈顺序是自右向左,那么往右的参数应该存放在高地址中,地址最低的
[ebp+8]自然就是第一个参数hWnd了,依次类推,[ebp+0Ch]是uMsg,[ebp+10h]是wParam,
[ebp+14h]是lParam。那么cmp uMsg, 111h有什么意义呢?这里应该想到对话框过程的常用
结构:
mov eax, uMsg
.if eax == WM_XXXX
...........
也就是说,111h应该是前缀为“WM_”的一个预定义常量,在一些头文件中可以查到具体的
常量是WM_COMMAND。(用IDA的话只要在111h上右击选择“use symbolic constant”即可
快速找到该值)于是,这段代码应该是判断用户是否按下按钮或点击菜单的。具体情况如
何暂时搁下,先根据.if分支的汇编情况(参考罗云彬先生的书中3.5节)找到下一个分支
也就是jnz short 00401087,同样道理把00401087处的cmp dword ptr [ebp+C], 10改成
cmp uMsg, WM_CLOSE。再往下看,这个分支完了之后就恢复最初保存的重要寄存器、释放
堆栈框架并且返回了,因此这是.if的最后一个分支,整个消息处理分支结构中只对
WM_COMMAND和WM_CLOSE进行处理。但返回的时候用了一句xor eax, eax,按照规定,某种
消息若被处理时eax中应该返回1(TRUE),所以这里是有问题的,需要再加一个.else分支,
对不处理的消息才返回FALSE,也就是说:
mov eax, uMsg
.if eax == WM_COMMAND
.....
.elseif eax == WM_CLOSE
.....
.else <======这里加一个.else分支
xor eax, eax
ret
.endif
mov eax, TRUE <======这一句改成现在这样
ret
现在来看各个分支中都执行了什么功能,WM_CLOSE分支很简单,只有一句调用:
invoke DestroyWindow, hWnd
如前所述把WM_COMMAND分支中的dword ptr [ebp+10]改成wParam,由于WM_COMMAND消息的
wParam参数是命令项ID,与ResHacker解析的结果相配合把3EBh定义为符号常量
ID_REGISTER,表示按下Register按钮。而3EAh定义为EDT_KEY,表示序列号输入框。于是
下面的代码作用也清楚了:当Register按钮被按下时,到输入框中取一个整数值。取来的
结果通过40104B到401051运算以后,看eax是否为1,是则显示成功信息,否则显示序列号
错误信息。照例可把MessageBox中最后一个参数的值10改成符号MB_ICONERROR等。注册算
法的分析过程与本文的关系已不大,但为了完整起见,还是继续分析下去。
过程40109E并不是一个标准的子过程。add esp, 10h和sub esp,0Ch的总效果是在esp
上加4,这样就废除了call指令推入的返回地址(但该地址仍在相关内存单元中,并未被
覆盖)。接下来把eax(在输入框中取得的整数值)低16位与0DEAFh异或,并交换高16位
与低16位,看结果是否等于3ADAFFCFh,如果是则把eax置1。但不论比较结果是否相等,
ret指令都不会返回到主调程序中,那么它返回到什么地方呢?注意在主调程序的401051
处的call指令前面有一句push 004010BF,这就是ret指令弹出到eip的值,也就是跳到
4010BF处执行,这里的pop eax弹出的是40104B处压进的值,也就是输入框中取得的整
数,把ah加上20h(注意:这与add eax, 2000h不是一回事,因为加法并不向高16位进
位),取负值,再通过变形的jmp跳回4010A1,执行sub esp,0Ch,现在esp指向的又是刚
进入子程序时被废除的返回地址了!(废除该地址时esp加4,ret定向到4010BF时esp加
4,pop eax又使esp加4,现在esp减0Ch,正好重新使该地址有效)接下去执行与刚才同样
的动作后与3ADAFFCFh比较,相等后将eax置1并返回,这次是真的返回主调程序了!
于是注册码可以通过逆运算倒推:
3ADAFFCFh
FFCF3ADAh 交换高16位与低16位
FFCFE475h 低16位与0DEAFh异或
30108Bh 取反
30FB8Bh ah减20h
注册码=3210123
另外还有一个比较隐蔽的注册码:如果关键比较的时候eax等于1,虽然比较会失败,但
返回到主调程序中仍然会判为成功!这样一来:
00000001h
00010000h 交换高16位与低16位
0001DEAFh 低16位与0DEAFh异或
FFFE2151h 取反
FFFE0151h ah减20h
注册码=4294836561
到这里已经可以还原出整个程序的代码了。再罗嗦几句:逆向是我们自己在写代码,
只要忠实于原来程序的功能,代码具体怎么写则可以随意,大可不必照搬原来的代码。象
这个程序我们只需要判定输入的整数是不是3210123或4294836561就行了,这是两个cmp语
句就能搞定的事情,没必要把原程序中一堆拖泥带水的运算以及变形跳转写到自己的程序
中来。
最后,希望大家看到一个Crackme是汇编写的SDK程序时,不仅仅是分析出它的算法,
最好能把它整个逆向出来。
祝君愉快。
上传文件内容:2.asm MASM源代码(含注册机条件汇编)
1.rc 原程序资源脚本
2.rc 注册机资源脚本
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)