2007年6月3日星期日

[Paper]逆向分析点滴

Author: void#ph4nt0m.org
Date: 2007.05.15
Publish Date: 2007.06.04
http://www.ph4nt0m.org

征女友!有意者请点击

Index
========================================
1. IDA的使用
2. 编译器优化
3. Viusal C++, Borland Delphi程序的逆向分析
4. 算法识别技巧(常用的加解密算法)

1. IDA使用
=========================================
      工欲善其事,必先利其器.在开始前,先熟悉下IDA的使用.
      [1] 先说一些常用的功能.
      (1)编译器设置(选项->编译器)用来指定IDA所分析文件是什么编译器生成的,如Visual C++,Borland C++,Delphi.这有什么用呢?
      比如下面是Delphi的反汇编片段,如果编译器设定为Viusal C++:

code:00479A8F                 lea     edx, [ebp+var_40]
code:00479A92                 mov     eax, [ebp
+someString]
code:00479A95                 call    @Sysutils@Trim$qqrx17System@AnsiString
code:00479A95
code:00479A9A                 mov     edx, [ebp
+var_40]
code:00479A9D                 lea     eax, [ebp
+someString]
code:00479AA0                 call    @System@@LStrLAsg$qqrpvpxv

      函数参数类型和个数很难一眼看出来.把编译器设定为Delphi后,一目了然:
code:00479A8F                 lea     edx, [ebp+var_40]
code:00479A92                 mov     eax, [ebp
+someString]
code:00479A95                 call    Sysutils::Trim(System::AnsiString)
code:00479A95
code:00479A9A                 mov     edx, [ebp
+var_40]
code:00479A9D                 lea     eax, [ebp
+someString]
code:00479AA0                 call    System::__linkproc__ LStrLAsg(
void *,void *)

      (2)签名(查看->打开下级查看->签名,右键->Apply new signature...添加签名文件)用来指定加载IDA的库函数签名文件.这个是IDA的非常有用的一个功能.逆向分析就像玩填字游戏,从提示线索出发,补完整个词句.而逆向分析的线索是什么呢?库函数就是其中之一.无论是MFC还是Delphi编写的程序,都要用到大量的库函数,而这些库函数就是我们分析的线索之一.IDA的FLIRT库函数签名能够识别出大部分的库函数并在汇编窗口中标注其名称.分析用户函数时,根据这些库函数,我们可以大致确定用户函数的作用.
      但是IDA的库函数签名也不是万能的,也会遗漏或者错误把用户函数识别成库函数.怎么办?
      如果你分析的文件较大或者CPU较慢时,观察导航器(查看->工具栏->导航器->导航器)就会发现IDA扫描分析会给指令,正则函数和库函数"染色",而库函数往往是在连续区域.我的经验是,如果IDA把这个连续区域外的函数标注成库函数,很大可能就是误识别,而把此连续区域内的函数"染"成用户函数,有可能就是遗漏,未识别.

      (3)创建MAP文件(文件->创建文件->创建MAP文件),能够导出库函数名,用户函数(自己命名的),字符串名等(但是不能导出添加的注释信息.哪位知道如何导出IDA的注释,告诉俺一下:-).导出map文件的目的主要是用于ollydbg,因为od自身不具有识别库函数的功能,所以在用od调试delphi程序的时候,往往误入歧途跟进库函数里面,浪费时间.要在od中导入map,需要一个插件LoadMap,它可以很方便的导入map.导入之后,库函数都会标注上名称,调试起来就容易些了.
      另外还有个OD的插件GODUP(IDA签名载入程序),也可以载入IDA的签名文件来识别库函数,也不错.

      [2] IDA的一些常用的快捷键.
      C 如果你发现一段数据没有被IDA识别成代码,那么可以手动转换.
      G 跳转到地址,就是od的Ctrl+G.
      Ctrl+Enter/Esc 就是od的+/-.
      N 重命名.
      U 如果一段数据被IDA错误识别成代码,用U可以撤销转换.
      X 显示交叉参考. 对函数,可查看该函数的所有调用者;对局部变量,可查看函数里涉及到该局部变量的所有指令.
      Y 在函数名上点Y,用来设置函数类型.
      : 添加注释.
      ; 可重复注释,在逆向过程中不断添加注释和重命名分析过的函数很重要,好记性不如烂笔头嘛.
      Shift+/ 在IDA中选中需要计算的数字,Shift+/即可调用计算器计算,比较方便.不用老去Win+R->calc了.


