-
-
[原创]ROP(Return-Oriented Programming)返回导向编程 · 游戏安全相关技术边界探索
-
发表于: 1天前 648
-
系列目录(共四篇)
- ROP 是什么,原理,初步认识 ROP(本文)
- 初步探索 — 基于ROP技术弹个MessageBox & 安全退出
- 深入探索 — 挖掘ROP在游戏安全领域的能力边界
- 反作弊对抗相关思考 & 检测手段
1,ROP 是什么,原理,初步认识 ROP
1, ROP 为返回导向编程技术,基于 ret = pop + jmp 这一技术细节。假设我们在栈上合适的位置放置一个地址,配合 ret 指令的执行,就可以达到修改 RIP 的效果,从而可以可控地改变 CPU 执行流。
2, 更进一步,假设我们在栈上多个合适位置放置多个地址,再配合 ret 指令,理论上就可以达到多次控制 CPU 执行流的可能。这就是 ROP。
一个最简单的模型:
线程栈 高地址
┌────────────────────────────────┐
│ 地址 F │
│ 地址 E │
│ 地址 D │
│ 地址 C │
│ 地址 B │
│ 地址 A ← RSP │
└────────────────────────────────┘
低地址
假设地址 A、地址 B、地址 C …… 地址 F 都指向任意不同的 ret 指令的地址。
- 假设地址 A = ntdll.dll 中任意一个 ret 指令的地址
- 假设地址 B = user32.dll 中任意一个 ret 指令的地址
- 假设地址 C = kernel32.dll 中任意一个 ret 指令的地址
- …
- 假设地址 F = dxgi.dll 中任意一个 ret 指令的地址
则神奇的现象发生了:当执行 ret 指令的时候,ret = 弹出地址 A + JMP 地址 A + RSP 指向地址 B = RIP 跳到 ntdll.dll 中。由于地址 A 指向的是 ret 指令,则继续 ret = 弹出地址 B + JMP 地址 B + RSP 指向地址 C = RIP 跳到 user32.dll 中。依此类推,栈上地址 C 至 F 会按同样方式连环触发 ret,最终 RIP 进入 dxgi.dll。
以上是一个无聊的、漫无目的的多模块横跳。只不过,这也就是 ROP 的核心思想,即:基于精心构造的栈布局,从而操控 CPU 指令流。
先介绍 2 个工具:
- Hawkeye ROP:Windows gadget 搜索工具(小零件搜索工具)
- Hawkeye Lab:Windows 游戏内核级作弊检测 / 评估工具
更进一步: 假设我们打算调用 HDC hDC = GetDC(NULL);。在 x64 调用约定下,rcx、rdx、r8、r9 依次承载前四个参数;对 GetDC(NULL) 而言,需要先把第一个参数置为 0,再转入 GetDC 执行。处理第一个参数时,直觉上会去找 mov rcx, 0; ret 这类 gadget——一条指令完成赋值并返回。但在真实模块里,这种「写寄存器 + 立刻 ret」的组合并不常见,往往并不好找。更务实的做法是换一条等效路径:在栈上预先放好常数 0,再用 pop rcx; ret 把它弹进 rcx(具体布局见后文「线程栈示意」)。
先介绍如下, 我们以 notepad.exe 为例。
1, Hawkeye ROP 的作用就是搜索 gadget(代码小零件)。这里我们发现 pop rcx; ret 组合的小零件非常多,如图 2。一般而言,我们更加喜欢 ntdll.dll 中的小零件。原因: 几乎每个进程都会加载 ntdll。
2, Hawkeye Lab:gadget 找到之后,也是需要验证的。毕竟可执行代码区中,有些地方是被重定位修复的,而被修复的地方碰巧也会命中小零件的二进制码序列,而这个我们尽量不要用。
3, 小零件 gadget 是不需要指令对齐的。如图 1,pop rcx; ret 的二进制码是:0x59 0xC3。我们完全可以把 0x00007ffc8e5877fe 地址拿来当小零件地址,这是没有任何问题的。Hawkeye ROP 工具支持截断搜索能力。

图 1. pop rcx; ret 指令与截断地址示例。

