BinCrypt Breaker (rev) – CTF Write-up

Challenge Overview

We’re given two files: an executable named checker and an opaque blob named file.bin. Running the checker eventually asks for a flag, but the actual validation logic is not present in the first binary.

  • The outer executable decrypts file.bin with a single-byte XOR key
  • The decrypted data is a second ELF containing the real flag checker
  • The checker transforms a 28-byte input using swaps, permutations and XOR
  • Reversing those operations recovers the original flag

Initial Reconnaissance

Running file confirms that checker is a 64-bit PIE executable, while file.bin is initially detected only as data:

$ file checker file.bin
checker:  ELF 64-bit LSB pie executable, x86-64, dynamically linked, not stripped
file.bin: data

Because the executable is not stripped, the interesting function names remain available. The main function calls decrypt(), builds a path below /proc/self/fd/ from the returned file descriptor and executes that descriptor with fexecve().

The first binary is therefore only a loader. The actual challenge is hidden inside file.bin.

Extracting the Hidden Binary

The relevant part of decrypt() is simple:

while ((c = fgetc(stream)) != EOF) {
    c ^= 0xab;
    write(fd, &c, 1);
}

Each byte from file.bin is XORed with 0xAB and written to an anonymous temporary file. The loader then reopens that file through /proc/self/fd/<fd> and executes it without giving the decrypted payload a normal filename.

XOR is its own inverse, so the hidden binary can be recovered by applying the same key again:

from pathlib import Path

encrypted = Path("file.bin").read_bytes()
decrypted = bytes(byte ^ 0xAB for byte in encrypted)
Path("file.bin.dec").write_bytes(decrypted)

The recovered file is another 64-bit ELF:

$ file file.bin.dec
file.bin.dec: ELF 64-bit LSB pie executable, x86-64, dynamically linked, stripped

Static Analysis

The hidden binary reads a flag without the surrounding HTB{} and passes it to the validation function:

printf("Enter the flag (without `HTB{}`): ");
scanf("%28s", flag);

if (check_flag(flag))
    puts("Wrong flag");
else
    puts("Correct flag");

The checker expects exactly 28 characters. It then applies four swaps to the complete input, divides it into two 14-byte blocks and transforms each half independently. The resulting string is compared against:

RV{r15]_vcP3o]L_tazmfSTaa3s0

Global Transpositions

Before splitting the input, the checker exchanges four pairs of characters:

swap(flag, 0, 12);
swap(flag, 14, 26);
swap(flag, 4, 8);
swap(flag, 20, 23);

A transposition is self-inverse. Swapping the same pair a second time restores the original positions.

Per-Block Transformation

Both 14-byte halves pass through the same permutation eight times:

[9, 12, 2, 10, 4, 1, 6, 3, 8, 5, 7, 11, 0, 13]

The checker then XORs indices 2, 4, 6, 8, 11 and 13. The first block uses key 0x02; the second uses 0x03.

The forward permutation performs:

tmp[i] = block[permutation[i]];

To reverse it, the assignment is inverted:

tmp[permutation[i]] = block[i];

The XOR positions are fixed points in this permutation, so the XOR and permutation reversal can be applied in either order. Applying XOR first makes the inverse sequence easier to read.

Recovering the Flag

The complete recovery can be implemented in a short Python script:

target = list("RV{r15]_vcP3o]L_tazmfSTaa3s0")

permutation = [9, 12, 2, 10, 4, 1, 6, 3, 8, 5, 7, 11, 0, 13]
xor_indices = [2, 4, 6, 8, 11, 13]


def reverse_block(block, xor_key):
    for index in xor_indices:
        block[index] = chr(ord(block[index]) ^ xor_key)

    for _ in range(8):
        restored = block.copy()
        for index, source in enumerate(permutation):
            restored[source] = block[index]
        block = restored

    return block


target[:14] = reverse_block(target[:14], 0x02)
target[14:] = reverse_block(target[14:], 0x03)

for left, right in ((0, 12), (14, 26), (4, 8), (20, 23)):
    target[left], target[right] = target[right], target[left]

print(f"HTB{{{''.join(target)}}}")

Result

$ python3 solve.py
HTB{cRyPto_r3V_15_aLways_aWeS0m3}

Key Takeaways

  • Follow the execution chain before spending time reversing the first binary; the outer ELF only decrypts and launches the real checker.
  • Simple XOR obfuscation does not protect an embedded executable when both the ciphertext and key are available.
  • Reverse transformations in the opposite direction: undo XOR, invert the permutation and finally repeat the self-inverse swaps.
  • Translating a permutation into a small script is faster and less error-prone than reconstructing the flag manually.