Author: void#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]=======