目录

Reverse学习笔记

反汇编工作原理

1、基本反汇编算法

  1. 区分指令与数据,确认文件中包含代码和代码入口点的位置。
  2. 读取指令起始地址的值,查表将二进制操作码与汇编语言助记符对应起来。
  3. 格式化汇编语言并进行输出
  4. 继续反汇编下一条指令

2、如何识别反汇编起点、如何选择下一条指令、何时完成反汇编?

书中介绍了两种算法:线性扫描反汇编递归下降反汇编

1.线性扫描反汇编

​ 线性扫描法通过寻找一条指令结束、另一条指令开始的地方来确定需要反汇编的指令位置。具体方法为:假设程序中标注为代码的程序段内全部是机器语言指令,则从第一个字节开始逐条反汇编。这种算法并不会通过识别分支等非线性指令来了解程序的控制流。

​ 线性扫描法的优点在于它能够完全覆盖程序的所有代码段,但其没有考虑到代码中可能混有数据。线性扫描法无法正确地将嵌入的数据与代码区分开来。

​ GNU调试器(gdb)、微软的WinDbg调试器和odjump的反汇编引擎均采用线性扫描算法。

2.递归下降反汇编

​ 递归下降法强调控制流的概念。控制流根据一条指令是否被另一条指令引用来决定是否对其进行反汇编。

​ 有以下几种指令:

1.顺序流指令

​ 顺序流指令将执行权传递给下一条指令。

2.条件分支指令

​ 当条件为真的时候修改指令指针使其指向分支目标,否则以线性模式执行指令。

​ 递归下降法会反汇编以上两条路径。分支目标指令地址被添加到稍后进行反汇编的地址列表中,推迟其反汇编过程。

3.无条件分支指令

​ 执行权传给一条指令,但这条指令不需要紧跟在无条件分支指令后面。

​ 递归下降法会尝试确定无条件跳转的目标,但如果跳转指令的目标取决于一个运行时的值,这时使用静态分析无法确定跳转目标,将会带来麻烦。

4.函数调用指令

​ 与无条件跳转指令相似,区别在于一旦跳转的函数执行完成,执行权将返还给紧跟在后面的指令。

​ 从被调用函数返回的时候,如果程序运行出现异常,可能导致递归下降算法的失败。函数中的代码有可能会有意篡改函数的返回地址。这样在函数完成的时候,控制权将返回到反汇编器无法返回的地址。

5.返回指令

​ 有时递归下降算法访问了所有的路径,而函数返回指令没有提供接下来将要执行的指令的信息。此时,若程序确实正在运行,则可以从运行时栈顶部获得一个地址,从这个地址开始恢复执行指令。但反汇编器不具备访问栈的能力,因此反汇编过程会突然终止。

​ 递归下降算法具有区分代码与数据的强大能力。IDA是典型的递归下降反汇编器。

栈帧相关

1、栈帧是什么?

​ 栈帧是在程序的运行时栈中分配的内存块,专门用于特定的函数调用。当函数被调用时,可能因为函数需要传参或是局部变量的使用而需要用到内存。编译器通过栈帧使得对函数参数和局部变量进行分配的过程对程序员透明。

2、函数调用的详细步骤

1.  函数调用方将调用函数所需要的任何参数放入调用约定指定的位置。
2.  调用方将控制权转交给被调用的函数。
3.  如有必要,被调用的函数会配置一个栈指针,并保存调用方希望保持不变的任何寄存器值。
4.  被调用的函数为局部变量分配空间。
5.  被调用的函数执行其操作。
6.  被调用的函数完成操作,为局部变量保存的栈空间被全部释放。
7.  将3.中保存的寄存器值恢复至原样。
8.  被调用的函数将控制权还给调用方。根据调用指令,可能清除程序栈中的一个或多个参数。
9.  调用方获得控制权,并对栈进行调整,以将程序栈指针恢复到1.之前的值。

