Description
There's crypto in here, but the challenge isn't about the crypto. Find another way.
Setup
Download the not-crypto binary from the challenge page.
Make it executable: chmod +x not-crypto
chmod +x not-cryptoSolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1
Analyze in GhidraObservationI noticed the challenge is named 'not-crypto' and the description says the crypto is present but not the point, which suggested static analysis in Ghidra to find the structural weakness, specifically the memcmp call where the decrypted flag is compared against my 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
memcmpcall.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.
Step 2
Attach GDB and find the memcmp callObservationI noticed Ghidra revealed a memcmp call at a specific file offset and that the binary is PIE-enabled, which suggested using 'info proc map' at runtime to find the actual load base address so I could set a valid breakpoint at base + 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.bashgdb ./not-cryptobashbreak mainbashrun $(python3 -c "print('A'*64)")bashinfo proc mapExpected 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 'Cannot access memory at address 0x13b9' or places the breakpoint somewhere nonsensical, because 0x13b9 is a file offset, not a runtime virtual address. With PIE, the binary is loaded at a randomly chosen base (shown in 'info proc map'), and the real runtime address is base + offset. You must read the map first, then compute the sum.
Tried: Running the binary without arguments or with fewer than 64 bytes, expecting to still reach the memcmp breakpoint
The program validates input length and exits early if the buffer is not exactly 64 bytes, so the AES routine and the subsequent memcmp call are never reached. The breakpoint never fires. Padding with exactly 64 dummy characters (e.g. 'A'*64) is required to advance execution to the comparison site.
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 mapin GDB displays the current process's memory map, including the address range where the main executable was loaded. The start of the.textsegment (the first executable mapping) is the base address. Adding the Ghidra-reported offset of thememcmpcall 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
memcmpbreakpoint where the expected flag value is loaded into a register, regardless of what the user input is.Step 3
Break at memcmp and read the expected flagObservationI noticed the x86-64 calling convention passes the first argument to memcmp in rdi, and Ghidra showed the binary decrypts the expected flag into that first buffer, which suggested examining 'x/s $rdi' at the breakpoint to read the plaintext flag directly from memory.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.bashbreak *0x<base>+0x13b9bashcontinuebashx/s $rdiWhat didn't work first
Tried: Running 'x/s $rsi' instead of 'x/s $rdi' to read the flag
On x86-64, rsi holds the second argument to memcmp, which is your transformed (AES-encrypted) user input - not the expected flag. The output is garbled binary data rather than a readable flag string. The expected plaintext flag is in rdi (the first argument); checking both registers and comparing which one starts with 'picoCTF{' quickly reveals the correct one.
Tried: Trying to reverse the AES encryption statically in Ghidra by tracing the key schedule and decrypting the hardcoded ciphertext bytes
This approach works in principle but requires correctly identifying the AES variant, extracting the exact key bytes, and implementing the full decryption - a significant effort under CTF time constraints with room for transcription errors. The debugger breakpoint technique reads the already-decrypted flag directly from memory in one command, making static cryptanalysis entirely unnecessary for this challenge.
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 inrsi, and the count inrdx. At the breakpoint,rdiandrsipoint to the two buffers being compared - one is your transformed input, the other is the expected (decrypted) flag.x/s $rdiis GDB's examine command:xfor examine,/sto interpret the memory as a null-terminated string. It reads and displays the bytes at the address inrdias 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.