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.
Setup
Launch the challenge instance and SSH into the server.
Find the password checker binary on the remote host.
Solution
Walk me through it- Step 1Understand why it only works on the remote hostThe 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>bashps aux | grep checker | wc -l # confirm ~65 childrenbashls /proc/$(pgrep passwordchecker)/memLearn more
The /proc filesystem is a virtual filesystem in Linux that exposes kernel data structures as files.
/proc/<pid>/memprovides 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 theCAP_SYS_PTRACEcapability. 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/memat exactly the right timing window (t = j + 0.5sfor checkerj), 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. - Step 2Race /proc/<pid>/mem to read the FP tableChecker
jsleepsjseconds, then populates its function pointer for a brief window. Time the read att = j + 0.5sso the read lands right after thetime.sleep(0.5)in the checker, mid-execution. CatchOSErroron every/proc/<pid>/memread so silent failures (window misses, dead PIDs) surface explicitly instead of being swallowed.pythonpython3 << '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)) EOFLearn more
/proc/<pid>/mapslists every memory region of a process with its address range, permissions (r/w/x/pfor read/write/execute/private), offset, device, inode, and filename. This is how debuggers know where code (r-xpsegments), stack, heap, and mapped libraries are located. By parsingmapsfor ther-xp(executable) region, you find the address of the text segment where function pointer tables live.Reading
/proc/<pid>/memrequires seeking to the target virtual address (usingf.seek(addr)) before reading. The file is a sparse file representing the entire 64-bit virtual address space; unreadable regions (unmapped pages) raiseOSError. Thestruct.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
/procfilesystem is extremely powerful and can be abused by processes with appropriate permissions. Privilege escalation exploits sometimes use/proc/memwrites to inject shellcode into privileged processes. Containerised environments (Docker, Kubernetes) restrict/procaccess with seccomp profiles and Linux capabilities to prevent this class of attack. - Step 3Submit the recovered passwordUse the reconstructed 33-character password to authenticate to the binary. The binary verifies it and prints the flag.bash
echo '<RECOVERED_PASSWORD>' | ./passwordcheckerLearn 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/meminterface is also the basis for GDB's memory inspection on Linux. When you usex/10xg $rspin GDB to examine the stack, GDB reads the target process's memory through this interface. Understanding/procdirectly 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.