Description
A binary accepts a 100-character hex key and validates it against a hardcoded target using a custom jumble function. Recover the key and decrypt the flag.
Setup
Download the otp binary and flag.txt from the challenge page.
Make the binary executable: chmod +x otp
Solution
- Step 1Analyze the binary in GhidraLoad otp in Ghidra and locate the validation function. It takes your 100-character hex input, applies a custom 'jumble' transformation, and compares the result to a hardcoded 100-byte target string. Note the target bytes -- you need to invert the jumble to recover the key.
Learn more
Ghidra is a free, open-source reverse engineering framework developed by the NSA. Its decompiler converts raw machine instructions back into readable C-like pseudocode, making it possible to understand a binary's logic without access to source code. For this challenge, Ghidra reveals the structure of the "jumble" transformation and the hardcoded target byte array that your transformed input must match.
The validation pattern here -- transform input, compare to hardcoded expected value -- is common in CTF license-check and crackme challenges. The key insight is that you do not need to understand the math well enough to algebraically invert it. You only need to find what input produces each output byte, which can be done empirically.
In real-world software, this pattern appears in DRM systems, hardware authentication tokens, and firmware validation routines. Reverse engineers routinely work from Ghidra output to understand proprietary protocols and find vulnerabilities.
- Step 2Leak the expected value with ltraceInstead of fully reversing the jumble math, use ltrace to intercept the strncmp() call at runtime. Run the binary with a candidate input and ltrace will print both arguments to strncmp -- the transformed input and the expected target -- revealing what each byte position should be.ltrace ./otp <candidate_key>
Learn more
ltrace is a Linux utility that intercepts and logs calls to shared library functions at runtime. When a program calls
strncmp(), ltrace prints both string arguments to your terminal. This is powerful because it shows you what the program computed internally -- in this case, the transformed version of your input alongside the hardcoded expected value -- without requiring you to reverse-engineer the math at all.This technique is sometimes called dynamic analysis as opposed to the static analysis you do in Ghidra. Dynamic analysis observes program behavior at runtime; static analysis reads the program text without running it. Combining both approaches is standard practice -- static analysis reveals structure, dynamic analysis reveals runtime values.
ltraceworks by interposing on the PLT (Procedure Linkage Table) -- the indirection layer that connects a program to its shared libraries. This is also why ltrace only sees library calls, not internal function calls: it hooks the linkage layer, not the program's own code. The related toolstracehooks system calls in the same way. - Step 3Brute-force one character at a timeWrite a loop that tests each of the 16 hex characters at each position. When ltrace shows a match for a given position, record that character and move to the next. After all 100 positions, you have the full valid key.for c in 0 1 2 3 4 5 6 7 8 9 a b c d e f; do ltrace ./otp ${known}${c}$(python3 -c "print('0'*remaining)") 2>&1 | grep strncmp; done
Learn more
This is an oracle attack: the binary itself acts as an oracle that tells you, byte by byte, whether your guess is correct. Instead of brute-forcing all 16^100 possible keys, you brute-force each of the 100 positions independently across 16 possible hex characters -- only 1,600 queries total. This transforms an astronomically large search space into a trivially small one.
Oracle attacks appear frequently in real cryptographic vulnerabilities. The POODLE attack against SSL 3.0 used a padding oracle. The Bleichenbacher attack against RSA PKCS#1 v1.5 used an error-message oracle. The common thread is that the system reveals partial information about its internal state, which an attacker exploits to recover secrets one piece at a time.
The fix is straightforward: use a constant-time comparison that returns the same result -- and takes the same time -- regardless of how many bytes match. Functions like
CRYPTO_memcmp()(OpenSSL) or Python'shmac.compare_digest()prevent timing and early-exit oracles. - Step 4XOR-decrypt the flagOnce you have the 100-byte key, XOR it byte-by-byte against flag.txt to recover the plaintext flag. The key is in hex, so decode it to bytes first.python3 -c " key = bytes.fromhex('<recovered_100_char_key>') flag_enc = open('flag.txt','rb').read() print(''.join(chr(a^b) for a,b in zip(key,flag_enc))) "
Learn more
The One-Time Pad (OTP) is the only cipher with information-theoretic security: if the key is truly random, as long as the plaintext, and never reused, it is mathematically unbreakable. XOR is the operation -- each plaintext byte is XORed with the corresponding key byte to produce ciphertext, and XORing the ciphertext with the same key byte recovers the plaintext.
The challenge's title is ironic: this is a broken OTP implementation. A real OTP requires a key generated from a cryptographically secure source and used exactly once. Here the key is deterministic (derived from a predictable algorithm) and the validation logic leaks it entirely. Real OTP key distribution is so impractical that the cipher is almost never used -- its theoretical perfection is undermined by the logistics of securely sharing key material.
In practice, stream ciphers like ChaCha20 approximate OTP security with a short key by expanding it into a long pseudorandom keystream. The critical rule remains: never reuse the same key+nonce pair. When that rule is broken -- as in the two-time pad attack -- an attacker can XOR two ciphertexts together, canceling the key and getting plaintext XOR plaintext, which leaks significant information.
Flag
picoCTF{...}
ltrace intercepts library calls at runtime -- when the program calls strncmp(), ltrace shows both arguments, leaking the expected value one byte at a time and making character-by-character brute-force trivial.