面向 Reverse 与 Pwn 的汇编¶
Reverse 关注“程序实际做了什么”,Pwn 基础关注“错误的内存操作如何破坏程序安全边界”。二者都依赖寄存器、内存、控制流和 ABI,但安全学习必须限定在授权题目、本地实验和防御性验证中。 本篇讲分析思路、缺陷成因、保护机制和修复方向,不提供真实目标利用步骤或可直接复用的武器化 payload。
从问题出发,而不是从指令表出发¶
拿到一个 ELF 后,先提出可验证的问题: - 程序从哪里接收输入? - 输入经过哪些长度、范围或格式检查? - 哪些函数决定成功、失败或权限状态? - 哪些数据影响间接调用、返回地址或对象生命周期? - 程序启用了哪些编译与链接保护? 随后用静态分析建立路径,用调试器在关键点验证数据。
Reverse 的基础工作流¶
- 用
file、readelf确认架构、ELF 类型和入口。 - 浏览导入函数、字符串、符号和交叉引用。
- 从
main、输入函数、比较函数或输出函数开始。 - 切分基本块,标记条件和循环。
- 根据 ABI 恢复参数与返回值。
- 为寄存器、栈槽和结构体偏移赋予临时语义名。
- 用不同输入动态验证关键路径。
重命名是推理工具。名称应随证据更新,例如从
v3改为input_len,而不是一开始就写成未经证实的password_len。
识别程序入口与 main¶
ELF 入口通常是 C 运行时启动代码,而不是 main。动态链接程序会初始化运行环境,再通过 libc 启动函数间接调用 main。
有符号时工具会直接标出 main;无符号时可从启动代码传给 libc 启动例程的函数指针、交叉引用或运行时断点定位。
不要把入口函数的参数规则误当成普通用户函数逻辑。
从导入和字符串建立地图¶
常见导入提供行为线索:
- read/fgets/getline:输入
- strlen/memcmp/strcmp:长度或内容比较
- malloc/free:动态对象生命周期
- open/read/write:文件操作
- printf/puts:输出与格式串
字符串交叉引用可以定位错误路径和成功路径。但未引用字符串、内联实现、运行时解码和间接调用都会让线索不完整。
恢复 if 条件¶
遇到:
只能先得到“eax 不等于 5 时走向 .fail”。接下来要追溯:
- eax 的值从哪里来?
- 宽度和符号是什么?
- .fail 是否真的失败,还是共同清理代码?
- 另一分支最终是否一定成功?
控制流标签和反编译器命名不应替代数据流证明。
恢复循环¶
循环通常包含: - 初始化:索引或指针起点 - 条件:长度、终止符或哨兵比较 - 更新:索引加一、指针增加步长 - 回边:跳回较低地址或循环头 数组处理可能使用索引:
也可能使用指针递增: 二者可对应相同源码语义。识别长度和边界检查¶
安全分析中最重要的是确认检查与访问是否匹配:
应继续确认: - 64 是否包括字符串终止符? - 比较是> 还是 >=?
- rdx 是否可能因截断、符号转换或整数溢出改变?
- 目标实际容量是否确实是 64?
- 复制后是否又追加额外数据?
- 检查与使用之间数据是否被修改?
孤立看到一次 cmp 不足以证明边界安全。
有符号与无符号边界错误¶
若长度先以有符号整数检查负数,后又转成无符号 size_t,负值可能变成很大的正数。汇编层面可观察:
- 使用 jl/jg 还是 jb/ja
- 参数进入 64 位寄存器时是 movsxd 还是零扩展
- 比较宽度与后续内存操作宽度是否一致
防御性修复应统一长度类型、在转换前验证范围,并使用目标对象的真实容量。
栈缓冲区与控制数据¶
局部数组可能与保存寄存器、帧信息和返回地址位于同一栈帧。如果写入超过数组边界,可能先破坏其他局部数据,再影响控制数据。 概念图:
真实布局由编译器、优化、对齐、Canary 和变量重排决定。不能仅根据源码声明顺序计算距离。安全地观察栈破坏¶
在自己编写的最小实验程序中,可以使用 GDB: 1. 在写入函数前后设置断点。 2. 记录目标缓冲区地址和容量。 3. 用观察点定位首次越界写。 4. 检查受影响内存属于哪个对象。 5. 使用 AddressSanitizer 复现并获得诊断。 推荐防御性构建:
Sanitizer 适合定位错误,不应被视为生产环境中所有内存问题的完整防护。栈 Canary¶
启用栈保护时,函数可能在入口读取一个线程相关随机值并放到栈帧,在返回前比较:
Canary 主要检测某些覆盖到函数尾部控制数据的连续栈写。它不能防止所有越界、任意地址写、堆错误或逻辑漏洞。 安全构建常使用-fstack-protector-strong 或发行版默认强化选项。
NX / DEP¶
NX(不可执行位)让可写数据页通常不可执行,例如栈和堆多为 rw- 而不是 rwx。它降低“把输入数据直接当指令执行”的风险。
NX 不阻止:
- 破坏数据但不改变控制流
- 跳转到已有可执行代码
- 逻辑和权限错误
防御上应遵循 W^X:页面不同时可写和可执行,并减少运行时生成代码的权限窗口。
ASLR 与 PIE¶
ASLR 随机化栈、堆、共享库等映射地址。主程序若编译为 PIE,其代码基址也可随机化。 - ASLR 增加攻击者预测地址的难度。 - PIE 让主可执行文件参与地址随机化。 - 信息泄露可能削弱随机化效果。 - 低熵、地址复用或部分覆盖等情况会影响实际强度。 调试时地址变化是正常现象,应使用“模块基址 + 偏移”记录位置。
RELRO 与 GOT¶
动态链接需要 GOT 保存解析后的函数或数据地址。RELRO 用于减少关键重定位区域在运行期保持可写的时间或范围。 - Partial RELRO 提供部分保护。 - Full RELRO 通常配合立即绑定,使相关 GOT 区域在初始化后只读。 RELRO 保护特定链接结构,不是通用内存安全机制。
FORTIFY 与安全 API¶
编译器在已知对象大小时,可通过 _FORTIFY_SOURCE 把部分库调用替换为带边界检查的版本。反汇编中可能看到 __memcpy_chk、__printf_chk 等符号。
FORTIFY 依赖编译优化和可推断对象大小,不能替代正确的长度计算、输入验证与内存安全设计。
ROP 的防御性认识¶
Return-Oriented Programming(ROP)是一类代码复用思想:不向数据区注入新指令,而是把已有可执行代码中的短指令序列按控制流效果组合。
理解它的防御意义在于:
- NX 并不能单独阻止所有控制流劫持。
- 返回地址和间接调用目标都是高价值控制数据。
- ASLR/PIE、Canary、控制流完整性和硬件辅助机制应分层使用。
- 程序仍需首先消除越界写、释放后使用等根因。
本章不构造 gadget 链或可执行 payload。授权实验中也应把目标限定为验证保护是否生效,而非迁移到真实系统。
ret 从栈上读取下一条执行地址,因此防御分析应确认缺陷能否触及控制数据、Canary 是否覆盖该路径、地址随机化是否有效,以及 CET shadow stack 等机制是否启用。只有证明根因和可达性后才能评价风险;“程序崩溃”不等于“可稳定控制执行”。
格式字符串的汇编视角¶
格式化输出函数把格式串解释为参数描述。安全分析要区分:
在 System V 反汇编中,检查rdi 指向固定 .rodata,还是指向可控缓冲区,并继续核对后续参数寄存器。
修复原则是使用固定格式串、开启编译器格式检查,并避免把不可信数据作为格式模板。
间接调用与函数指针¶
可能对应回调、虚函数表或分发表。逆向时: 1. 追踪目标地址来源。 2. 列出所有合法赋值点。 3. 按调用点 ABI 推断函数原型。 4. 检查索引和对象生命周期是否受控。 防御性设计应验证索引、保护只读函数表,并采用 CFI 等机制降低非法目标风险。堆问题的基础识别¶
汇编层面常见线索:
- malloc/calloc/realloc/free 调用
- 对返回指针的空值检查
- 固定偏移字段访问
- 释放后仍使用旧指针
- 长度乘法后分配,存在溢出可能
- 两条路径释放同一对象
堆分配器内部细节随 libc 版本变化。初学阶段应先确认对象生命周期和边界,不要把所有堆崩溃都归因于分配器元数据。
整数溢出到内存错误¶
内存大小常由算术生成:
若乘法按固定宽度回绕,分配可能小于后续写入。汇编中检查: -imul 的宽度
- 是否检查 CF/OF 或预先比较上限
- count 的符号和扩展方式
- 分配大小与循环边界是否使用同一语义
修复应在乘法前检查 count > SIZE_MAX / element_size,或使用提供溢出检测的安全辅助函数。
从汇编验证修复¶
修复后应确认边界检查覆盖所有入口、比较宽度与有符号性正确、检查位于使用之前,并验证最终二进制仍包含预期检查和默认保护。单元测试、模糊测试与 Sanitizer 应覆盖原始触发场景。
Pwn 题目的防御性分析顺序¶
在授权题目中,可按以下顺序做风险评估: 1. 识别输入面和信任边界。 2. 确认缺陷类型与精确写入/读取范围。 3. 建立崩溃根因,不把症状当原因。 4. 检查 NX、PIE、ASLR、Canary、RELRO、FORTIFY、CET。 5. 判断缺陷影响的是普通数据、敏感数据还是控制数据。 6. 用最小、无害的实验验证判断。 7. 给出源码修复和构建加固建议。 这种顺序比先寻找“可利用技巧”更适合建立可迁移的安全能力。
防御性修复原则¶
- 使用携带长度的接口,并让容量和实际长度采用
size_t等一致类型。 - 在所有算术和类型转换前后验证范围。
- 不依赖终止符来弥补缺失的容量信息。
- 明确对象所有权和生命周期,释放后清理或废弃引用。
- 默认启用编译器警告、Canary、PIE、RELRO、FORTIFY 和平台控制流保护。
- 在 CI 中使用 AddressSanitizer、UndefinedBehaviorSanitizer 和模糊测试。
- 对解析器和高风险组件考虑内存安全语言或隔离边界。 缓解措施降低缺陷后果,正确的设计和边界检查才消除根因。
最终分析检查表¶
完成一个基础 Reverse/Pwn 分析时,应能清楚写出: 1. 文件架构、格式、ABI 和保护状态。 2. 输入如何到达关键函数和内存对象。 3. 每个关键比较的宽度、有符号性与分支含义。 4. 缺陷发生的第一条错误访问及其根因。 5. 受影响数据范围,以及是否涉及控制数据。 6. 现有缓解措施能阻止什么、不能阻止什么。 7. 无害验证方法、源码修复和回归测试方案。 8. 哪些结论已动态验证,哪些仍是假设。 回到章节首页复习整体路线,或结合工具、反汇编与调试建立自己的实验记录模板。