basic-file-exploit picoCTF 2022 Solution

Published: July 20, 2023

Description

A simple netcat binary lets you write and read back numbered entries. An integer underflow or unchecked index allows reading entry 0 before writing anything - which triggers a special code path that prints the flag.

Launch a challenge instance from the picoCTF web panel; the panel shows the host and a port placeholder shown here as <PORT_FROM_INSTANCE>.

Connect with netcat. No binary download required.

Explore the menu: option 1 writes a numbered entry, option 2 reads one back.

bash
nc saturn.picoctf.net <PORT_FROM_INSTANCE>
  1. Step 1Explore the program menu
    Connect and walk through the menu. The interesting option is the read path: it prints whatever entry index you ask for.
    Learn more

    The C source tends to ship with these challenges. Read it before poking at inputs: the bounds check tells you whether index 0 is allowed, what index 0 actually maps to, and where the flag is printed.

    The flag is usually wired to a sentinel/special-path: an if (idx == 0) print_flag() branch or an entry initialised with the flag string at slot 0. Triggering that path is a validation bypass, not a memory corruption.

  2. Step 2Trigger the index validation bypass
    Ask the read option for entry 0 before writing anything. The upper-bound check passes; the missing lower-bound check lets index 0 hit the flag-printing path.
    bash
    nc saturn.picoctf.net <PORT_FROM_INSTANCE>
    bash
    # Select option 2 (read), then enter: 0
    Learn more

    Two distinct bug classes can both end up at the same exploit. They look the same from the outside but the root cause is different.

    Missing lower-bound check. The code checks idx < num_entries but never checks idx >= 1. Entry 0 is a sentinel slot pre-populated with the flag, so reading it dumps the flag. Example:

    int idx = atoi(line);
    if (idx >= num_entries) { puts("bad index"); return; }
    puts(entries[idx]);   // entries[0] holds the flag

    Signed/unsigned cast confusion. Input is read as int (signed) and validated, but indexed as size_t (unsigned). Negative inputs sail through a signed upper-bound check, then wrap to huge positives at the array access:

    int idx = atoi(line);
    if (idx > num_entries) { puts("bad index"); return; }
    // idx = -1 passes (since -1 < num_entries).
    // At access, (size_t)-1 = 0xFFFFFFFF, OOB read.

    The fix is to validate against both bounds and to use a typed-correct comparison: if (idx < 0 || (size_t)idx >= num_entries).

  3. Step 3Collect the flag
    Once the special path runs, the server prints the flag. Nothing else to do.
    Learn more

    The fix is to validate both lower and upper bounds and to use a single integer type end-to-end (size_t for array indexing). Static analyzers (Coverity, cppcheck, Clang Static Analyzer) flag this; fuzzers like AFL++ surface it through edge cases (0, -1, INT_MAX).

Flag

picoCTF{M4K3_5UR3_70_CH3CK_Y0UR...}

Read entry 0 (or enter a very large number) before writing any entries to trigger the unchecked code path that prints the flag.

Want more picoCTF 2022 writeups?

Useful tools for Binary Exploitation

Related reading

What to try next