batcave-bitflips
batcave-bitflips
Platform: Umasscybersec | Category: Reversing | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | 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.
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
umasscybersec| ID:20260411_umasscybersec_batcave_bitflips - Tags: elf64, static_analysis, xor, bit_flips, corrupted_binary
- Indicators: multiple nearby operations look like single-bit corruptions rather than deliberate logic, encrypted flag bytes and expected bytes are both stored in .data, a decryption routine uses OR where XOR is the only operation that makes sense, the recovered plaintext contains a valid flag plus a trailing NUL terminator
- Source:
20260411_umasscybersec_batcave_bitflips.md
Foothold
Vulnerability / Misconfiguration
- Data_section_recovery
- Static_reverse_engineering
- Xor_plaintext_recovery
<command>
Exploitation
- See original writeup content for detailed exploitation.
Privilege Escalation
Enumeration
sudo -l find / -perm -4000 2>/dev/null getcap -r / 2>/dev/null cat /etc/crontab ps aux
Exploitation
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
Key Takeaways / Lessons
- data_section_recovery
- static_reverse_engineering
- xor_plaintext_recovery
- Tags: elf64, static_analysis, xor, bit_flips, corrupted_binary
Original Writeup
<details><summary>Click to expand original content</summary>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:
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
- 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.
#!/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.
Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR