实现逆向工程——理解 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)之间的区别,并认识到栈在应用逆向工程中的重要性------这一部分切勿忽略。在下一章,我们将介绍业界专业人士常用的各类逆向工程工具。

相关推荐
用户9623779544811 小时前
DVWA 靶场实验报告 (High Level)
安全
数据智能老司机14 小时前
用于进攻性网络安全的智能体 AI——在 n8n 中构建你的第一个 AI 工作流
人工智能·安全·agent
数据智能老司机14 小时前
用于进攻性网络安全的智能体 AI——智能体 AI 入门
人工智能·安全·agent
用户9623779544816 小时前
DVWA 靶场实验报告 (Medium Level)
安全
red1giant_star16 小时前
S2-067 漏洞复现:Struts2 S2-067 文件上传路径穿越漏洞
安全
用户9623779544819 小时前
DVWA Weak Session IDs High 的 Cookie dvwaSession 为什么刷新不出来?
安全
cipher2 天前
ERC-4626 通胀攻击:DeFi 金库的"捐款陷阱"
前端·后端·安全
一次旅行5 天前
网络安全总结
安全·web安全
red1giant_star5 天前
手把手教你用Vulhub复现ecshop collection_list-sqli漏洞(附完整POC)
安全
ZeroNews内网穿透6 天前
谷歌封杀OpenClaw背后:本地部署或是出路
运维·服务器·数据库·安全