//Where code
>TL;DR
challengeis a stripped x86-64 ELF that asks for a flag, XORs it with a ChaCha20 keystream, and compares the result against a ciphertext embedded in.rodataaftermainexits.- Reading the key/nonce/target bytes directly from
.rodataand reproducing the ChaCha block allowed recovering the correct input without patching the binary. - Final flag:
flag{iN1_f!ni_Min1_m0...1_$e3_yO}.
>1. Recon & Initial Probing
- List the workspace: only two files showed up,
challengeand a WindowsZone.Identifierstream. - Determine file type:
bash
file challengeOutput shows an ELF64 PIE, dynamically linked, stripped.
- Ensure it runs:
bash
chmod +x challenge && ./challengeIt simply prompts Enter the flag:.
- Quick
stringsrevealed only the usual C++ symbols and the user-facing messages (correct/incorrect), so deeper reversing was needed.
>2. Static Analysis
Because symbols are stripped, I used objdump to disassemble:
bash
objdump -d -M intel challenge > disasm.txt
Key observations:
- A long function at
0x186aismain. It reads up to 32 bytes usingstd::getline, stores them on the stack, and immediately returns without checking them. - A destructor-like function at
0x19a1(registered in.fini_array) runs aftermain. That function iterates over the user buffer (.bssat0x4280) and compares each byte to a reference array located at.rodata + 0x2040. If all bytes match, it prints the “Correct!” message; otherwise “Wrong flag.” - Just before the comparison, another large helper (
0x1592) is called with three pointers: the user buffer (destination), the.bssread buffer (source), and the length. That helper is clearly implementing ChaCha20: - Constants
0x61707865,0x3320646e,0x79622d32,0x6b206574(“expand 32-byte k”). - 32-byte key pulled from
.rodata + 0x2060. - 12-byte nonce from
.rodata + 0x2080(00 00 00 00 00 00 00 4a 00 00 00 00). - Counter hard-coded to
1. - XOR loop processes chunks of 0x40 bytes, but only one block is needed because the ciphertext is 32 bytes long.
- The ciphertext (expected XOR result) is the 32-byte blob at
.rodata + 0x2040.
>3. Reconstructing the Flag
Since the binary computes user_input ⊕ keystream = ciphertext, the valid flag is simply:
flag = keystream ⊕ ciphertext
Reproducing the ChaCha block externally is easier than patching the binary. I wrote a short Python script to:
- Read the key, nonce, and ciphertext bytes directly from the ELF.
- Reimplement the ChaCha quarter-round and block function (based on RFC 8439).
- Generate the first 32 bytes of keystream using counter = 1.
- XOR them with the ciphertext.
python
import struct
from pathlib import Path
def rotl32(v, n):
return ((v << n) & 0xffffffff) | (v >> (32 - n))
def quarter(state, a, b, c, d):
state[a] = (state[a] + state[b]) & 0xffffffff
state[d] ^= state[a]; state[d] = rotl32(state[d], 16)
state[c] = (state[c] + state[d]) & 0xffffffff
state[b] ^= state[c]; state[b] = rotl32(state[b], 12)
state[a] = (state[a] + state[b]) & 0xffffffff
state[d] ^= state[a]; state[d] = rotl32(state[d], 8)
state[c] = (state[c] + state[d]) & 0xffffffff
state[b] ^= state[c]; state[b] = rotl32(state[b], 7)
def chacha_block(key_bytes, counter, nonce_bytes):
const = [0x61707865, 0x3320646e, 0x79622d32, 0x6b206574]
key_words = list(struct.unpack('<8I', key_bytes))
nonce_words = list(struct.unpack('<3I', nonce_bytes))
state = const + key_words + [counter] + nonce_words
working = state.copy()
for _ in range(10):
quarter(working, 0, 4, 8, 12)
quarter(working, 1, 5, 9, 13)
quarter(working, 2, 6, 10, 14)
quarter(working, 3, 7, 11, 15)
quarter(working, 0, 5, 10, 15)
quarter(working, 1, 6, 11, 12)
quarter(working, 2, 7, 8, 13)
quarter(working, 3, 4, 9, 14)
out_words = [(working[i] + state[i]) & 0xffffffff for i in range(16)]
return struct.pack('<16I', *out_words)
path = Path('challenge')
with path.open('rb') as f:
f.seek(0x2060); key = f.read(32)
f.seek(0x2080); nonce = f.read(12)
f.seek(0x2040); target = f.read(32)
keystream = chacha_block(key, 1, nonce)[:len(target)]
flag_bytes = bytes(a ^ b for a, b in zip(keystream, target))
print(flag_bytes.decode())
Running the script outputs:
flag{iN1_f!ni_Min1_m0...1_$e3_yO}
>4. Verification
Finally, pipe the recovered flag back into the binary:
bash
echo 'flag{iN1_f!ni_Min1_m0...1_$e3_yO' | ./challenge
The program responds with Correct! You found the flag!, confirming the solution.
>5. Key Takeaways
- Always inspect
.fini_array/.init_arrayin C++ binaries; sometimes validation logic runs outsidemain. - When you encounter an unfamiliar cipher, look for well-known constants. The ChaCha constant words immediately give it away.
- Pulling raw bytes directly from the ELF and reproducing the algorithm offline is often faster than dynamic instrumentation.
Flag: flag{iN1_f!ni_Min1_m0...1_$e3_yO}