batcave-bitflips
batcave-bitflips
Platform: umasscybersec | Category: Reversing | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-04-11 | Status: Solved Techniques: data_section_recovery, static_reverse_engineering, xor_plaintext_recovery
Summary
Task: a not-stripped ELF64 license checker was intentionally damaged by bit flips, leaving several obviously broken arithmetic and crypto-looking routines. Solution: ignore the noisy verifier, recover the plaintext directly by XORing the stored FLAG and EXPECTED blobs from .data, and use the embedded NUL to explain the printed flag boundary.
batcave-bitflips — UMass Cybersecurity CTF
Description
Organizer description was not preserved in the local task files.
English summary: the challenge provided a corrupted ELF64 license checker with enough symbols left intact to inspect its globals and helper routines. The intended verification path was damaged, but the plaintext flag could still be recovered directly from the data section.
Analysis
The binary was not stripped, which immediately exposed useful globals: LICENSE_KEY, EXPECTED, FLAG, and SBOX, plus functions such as rotate, decrypt_flag, hash, and verify. Several instructions looked wrong in a way that strongly suggested bit-flip corruption rather than ordinary obfuscation.
The most suspicious examples were:
rotateused(b * 8) | (b >> 6)instead of a normal 3-bit rotate-left like(b << 3) | (b >> 5).decrypt_flagused bitwise OR where XOR made much more sense for symmetric recovery.hashiterated for0xBEEEEErounds, which looked deliberately corrupted or at least untrustworthy.
At that point, fully repairing the verification path was unnecessary. The key observation was that both EXPECTED and FLAG were stored in .data, and XORing them immediately produced readable output:
text read-only • copy allowedUMASS{REDACTED}\x00\xee
The embedded NUL explains why the program would print only UMASS{REDACTED} with %s. The remaining byte 0xee is just trailing garbage after the string terminator. The embedded LICENSE_KEY string !_batman-robin-alfred_((67||67)) is likely the intended thematic license value, but recovering the flag did not require repairing the broken hash pipeline enough to validate it cleanly.
Solution
- Load the ELF in a disassembler and note that it is not stripped.
- Inspect the named globals and extract the bytes stored in
EXPECTEDandFLAG. - Notice that several routines are obviously corrupted by bit flips, especially
rotateanddecrypt_flag. - Skip the unreliable verifier path and directly XOR the two stored blobs.
- Decode the result as bytes and stop at the embedded NUL terminator.
- Read the flag string before the trailing garbage byte.
python read-only • copy allowed#!/usr/bin/env python3 EXPECTED_HEX = "3b54751a2406af05778047c5e483d348cb8730de1a9145ab15c79b2204022bee" FLAG_HEX = "6e193449777df05a07b433a68ce6e617fbe96fae2ee526c370e3c47d277f2b00" def xor_bytes(a: bytes, b: bytes) -> bytes: return bytes(x ^ y for x, y in zip(a, b)) def main(): expected = bytes.fromhex(EXPECTED_HEX) flag_blob = bytes.fromhex(FLAG_HEX) plaintext = xor_bytes(expected, flag_blob) print("[+] Raw plaintext bytes:", plaintext) print("[+] Printed flag:", plaintext.split(b"\x00", 1)[0].decode()) if __name__ == "__main__": main()
Running the script yields the raw bytes UMASS{REDACTED}\x00\xee, so the visible flag is the prefix before the NUL byte.