2. 编译器优化
====================================
      编译器优化体现在很多方面,下面举例说明:
      看看下列指令(这是从rc4的密钥初始化中截取的片段):
.text:0041E208                 and     edx, 800000FFh
.text:0041E20E                 jns     
short loc_41E218
.text:0041E210                 dec     edx
.text:0041E211                 or      edx, 0FFFFFF00h
.text:0041E217                 inc     edx
.text:0041E218

      其实这是edx%256,如果求余运算的求余数是2^n,如16,256等,就会优化成上述形式,因为用除法指令进行求余运算需要的时钟周期较多,执行效率不高,所以编译器尽量避免使用除法指令,就像上面所示.

      再看另一个片段:
.text:0040100D                 mov     ecx, eax       ; eax是被除数
.text: 0040100F                mov     eax, 55555556h
.text:
00401014                 imul    ecx            ; 
.text:
00401016                 mov     eax, edx       ;\注意imul是有符号数乘法,这相当于edx>>31
.text:00401018                 shr     eax, 1Fh       ;/,这样eax存放的是符号位
.text:0040101B                 add     edx, eax       ;加上符号位, 对负数相当于向下舍入为一个较小数

      这段实际上是用乘法来模拟除法运算,翻译上面的汇编代码可以得到(var*0x55555556)>>32,即var*0.333333,所以模拟的除法运算是var/3.


Viusal C++, Borland Delphi程序的分析
======================================
      [1] Viusal C++程序的分析
      先将编译器设定为Visual C++,载入签名如vc32rtf,vc32mfc等.待IDA扫描分析完毕就可以开始工作了.
      逆向前,先要说下函数有4种调用约定,即stdcall,cdecl,fastcall,pascal.它们之间的区别体现在参数入栈顺序和清理入栈参数的方式上.
      stdcall的参数入栈顺序是从右到左,且在函数返回前清理入栈参数,在反汇编代码上体现为retn xx,比如压入2个寄存器作为参数,函数返回时就是retn 8.采用stdcall约定的有WINAPI,以及CALLBACK回调函数等.
      cdecl的参数入栈顺序也是从右到左,但是在函数返回后清理入栈参数,在反汇编代码上体现为add esp, xx,比如压入3个寄存器作为参数,返回时就是add esp, 0Ch.采用cdecl约定的有c标准函数库等.
      可能有人要问stdcall和cdecl貌似没什么区别嘛,这样做不是多此一举吗?呵呵,想想最常用的printf函数族,发现什么了?它的入栈参数个数是不固定的,也就是说在编译期才能确定入栈参数,所以用stdcall是无法实现这类不固定参数的函数,只能用cdecl.
      pascal的参数入栈顺序与上面二者相反,是从左到右.在函数返回前清理入栈参数,这与stdcall一致.
fastcall的特点就是采用寄存器来传递部分函数参数,但具体细节依赖于编译器.如Visual C++编译器的fastcall约定是前两个参数依次用ecx,edx,第3个开始push入栈.
      归纳如下:
调用约定入栈参数清理参数入栈顺序
cdecl调用者处理右->左
stdcall函数自己处理右->左
pascal函数自己处理左->右
fastcall函数自己处理依赖于编译器

      直接调用Windows SDK写的C代码编译后是比较好分析的,只要熟悉Windows API基本就能找到和分析自己感兴趣的部分.比较棘手的是MFC一类C++代码的逆向问题.因为出现call dword ptr [esi+XXh]一类的虚函数调用,不能一路很顺利的跟下去.当然,你可以从调用点回溯到类实例创建的地方,从而知道调用的是什么函数.不过这样比较麻烦,投机取巧的办法,是用od到虚函数调用的地方前下断,然后由this指针得到虚函数表地址(this指针指向的类实例存储结构的第一项就是虚函数表地址),偏移XXh得到虚函数地址.

      [2] Borland Delphi程序的分析
      先把编译器设定为Delphi,载入签名d5vcl等.待IDA扫描分析完毕.
      Delphi的函数调用是fastcall.Borland Delphi的fastcall约定是前3个参数依次用eax,edx,ecx传递,第4个开始push入栈.如果是虚函数,第一个参数eax就是this指针. 形象点就是function(eax, edx, ecx, push...).
      举个例子:
