JITFP picoCTF 2026 Solution

Published: March 20, 2026

Description

If we can crack the password checker on this remote host, we will be able to infiltrate deeper into this criminal organization. The catch is it only functions properly on the host on which we found it.

Launch the challenge instance and SSH into the server.

Find the password checker binary on the remote host.

  1. Step 1Understand why it only works on the remote host
    The binary spawns 65 checker processes for a 33-character password, so the password is encoded across two passes (each char checked twice for redundancy or layered validation). Verify the count empirically: ps aux | grep checker should list ~65 checker_N children.
    bash
    ssh <user>@<HOST> -p <PORT_FROM_INSTANCE>
    bash
    ps aux | grep checker | wc -l   # confirm ~65 children
    bash
    ls /proc/$(pgrep passwordchecker)/mem
    Learn more

    The /proc filesystem is a virtual filesystem in Linux that exposes kernel data structures as files. /proc/<pid>/mem provides direct read/write access to a process's virtual address space, subject to permission checks. A process can read another process's memory if it has the same UID or the CAP_SYS_PTRACE capability. This interface is used by debuggers like GDB and analysers like Valgrind.

    This challenge exploits a time-of-check to time-of-use (TOCTOU) race: the checker processes populate their function pointer tables at a specific time after startup. By reading /proc/pid/mem at exactly the right timing window (t = j + 0.5s for checker j), you catch the table in a valid state. On a different host with different timing characteristics, the window might be missed, explaining why the binary "only works on the remote host."

    The fact that the binary spawns 65 separate processes (one per password character) and uses interprocess communication via /proc/memis a deliberate obfuscation technique. It makes the password checker host-dependent and resistant to simple static analysis, since the "password" is only assembled at runtime from the live processes.

  2. Step 2Race /proc/<pid>/mem to read the FP table
    Checker j sleeps j seconds, then populates its function pointer for a brief window. Time the read at t = j + 0.5s so the read lands right after the time.sleep(0.5) in the checker, mid-execution. Catch OSError on every /proc/<pid>/mem read so silent failures (window misses, dead PIDs) surface explicitly instead of being swallowed.
    python
    python3 << 'EOF'
    import time, struct
    
    # Find checker PIDs (spawned sequentially after the main process)
    main_pid = int(open("/proc/self/stat").read().split()[3])
    checker_pids = list(range(main_pid + 1, main_pid + 66))
    
    password_chars = []
    for j, pid in enumerate(checker_pids):
        # Wait until t = j + 0.5s for checker j (its sleep is j s + 0.5s window)
        time.sleep(0.5)
        try:
            with open(f"/proc/{pid}/maps") as f:
                for line in f:
                    if "r-xp" in line:
                        addr = int(line.split("-")[0], 16)
                        break
            with open(f"/proc/{pid}/mem", "rb") as f:
                f.seek(addr)
                ptr = struct.unpack("<Q", f.read(8))[0]
                password_chars.append(chr(ptr & 0xFF))
        except OSError as e:
            # Don't silently swallow: missed window, dead PID, or perms
            print(f"[!] checker {j} pid {pid} failed: {e}")
            password_chars.append("?")
    
    print("Password:", "".join(password_chars))
    EOF
    Learn more

    /proc/<pid>/maps lists every memory region of a process with its address range, permissions (r/w/x/p for read/write/execute/private), offset, device, inode, and filename. This is how debuggers know where code (r-xp segments), stack, heap, and mapped libraries are located. By parsing maps for the r-xp (executable) region, you find the address of the text segment where function pointer tables live.

    Reading /proc/<pid>/mem requires seeking to the target virtual address (using f.seek(addr)) before reading. The file is a sparse file representing the entire 64-bit virtual address space; unreadable regions (unmapped pages) raise OSError. The struct.unpack("<Q", data) call parses an 8-byte little-endian 64-bit integer - the format for a function pointer on x86-64.

    This challenge beautifully illustrates a theme in Linux security: the /proc filesystem is extremely powerful and can be abused by processes with appropriate permissions. Privilege escalation exploits sometimes use /proc/mem writes to inject shellcode into privileged processes. Containerised environments (Docker, Kubernetes) restrict /proc access with seccomp profiles and Linux capabilities to prevent this class of attack.

  3. Step 3Submit the recovered password
    Use the reconstructed 33-character password to authenticate to the binary. The binary verifies it and prints the flag.
    bash
    echo '<RECOVERED_PASSWORD>' | ./passwordchecker
    Learn more

    This challenge is a creative combination of process forensics, timing attacks, and Linux internals. The password is never stored as a static string in the binary; it is assembled dynamically from the function pointer values of 65 concurrent child processes. This is a novel form of anti-analysis: the binary intentionally resists standard reverse engineering approaches by distributing the secret across runtime state rather than compiled data.

    In the broader context of malware analysis, similar techniques appear as process hollowing (injecting code into a suspended process), reflective DLL injection (loading a DLL from memory without going through the Windows loader), and process doppelgänging (using transactional NTFS to execute code that never touches the filesystem as a normal file). These all exploit the fact that running processes have state that static analysis tools cannot see.

    The /proc/pid/mem interface is also the basis for GDB's memory inspection on Linux. When you use x/10xg $rsp in GDB to examine the stack, GDB reads the target process's memory through this interface. Understanding /proc directly is therefore directly useful for understanding how debuggers work at the kernel level. See the Python for CTF guide for more timing-window and process scripting patterns.

Flag

picoCTF{...}

JITFP exploits a timing window: the 65 checker functions populate a function pointer table in /proc/pid/mem at t = j + 0.5s after start. Reading that table at the right time for each of the 65 checker PIDs reconstructs the full 33-character password.

Want more picoCTF 2026 writeups?

Useful tools for Reverse Engineering

Related reading

What to try next