Skip to main content

Forky picoCTF 2019 Solution

Trace program execution through a forked process tree to identify which branch produces the flag.

Published: April 2, 2026Updated: August 25, 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 1Read doStuff and count the forks
    Observation
    The challenge talks about parent and child processes and a function called doStuff. So the binary uses fork() to spawn several processes, and disassembling doStuff in Ghidra will show how many forks happen and what each one 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 prints nothing useful, because doNothing discards the value silently and the process just exits 0. 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 in the binary. The constant 0x499602d2 is raw bytes in the instruction stream, not a string, so it never shows up. The fork count is invisible to strings too; 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 2Compute the result under 32-bit signed overflow
    Observation
    Sixteen processes each adding 0x499602d2 to a starting value of 1000000000 blows well past the 32-bit signed maximum of 2147483647. The accumulator wraps, so the value has to be read as a signed 32-bit int to get what the binary actually 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 cheerfully gives 20753086240 with no wrapping, and that is not the flag. The binary's accumulator is a 32-bit signed int, so wrap it with ctypes.c_int32(total).value to get the -721750240 the program actually stores.

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

    With follow-fork-mode parent, gdb detaches from each child the moment it forks and traces only the parent. The parent calls doNothing with a partially accumulated value after each fork, not the 16-process total. Use follow-fork-mode child, which tracks the deepest surviving child through all four forks, or do the arithmetic statically.

    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() call duplicates the calling process, and each nested fork doubles the count again, so n nested forks give 2^n processes. When all of them write to the same shared memory, their contributions stack, and the total can wrap past an integer boundary without a word of warning. Integer overflow is not academic: it is behind signedness bugs in length checks, allocation-size wraparounds in malloc, and counter reuse in cryptographic nonces.

Related reading

Tools used in this challenge

Where to go next