首页
社区
课程
招聘
[原创]Return-Oriented Programming)返回导向编程 · 游戏安全相关技术边界探索【3】
发表于: 1天前 233

[原创]Return-Oriented Programming)返回导向编程 · 游戏安全相关技术边界探索【3】

1天前
233

ROP(Return-Oriented Programming)返回导向编程 · 游戏安全相关技术边界探索【3】

系列目录(共四篇)

  1. ROP 是什么,原理,初步认识 ROP
  2. 初步探索 — 基于 ROP 技术弹个 MessageBox & 安全退出
  3. 深入探索 — 挖掘 ROP 在游戏安全领域的能力边界(本文)
  4. 反作弊对抗相关思考 & 检测手段(待发布)

深入探索 — 挖掘 ROP 在游戏安全领域的能力边界

历史回顾:第 2 期:基于 ROP 技术弹个 MessageBox & 安全退出

上一期,我们走完了一个最简单的 ROP 技术闭环——弹个框结束。

这一期:我们打算在 draw-test 里做一次加压实战,用 ROP 搭出带 循环 与 分支 的绘制逻辑,看看能否像常规程序一样支持 判断、循环、跳转——这也是 ROP 在游戏安全里常被讨论的「能力边界」。

伪代码如下(图 1):
图片描述
图 1. Draw / getDrawMode 伪代码(循环 + 点/线分支)。

运行效果(图 2):
图片描述
图 2. draw-test 运行效果(跟随鼠标画点/画线)。


一:整体布局

基于伪代码复杂度分析,实现上要拆成 两条 ROP 链:主链跑 Draw,子链负责 get drawMode。链里还要做出 分支(画点 / 画线)和 循环(线程不退出、反复执行,类似外挂里的常驻逻辑)。画点还是画线,可改工程源码切换对比(图 3):
图片描述
图 3. 工程中修改 drawMode 等字段,对比画点与画线路径。

主链块与子链块(图 4):
图片描述
图 4. 主链、子链各自「ROP 链区 + 数据区」分开存放。

讲解:

  • 主链块(MainRopBlock):仍按第二期习惯,ROP 链 与 数据区 分开排布。数据区需保留 hDC、鼠标坐标、Sleep 间隔、drawMode 字段位置,供后续 gadget 读/写。
  • 子链块(SubRopBlock):对应伪代码里 get drawMode 一段,单独再占一块远程内存;内部同样是 链区 + 数据区,与主链 各管各的,避免切 RSP 时在对方区域上踩栈、踩数据。

五个 API 调用的专用栈(各 8KB)(图 5):
图片描述
图 5. GetDC / GetCursorPos / NtGdi / Sleep 等调用各用一块 8KB 栈,并预留回到主链的布局。*

讲解:

  • 专用栈(同第 2 期):每个 API 调用前切到自己的 8KB 栈,并在栈里留好回到主链的槽位;本期五个调用对应图 5 里五块栈区。
  • API 选型:SetPixel、LineTo、Sleep 等 Win32 壳函数带 Shadow space 写入,易踩 ROP 布局;Demo 改用 NtGdiSetPixel、NtGdiLineTo、NtDelayExecution 这些 NT 函数,栈更「干净」。

补充: 上一期已介绍 Shadow space;用 IDA 很容易分辨并规避。


二、主 ROP 链 — GetDC(hDC 写入数据区)

如图 6,主链 gadgets 调用 GetDC,hDC 存回 MainRopBlock 数据区,供后续读取使用。

图片描述
图 6. GetDC 返回的 hDC 写入 MainRopBlock 数据区。


三、主 ROP 链 — GetCursorPos

如图 7 中选中的 gadgets 调用 GetCursorPos。此处为 循环起点(A_Pin)。后续主 ROP 链末尾会切 RSP 回到此处(图 7、图 8 支撑起循环逻辑)。

图片描述
图 7. 循环锚点 A_Pin。
图片描述
图 8. 主 ROP 链末尾切 RSP,回到循环起点 A_Pin。


四、主 ROP 链 — 参数准备

