Reverse picoCTF 2023 Solution

Published: April 26, 2023

Description

A stripped binary named ret hides a password in plain sight. Retrieve it via static inspection.

Triage the binary first: file ret tells you the architecture, checksec lists mitigations.

If it looks executable, scan strings with a length filter and grep for pico.

If strings comes up empty, switch to ltrace or gdb (the flag may be built at runtime).

bash
wget https://artifacts.picoctf.net/c/270/ret
bash
file ret && checksec --file=ret
bash
strings -n 8 ret | grep pico

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
For deeper static analysis, the Ghidra reverse engineering guide covers decompilation, and the GDB for CTF guide is the right next step when strings comes up empty.
  1. Step 1
    Triage with file and checksec
    Observation
    I noticed the challenge provides a stripped binary named ret with no extension or description of its format, which suggested confirming the file type and mitigation profile before picking any analysis tool to avoid wasting effort on the wrong approach.
    file confirms it is an ELF you can inspect, and checksec tells you which mitigations are present. Both run in a fraction of a second and decide your next move.
    bash
    file ret
    bash
    checksec --file=ret
    What didn't work first

    Tried: Skip triage and run strings immediately on the downloaded file.

    Without file, you miss that some challenge downloads are disguised archives (zip, tar, or custom containers) that report 'data' instead of ELF. Running strings on a zip produces hundreds of garbage runs and you never find the flag. file tells you in one second whether you have an ELF at all before investing any analysis effort.

    Tried: Run checksec without first confirming the file is an ELF.

    checksec on a non-ELF file either errors out or reports misleading results because it parses ELF headers that are not there. The correct order is file first to confirm the binary type, then checksec to read the actual mitigation fields from a valid ELF header.

    Learn more

    Leading with file and checksec is the habit that prevents wasted effort. file ret reports architecture, bitness, dynamic vs static, and stripped vs not. If file says "data" instead of ELF, the challenge is probably a packaged blob (zip, tar, custom container) and you are about to waste 10 minutes on the wrong tool. checksec --file=ret lists ASLR, stack canaries, NX, PIE, RELRO; even when the challenge is just static recovery, knowing the mitigations is useful for the follow-on challenges.

  2. Step 2
    Filter strings with -n 8 and grep
    Observation
    I noticed the binary is stripped, meaning symbol names are gone but .rodata string literals remain intact, which suggested that strings with a noise-cutting length filter and a grep for 'pico' would surface a hard-coded flag without any dynamic analysis.
    strings -n 8 drops short noise like 4-byte symbol fragments. Most flags are at least 12 characters, so an 8-character cutoff keeps real candidates without hiding anything useful.
    bash
    strings ret | wc -l
    bash
    strings -n 8 ret | wc -l
    bash
    strings -n 8 ret | grep pico

    Expected output

    picoCTF{3lf_r...f62bc8}
    What didn't work first

    Tried: Run strings ret without the -n 8 flag and grep pico in the raw output.

    The default minimum length is 4 characters, which means the output contains thousands of 4-byte compiler-generated fragments (register names, short symbol stubs, table entries). The flag candidate is buried in thousands of lines and easy to scroll past. Adding -n 8 reduces the line count dramatically so the flag-shaped run is immediately visible.

    Tried: Use grep -i picoctf instead of grep pico to search the strings output.

    Case-insensitive grep on a stripped binary does not hurt accuracy, but some solvers type the wrong prefix entirely (searching for 'flag{' or 'ctf{') because they forget this competition uses the picoCTF{} wrapper. The flag literal in .rodata starts with 'picoCTF', so grep pico catches it with the fewest characters while grep ctf{ misses the leading 'pico' and returns zero results.

    Learn more

    Static analysis examines a binary without executing it. The cheapest static tool is strings, which extracts printable runs from a binary file. C string literals live in .rodata verbatim, so any hard-coded password, URL, error message, or flag shows up directly in strings output.

    A stripped binary has had its symbol table and debug info removed (via strip or -s). Stripping hides function names and variable names, but it does not affect string literals stored in the data section. That is why strings still works: the flag is a string constant the linker placed in .rodata regardless of whether the binary is stripped.

    The -n flag sets the minimum string length (default 4). The before/after counts make the noise reduction concrete: at the default the output is filled with 4-byte fragments from compiler-generated tables; at -n 8 only meaningful runs survive, and the flag jumps straight to your eye. -e l handles 16-bit little-endian (Windows binaries); -e b handles big-endian.

  3. Step 3
    Decision tree if strings is empty
    Observation
    I noticed that if strings | grep pico returns nothing the flag must be built at runtime and compared via a library call, which suggested using ltrace to intercept the strcmp arguments before reaching for a full decompiler.
    When strings | grep pico returns nothing, the flag is built at runtime. Use ltrace to catch the comparison, or gdb to break before the strcmp.
    bash
    ltrace -e strcmp ./ret
    bash
    ltrace ./ret
    What didn't work first

    Tried: Run ltrace ./ret without filtering and look for the flag manually in the full output.

    ltrace without -e strcmp logs every library call (malloc, printf, read, free, write, memcpy, etc.) and on a moderately complex binary this produces hundreds of lines per second of execution. The strcmp call with the flag argument is buried or scrolls off the terminal. Using -e strcmp filters to only comparison calls and the flag appears as the second argument on the first matching line.

    Tried: Use strace instead of ltrace when strings comes up empty.

    strace intercepts kernel syscalls (open, read, write, mmap), not library function calls. A strcmp that compares the password happens entirely in user space without any syscall, so strace never sees the comparison arguments and reports nothing useful. ltrace is the correct tracer here because it hooks the C library boundary where strcmp lives.

    Learn more

    When strings shows nothing useful, the binary is constructing the flag dynamically: XOR-decoding it from another buffer, building it character-by-character on the stack, or comparing against a hash. Real flags still have to exist in memory at the moment of comparison.

    ltrace intercepts library calls and prints arguments. ltrace -e strcmp ./ret filters down to strcmp calls; on a typical "guess the password" binary you will see strcmp("your_input", "real_flag") printed plainly. That is the entire reversal effort, eliminated by knowing which library function the binary uses to compare.

    When ltrace cannot see the comparison (statically linked binary, custom comparison loop, or stripped symbols), drop into gdb: break on the call to strcmp or the inline comparison address, run the program with a placeholder input, and read the second argument register on x86_64 (x/s $rsi). Ghidra is the heaviest tool but gives you a decompiled C view of the flag-building logic when the others fall short.

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{3lf_r...f62bc8}

No reversing tools beyond strings are required for this warm-up.

Key takeaway

Hard-coded string constants in compiled binaries live verbatim in the read-only data section regardless of whether the binary has been stripped, because stripping only removes symbol table metadata, not the program data itself. The strings utility extracts these printable runs without executing the binary, making it the fastest first pass in any reverse engineering workflow. The same principle applies to firmware images, mobile APKs, and DLL files: before reaching for a decompiler, scan for cleartext secrets that were never meant to be visible.

Related reading

Want more picoCTF 2023 writeups?

Tools used in this challenge

What to try next