Ret2csu
原理
在 glibc 的源码中,有一个函数叫 __libc_csu_init,它在程序启动时负责调用 .init_array 中的初始化函数
这个函数末尾有一段普适性很强的指令,经常被用作 ROP gadget。安全研究者最早将利用这段代码的技术称为 Return-to-csu,简称 Ret2csu。
因为 __libc_csu_init 是 libc 标准启动代码的一部分,只要程序是动态链接的,几乎一定存在,而且在没有 PIE 的情况下地址固定。这给了攻击者在 “找不到常规 pop rdi” 时一个可靠的备胎
实战案例
Pwn题滞销,帮帮我好吗_(Ret2csu)

代码只有两行

进入 sub_401132() 函数,看不出来啥问题

查看伪代码
1 | push rbp ; 保存父函数的栈底(Old RBP) |
使用 Pwndbg 动态调试,pwn cyclic 生成垃圾数据

rsi 寄存器在伪代码中为参数 buf 的地址,值为 0x7fffffffda80
rsp 寄存器指向返回地址,值为 0x7fffffffdaa8
我们用这两个真实的动态地址做一下减法,得到要覆盖的垃圾数据为 40 个字节

因为没有后门,所以我们需要调用 syscall 并让系统去执行 execve("/bin/sh", 0, 0)
为了启动这个 syscall,Linux 规定了死命令,必须提前在寄存器里摆好参数:
rax寄存器里必须是 59(调用execve函数)rdi寄存器里必须是"/bin/sh"字符串的内存地址rsi寄存器里必须是 0rdx寄存器里必须是 0
我们可以利用程序里的残余指令,比如 pop rdi; ret。但是查了一遍没给,可惜可惜

先来计算 /bin/sh 的地址
1 | 地址 | 字符 |

所以起始地址就是 0x402016
虽然常规的 pop rdi 没了,但只要是 C 语言编译的程序,系统都会默认自带一个初始化功能,叫 __libc_csu_init
在 __libc_csu_init 函数的尾部,躺着两段连续的汇编指令

1 | .text:0x4011F2 pop rbx ; 弹出给 rbx |
1 | .text:0x4011D8 mov rdx, r14 |
思路如下:
pop rbx:这里的结果会影响到最后的call qword ptr [r15+rbx*8],因为r15本身就由我们控制。选择填 0,这样就直接调用r15指向的函数pop rbp:这里的结果会影响到后面的cmp rbp, rbx,再判断之前就已经add rbx, 1了,所以要填入 1,不然会重新循环pop r12:这里的结果会影响到后面的mov edi, r12d,edi就是rdi,填入/bin/sh地址pop r13:这里的结果会影响到后面的mov rsi, r13,也就是execve("/bin/sh", 0, 0)第二个参数,填 0pop r14:同理,这里是第三个参数,填 0pop r15:这里的结果会影响到后面的call qword ptr [r15+rbx*8],因为call必须得给一个真实的函数地址,否则会报错。我们选择alarm,调用它只是定个闹钟
1 | payload = b'A' * 40 # 垃圾数据填满栈空间 |
到跳转到 0x4011D8 处时,执行过程如下
rdx与rsi为 0,rdi为/bin/sh地址call alarm,r15被填充为了alarm函数地址比较
rbp和rbx,相等则进去进入0x4011EE中执行
1 | .text:0x4011EE add rsp, 8 |
这样又进入了 pop 段中,但是我们已经不需要了,所以填充垃圾数据进去
1 | payload += p64(0) * 7 # 抵消掉这 1 个 add 和 6 个 pop 的干扰 |
此时来到了最后的 .text:0x4011FC retn 处,我们劫持
1 | payload += p64(pop_rax_syscall) |
1 | from pwn import * |