challenge: Logic Bypass / Auth (Reverse Engineering)

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 from 0x402040 to 0x402075.

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_decoy and the blob at 0x4021c0 are 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.