读完本篇你将能:理解 x86/x64 汇编的基本语法和寄存器用途,阅读简单的反汇编输出并还原程序逻辑,掌握 PE 和 ELF 文件格式的核心结构(节区、导入表、导出表、入口点),为后续使用 Ghidra、radare2 和 GDB 进行静态分析和动态调试打下基础。
模块 4 解决了"怎么利用漏洞"的问题,模块 4-2 解决"怎么理解编译后的程序"。当你拿到一个没有源码的恶意样本或闭源软件,汇编语言是唯一的对话方式。逆向工程不是把 0 和 1 翻译回 C 代码——那是编译器做的事。逆向是理解程序"在做什么"和"怎么做"的过程,汇编是中间语言。
x86 汇编是 Intel 32 位指令集的汇编语言。虽然现代系统多为 64 位(x64),但恶意软件、shellcode 和漏洞利用中 x86 仍然大量出现——因为 32 位代码更短、兼容性更好。理解 x86 是学习 x64 的前提。
x86 有 8 个通用寄存器,每个 32 位。它们各有"历史角色"——虽然现代指令集允许任意寄存器做通用运算,但函数调用约定仍然依赖这些角色分工。
| 寄存器 | 全称 | 传统角色 | 16位 | 8位低 |
|---|---|---|---|---|
| EAX | Accumulator | 返回值 / 乘除运算 | AX | AL |
| EBX | Base | 内存基址指针 | BX | BL |
| ECX | Counter | 循环计数器 | CX | CL |
| EDX | Data | I/O 端口 / 乘除高位 | DX | DL |
| ESI | Source Index | 字符串操作源指针 | SI | SIL |
| EDI | Destination Index | 字符串操作目标指针 | DI | DIL |
| EBP | Base Pointer | 栈帧基址指针 | BP | BPL |
| ESP | Stack Pointer | 栈顶指针 | SP | SPL |
E 是 Extended 的缩写——16 位时代寄存器叫 AX/BX/CX,32 位扩展后在前面加 E。64 位时前面改用 R(如 RAX/RBX),但 32 位的 EAX 仍然可以使用。
xor eax, eax 是最常见的清零写法——因为 XOR 自身结果必为 0,且比 mov eax, 0 短一个字节(机器码 31 C0 vs B8 00 00 00 00)。在 shellcode 中,节省的每个字节都至关重要。函数调用是逆向分析的核心——程序逻辑由一个个函数调用组成,每个函数有自己的栈帧。理解栈帧是理解函数参数、局部变量和返回地址的基础。
x86 栈从高地址向低地址增长(push 时 ESP 减小)。一次函数调用时,栈帧布局如下:
逆向时看到 push ebp; mov ebp, esp 这两条指令就知道函数开始了——这是函数序言的标准模式。相对地,mov esp, ebp; pop ebp; ret 是函数结语。参数在 EBP+8 以上,局部变量在 EBP-4 以下。
x86 最常见的调用约定是 cdecl(C declaration),规则如下:
| 规则 | cdecl | stdcall | fastcall |
|---|---|---|---|
| 参数传递 | 栈(右到左) | 栈(右到左) | 寄存器+栈 |
| 清理方 | 调用者 | 被调用者 | 被调用者 |
| 返回值 | EAX | EAX | EAX |
| 可变参数 | 支持 | 不支持 | 不支持 |
Windows API 使用 stdcall(函数名带 WINAPI/PASCAL 声明),C/C++ 默认使用 cdecl。逆向时通过 ret N(stdcall)还是 ret(cdecl)可以区分调用约定。
x64 在 x86 基础上扩展了寄存器——从 8 个增加到 16 个,从 32 位扩展到 64 位。前缀从 E 改为 R(如 RAX/RBP/RSP),新增 R8-R15。
x64 最重要的变化是参数通过寄存器传递而非栈——前 4 个(Linux)/6 个(Windows)参数直接放入寄存器,溢出的才入栈。这直接影响逆向分析时的参数定位方式。
| 参数序号 | Linux x64 (System V) | Windows x64 (Microsoft) |
|---|---|---|
| 第1参数 | RDI | RCX |
| 第2参数 | RSI | RDX |
| 第3参数 | RDX | R8 |
| 第4参数 | RCX | R9 |
| 第5参数 | R8 | 栈 |
| 第6参数 | R9 | 栈 |
call printf 前有 mov rdi, format_str 和 mov rsi, value——说明 printf 的第一个参数(格式串)在 RDI,第二个参数(值)在 RSI。x86 下两个参数都在栈上,需要 push value; push format_str; call printf。PE(Portable Executable)是 Windows 上 .exe/.dll/.sys 文件的格式。理解 PE 结构是分析 Windows 恶意软件的基础——第 43 篇提到的免杀技术大量利用 PE 结构操作。
入口点(AddressOfEntryPoint)是程序执行的第一条指令地址——逆向时从这里开始分析。导入表(Import Table)列出了程序依赖的 DLL 和调用的 API 函数——看到 VirtualAllocEx 和 WriteProcessMemory 就知道可能有进程注入行为。
ELF(Executable and Linkable Format)是 Linux 上的可执行文件格式,结构与 PE 类似但细节不同。Kali 上的安全工具多为 ELF 格式。
| 概念 | PE (Windows) | ELF (Linux) |
|---|---|---|
| 文件头 | DOS Header + PE Header | ELF Header (64 bytes) |
| 节区表 | Section Table | Section Header Table |
| 入口点字段 | AddressOfEntryPoint | e_entry |
| 导入表 | Import Directory | .dynamic + .got.plt |
| 代码节区 | .text | .text |
.got.plt 节区特别重要——它存储动态链接函数的地址。恶意软件经常修改 GOT 表实现函数劫持(GOT overwrite),用自定义函数替换 libc 函数实现持久化或数据窃取。任务:在 Kali 上用 objdump -d /bin/ls | head -50 查看 ls 命令的反汇编前 50 行。找到函数序言(push ebp; mov ebp, esp),记录入口点地址。用 readelf -h /bin/ls | grep Entry 验证。
参考解法:readelf -h 显示的 Entry point address 就是 e_entry 字段。objdump -d 的输出中,地址与 e_entry 匹配的指令是程序入口。push rbp; mov rbp, rsp(x64)或 push ebp; mov ebp, esp(x86)是函数序言。
任务:用 C 语言编写一个简单的 hello.c(调用 printf 打印 "Hello"),分别用 gcc 编译为 x86 和 x64 版本(gcc -m32 -o hello32 hello.c 和 gcc -o hello64 hello.c)。用 objdump 反汇编 main 函数,对比两个版本在调用 printf 时参数传递方式的差异(x86 用 push 入栈,x64 用 mov 到 rdi/rsi)。
提示:x86 版本会看到 push OFFSET format 然后 call printf。x64 版本会看到 lea rdi, [rip+format] 然后 call printf。注意 x64 使用 RIP 相对寻址。
任务:找到一个简单的 ELF 或 PE 二进制文件(如 /bin/true 或 Windows notepad.exe),用 objdump 或 readelf 完成以下分析:(1) 确定文件格式(32/64位、链接方式);(2) 列出所有节区和各自权限(可执行/可写/可读);(3) 找到入口点地址,反汇编入口点前 20 条指令;(4) 列出导入的动态库和调用的函数。写一份简短的分析报告。
参考方向:节区权限通过 readelf -S 的 FLAG 列查看(X=可执行,W=可写,A=分配)。注意 .text 节区应为 X(可执行),.data 应为 W(可写),.rodata 应为只读。异常权限(如 .text 同时可写可执行)是恶意软件的特征之一。
本篇建立了汇编语言和文件格式的基础认知——寄存器、指令、栈帧、PE/ELF 结构。下一篇将把这些知识应用到实际工具中:静态分析工具 Ghidra 与 radare2。Ghidra 是 NSA 开源的逆向工程框架,radare2 是命令行逆向利器。我们将用它们反编译一个简单的 C 程序,从汇编视图到伪 C 代码,体验从"看机器码"到"读代码"的转换过程。
关注本公众号,持续获取网络安全 Kali 学习系列更新。如果觉得有帮助,欢迎点赞、在看、分享给同样在学习网络安全的朋友。