code:004531CC                 lea     eax, [ebp+var_30]    
code: 004531CF                 push     eax                     ; 第4个参数
code:004531D0                 lea     ecx, [ebp
+var_38]      ; 第3个参数
code:004531D3                 mov     edx, esi                ; 第 2个参数
code:004531D5                 mov      eax, ebx                ; 第1个参数,this指针
code:004531D7                 mov     edi, [eax]             ; edi 
<- vmt ptr
code:004531D9                 call    dword ptr [edi
+0Ch]    ; 虚函数调用

      先前说过库函数是我们分析一个用户函数的重要线索,因为Delphi对Windows API做了封装,所以在Delphi程序里鲜有直接调用Windows API,基本是对库的调用,所以在分析的时候需要常翻Delphi Help.
      另外,Delphi的字符串处理方式和C库有很大不同,没有熟知的str*函数族.推荐阅读看雪上firstrose整理的"Delphi的内部字符串处理函数/过程不完全列表"一文.

算法识别技巧
===================================
      这里指的识别比较窄,就是一些通用加解密算法的识别.
      算法识别当然依靠算法的特征.其中最明显的特征莫过于通用算法使用的一些初始化数据了.
      比如下面这段代码截取自Blowfish的初始化函数:
code:004513A0                 mov     eax, offset S_Box_blowfish
code:004513A5                 mov     ecx, 1000h
code:004513AA                 call    @Move
code:004513AF                 lea     edx, [esi
+103Ch]
code:004513B5                 mov     eax, offset P_Box_blowfish

      我们跳转到S_Box_blowfish处,可以看到如下的初始化数据(BTW:其实这是pi的16进制表示)
data:0048D308 P_Box_blowfish  dd 243F6A88h, 85A308D3h, 13198A2Eh, 3707344h, 0A4093822h, 299F31D0h, 82EFA98h, 0EC4E6C89h
data:0048D308                         dd 452821E6h, 38D01377h, 0BE5466CFh, 34E90C6Ch, 0C0AC29B7h, 0C97C50DDh, 3F84D5B5h, 0B5470917h
data:0048D308                         dd 9216D5D9h, 8979FB1Bh
data:0048D350 S_Box_blowfish  dd 0D1310BA6h, 98DFB5ACh, 2FFD72DBh, 0D01ADFB7h, 0B8E1AFEDh, 6A267E96h, 0BA7C9045h, 0F12C7F99h

      假设此时你还不知道这个算法是什么,我的做法是把其中一段初始化数据,比如S盒的开始这段0D1310BA6h, 98DFB5ACh, 2FFD72DBh, 0D01ADFB7h提交到http://www.google.com/codesearch去查询.呵,看到什么了?google返回了blowfish的代码.现在你可以初步确定这个算法是Blowfish了.
      有时候仅靠算法的初始化数据是不够,因为在google codesearch命中的结果太多了.比如:
code:0047EEB7                 mov     ecx, 9E3779B9h  ; Magic Number
code:0047EEBC                 mov     esi, ecx
code:0047EEBE                 mov     edx, ecx
code:0047EEC0                 mov     [esp
+18h+delta1], ecx
code:0047EEC4                 mov     [esp
+18h+delta2], ecx
code:0047EEC8                 mov     [esp
+18h+delta3], ecx
code:0047EECC                 mov     [esp
+18h+delta4], ecx
code:0047EED0                 mov     [esp
+18h+delta5], ecx

      这里仅有一个初始化数据9E3779B9,提交到google codesearch命中了600个结果.这时就结合算法的特征了,比如这里将9E3779B9赋值给esi,edx,delta[1~5]共7个变量.我们在google返回的gnubg-0.14.3/lib/isaac.c里面找到了这样的特征:
    70:    r=ctx->randrsl;
           a
=b=c=d=e=f=g=h=0x9e3779b9;  /* the golden ratio */
alpha.gnu.org
/gnu/gnubg/gnubg-0.14.3.tar.gz - GPL - C

      呵呵,原来这是Isaac伪随机数算法.
      这里只是为了说明的方便,所以省略了很多,我当时判断这个算法的时候,将整个Issac_Rand_Initial函数和Issac的c代码粗略的比对了一遍后,在od中断下Issac_Rand_Initial,将输入的种子扔到c代码中编译测试,与od的结果一致,确定为Issac伪随机数算法.

=======[EOF]=======