从 ROP 到 JOP 攻击(amd64)
源:MoeCTF 2025 call_it official writeup
其实 JOP 更像是对 ROP 的概念扩充而不是一个新的攻击方式,让我们先来回顾下熟悉的 amd64 ROP。在 ROP 中,我们大概可以将 gadgets 分为三类:
- 传递参数或做准备的 gadgets(例如
pop rdi
)、收集函数返回值的 gadgets(例如mov rdi, rax
) - 执行有意义操作的 gadgets(例如
system
、orw
、syscall
、shellcode
)。 - 控制 ROP 本身的 gadgets(例如
ret
、leave
栈迁移)
以上 gadgets 通过 ret
指令和 rsp
寄存器串联起来。
为了引出 JOP,我想到了一个好方法,一个“分解”x86 中较复杂指令的游戏:
(下文 reg
表示任意通用寄存器,tmp
表示内部临时寄存器。)我们来“分解”pop reg
指令,它“等效”于 mov reg, qword ptr [rsp]; add rsp, 8
。(我们忽略了对标志位的影响,如果考虑的话第二个指令应为 lea rsp, [rsp + 8]
。)JOP 链的位置完全可以不在栈上,最终我们可以提取出:
mov reg1, qword ptr [reg2 + offset]
或
mov qword ptr [reg2 + offset], reg1
(由于有 + offset
,我们不需要 add
,下同。)这就是 JOP 中类似 ROP 的“传参准备”或“收集结果”gadgets,例如考虑 reg1
是 rdi
的情况。reg2
就是 JOP 链基地址。
但是可以看到这并不能构造链式执行。现在我们来“分解”ret
指令,它“等效”于 pop tmp; jmp tmp
,由上文进而“展开”为 mov tmp, qword ptr [rsp]; add rsp, 8; jmp tmp
,现在我们又提取出了:
mov reg1, qword ptr [reg2 + offset]
jmp reg1 或 call reg1 或 ...(跳转)
我们只需预先在 reg2 + offset
位置布置好下一个 JOP gadget,这就是 JOP 中类似 ROP 的“控制”gadgets。
把上述两部分拼起来,我们最终获得了和 ROP 相同的能力。它与栈无关,与函数返回无关。这就是 JOP。希望大家能通过本文从 ROP 流畅过渡到更广义的 JOP 概念。以上只是我个人极不严谨的理解,可能和学术界普遍的描述有较大偏差,仅供参考。