challenge: Logic Bypass / Auth (Reverse Engineering)
On this page
Flag: KPMG_CTF{7g5z_Vsmg1SA913_FPOwbN0i262b_EdE-D1TcjDDRTen}
File: challenge, ELF 64-bit LSB executable, x86-64, dynamically linked, not stripped
Category: Reverse engineering (keygen-style “auth bypass”)
Overview
The program asks for an “access key”. If an internal check passes it prints [ACCESS GRANTED]
and then a flag. The twist (and the intended lesson, stated in the banner: “the decision is
made here, not by a memory corruption bug”) is that the flag is not derived from the
password. It is a static repeating-key XOR blob baked into the binary, and the password check
only gates whether the decode routine runs. So the flag can be recovered without ever solving
or bypassing the password logic: just read the two data blobs out of the file and XOR them.
I analyzed a copy in /tmp/chal/ so the original in ~/Downloads stayed untouched.
Step 1: Triage
$ file challenge
challenge: ELF 64-bit LSB executable, x86-64, ... not stripped
$ strings -n 4 challenge | grep -iE 'flag|pass|access|{'
LOGIC BYPASS // AUTH
Enter access key:
[!] Input error.
Flag: %s
FLAG_KEY
ENC_FLAG
transform_password
validate_password
decode_flag
Because it is not stripped, the symbol table names the interesting pieces:
$ nm challenge | grep -iE ' t | T '
... decode_flag validate_password transform_password fnv1a32
looks_reasonable secondary_decoy weird_mix rol32 ror32 banner main
$ nm challenge | grep -iE 'FLAG_KEY|ENC_FLAG'
0000000000402040 r ENC_FLAG
0000000000402020 r FLAG_KEY
The shape is a classic keygen: transform_password → validate_password (an FNV-1a hash
check) → decode_flag using ENC_FLAG + FLAG_KEY. secondary_decoy / looks_reasonable
are red herrings.
Step 2: Read the data blobs
$ objdump -s -j .rodata challenge
402020 5a17c0de 13374269 00000000 00000000 <- FLAG_KEY (8 bytes)
402040 11478d99 4c74162f 2120a7eb 6968141a <- ENC_FLAG starts here
402050 3770f18d 520e735a 05519091 64550c59
402060 3325f6ec 7168070d 1f3a84ef 4754282d
402070 1e4594bb 7d4a0000 <- ENC_FLAG ends (00 00 terminator)
FLAG_KEY=5a 17 c0 de 13 37 42 69(8 bytes)ENC_FLAG= 54 bytes from0x402040to0x402075.
There is a second encrypted blob at 0x4021c0; that belongs to secondary_decoy, a fake path
main never calls.
Step 3: Understand decode_flag
$ objdump -d -M intel challenge # decode_flag @ 0x4015da
Translated, the loop is:
// decode_flag(char *out, size_t n) called as decode_flag(buf, 0x37)
for (i = 0; i <= 0x35; i++) // 54 iterations, i = 0..53
out[i] = ENC_FLAG[i] ^ FLAG_KEY[i & 7]; // repeating 8-byte XOR key
out[0x36] = 0; // NUL terminator
Key observation from main (@ 0x4016ac): validate_password is only used as a branch: on
success main calls decode_flag(buf, 0x37) and prints Flag: %s. decode_flag takes no
password-derived input. The password never influences the bytes, so solving it is unnecessary.
main:
call validate_password
cmp eax, 0 ; je -> "[ACCESS DENIED]"
...
call decode_flag ; uses only ENC_FLAG + FLAG_KEY
printf "Flag: %s"
Step 4: Recover the flag offline
The ENC_FLAG bytes are the 54 bytes read from the objdump output above
(0x402040–0x402075), and FLAG_KEY is the 8 bytes at 0x402020. XOR them with the key
repeating every 8 bytes, exactly what decode_flag does:
$ python3 -c "
enc=bytes.fromhex('11478d994c74162f2120a7eb6968141a3770f18d520e735a05519091'
'64550c593325f6ec7168070d1f3a84ef4754282d1e4594bb7d4a')
key=bytes.fromhex('5a17c0de13374269')
print(bytes(enc[i]^key[i%8] for i in range(len(enc))).decode())"
KPMG_CTF{7g5z_Vsmg1SA913_FPOwbN0i262b_EdE-D1TcjDDRTen}
54 bytes in, 54 printable characters out, well-formed KPMG_CTF{...}.
Why this works / takeaways
- Not stripped is a gift: symbol names mapped the whole algorithm instantly.
- The “auth” is pure theater: the password gates the call to
decode_flag, but the flag bytes come only from static.rodata. Whenever a decode routine’s inputs don’t depend on user data, you can lift the secret statically and skip the check entirely. - Repeating-key XOR against a fixed, short key stored in the binary offers no protection once you can read the file.
- Ignore decoys:
secondary_decoyand the blob at0x4021c0are a separate, unused path.
(Optional) solving it the “intended” way
You could instead reverse transform_password + validate_password (an FNV-1a hash with
rol32/ror32/weird_mix mixing) to find an input that makes validate_password return
nonzero, then just run ./challenge and type that key to get the same flag. Unnecessary here,
but that is the path the challenge author expected.