实现逆向工程——理解 x86 机器架构

未来,每一个设备或机器都将变得"智能化"。普通设备(或我们称之为"传统设备")与智能设备之间的最大区别在于,智能设备具备互联网功能。所谓"智能",意味着该设备经过编程,可以以智能化的方式运行,并可通过互联网远程操作。如今,我们家庭中使用的大多数设备都已具备联网功能,或可称为智能设备。电视已成为智能电视,洗衣机已成为智能洗衣机,冰箱也已成为智能冰箱,等等。所有这些,都得益于在传统设备(如电视、洗衣机、冰箱等)中引入的一台微型计算机。那么,这些微型计算机内部都包含了什么,它们是如何工作的呢?这些微型计算机由若干小型组件构成,每个组件在整个系统的运行中都扮演着重要角色。可以将它们想象成个人计算机的微型版本。

所有这些设备统称为现代计算设备。现代计算设备由用于处理、数据存储、数据传输等多种功能的组件构成。现代计算设备结合软件被编程来完成各种任务。要在现代计算设备上理解逆向工程(RE),我们首先需要了解这些计算设备内部的构成及其工作原理。

章节结构

本章将涵盖以下主题:

  • 计算机系统的架构
  • 计算机系统的构建模块
  • 不同类型处理器的历史
  • 寄存器、寄存器类型及其作用
  • 栈的概念

学习目标

在本章中,我们将讨论计算机系统及其分类,介绍现代计算机系统的各个组件;接着探讨处理器及其不同型号之间的差异和编号规则;最后,我们将关注栈在逆向工程中的作用,以理解调用者(caller)与被调用者(callee)之间的区别。

计算机系统的架构

我们周围的任何计算设备都由一些基本构建模块组成。当我们提到计算机系统时,可以是个人电脑、笔记本、移动设备、物联网设备,以及其他能够执行任务的设备。基本上,计算机系统可分为两类:

  1. 定程计算系统(Fixed-Program Computing System)
    这类系统的架构专用于执行特定任务,例如计算器。
  2. 存储程序计算系统(Stored-Program Computing System)
    与之相对,这类系统的架构可根据需求进行编程,能够同时运行多项任务,并可存储和执行各种应用程序,例如现代计算机。此类架构由约翰·冯·诺依曼(John von Neumann)于1945年提出。

冯·诺依曼架构基于"存储程序"理念,将程序指令和数据一并存放在同一内存中。现代计算机系统正是采用了这一设计,其主要构建模块包括:

CPU(中央处理单元)

中央处理单元控制着计算设备或系统的各种操作。在我们的计算系统中,CPU 也称为处理器,是系统的大脑。CPU 的工作是从内存中取指令,将指令解码为一系列操作,然后按顺序执行这些操作。CPU 内部包含多个组成部分,其中包括:

  • 控制单元(Control Unit) :负责从内存或 RAM 中检索并解码指令。
  • 执行单元(Execution Unit) :借助寄存器执行指令。
  • 寄存器(Registers) :为了节省时间,CPU 不会每次都访问 RAM 取指令,而是在自身内部配备了基本的存储单元------寄存器。寄存器有多种类型,我们会在后续章节中详细介绍。其中,指令指针寄存器(Instruction Pointer)用于存储下一条待执行指令的内存地址。
  • 标志(Flags) :也是一种寄存器,用于记录算术运算后的 CPU 状态。

内存(Memory)

内存可以是随机存取存储器(RAM)或只读存储器(ROM),也可以是硬盘(HDD)、光盘等外部存储设备。内存的主要用途有两点:

  1. 存储计算机或系统执行的指令序列(即程序代码)。
  2. 存放计算机运行所需处理的数据。

输入/输出设备(I/O Devices)

所有与计算系统相连的外围设备都称为输入/输出设备,包括键盘、鼠标、显示器等。这些设备通过端口(ports)与系统接口,分为:

  • 输入端口(Input Ports) :用于将外围设备的数据读入系统。
  • 输出端口(Output Ports) :用于将系统数据发送到外围设备,如显示器、打印机等。