图片描述
图 9. 为 NtGdiSetPixel / NtGdiLineTo 共用前四参;读 MainRopBlock 数据区填入寄存器。

讲解:

  • 准备好参数:NtGdiSetPixel 与 NtGdiLineTo 调用约定相同,图 9 先把 rcx、rdx、r8、r9 填入。
  • 读数据区:GetDC、GetCursorPos 等步骤已把 hDC、坐标等放进 MainRopBlock 数据区,此处 gadgets 读出「内存 → 寄存器」。

提醒: rcx~r9 填好后尽量保持不动;后续画点 / 线还要沿用这组值,如必须变动,需先保存再恢复。


五、主 ROP 链 — 调用子 ROP 链

四参已在主链就绪,接下来要按 drawMode 决定画点还是画线——对应图 1 里 get drawMode 的逻辑。因此切 RSP 入 SubRopBlock 子链做判断(图 10)。
图片描述
图 10. 修改 RSP,进入 get drawMode 子链。

讲解:

  • 控制流看 RSP:ROP 里很少用「call 进去、ret 回来」这一套叙事——把 RSP 指到另一段链区,接下来就执行那里的 gadgets,等价于切换控制流;图 10 即主链把 RSP 指到 SubRopBlock。
  • 主链、子链可互切:子链跑完 get drawMode 后,还会再 切 RSP 回主链,继续画点或画线。普通顺序代码里不常这样「随时换栈、换链」,多链 ROP 里却很常见。
  • 横跳要守上下文:主链与子链并存时,来回切 RSP 会是常态;第四节备好的 rcx~r9 可能被下一链里的 gadget 改掉,所以时刻保持思路清晰,该存则存,别把已准备好的参数弄丢。

六、子 ROP 链 — 保存上下文

切入子链后,先落实上一节的「该存则存」——图 11 在 ropSubChain 链头用 gadgets 把 rcx 写入 save_rcx_store、rdx 写入 save_rdx_store(SubRopBlock 数据区里的两个槽位,Demo 中为 subDataBase+0x00 / +0x08)。
图片描述
图 11. 子链入口保存寄存器上下文。

讲解:

  • 存 rcx、rdx:第四节在主链已把 hDC、坐标等填进这两枚寄存器,正是 NtGdiSetPixel / NtGdiLineTo 的第一、第二参数。子链里 get drawMode 与 IF 判定 还要用 rcx、rdx 做别的事,会覆盖原值——链头先写入 save_rcx_store / save_rdx_store,分支走完再从这里恢复,绘图参数才不会丢。
  • 不存 r8、r9:第三、第四参数(如颜色)已在 r8、r9 里就绪;ropSubChain 中未对其进行修改,回到主链后仍可原样用于 NtGdi* 调用。

七、子 ROP 链 — 获取绘制模式 + IF 判定

get drawMode 之后,图 12 选中部分在 ropSubChain 里完成 IF 判定——按 drawMode 在画点、画线两条 ROP 路径间分支(LineTo 一支见图 13)。
图片描述
图 12. test / cmovz / jmp rax 实现分支。

讲解:

  • 分支 = 改 RSP:这里没有常规 if/else,而是先备两枚「跳栈」gadget(add rsp, N; ret),再用 IF 判定 选中其一,jmp rax 跳过去执行——本质仍是改 RSP 指向,落到链上不同编号。
  • 两路偏移:rax ← add rsp, 8h; ret(drawMode 非 0,画点支),rdx ← add rsp, 58h; ret(drawMode 为 0,LineTo 支,对准编号 [26])。
  • 技术重点:test ecx, ecx; cmovz rax, rdx; ret 用 ecx(已载入 drawMode)做零测;为 0 时 cmovz 把 rdx 写入 rax,即选中 58h 偏移那一支。
  • 执行跳转:编号 [14] 的 jmp rax 跳入当前 rax 上的 gadget,RSP 一次跳过 58h,下一格即编号 [26](与图 13 高亮段一致)。
  • LineTo 收尾(图 13):编号 [26]~[33] 从 save_rcx_store / save_rdx_store 恢复 rcx、rdx;[34]~[35] pop rsp 切回主链 mainRop_lineToPin(mainRopChain[35]),接上第八节 API 调用。
    图片描述
    图 13. 走 LineTo 路径时的栈偏移。