图 2. Hawkeye ROP 搜索 pop rcx; ret(notepad 示例)。
线程栈示意(pop rcx; ret 时的布局):
线程栈 高地址
┌────────────────────────────────┐
│ 地址 E │
│ 地址 D │ (4)
│ 地址 C │ (3)
│ 地址 B 下一个 gadget 地址 │ (2)
│ 0ull ← RSP │ (1)
└────────────────────────────────┘
低地址
假设此刻 RIP -> 7ffc8e58429e;RSP -> (1) 会怎么样?
- 执行 pop rcx = 弹出 0ull 给 rcx + RSP 移动到 (2)
- 执行 ret = 弹出下一个 gadget 地址给 RIP + RSP 移动到 (3)
结果就是 RIP 开始执行第二个 gadget。至此,完成了 rcx = 0 的赋值操作。
那么 ROP 核心是什么呢? 现在我们应该知道了:ROP 的核心就是在栈上精心布局、一步步的行动。
ROP 与 ShellCode 有何不同?
从实现方式、检测面与开发成本上,可以这样对照理解:
- ShellCode 本身是一段可执行指令,通常需要在目标进程里分配或映射可执行内存,并把 RIP 引到该处。
- ShellCode 字节序列较固定,容易被做特征 / 签名;在开启 DEP / NX 时,「写入再执行」的路径往往直接受阻。
- ShellCode 编写相对自由:可按意图安排 push / pop 等指令,一般不必像 ROP 那样为栈上每一格做精确规划。
- ROP 几乎不注入完整指令块,而是串联进程内已有的 gadget;攻击意图主要体现在栈上的返回地址链,难以用固定指令模板做特征。
- ROP 控制流在既有模块的代码段里跳转,对 DEP / NX 相对宽松;CET 等强化控制流完整性的机制则另当别论(本系列后文再谈)。
- ROP 实现门槛更高:即便有 gadget 搜索工具或 AI 辅助,仍常因小零件不足而换模块、改方案。
- ROP 不能随意选用带 push / pop 的 gadget,否则会打乱栈布局。胜负手仍在于对栈的精细设计与逐步推进。
本篇小结
本篇从 ret 与栈的关系入手,说明 ROP 的本质:在栈上排好一串返回地址,用多次 ret 把 RIP 逐步导到进程内已有的 gadget,从而可控地改变执行流。
- 以
GetDC(NULL)为例:x64 下需令 rcx = 0;理想的mov rcx, 0; ret在常见模块里往往难找,更务实的路径是栈上放 0,配合pop rcx; ret。 - Hawkeye ROP 负责搜索 gadget,Hawkeye Lab 负责在真实进程里验证;选型时注意重定位误命中,并理解「截断地址」与指令对齐在文中的含义。
- 与 ShellCode 对照:ROP 一般不另写、注入一段 shellcode,而是复用进程里已有的 gadget,因此对 DEP / NX 更友好,但栈布局约束更强、工程成本更高;CET 等保护措施随着硬件和Win11的迭代将会越来普及。
下一篇:将以Notepad进程为目标进行实验,用完整 ROP 链弹 MessageBox 并安全退出。
同时我们一起面对Shadow space。看看影子空间对于ROP的影响。
开源
本系列相关 Hawkeye 开源仓库:
- hawkeye-rop:5d3K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6Z5j5i4N6C8k6i4W2W2i4K6u0V1e0r3g2G2i4K6u0r3K9r3q4%4K9$3g2&6k6g2)9J5k6s2u0G2M7q4!0q4c8W2!0n7b7#2)9^5z5r3N6S2k6r3N6W2N6l9`.`. 搜索工具)
预编译包:b46K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6Z5j5i4N6C8k6i4W2W2i4K6u0V1e0r3g2G2i4K6u0r3K9r3q4%4K9$3g2&6k6g2)9J5k6s2u0G2M7q4)9J5c8Y4u0W2L8r3g2S2M7$3g2K6 - hawkeye-community:279K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6Z5j5i4N6C8k6i4W2W2i4K6u0V1e0r3g2G2i4K6u0r3K9r3q4%4K9$3g2&6k6g2)9J5k6r3y4G2L8h3#2#2L8X3W2@1P5g2!0q4c8W2!0n7b7#2)9^5z5l9`.`.社区与相关项目)
下一节: 初步探索 — 基于ROP技术弹个MessageBox & 安全退出。
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。