//To jmp or not jmp
Author: v1bh475u (challenge)
>TL;DR
- Binary is a 64-bit PIE ELF that expects a flag in printf/iostream style. After a length check, a stream cipher (RC4-like) XORs and validates the buffer against a stored ciphertext.
- The keystream was recovered by running the binary with a known input (all 'A's) and dumping the processed buffer; the keystream was derived as processedByte XOR 0x41.
- Using the keystream, XOR the ciphertext from
.rodatato retrieve the flag.
Target flag: flag{$t0p_JUmp1n9_@R0uNd_1!k3_A_F00l_4nd_gib3_M3333_7H@t_f14g!!!!}
>Environment / Tools used
- Linux (same environment as challenge worked on)
- objdump (disassembly)
- gdb (interactive runtime inspection; used with
set disable-randomization onto avoid address randomization) - ltrace (library trace to quickly see
strlen,getline) - strings (quick text scan)
- python3 (to craft keystream/rc4 and compute flag)
Note: tools like radare2 or ghidra would speed up some static analysis but were not necessary.
>What I saw first — quick checks
Start with a basic file check:
$ file challenge
challenge: ELF 64-bit LSB pie executable, x86-64, ... stripped
Run it (add exec bit if needed):
$ chmod +x challenge
$ ./challenge
Enter the flag:
Wrong! Try again.
A quick strings reveals the prompted text:
$ strings -n 4 challenge | grep -E "Enter the flag|Correct|Wrong"
Enter the flag:
Correct! You got the flag!
Wrong! Try again.
So it's checking the flag and printing success/failure.
>Step 1 — inspect rodata for ciphertext and key
The .rodata segment contains the strings and some binary blobs that are likely constants (key, ciphertext). Use objdump (or xxd) to inspect it:
$ objdump -s -j .rodata challenge | sed -n '1,120p'
# or with xxd
$ xxd -g1 -s 0x2020 -l 0x60 challenge
From that dump we find a small printable sequence followed by two important binary chunks, e.g.:
- A 16 byte ASCII sequence at
.rodataoffset 0x2020; this looks like a key:!a1 a&\\r9a+\\r 1fsR(hex:2161312061260d39612b0d2031667352). - A binary blob at
.rodataoffset 0x2040 and length 0x42 that looks random — this is likely the ciphertext.
We now suspect the program uses this key and ciphertext to check the flag.
>Step 2 — examine disassembly to find algorithm (KSA/PRGA)
Use objdump -d -M intel to disassemble and identify the flow and comparisons.
We see a loop which:
- Gets a buffer index variable (compares with a fixed max),
- Uses a bunch of
movzbl/xor/add/movoperations that read from a table and write into a destination buffer.
This key-scheduling + PRGA-like logic strongly matches an RC4-style XOR stream cipher. Important observations:
- The algorithm reads a key slot and mixes into a state array
- Then a keystream byte is produced and XORed with the input bytes to produce a buffer which is compared against the stored ciphertext
- The comparisons occur sequentially for a fixed number of bytes (0x42 bytes)
Because the binary is PIE (position-independent), we used gdb with ASLR disabled to get deterministic addresses (set disable-randomization on).
>Step 3 — confirm behaviour dynamically (trace input)
A quick library-level trace (ltrace) shows a getline call to accept the input and a strlen call for length. This confirms there's a length check, so we need the input to match expected length.
$ ltrace ./challenge <<< "flag{test}"
# We saw length = 10 for `flag{test}`
Disassembly and debug indicated a length check that ensures input is long enough (the expected count is 0x42, i.e., 66 bytes) before comparing each resulting byte.
>Step 4 — keystream recovery trick (easiest approach)
Instead of fully reversing the cipher or reimplementing the KSA to produce the keystream by reading S-box operations, we used a simple oracle trick: provide a known input (e.g., A x 66) and dump the processed buffer before comparison. That processed buffer = plaintext XOR keystream.
- The keystream for a position i is keystream[i] = processed_buffer[i] XOR input[i]. For
A(0x41), keystream[i] = processed[i] XOR 0x41. - The stored ciphertext (from
.rodata) is ciphertext[i] = flag[i] XOR keystream[i], so flag[i] = ciphertext[i] XOR keystream[i].
Steps to do this:
- Run the binary with a 66-character known input and instrument the comparison break.
# Disable ASLR for consistent addresses, then run in gdb to break at the byte comparison loop address
$ gdb -q ./challenge
(gdb) set disable-randomization on
(gdb) starti # run to `_start` then pause
(gdb) b *0x5555555555b3 # this was the comparison instruction's address in my run
(gdb) run < <(printf 'A%.0s' {1..66})
(gdb) x/66bx 0x555555558420 # the buffer where processed bytes were written
- Dumped bytes look random in general; these are the processed bytes. Example (first 16):
0x555555558420: 0xa8 0x1b 0xef 0x5b 0x3e ...
- Compute keystream: keystream[i] = processed[i] XOR 0x41 (since 'A' is 0x41).
- Decrypt ciphertext (from offset
.rodata, bytes at 0x2040) as:flag[i] = ciphertext[i] XOR keystream[i].
>Step 5 — decrypt with Python 🐍
Here are the reproducible python steps (works in one shot):
# Read constants from binary and decrypt using Python
# We will: (1) extract the ciphertext bytes from file offsets, (2) use the captured processed buffer (observed via gdb), (3) compute keystream, (4) compute flag
python3 - <<'PY'
from pathlib import Path
# ciphertext read from .rodata offset 0x2040 length 0x42
ct = bytes.fromhex('8f36cf7d048e35ac0fe83f538b87ac26185b13c7ffa61d9229b762afa9b0cf74d2994e5547a9773b6728cb52749047241594e14e4df257ad7f5d221705088b2aedf1')
# processed buffer observed during unencrypted run (with 'A'*66)
buf = bytes([
0xa8,0x1b,0xef,0x5b,0x3e,0xeb,0x00,0xdd,0x3e,0xf6,0x34,0x47,0xa7,0xb6,0xdc,0x09,
0x60,0x45,0x12,0xd4,0x8e,0x92,0x12,0xb7,0x37,0xc7,0x02,0x85,0xdb,0xae,0xcf,0x6a,
0xd5,0xe8,0x3f,0x78,0x59,0xdc,0x58,0x1e,0x79,0x0e,0xe3,0x71,0x06,0x8e,0x4b,0x56,
0x67,0xe6,0x93,0x50,0x3b,0xfb,0x56,0x98,0x61,0x7a,0x52,0x62,0x23,0x68,0xeb,0x4a,
0x8d,0xcd
])
ks = bytes([b ^ 0x41 for b in buf]) # keystream because input was 'A' (0x41)
flag = bytes([c ^ k for c, k in zip(ct, ks)])
print(flag)
print(flag.decode())
PY
You should get the flag string printed out. Example output:
flag{$t0p_JUmp1n9_@R0uNd_1!k3_A_F00l_4nd_gib3_M3333_7H@t_f14g!!!!}
>Extra verification — key and cipher approach
We also found the key bytes near $RODATA + 0x2020 (ASCII string !a1 a&\\r9a+\\r 1fsR). A full KSA/PRGA RC4 re-implementation using this key would produce the exact keystream. However, since we already recovered the keystream by the known-plaintext trick, reimplementing the full KSA/PRGA was not necessary.
If you want to implement KSA/PRGA to recreate the keystream programmatically, here's a short script to do it from the key:
# RC4 sample (if you'd like to rebuild key->keystream):
def rc4_keystream(key, length):
S = list(range(256))
j = 0
# KSA
for i in range(256):
j = (j + S[i] + key[i % len(key)]) % 256
S[i], S[j] = S[j], S[i]
# PRGA
i = j = 0
out = bytearray()
for _ in range(length):
i = (i + 1) & 0xff
j = (j + S[i]) & 0xff
S[i], S[j] = S[j], S[i]
out.append(S[(S[i] + S[j]) & 0xff])
return bytes(out)
Then decrypt ct with flag[i] = ct[i] XOR keystream[i].
>Summary & final flag
- The program checks a 66-byte long flag. It does an RC4-like encryption/keystream XOR and compares to a ciphertext embedded in
.rodata. - I ran the program with the
A* 66 test input, dumped the processed buffer, derived the keystream, and decrypted the ciphertext to reveal the flag.
Final flag is:
flag{$t0p_JUmp1n9_@R0uNd_1!k3_A_F00l_4nd_gib3_M3333_7H@t_f14g!!!!}