逆向分析之核心代码的分析
个人感觉分析正常的(没有花指令)反汇编代码最大的挑战就是难以分清反汇编出的代码是用户的代码还是库的代码,还是编译器插入的代码。克服这一关需要丰富的经验。因此,逆向的入门大多数是难倒在这一关上。
1、识别一个函数
程序都是以函数为一个单位。函数内的多条代码使函数完成了特定的功能,在分析当中也是以一个函数作为一个分析单位。在分析时,应当尽可能多的分析出这个函数所完成的具体功能。
反汇编中的函数一般都有如下格式:
1.1 第一种函数格式
push ebp
mov esp,ebp
....
pop ebp
ret
1.2 第二种函数格式
这种函数没有特定的格式,但可以肯定是在函数的末尾肯定有ret语句。
2、识别函数的返回值,形参,局部变量
- 函数一般是使用eax寄存器来保存返回值。但是一些编译器可能用其他的方式来传递返回值,因此,在分析程序的前期,需要确定该程序是用什么编译器进行编译的(通过PEId可以产看),才能确定其返回值是用什么方式进行传递的。
- 函数的形参个数可以通过函数平衡多少字节的栈空间来推理出。因为存在着许多的传参方式,所以这种方法并不是绝对正确的,因此要先观察出一个函数的传参方式是何种方式。
和函数的传参方式息息相关的函数调用方式:
不同的调用方式平衡堆栈的方式是不同的。C方式调用时,是调用者进行堆栈平衡。传参时,一般是自右往左将实参入栈。在32位下实参一般是4个字节一个,但也有例外的情况(比如传递一个结构体变量)。
在平衡堆栈时,一般是在函数调用语句后由调用者平衡堆栈。
例如:
call fun ; 调用一个函数
add esp , 4 ; 平衡堆栈.
stdCall 是被调用自己平衡
stdCall方式调用时, 形参入栈的方式和 C方式相同, 不同就在于平衡堆栈的形式. std方式调用是由被调用的函数自己本身来平衡堆栈也就是在函数返回的时候平衡堆栈.
ret 0x4 // stdCall平衡堆栈的实例
如果函数传入形参每个都是 4个字节的, 那么就可以算出有多少个形参公式就是 :
形参个数 = 平衡堆栈用到的字节数 / 4
函数形参在汇编语言中随着调用方式不同其表现形式也跟着不同.
C方式和 std方式调用时,形参的表现形式为: [ebp+8] ,即 ebp+一个数字去索引.
附注:
[ebp] : 保存的一般是上层函数的栈底
[ebp+4] : 保存的一般是返回地址
[ebp+8] : +8 之后保存的是形参.
一种函数格式的前提之上的. 在第二种函数格式下,函数的形参用的就不是这种索引方式了.
函数的局部变量
在第一种函数格式下, 局部变量的表现形式为: [ebp - XXX]
在第二种函数格式下, 局部变量可能是[esp + XXX].
3.识别出分支选择语句
• 在C语言中, 使 if语句产生分支的可以是由丰富的运算符来组成的表达式 , 在汇编中, 没有这么多的运算符, 表达式也不能太复杂, 但在汇编中有标志位, 标志位与条件跳转的组合实现了 C语言中的逻辑运算符(>,<,<=,>=,&&,||).
• 因此, 汇编中凡是出现条件跳转语句的,条件跳转语句之上必定有会使标志位产生变化的指令(一般是 cmp, test等),这两种语句的组合就形成 C语言当中的分支选择语句.
比如:
01285000 837D FC 00 cmp dword ptr ss:[ebp-0x4],0x0
01285004 74 FA +-{ je 0128500B ;当 zf 标志位==1 这条语句才执行
01285006 B8 01000000 | mov eax,0x1
0128500B C3 +-> ret
这样的反汇编代码用 C 语言表示:
if([ebp-4] == 0 )
return ;
eax = 1;
return ;
4. 识别出循环控制语句
循环语句是在流程分支选择语句的前提上形成的.如果在分支选择语句中发现了有向上的短跳转, 且在向上的短跳转的范围中发现有累加的寄存器或内存(一般是寄存器),那么这种汇编代码一般都是循环语句.
当然,汇编语句中,带有 rep前缀的指令也是一种循环指令.
例如:
01285000 B8 00000000 mov eax,0x0
01285005 83F8 10 +—> cmp eax,0x10
01285008 7D 03 | +-{ jge 0128500D ; 向下跳转
0128500A 40 | | inc eax ; 累加寄存器
0128500B EB F3 +-+-{ jmp 01285000 ; 向上跳转
0128500D C3 +-> ret
5. 数组的识别
数组的空间是连续的,并且没个元素的大小都是相等的, 因此一般在访问数组元素的时候, 一般都是通过基址加偏移的形式去访问的. 因此, 在反汇编中, 如果碰到一些基址变址或相对基址变址访问内存时, 基址一般都是数组的首地址.
例如:
01285000 B8 00000000 mov eax,0x0
01285005 8D35 00004000 lea esi,dword ptr ds:[0x400000] ;取基址
0128500B 8D3D 10004000 lea edi,dword ptr ds:[ebp+4] ;取基址
01285011 83F8 10 +—> cmp eax,0x10
01285014 74 EA | +-{ je 0128501F
01285016 8B0C30 | | mov ecx,dword ptr ds:[eax+esi] ;基址加偏移
01285019 890C38 | | mov dword ptr ds:[eax+edi],ecx ;基址加偏移
0128501C 40 | | inc eax
0128501D EB F2 +-+-{ jmp 01285011
0128501F C3 +-> ret
6. 结构体的识别
结构体和数组是类似的, 结构体中的成员都是在以一个基址为偏移的连续空间中. 但
结构体中各个成员的字节数可能是不相等的,因此和数组是有区别的.一般在访问结构体中
的成员时, 是以结构体的首地址作为基址,然后加上成员在结构体中的偏移进行访问.
例如有这样一个结构体:
struct NODE
{
char ch;
char array[8];
int num;
}NODE;
假设在函数中定义了一个结构体变量: [ebp+4], 那么访问各个成员的形式是这样的(假设结构体以 4字节对齐):
lea ecx , [ebp+0x4] ; 取结构体变量的首地址
mov eax , [ecx] ; 访问 ch的值
lea eax , [ecx+0x4] ; 得到 array成员的首地址
mov eax , [ecx+0xc] ; 访问 num成员的值