系统总线(System Bus)

系统总线可视为一组电线,用于在计算系统的不同组件之间传输信息或数据。根据所承载信息的类型,总线可分为:

  • 地址总线(Address Bus) :并行信号线,用于发送要读写的内存地址。CPU 可寻址的内存位置数量取决于地址线的数量:若有 N 根地址线,则可寻址 2^N 个位置。例如,8 根地址线可寻址 256 个位置;16 根可寻址 65,536 个位置。

  • 数据总线(Data Bus) :并行信号线,用于在 CPU 与内存之间传输数据。

  • 控制总线(Control Bus) :并行信号线,传输同步信号以控制连接到 CPU 的各种外围设备,协调多项任务。典型信号包括 I/O Read、I/O Write、Memory Read、Memory Write。以从内存读取一个字节为例,流程如下:

    1. CPU 在地址总线上发送目标字节的内存地址;
    2. CPU 在控制总线上发送 Memory Read 信号;
    3. Memory Read 信号使被寻址的内存设备将数据(字节)输出到数据总线;
    4. 数据通过数据总线从内存传输到 CPU。

计算机系统的构建模块

要理解逆向工程,必须掌握基本的数据构建块,包括位(Bit)、半字节(Nibble)、字节(Byte)、字(Word)和双字(DWORD)。这些概念可通过下图说明:

人类可以根据所在国家使用不同的语言进行交流。但对于计算机这样的计算系统而言,它们只能理解二进制,也就是 0 或 1。计算机通过发送或交换数据来相互通信。数据的最小单位称为"位"(bit),其值可以是 0 或 1。

  • 半字节(Nibble) :由 4 个位组成。
  • 字节(BYTE) :1 BYTE = 2 Nibbles = 8 位
  • 字(WORD) :1 WORD = 2 BYTEs = 16 位
  • 双字(DWORD) :1 DWORD = 4 BYTEs = 32 位

微处理器

如前所述,CPU 是计算系统的大脑。CPU 周围的一整套电路被称为微处理器(microprocessor)。一个微处理器内部可以包含多个"处理器核心",例如图形处理器(GPU)也是一种专用核心。因此,CPU 只是微处理器的一部分,而微处理器本身可能集成了多个核心。市面上知名的微处理器制造商包括 Intel、AMD 等。

第一代微处理器中较为经典的型号有:

  • 8086

    Intel 于 1978 年推出的 8086,是其首款 16 位微处理器。这一产品催生了后来的 x86 架构,并成为处理器市场的重要分支。Intel 8086 的主要规格为:

    • 数据总线宽度:16 位
    • 地址总线宽度:20 位
  • 80186

    Intel 80186 于 1982 年推出,是 Intel 家族中的第二款处理器。Intel 80186 处理器的规格为:

    • 数据总线宽度:16 位
    • 地址总线宽度:20 位
  • 80286

    Intel 80286 于 1982 年推出,是 Intel 家族中的第三款处理器。Intel 80286 处理器的规格为:

    • 数据总线宽度:16 位
    • 地址总线宽度:24 位
  • 80386

    Intel 80386 于 1985 年推出,是 Intel 家族中的第四款处理器。Intel 80386 处理器的规格为:

    • 数据总线宽度:32 位
    • 地址总线宽度:32 位
  • 80486

    Intel 80486 于 1989 年推出,是 Intel 家族中的第五款处理器。Intel 80486 处理器的规格为:

    • 数据总线宽度:32 位
    • 地址总线宽度:32 位

因此,上述所有处理器统称为 Intel x86 系列。

通常,我们这样称呼 Intel 处理器:

  • x86-16:表示 16 位处理器。
  • x86-32(即 IA32) :表示 32 位处理器(IA 即 Intel Architecture),也简称 x86。
  • x86-64:表示 64 位处理器,也称 x64。

