Forky picoCTF 2019 Solution

Published: April 2, 2026

Description

In the function doStuff with parent and child processes, what does the child process return? There is no exploit here. This is a value-tracking exercise: the binary forks several times, every process adds the same constant to one shared integer, and you have to compute the final value, accounting for 32-bit signed overflow.

Download the binary and disassemble it.

bash
wget <url>/Forky
bash
chmod +x Forky

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1
    Read doStuff and count the forks
    Observation
    I noticed the challenge describes parent and child processes and a function called doStuff, which suggested the binary uses fork() to spawn multiple processes and that disassembling doStuff in Ghidra would reveal how many forks occur and what value each process contributes to the shared result.
    In Ghidra, doStuff sets up a shared integer (mmap with MAP_SHARED) initialized to 1000000000, then calls fork() four times in a nested fashion. Four forks produce 2^4 = 16 processes that all reach the final code path. Every one of those 16 processes adds the constant 0x499602d2 (1234567890 in decimal) to the shared integer.
    bash
    ghidra Forky &
    bash
    # Find: shared int initialized to 1000000000, 4 nested fork() calls,
    bash
    # each surviving process does  shared += 0x499602d2  before doNothing(shared).

    Expected output

    -721750240
    What didn't work first

    Tried: Run ./Forky directly to observe the output and read the return value from there

    Running the binary produces no useful printed output because doNothing discards the value silently. You see the process exit with code 0, giving you nothing to work with. You need to disassemble doStuff in Ghidra to find the shared-mapping setup, the fork count, and the constant being added.

    Tried: Use 'strings Forky' to find the constant and count the fork calls

    strings only surfaces printable ASCII sequences embedded in the binary. The integer constant 0x499602d2 is encoded as raw bytes in the instruction stream, not as a string, so it never appears in strings output. The fork call count is also invisible to strings - only disassembly reveals the nested control flow that produces 16 processes.

    Learn more

    fork() creates an exact copy of the calling process. In the parent it returns the child PID; in the child it returns 0. Crucially, after a fork both processes keep running the code that follows, so a single fork() turns one process into two. Four nested forks turn one process into 2^4 = 16.

    Because the integer is in a MAP_SHARED mapping, all 16 processes increment the same memory, not private copies. So the constant is added 16 times to the starting value.

  2. Step 2
    Compute the result under 32-bit signed overflow
    Observation
    I noticed that 16 processes each adding 0x499602d2 to a starting value of 1000000000 produces a sum far exceeding the 32-bit signed integer maximum of 2147483647, which suggested the accumulator wraps around and must be interpreted as a signed 32-bit value to get the actual number the binary hands to doNothing.
    The accumulator is a 32-bit int. Compute 1000000000 + 16 * 0x499602d2, then take it modulo 2^32 and interpret as signed. The result is -721750240, which is the value the final process hands to doNothing.
    python
    python3 - <<'PY'
    import ctypes
    total = 1000000000 + 16 * 0x499602d2
    print(ctypes.c_int32(total).value)   # -> -721750240
    PY
    bash
    # Or confirm dynamically by following forks in gdb:
    bash
    gdb ./Forky -ex 'set follow-fork-mode child' \
    bash
      -ex 'dprintf *doNothing+19, "%d\n", $eax' -ex 'run' -ex 'quit'

    The flag for this challenge is literally that number wrapped in the picoCTF format. No decoding or extraction is needed once you have the signed 32-bit result.

    What didn't work first

    Tried: Submit the unwrapped sum 20753086240 as the flag without applying 32-bit overflow

    Python integers are arbitrary precision, so '1000000000 + 16 * 1234567890' happily gives 20753086240 without any wrapping. That number is not the flag. The accumulator in the binary is a 32-bit signed int (4 bytes), so you must wrap it with ctypes.c_int32(total).value or equivalent to get the -721750240 value the real program stores.

    Tried: Use 'set follow-fork-mode parent' in gdb to trace the shared value

    With follow-fork-mode parent, gdb detaches from every child process the moment it is forked and continues tracing only the parent. The parent calls doNothing with the partially-accumulated value after each fork, not the final 16-process total. You need either follow-fork-mode child (which tracks the deepest surviving child through all four forks) or the static calculation to see the correct final value.

    Learn more

    Why the value goes negative. A 32-bit signed integer maxes out at 2147483647. The unwrapped sum here is 1000000000 + 16 * 1234567890 = 20753086240, which overflows several times. Reducing modulo 2^32 and reading the top bit as the sign gives -721750240. This is the entire point of the challenge: recognize that the addition silently wraps.

Interactive tools
  • Strings ExtractorPull printable text from any binary, library, or image. ASCII and UTF-16 detection, configurable minimum length, flag-like highlight, no command line needed.

Flag

Reveal flag

picoCTF{-721750240}

Four nested fork() calls create 16 processes that each add 0x499602d2 to a shared integer starting at 1000000000. 1000000000 + 16*1234567890 wrapped to a signed 32-bit int is -721750240, which is the flag.

Key takeaway

The Unix fork() system call duplicates the calling process, and every nested fork doubles the process count again, so n nested forks produce 2^n concurrent processes. When all of them write to the same shared memory region, their contributions stack, and the total can silently wrap around integer boundaries. Integer overflow is not just a theoretical concern: it underlies real vulnerabilities like signedness bugs in length checks, allocation-size wraparounds in malloc calls, and counter reuse in cryptographic nonces.

Related reading

Want more picoCTF 2019 writeups?

Tools used in this challenge

What to try next