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.binwith 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.