思考: 另一分支(画点路径)此处不展开,请自行对照工程吃透。


八、主 ROP 链 — mainRop_lineToPin 落脚点

子链在 drawMode == 0 时经 [34]~[35] 把 RSP 落到主链 mainRop_lineToPin(mainRopChain[35])。图 14 即 LineTo 路径在主链上的入口:[35] 切 LineTo 专用栈,[36] 发起 NtGdiLineTo(栈帧布局参考以前的教程)。

图片描述
图 14. mainRop_lineTo_pin:Sub ROP 在 DrawMode == 0 时落入 [35],[36] 指向 LineTo 专用栈。

rcx~r9 在第四节已备好,子链又从 save_rcx_store / save_rdx_store 恢复了 rcx、rdx,此处只剩换栈 + 调 API——与第二期 MessageBox 段同一套路,故不再逐步展开。调用返回后两路会在 A_pos3(mainRopChain[37])汇合,进入第九节 Sleep 与循环。


九、主 ROP 链 — 执行 Sleep

画点 / 画线两路在 A_pos3(mainRopChain[37])汇合后,图 15 选中段负责节流并闭合循环:先 Sleep 约 500ms(Demo 为 NtDelayExecution),再把 RSP 切回第三节的循环锚点 A_Pin(链上编号 [7])。

图片描述
图 15. [37]~[42] Delay 专用栈;[43]~[44] 回到 A_Pin([7])。

讲解:

  • A_pos3([37]~[42]):rcx = 0(Non-alertable),rdx 指向数据区里的延时(Demo 为 -500ms);pop rsp 切到 Delay 专用栈,调用 NtDelayExecution,返回 A_pos4([43])。
  • 闭合循环([43]~[44]):再 pop rsp 写入 A_Pin,下一格从编号 [7] 继续执行——即再次 GetCursorPos,与图 7、图 8 的循环设计首尾相接,进入下一轮 Draw。

收尾问题

  • 能做复杂外挂吗? 能——要维护整块「ROP 链王国」,往往还得叠多条 子 ROP 链(本期 draw-test 就是高复杂度样例)。
  • 适合当常规开发手段吗? 对半开:调试难、硬伤多;但若防御方不熟 ROP,短期内也可能拦不住。
  • ROP 还能玩多久? CET 是方向性克星,落地节奏受生态制约;从攻防时效看,不会一夜退场。
  • 缺 gadget 怎么办? 很常见。本期用 test ecx, ecx; cmovz rax, rdx; ret 而非 add rsp, rax; ret 正是此原因;授权研究里也可在目标进程 RWX 区塞小零件(勿滥用)。
  • gadget 去哪找? 渗透常扫 系统模块;外挂更宜盯 游戏模块——跟版本、体量大、命中率高。
  • 真有人用 ROP 做外挂吗? 有,只是很少公开讲。

本篇小结

  • 本期在 主链 + 子链 双块内存上,用 GetDC / GetCursorPos / NtGdi / Sleep* 与 5 个 8KB 专用栈,实现 循环(A_Pin)、主链子链切换 与 IF 分支(cmovz + jmp rax 改 RSP)。
  • 修改 RSP = 改控制流;多链横跳是常态,必要时,别忘记保存上下文数据。
  • 延续第 2 期:带 Shadow space 的 Win32 API 易破坏链布局,NT 函数或许更加合适;小零件凑不齐时,授权研究可在目标进程 RWX 区自备。
  • 从能力边界看:ROP 能 堆高复杂度逻辑,但 工程成本与硬伤 明显,适合作为 边界探索,而非首选开发范式。

开源

本系列相关 Hawkeye 开源仓库:

本篇对应 Demo:553K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6Z5j5i4N6C8k6i4W2W2i4K6u0V1e0r3g2G2i4K6u0r3K9r3q4%4K9$3g2&6k6g2)9J5k6s2u0G2M7q4)9J5c8Y4c8J5k6h3g2Q4x3V1k6E0j5h3W2F1i4K6u0r3c8r3g2E0L8#2)9J5c8X3c8J5j5i4N6Q4x3X3c8@1k6i4y4@1


下一期: 反作弊对抗相关思考 & 检测手段。


传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!

收藏
点赞 ・0
打赏
分享
最新回复 (1)
雪    币: 8410
活跃值: (13534)
能力值: (RANK:385 )
在线值:
发帖
回帖
粉丝
2 楼

感谢楼主分享 ROP 多链绘制与分支循环的实战分析思路

帖子要点:
1. 整体拆成主链跑 Draw、子链做 get drawMode,链区与数据区分开存放,避免切 RSP 时踩栈
2. 五个 API 各用 8KB 专用栈并预留回主链槽位,改用 NtGdi/NtDelayExecution 规避 Shadow space
3. 主链先调 GetDC 把 hDC 写入数据区,再调 GetCursorPos 并设为循环锚点 A_Pin
4. 主链备好 rcx~r9 四参后切 RSP 进子链,子链入口先保存 rcx、rdx 到数据区槽位
5. 子链用 test/cmovz 选两枚跳栈 gadget,jmp rax 改 RSP 实现画点与画线分支
6. LineTo 支恢复 rcx、rdx 后 pop rsp 回主链入口,换栈调 NtGdiLineTo,两路在 A_pos3 汇合
7. 汇合后调 NtDelayExecution 延时约 500ms,再 pop rsp 回 A_Pin 闭合循环
8. 收尾讨论复杂度、CET 影响、gadget 来源与工程成本,定位为边界探索而非首选范式

资源链接:
· draw-test 示例源码(GitHub):a91K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6Z5j5i4N6C8k6i4W2W2i4K6u0V1e0r3g2G2i4K6u0r3K9r3q4%4K9$3g2&6k6g2)9J5k6s2u0G2M7q4)9J5c8Y4c8J5k6h3g2Q4x3V1k6E0j5h3W2F1i4K6u0r3c8r3g2E0L8#2)9J5c8X3c8J5j5i4N6Q4x3X3c8@1k6i4y4@1
· hawkeye-rop 工具仓库:a5bK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6Z5j5i4N6C8k6i4W2W2i4K6u0V1e0r3g2G2i4K6u0r3K9r3q4%4K9$3g2&6k6g2)9J5k6s2u0G2M7l9`.`.
· hawkeye-rop 预编译包下载:585K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6Z5j5i4N6C8k6i4W2W2i4K6u0V1e0r3g2G2i4K6u0r3K9r3q4%4K9$3g2&6k6g2)9J5k6s2u0G2M7q4)9J5c8Y4u0W2L8r3g2S2M7$3g2K6
· hawkeye-community 社区仓库:dc9K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6Z5j5i4N6C8k6i4W2W2i4K6u0V1e0r3g2G2i4K6u0r3K9r3q4%4K9$3g2&6k6g2)9J5k6r3y4G2L8h3#2#2L8X3W2@1P5b7`.`.

谢谢楼主的实战拆解,读完对多链 ROP 的取舍有些想法:
· 优点:把循环、分支、主链子链切换都落到具体 gadget 和栈偏移上,能力边界讲得比较清楚
· 局限:链一多,上下文保存和 RSP 横跳的维护成本明显上升,调试和排错的门槛不低
· 对比:相比常规注入后直接跑 shellcode,ROP 依赖目标模块已有 gadget,隐蔽性可能更好,但受版本和 CET 影响更大
大家对多链 ROP 的维护成本或 CET 影响有什么看法,欢迎一起聊聊

小结由 AI 辅助整理,方便大家先看个大概,完整内容还是看楼主原帖更准确。

最后于 1天前 被TkBinary编辑 ,原因:
1天前
1
游客
登录 | 注册 方可回帖
返回