Skip to main content

not crypto picoMini by redpwn Solution

Reverse engineer a binary to understand how it validates input and extract the expected flag value.

Published: April 2, 2026Updated: August 13, 2026

Description

There's crypto in here, but the challenge isn't about the crypto. Find another way.

Download the not-crypto binary from the challenge page.

Make it executable: chmod +x not-crypto

bash
chmod +x not-crypto

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1Analyze in Ghidra
    Observation
    The name says not-crypto and the description says the cryptography is present but beside the point. Look in Ghidra for the structural weakness: the memcmp where the decrypted flag meets your input.
    Load the binary in Ghidra and locate main. It applies a complete AES encryption routine to your 64-byte input and compares the result to a hardcoded encrypted value using memcmp. Reversing the AES math is the hard path - instead, look for a smarter approach.
    Learn more

    Ghidra is a free, open-source reverse engineering suite developed by the NSA and released publicly in 2019. Its decompiler converts x86/x86-64 (and many other architectures) machine code into C-like pseudocode, making it far easier to understand a binary's logic than reading raw assembly. For this challenge, Ghidra reveals the comparison structure and identifies the memcmp call.

    The challenge name "not-crypto" is a deliberate hint: the binary contains cryptographic operations, but solving it does not require understanding or reversing the cryptography. This is a common CTF design philosophy - the intended solution exploits a structural weakness (the plaintext flag being in memory at the comparison point) rather than breaking the cryptographic algorithm itself.

    Reversing AES, even a simplified variant, is mathematically intensive and time-consuming under CTF time constraints. Recognizing when a challenge is designed to be solved via dynamic analysis rather than static cryptanalysis is an important meta-skill in CTF competitions.

  2. Step 2Attach GDB and find the memcmp call
    Observation
    Ghidra gives the memcmp at a file offset, and the binary is PIE, so that offset is not an address. Read the load base at runtime and break at base plus offset.
    PIE is enabled, so get the base address first. Then set a breakpoint at the memcmp call inside the comparison function. Ghidra shows the call site offset.
    bash
    gdb ./not-crypto
    bash
    break main
    bash
    run $(python3 -c "print('A'*64)")
    bash
    info proc map

    Expected output

    0x<addr>:	"picoCTF{c0mp1l3r_0pt1m1z4t10n_15_pur3_w1z4rdry_but_n0_pr0bl3m?}"
    What didn't work first

    Tried: Setting a breakpoint directly at the Ghidra-reported offset (e.g. break *0x13b9) without adding the PIE base address

    GDB reports it cannot access that memory, or plants the breakpoint somewhere meaningless, because the number is a file offset rather than a runtime address. PIE loads the binary at a random base, which the process map shows. Read the map, then add.

    Tried: Running the binary without arguments or with fewer than 64 bytes, expecting to still reach the memcmp breakpoint

    The program checks the input length and exits early unless it is exactly 64 bytes, so the encryption and the comparison never run and the breakpoint never fires. Pad to exactly 64 characters to reach the comparison.

    Learn more

    PIE (Position Independent Executable) is a compiler/linker option that makes the binary load at a randomly chosen base address (courtesy of ASLR) rather than a fixed address. This means the actual address of any instruction = base address + offset shown in Ghidra/objdump. Without knowing the base address, breakpoints set at absolute addresses will fail.

    info proc map in GDB displays the current process's memory map, including the address range where the main executable was loaded. The start of the .text segment (the first executable mapping) is the base address. Adding the Ghidra-reported offset of the memcmp call to this base gives the runtime address to break on.

    Running with 64 'A' characters ensures the program proceeds far enough to reach the comparison (it expects a 64-byte input). The padding value doesn't matter - the goal is to reach the memcmp breakpoint where the expected flag value is loaded into a register, regardless of what the user input is.

  3. Step 3Break at memcmp and read the expected flag
    Observation
    The first argument to memcmp arrives in rdi, and Ghidra shows the binary decrypting the expected flag into that buffer. Print the string at rdi when the breakpoint fires.
    Set a breakpoint at the memcmp call site (PIE base + offset shown in Ghidra, e.g. 0x13b9). Run with 64 junk bytes. When the breakpoint hits, the rdi register points to the expected plaintext flag.
    bash
    break *0x<base>+0x13b9
    bash
    continue
    bash
    x/s $rdi
    What didn't work first

    Tried: Running 'x/s $rsi' instead of 'x/s $rdi' to read the flag

    rsi holds the second argument, which is your own input after encryption, not the expected flag, so you get garbled binary. The plaintext is in the first argument. Print both and take whichever starts with the flag prefix.

    Tried: Trying to reverse the AES encryption statically in Ghidra by tracing the key schedule and decrypting the hardcoded ciphertext bytes

    That works in principle and asks you to identify the AES variant, extract the exact key bytes, and reimplement the decryption, all under time pressure and all easy to transcribe wrong. The breakpoint reads the already-decrypted flag out of memory in one command.

    Learn more

    memcmp(ptr1, ptr2, n) compares n bytes at two memory addresses and returns 0 if they are identical. On x86-64, function arguments are passed in registers: the first argument goes in rdi, the second in rsi, and the count in rdx. At the breakpoint, rdi and rsi point to the two buffers being compared - one is your transformed input, the other is the expected (decrypted) flag.

    x/s $rdi is GDB's examine command: x for examine, /s to interpret the memory as a null-terminated string. It reads and displays the bytes at the address in rdi as text. If the flag is stored as a null-terminated string in that buffer, this single command reveals the entire expected value without any cryptographic analysis.

    This technique - breaking at comparison functions to read expected values - is applicable to many CTF challenges and real-world scenarios: license key validation, password checking, and authentication tokens that are compared in memory are all vulnerable to this approach. Countermeasures include constant-time comparison (not vulnerable to timing attacks but still readable in a debugger), and anti-debugging checks that detect GDB's presence via ptrace(PTRACE_TRACEME).

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{c0mp1l3r_0pt1m1z4t10n_15_pur3_w1z4rdry_but_n0_pr0bl3m?}

Debugger-based approaches often bypass complex encryption - if memcmp compares your input against the decrypted flag, reading the $rdi register at the breakpoint reveals the expected value before any comparison occurs.

Key takeaway

However strong the cryptography inside a program, any secret compared against user input exists in plaintext in memory at the moment of comparison. A breakpoint on memcmp, strcmp, or an inline loop exposes both operands in registers before the comparison runs. Meeting the program at the comparison defeats license-key validation, hardcoded password checks, and challenge-response validators, whatever algorithm produced the expected value.

Related reading

Tools used in this challenge

Where to go next