ROP

2025-01-01

终于到了最怀念的ROP了(不是

0. ROP的起源

0.1 shellcode的限制: No-execute bit

回想起之前在shellcode的章节里我们有说过No-execute这个功能,就是会给内存区域设置权限:

PROT_READ: 允许进程读取
PROT_WRITE 允许进程写
PROT_EXEC: 允许进程执行

通常情况下,代码都位于elf文件的.text segment中,stack和heap是没有必要授予执行权限的
事实也是如此,现代系统的栈和堆都不可执行。
这也让我们无法注入代码让其执行了。但是这不是终点,缺少了代码注入,我们将目标转向了代码复用

0.2 思想起源: Return-to-libc

过去在x86时代,函数参数也会通过stack来传递,在基于栈的溢出中,我们可以覆盖返回地址和参数
通过精选构造参数,让函数返回值执行system("/bin/sh")

0.3 x86->x64时代的变迁

到了x64时代,函数参数不通过栈来传递(参数太多还是会用栈,不过绝大多数用不到)
要通过栈溢出执行我们所需的函数,需要解决参数的问题。
解决办法就是代码复用(Code Reuse)
参考这段代码:

01 int main() {
02    char name[16];
03    read(0, name, 128);
04 }
05 int win() {
06    sendfile(1, open("/flag", 0), 0, 1024);
07 }

其运行main函数的栈结构如下图所示:
通过精心构造输入,可让其执行win函数,窃取信息。
ret2libc的推广(或者说思路的一般化)就是ROP(Return Oriented Programming,面向返回的编程)
这个技术非常强大,到现在还在持续使用

1. ROP

1.1 ROP 归纳

总结来说,ROP就是递归的完成如下步骤:

Step 0: overflow the stack 

Step n: by controlling the return address, you trigger a gadget:
0x004005f3: pop rdi ; ret

Step n+1: when the gadget returns, it returns to an address you control (i.e., the next gadget)

通过链接这些gadgets,我们就可以构建一个rop练,并执行任意动作。

1.2 ROP gadgets

ROP中有许多小技巧需要牢记

1、stack fix-up gadgets
这些gadget可以清理栈结构,跳过数据(data)部分。
pop r12; pop rdi; pop rsi; ret
add rsp, 0x40; ret
因为pop和add rsp并不会清空栈中的内容

2、storing values into registers
pop rax, ret

3、常见和不常见 gadgets
ret (at the end of every function)
leave; ret (at the end of many functions)
pop REG; ret (restoring callee-saved registers before returning)
mov rax, REG; ret (setting the return value before returning)
因为我们可以跳到汇报指令的中间部分,指令不必常见也能够出现在gadgets当中
比如: add rsp, 0x08; ret 中肯定包含了 add esp, 0x08

4、storing addresses into registers
lea gadgets有最好,但是实际上很稀有,绝大多数被使用在函数的开头或中间,不会靠近ret指令。
Alternative #1: push rsp; pop rax; ret (equivalent to mov rax, rsp) will get the stack address into rax.
Alternative #2: add rax, rsp; ret (not perfect, but will conceptually get rsp into rax)
Alternative #3: xchg rax, rsp; ret (swap rax and rsp. DANGEROUS, be careful)

5、stack pivot
俗称栈迁移
xchg rax, rsp; ret
pop rsp; ...; ret

6、data transfer
add byte [rcx], al ; pop rbp ; ret 需要提前设置好rcx

7、syscalls
syscall指令非常稀少,最好还是用libc里的函数

8、KNOW YOUR ENVIRONMENT
rop链不是运行在空的环境中的,运行环境中到处都是有用的地址,例如:
code, stack, heap addresses in registers
code, stack, heap addresses all over the stack
所以调试很重要!!!

如何才能找到ROP gadgets呢?
当然是有现成的工具啦:
https://github.com/zardus/ctf-tools

1.3 ROP限制

使用ROP时并不总能一帆风顺,现代的计算机也使用了很多方法来抵御ROP攻击;在这种情况下,如何继续利用ROP完成利用呢?

issue 1: Limited Control:能溢出的size有限,或者遇到null byte截断
解决办法: 找到 one gadget(某种环境下直接调用execve函数就能获得shell)
往外延申,触发execve(some_garbage); 创建一个some_garbage文件,这个文件的作用是读取flag

issue 2:Address Space Layout Randomization(aslr)
缓解措施1: 部分覆盖地址,之前提到过,一个page内,相对偏移是固定的,如果能够重新回到程序开始,这会是个有用的方法
缓解措施2:其他漏洞导致的信息泄露,泄露地址

issue 3:stack canaries
缓解措施: 其他漏洞泄露canary
issue 4:奇异的学术解决方案
rop是攻击者和防御者之间长期的猫抓老鼠游戏的一部分(还有其他技术手段),学术圈也提出了很多rop缓解措施:
一、移除rop gadgets(太繁重,不现实):
G-Free: Defeating Return-Oriented Programming through Gadget-less Binaries
二、运行时检测ROP(可部署,但是可以绕过):
kBouncer:  Efficient and Transparent ROP Mitigation
ROPecker: A Generic and Practical Approach for Defending Against ROP Attacks
三、control flow integrity:
这个方法的核心思想是:每当发生劫持的控制流转移时,确保其目标是它应该能够返回的地方!
这个思路引起了一场军备竞赛,很多奇异的ROP技术因此诞生:


针对cfi, intel提出了一个它自己的版本:
Intel recently (like, September 2020) released processors with Control-flow Enforcement Technology (CET).
核心思路是在函数的开头,添加endbr64指令
在使能CET的cpu上,间接跳转(ret,jmp rax, call rdx,etc) 必须以endbr64指令结束,否则程序会终止。
这个方法很强大,然而,道高一尺魔高一丈,仍然有高级ROP(Block Oriented Programming, SROP, etc)可以绕过它;只是需要更复杂的利用

再来一个好玩的问题:

issue 5: hacking blind
在某种特定的场景下,即使没有程序,也可以尝试利用
这种利用的前提条件是: 程序的内容只在程序起始进行随机化,随后就不会变化
标准的 blind attack要求服务是可以fork的,然后进行如下步骤:
一、Break ASLR and the canary byte-by-byte. Now we can redirect memory semi-controllably.
二、Redirect memory until we have a survival signal (i.e., an address that doesn't crash).
三、Use the survival signal to find non-crashing ROP gadgets.
四、Find functionality to produce output.
五、Leak the program.
六、Hack it.
当然是很难的啦~