0%

《加密与解密》第四章 逆向分析技术

0x00 写在开头

本文是我学习《加密与解密》(第四版)过程中的笔记与心得。仅用于记录,不保证内容的绝对正确性,但如果能够对后来人有所帮助则是再好不过。

前面三章讲的都是软件安装和操作,没什么记录的必要。直接从第四章开始,目前已经有了一定的汇编和c/c++语言基础,读起来难度不大。

更新记录

  • 2019-05-09 0x01更新完毕
  • 2019-05-12 0x02更新完毕
  • 2019-05-12 全文更新完毕

    0x01 32位软件逆向技术

    启动函数

    Win32程序中必须包含一个WinMain函数,但第一句被执行的代码不是该函数,而是启动函数的相关代码。启动函数的作用为检索命令指针、环境变量指针,初始化全局变量、内存堆栈等,也就是为后续代码准备运行环境。

执行完毕后就会调用入口函数,如main,开始程序的执行。

函数

学过C语言的都知道函数是个什么,这里就不赘述了。在汇编中,函数通过call来调用,例如call function但是也可以通过寄存器来动态计算function的地址,例如call [ax*4+10h]

函数传参

函数传参则可以有多种方式:通过栈;通过寄存器;通过全局变量。

其中通过全局变量没什么好说的,就是利用全局变量存放参数,以前写c程序的时候偶尔也用过。

通过栈传参时,C规范约定了调用者来平衡栈,在调用时把参数从左到右压入栈中。

平衡栈就是保证栈在调用前后的状态一致,防止内存浪费和错误访问。

同时注意,不同的约定对调用时栈的操作也不同,需要区分。

堆栈传参过程

  1. 调用者把参数和结束后的返回地址先后压栈 push eip

  2. 子程序使用ebp+偏移量访问参数

  3. 子程序ret时,cpu执行 pop eip,返回调用位置。

同时注意,指令enter和leave能够辅助进行栈的维护。enter相当于

1
2
3
push ebp
mov ebp,esp
sub esp xxx

leave则等价于
1
2
add esp,xxx
pop ebp

由于我之前稍微了解了一点pwn相关的知识,所以对函数调用和缓冲区溢出啥的还是有点了解的,不细讲了。

寄存器传参

寄存器传参没有标准,不同的编译器的开发者对传参的方式都有自己的一套方法。

函数返回值

一般情况下返回值通过eax寄存器送回,如果eax不足以容纳,返回值的高32位会放入edx寄存器中。
如果是引用形式传参的话,就和C语言中几乎一样了。

数据结构

确定数据结构后能够为确定算法减轻工作量。

局部变量

生命周期在被调用函数内部,一般在栈和寄存器中进行分配,函数返回时会销毁数据。

利用栈存放

函数调用时用sub esp,x来为变量分配空间,用[ebp-xxx]访问局部变量。注意,有时编译器会通过push reg来代替sub esp,4。根据之前利用栈传参时的约定,函数参数相对与ebp的偏移量是正的,通过[ebp+xxx]访问,在逆向过程中可以通过正负号来轻易区分。
当函数退出时,用add esp,x平衡栈,或者直接mov esp,ebp(如果进入之前保存了)

顺便一说,我感觉现在见过的函数调用开场白和收场白有点类似于

1
2
3
4
5
6
7
8
push ebp
mov ebp,esp
sub esp,xxxx
...
...
mov esp,ebp
pop ebp
ret

其中ret相当于pop eip,也就是说如果栈中存放的局部变量产生了溢出,就能覆盖掉栈中保存的ebp下面的ret的值,从而操作eip,修改程序流程。这就是栈溢出的原理。

利用寄存器存放

八个寄存器中除了与栈有关的两个外,剩余六个寄存器都可以被编译器用于储存局部变量。如果寄存器不够用,编译器就会把变量存入栈中。在逆向分析时要注意,局部变量的生存期较短,务必及时确定当前寄存器的变量是哪个变量。

全局变量

存放于内存区中,在整个程序的生存周期中持续存在。大多数程序中,常数一般存放于全局变量中,如注册标记,测试标记等。
全局变量一般位于数据块(.data)的固定地址处,很容易识别。访问全局变量时一般会用一个固定的硬编码地址进行访问,例如mov eax,dword ptr[123456h]

数组

汇编状态下利用基址+变址寻址实现。
在内存中,数组可存在于栈,数据段及动态内存中。寻址类似于mov eax,[123456h+eax] 其中123456h是基址,eax存放着偏移量。
数组在声明时可以直接计算偏移地址,针对数组成员的寻址是采用实际的偏移量完成的。

虚函数

虚函数的地址不能在编译时确定,需要在调用时即时确定。对虚函数的引用一般放在虚函数表中,一个特殊的数组。每个数组元素就是类中的虚函数的地址。
调用虚函数时,程序先访问虚函数表,再从表中访问对应的虚函数。

控制语句

本部分内容以及后面的算术、循环、串操作均是基础的汇编知识,在此不赘述了。

0x02 64位软件逆向

本章内容与前章有所重叠。

寄存器

64位通用寄存器命名与32位的不同,第一个字母由E改为R,大小扩大了一倍,同时增加了8个(R8~R15)。也增加了8个128位的xmm寄存器,常常被用来优化代码。
寄存器向下兼容,新增寄存器可以通过增加后缀来访问,如R8D(低32位)R8W(低16位)

函数

调用约定

64位应用程序只有一种寄存器快速调用约定。前四个参数通过寄存器传递,如果参数多于四个,超出部分存储于栈中,入栈顺序从右到左,由函数调用方平衡栈。从左到右前四个参数的对应寄存器分布为RCX, RDX, R8, R9,其他参数依次入栈。任何大于八字节或者不是2的整数方的参数必须使用引用传递(地址传递)。所有浮点参数的传递使用XMM寄存器,使用XMM0~3完成。如果参数既有浮点数,又有整数类型,传递依据参数的绝对位置选择寄存器完成。

thiscall 约定

在C++的类的成员函数调用时,除了压入其他的参数外,还会额外在RCX位置压入一个this指针。

函数返回值

同样使用RAX储存返回值,如果为浮点数,则由XMM0传回。当返回值大于8字节时,可以使用栈空间的地址作为参数间接访问达到返回目的。

数据结构

局部变量

程序访问寄存器会比访问栈空间有更高的性能,所以大部分局部变量会尽可能的储存在寄存器中,当寄存器不够时才会使用栈进行存储。

全局变量

全局变量在定义时会按照在源码中的定义顺序,先定义的放在低地址,后定义的放在高地址。根据此特征就能还原源码中的变量定义顺序。

数组

数组的描述也基本和前章一样,本章新增了数组的寻址公式,如下

1
数组元素地址=数组首地址+sizeof(元素类型)X下标

控制语句

本章介绍了多种流程控制语句,诸如if,else if,while等,但是在ida中可以轻松的利用f5大法反汇编,在这里就跳过了。

#0x03 本章总结
在这一章中介绍了C语言的汇编实现,了解了函数之间的调用约定。虽然ida的f5大法可以省去我们读汇编源码的过程,但是对汇编的底层实现还是需要有所认识的。下篇我想跳过第五章,直接进入第六章加密算法。因为毕竟再等不到一个月就要去线下赛了,先把重要的东西学到手。