Skip to content

SECURE_CONNECTION//PRESS[CTRL+J]FOR ROOT ACCESS

BACK TO INTEL
ReverseEasy

Where Code

CTF writeup for Where Code from Backdoor

//Where code

>TL;DR

  • challenge is 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 .rodata after main exits.
  • Reading the key/nonce/target bytes directly from .rodata and 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

  1. List the workspace: only two files showed up, challenge and a Windows Zone.Identifier stream.
  2. Determine file type:
bash
file challenge

Output shows an ELF64 PIE, dynamically linked, stripped.

  1. Ensure it runs:
bash
chmod +x challenge && ./challenge

It simply prompts Enter the flag:.

  1. Quick strings revealed 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 0x186a is main. It reads up to 32 bytes using std::getline, stores them on the stack, and immediately returns without checking them.
  • A destructor-like function at 0x19a1 (registered in .fini_array) runs after main. That function iterates over the user buffer (.bss at 0x4280) 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 .bss read 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:

  1. Read the key, nonce, and ciphertext bytes directly from the ELF.
  2. Reimplement the ChaCha quarter-round and block function (based on RFC 8439).
  3. Generate the first 32 bytes of keystream using counter = 1.
  4. 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_array in C++ binaries; sometimes validation logic runs outside main.
  • 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}