注: 在本书中,我们将重点关注 Intel x86-32 处理器。

内存

在 x86-32 架构下运行的单个进程所使用的内存(即 RAM)被划分为以下几个部分:

内存地址范围为 0x00000000 -- 0xFFFFFFFF。前缀 0x 表示十六进制数。每个十六进制位占 4 位,因此 x86-32 架构的任何内存地址都由 8 个十六进制数字组合表示,总共 4 × 8 = 32 位。这就是为什么 x86-32 计算机的内存地址为 32 位。

  • 内核空间(Kernel Space) :为操作系统内核保留 1 GB。
  • 栈(Stack) :为函数的局部变量和参数保留的空间。栈的增长有固定的大小限制,从高地址向低地址扩展。
  • 库(Libraries) :加载我们的共享库的区域。常见的对话框(如"保存"对话框)存放在这里,可被多个程序共享。
  • 堆(Heap) :堆向上增长。当加载映像时,会根据映像大小在程序执行期间申请动态内存;程序结束后,这部分内存将被释放。堆内存会在程序执行过程中动态变化,从低地址向高地址扩展。
  • 数据区(Data) :用于存放代码中的静态变量和全局变量。
  • 代码区(Text) :包含要执行的指令或代码,用于完成相应操作。

寄存器

为了节省时间,CPU 提供了一小部分临时存储单元,称为寄存器。在 x86 处理器中,寄存器可分为以下几类:

通用寄存器

x86 架构共有 8 个通用寄存器:

  • EAX:用于算术和逻辑运算,也用于存储函数返回值。
  • EBX:用作数据指针。
  • ECX:用于循环操作。
  • EDX:用于 I/O 操作和算术运算。
  • ESI:在字符串操作中用作源地址指针。
  • EDI:在字符串操作中用作目的地址指针。
  • ESP:指向栈顶的指针。
  • EBP:指向栈帧基址的指针。

所有通用寄存器均为 32 位,也可以分别以 16 位和 8 位的形式访问。寄存器的较小部分可按如下方式引用:

  • EAX 的完整大小为 4 字节(32 位)。前缀 "E" 表示 Extended(扩展)。

AX 是 EAX 寄存器的低 16 位部分。 AX 进一步可分为 AH(A-High,高 8 位)和 AL(A-Low,低 8 位)。其他通用寄存器亦同理。

  • 32 位通用寄存器:EAX、EBX、ECX、EDX、ESI、EDI、EBP、ESP
  • 16 位通用寄存器:AX、BX、CX、DX、SI、DI、BP、SP
  • 8 位通用寄存器:AH、AL、BH、BL、CH、CL、DH、DL

段寄存器

段寄存器 CS、DS、SS、ES、FS 和 GS 均为 16 位,用于指向内存中各段的起始位置。段可按存储类型分为三类:代码段、数据段和栈段。

  • 代码段(Code Segment) :存放所有待执行指令,CS 寄存器指向该段起始。

  • 栈段(Stack Segment) :保存函数调用的参数、局部变量等,SS 寄存器指向该段起始。

  • 数据段(Data Segment) :为提高效率与安全,划分为四个独立数据段:

    1. 当前加载模块的数据结构
    2. 从第三方模块导出的数据
    3. 动态创建的数据结构
    4. 不同程序间共享的数据
      使用 DS、ES、FS、GS 寄存器分别访问这些不同的数据段。

状态寄存器

状态寄存器称为 EFLAGS,长度为 32 位(4 字节),其中每个位代表一面标志(flag),其值为 0 或 1,反映 CPU 运算的结果状态。逆向工程中常用的标志包括:

  • 零标志(ZF) :运算结果为 0 时置位(1),否则清零(0)。
  • 进位标志(CF) :运算结果超出目的操作数可表示范围时置位,否则清零。
  • 符号标志(SF) :运算结果为负时置位,结果为正时清零。
  • 陷阱标志(TF) :置位时 CPU 每次执行一条指令,用于调试。
  • 奇偶标志(PF) :当结果最低有效位(LSB)中 1 的个数为偶数时置位,否则清零。
  • 溢出标志(OF) :当有符号运算结果超出可表示范围时置位,否则清零。

