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.
Setup
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.
nc saturn.picoctf.net <PORT_FROM_INSTANCE>Solution
Walk me through it- Step 1Explore the program menuConnect 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. - Step 2Trigger the index validation bypassAsk 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: 0Learn 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_entriesbut never checksidx >= 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 flagSigned/unsigned cast confusion. Input is read as
int(signed) and validated, but indexed assize_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). - Step 3Collect the flagOnce 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_tfor 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.