文档库 最新最全的文档下载
当前位置:文档库 › 暑假前最后一笔:例析SDK程序的逆向(简单的例子,新手可以看看)

暑假前最后一笔:例析SDK程序的逆向(简单的例子,新手可以看看)

暑假前最后一笔:例析SDK程序的逆向(简单的例子,新手可以看看)
标 题: 暑假前最后一笔:例析SDK程序的逆向(简单的例子,新手可以看看)
作 者: 冲天剑
时 间: 2006-07-15 19:56
附 件: fwdv5.zip
链 接: https://www.wendangku.net/doc/3b15602077.html,/showthread.php?threadid=29055
详细信息:
所谓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 ; \GetModuleHandleA
00401007 |. 6A 00 push 0 ; /lParam = NULL
00401009 |. 68 22104000 pu

sh 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 ; \DialogBoxParamA
0040101B |. 6A 00 push 0 ; /ExitCode = 0
0040101D \. E8 C2000000 call ; \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 ; \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 ; \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 ; \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 ; \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_COM

MAND消息的
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 注册机资源脚本
上传文件内容: 2.zip



?2000-2007 https://www.wendangku.net/doc/3b15602077.html, All rights reserved.
By PEDIY

相关文档