指令指针寄存器

指令指针寄存器 EIP 保存下一条要执行的指令的内存地址,告诉 CPU 下一步执行的位置。从安全角度看,EIP 至关重要:攻击者若能控制 EIP,就能劫持程序流程,执行任意恶意代码。

栈的概念

这是逆向工程中最重要的概念之一。将"栈"想象成餐厅里一摞盘子:取盘子,只能从顶端拿起(POP);放盘子,则只能叠到最顶端(PUSH)。要取最底下的盘子,必须先依次拿掉上面的每个盘子。

栈遵循"后进先出"(LIFO)原则:最后压入(PUSH)的元素,将最先弹出(POP)。为了理解栈的工作原理,我们可以看下面的伪代码:

在此伪代码中,有两个函数:一个是 main 函数,程序的执行从这里开始;另一个是 Foo 函数。Foo 函数有 3 个参数(由 main 函数传入)和 2 个局部变量。在代码执行过程中,main 函数会调用 Foo 函数。在此示例中,main 是调用者(Caller),Foo 是被调用者(Callee)。

我们假设代码中提到的 3 个参数和 2 个局部变量均为 int 类型。int 类型在内存中占用 4 个字节(sizeof(int) = 4)。

当执行流程进入 Foo 函数时,典型的栈状态大致如下:

每个函数都有自己的栈帧。由于我们有两个函数 mainFoo,因此将会有两个栈帧。栈帧的最高地址由该帧的 EBP(基指针)指向。

下面我们逐步了解在函数调用和返回时,栈帧是如何建立和清理的。

调用者在调用被调用者之前

本节中,我们来看当 main 函数即将调用 Foo 函数时的栈状态。假设调用 Foo 的代码如下:

csharp 复制代码
int main()
{
    FooReturnValue = Foo(10, 20, 30);
}

此处,调用者(Caller)是 main,被调用者(Callee)是 Foomain 函数拥有自己的栈帧,其中 ESP 指向栈顶,EBP 则作为该栈帧的基址指针。

在调用 Foo 函数之前,main 会将需要保留的寄存器(如 EAX、ECX、EDX)的内容依次压入栈中。接着,mainFoo 的参数按顺序压入栈:

ini 复制代码
FooReturnValue = Foo(10, 20, 30);  // 参数依次为 10、20、30

参数按"从右到左"顺序压栈,因此先压入 30,再压入 20,最后压入 10。对应的汇编指令为:

复制代码
PUSH 30
PUSH 20
PUSH 10

压完参数后,通过 CALL 指令调用 Foo

objectivec 复制代码
CALL Foo

执行 CALL 时,CPU 会将当前 EIP(指向 CALL 指令后面下一条指令的地址)压入栈顶,作为返回地址。函数执行结束后,CPU 会从该返回地址继续在 main 中执行。

被调用者在接管控制权后

Foo 函数开始执行时,会依次完成以下三项工作:

  1. 设置 Foo 的栈帧。
  2. Foo 的局部变量分配空间。
  3. (如有必要)保存 EBX、ESI 和 EDI 寄存器的内容。

设置栈帧的汇编指令为:

复制代码
PUSH EBP
MOV  EBP, ESP
  • PUSH EBP:将调用者(main)的 EBP(基指针)压入栈中,以便函数返回时恢复。
  • MOV EBP, ESP:将当前栈顶指针 ESP 的值复制到 EBP,使 EBP 成为 Foo 的新栈帧基址。

main 的 EBP 压入栈中后,ESP(当前栈顶指针)的值就会成为新的 EBP(Foo 函数的基指针)。之后,就可以通过 EBP 加上偏移量来访问栈上的变量。如上图所示,第一个参数可通过 EBP + 8 字节来访问(4 字节用于保存 main 的 EBP,4 字节用于返回地址)。

