← Back to Writeups
CUSTOMMediumReversing

batcave-bitflips

XESXOR9/10/20262 min read
#reversing#custom#medium#data_section_recovery#static_reverse_engineering#xor_plaintext_recovery

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:

  • rotate used (b * 8) | (b >> 6) instead of a normal 3-bit rotate-left like (b << 3) | (b >> 5).
  • decrypt_flag used bitwise OR where XOR made much more sense for symmetric recovery.
  • hash iterated for 0xBEEEEE rounds, 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
UMASS{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

  1. Load the ELF in a disassembler and note that it is not stripped.
  2. Inspect the named globals and extract the bytes stored in EXPECTED and FLAG.
  3. Notice that several routines are obviously corrupted by bit flips, especially rotate and decrypt_flag.
  4. Skip the unreliable verifier path and directly XOR the two stored blobs.
  5. Decode the result as bytes and stop at the embedded NUL terminator.
  6. Read the flag string before the trailing garbage byte.
python
#!/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.