3、调用约定相关

​ 调用约定指定调用方放置函数所需参数的具体位置。调用约定可能要求将参数放置在寄存器、程序栈或寄存器和栈中。同时,在传递参数时,程序栈还需要决定被调用函数完成它的操作后,谁负责删除这些参数。

1.C调用约定

​ C/C++程序中常用的_cdecl修饰符会迫使编译器利用C编译规定,因此这种调用约定别叫做cdecl调用约定。该调用约定规定:调用方按从右到左的顺序将函数参数放入栈中,在被调用的函数完成其操作后,调用方负责从栈中清除参数。

​ 从右到左在栈中放入参数导致最左边(第一个)参数将始终位于栈顶,我们可以轻易找到第一个参数,非常适用于参数数量可变的函数如printf

​ 要求调用函数从栈中删除参数,意味着指令由被调用函数返回后,会立即对程序栈指针进行调整。如果函数参数数量可变,则调用方比被调用方更加清楚参数的个数,从而更加适应这种调整。

2.标准调用约定

​ “标准”是微软为自己的调用约定所起的名称,在函数声明中使用了修饰符_stdcall,为避免混淆,将这种调用约定称为stdcall调用约定。

stdcall调用约定规定:调用方按从右到左的顺序将函数放入栈中,但在函数结束执行的时候,由被调用函数负责删除栈中的函数参数。这要求被调用函数清楚栈中由多少个参数,因此printf这种接受参数个数可变的命令不适用于stdcall调用约定

stdcall调用约定的优点在于可以生成体积稍小、速度稍快的程序。

​ **微团对所有由共享库文件(DLL)输出的参数数量固定的函数使用stdcall约定。

3.x86 fastcall约定

fastcall约定是stdcall约定的一个变体。传递给函数的前两个参数将分别位于ECX和EDXX寄存器中,其余的则以类似stdcall的方式从右往左放入栈中。返回时同样通过被调用方删除栈中的函数参数。

4.C++调用约定

​ C++类中的非静态成员函数函数需要使用this指针,指向用于调用函数的对象。

Microsoft Visual C++提供thiscall调用约定,将this传入ECX寄存器中, 并要求非静态成员函数清除栈中的参数

GNU G++编译器将this看成是非静态成员函数的第一个隐藏参数,其他方便与cdecl调用约定相同。

数据结构的识别

1. 数组

1.全局分配的数组

例如:

1
2
3
4
5
6
7
8
9
int global_array[3];

int main() {
	int idx = 2;
	global_array[0] = 10;
	global_array[1] = 20;
	global_array[2] = 30;
	global_array[idx] = 40;
}

在IDA中,我们将可以看到(部分):

.text:00401000 idx = dword ptr - 4

………

.text:0040100B mov dword_40B720, 10①

.text:00401015 mov dword_40B724, 20②

.text:0040101F mov dword_40B728, 30③

.text:00401029 mov eax, [ebp+idx]

.text:0040102C mov dword_40B720[eax*4], 40④

……….

从①②③看,这个程序似乎使用了4个全局变量,但④对偏移量的计算(eax*4)暗示了全局数组的存在,变量名与①处相同。当函数中使用固定索引(0, 1, 2)的时候,数组元素将以全局变量的形式存在,不提供任何数组存在的证据。但通过对④的观察,我们可以知道数组的基址(dword_40B720,即数组开头的地址)和数组每个元素的大小(eax*4代表每个数组元素占位4)

2.栈分配的数组

编译器以完全相同的方式处理栈分配的数组和全局分配的数组。

1
2
3
4
int main() {
	int stack_array[3];
	int idx = 2;
}

不同的是,如上代码所示,观察栈中idx的位置将有助于确定数组最多包含的元素个数。

3.堆分配的数组

堆分配的数组是由动态内存分配函数(如mallocnew)分配而得。此时编译器需要根据内存分配函数返回的地址值生成对数组的引用。