接下来,需要在栈上为 Foo 函数的局部变量分配空间。通过从栈指针中减去 20 字节来完成,其中 20 字节包括 8 字节(两个 int 类型局部变量各 4 字节)和 12 字节的临时存储空间。对应的汇编指令为:

vbnet 复制代码
SUB ESP, 20

局部变量和临时变量可通过相对于 EBP 的偏移量来访问。为局部变量在栈上分配完空间后,如果需要保留 EBX、ESI 和 EDI 的值,就将它们依次压入栈中。

保存了 EBX、ESI 和 EDI 后的栈状态如下:

Foo 函数执行过程中,栈上会有多次 PUSHPOP 操作,此时 ESP 会上下移动,但 EBP 保持不变。借助 EBP,就可以通过相对于 EBP 的偏移量来访问各个变量。

被调用者返回前的准备

Foo 函数执行完毕后,被调用者需要进行以下操作:

  1. Foo 函数的返回值存入 EAX 寄存器;

  2. 恢复之前保存的 EBX、EDI 和 ESI 寄存器的值;

  3. 通过下列汇编指令拆除 Foo 的栈帧并返回调用者:

    复制代码
    MOV ESP, EBP
    POP EBP
    RET
    • MOV ESP, EBP:将栈指针 ESP 恢复到该函数栈帧的基址位置;
    • POP EBP:弹出之前保存的 EBP(即调用者 main 的基指针);
    • RET:弹出返回地址,并跳转回调用点,恢复到调用前的栈状态。

RET 指令会从栈中弹出返回地址并加载到 EIP 中,使指令指针(EIP)回到 main 函数,以继续执行 main 的后续代码。

调用者返回后

由于指令指针已回到 main 函数,此时栈上的函数参数已不再需要,可以进行清理。

通过将 ESP 增加 12 字节(每个参数 4 字节,共 3 个参数),清理栈上的所有参数:

sql 复制代码
ADD ESP, 12

由于 EAX 寄存器中保存了 Foo 函数的返回值,可将其内容移动到其他寄存器。最后,通过 POP 指令依次弹出之前保存的 EAX、ECX 和 EDX,将 ESP 恢复到最初的位置。

结论

我们介绍了现代计算设备的架构及其各组成部分,并讲解了微处理器、内存与各类寄存器的基本原理。还讨论了不同处理器型号及 x86 编号约定的意义。通过栈的示例代码,我们理解了调用者(caller)与被调用者(callee)之间的区别,并认识到栈在应用逆向工程中的重要性------这一部分切勿忽略。在下一章,我们将介绍业界专业人士常用的各类逆向工程工具。

相关推荐
数据智能老司机3 小时前
实现逆向工程——逆向工程的影响
安全·逆向
2301_780789667 小时前
边缘节点 DDoS 防护:CDN 节点的流量清洗与就近拦截方案
安全·web安全·ddos
江拥羡橙8 小时前
【基础-判断】HarmonyOS提供了基础的应用加固安全能力,包括混淆、加密和代码签名能力
安全·华为·typescript·harmonyos
小木话安全9 小时前
ISO27001 高阶架构 之 支持 -2
网络·安全·职场和发展·学习方法
ayaya_mana13 小时前
Nginx性能优化与安全配置:打造高性能Web服务器
运维·nginx·安全·性能优化
观北海21 小时前
网络安全蓝队常用工具全景与实战指南
安全·web安全
lingggggaaaa1 天前
小迪安全v2023学习笔记(六十一讲)—— 持续更新中
笔记·学习·安全·web安全·网络安全·反序列化
紫金桥软件1 天前
紫金桥RealSCADA:国产工业大脑,智造安全基石
安全·系统安全·软件工程
柑木1 天前
隐私计算-SecretFlow/SCQL-SCQL的两种部署模式
后端·安全·数据分析