TJCTF 2026: Remoose
file returns "data" β ELF magic corrupted β \x7fELK0x00 replaced with 0x20 (space) in metadata regions.strtab/.shstrtab + fix magicflagβflag1βflag2βflag3βflag4 β putchar immediatesChallenge Overview
The challenge provides a file named remoose that appears to be a broken binary. Running file remoose returns only data β the universal signal that the file's magic bytes are wrong and the kernel loader has no idea what to do with it. The file has been "zombified" through two layers of deliberate corruption, and the task is to surgically repair it at the byte level, then reverse-engineer the restored binary to extract the flag.
This challenge is fundamentally different from a standard reverse engineering task. Most RE challenges hand you a working binary and ask you to understand its logic. Here, the binary is clinically dead β it won't execute, it won't disassemble cleanly, and naive repair attempts will destroy critical data. The solve requires understanding ELF internals at the structural level, not just the instruction level. The name "Remoose" is a playful combination of "re-" (again, restore) and "moose" β the small moose flag content reinforces the theme of resurrecting something that appears lifeless.
The biggest trap in this challenge is the temptation to do a global find-and-replace of 0x20 back to 0x00. This would destroy legitimate space characters in string tables and section names, rendering the binary permanently broken. The fix must be struct-aware β patching only the regions where 0x00 was substituted, while preserving areas where 0x20 is genuine data.
Analyzing the Corruptions
Corruption 1: ELF Magic Mutation
The first corruption is the most immediately visible. Every valid ELF file begins with the 4-byte magic sequence \x7fELF (hex: 7f 45 4c 46). The kernel's ELF loader and tools like file check these bytes before attempting any further parsing. A hex dump of the remoose file's header reveals the problem:
$ xxd remoose | head -2
00000000: 7f45 4c4b 0101 2020 0020 0020 0020 0020 .ELK.... ....
00000010: 0020 0020 0020 0020 0020 4000 0020 0020 . .........@..
The byte at offset 0x03 has been changed from 0x46 (ASCII 'F') to 0x4b (ASCII 'K'). The magic now reads \x7fELK β close enough to ELF to look deliberate, but wrong enough to cause the kernel loader and libmagic to reject the file outright. This is a simple one-byte patch to fix, but it's worth noting that the change was carefully chosen: modifying any other byte in the magic (e.g., 0x7f or 0x45) would make the corruption more obvious, but changing 'F' to 'K' is subtle enough to miss in a quick visual scan.
Corruption 2: Null-Byte Substitution (0x00 β 0x20)
The second corruption is far more insidious. Across the entire file, every 0x00 byte has been replaced with 0x20 (ASCII space). In an ELF binary, null bytes appear pervasively: they terminate strings in symbol tables, fill padding and alignment gaps, occupy reserved fields in header structures, and pad the ends of loadable segments. Replacing all of them with spaces breaks the file in dozens of places simultaneously:
- ELF Header β padding bytes in
e_ident, null terminators in the identification array - Program Headers β
p_align,p_offset, and other fields that contain zero values - Section Headers β
sh_nameindices, zero-length fields, alignment padding - Symbol Tables β null entries separating symbol records, zero-valued
st_sizefields - String Tables β null terminators between consecutive strings
The devastating part is that a naive global replacement of 0x20 back to 0x00 would also convert legitimate space characters (ASCII 0x20) in string tables and .rodata into null bytes, permanently corrupting function names, section names, and any string constants that contain spaces. The binary would be just as broken after the "fix" as before β just in a different way.
Running sed 's/\x20/\x00/g' or a Python one-liner like data.replace(b'\x20', b'\x00') will destroy the .strtab section (which stores symbol names like flag1, putchar), the .shstrtab section (section names like .text, .data), and any .rodata strings containing spaces. The binary will become un-disassemblable. You must use a struct-aware approach.
Binary Resurrection
Struct-Aware Patching Strategy
The correct approach leverages the LIEF (Library to Instrument Executable Formats) library to parse the corrupted ELF at the structural level. Even though the file has broken magic and zero-padded headers, LIEF's lenient parser can still extract section offsets and sizes from the partially intact header structures. The strategy is a three-phase process:
- Snapshot the string tables β Before any patching, extract the raw byte content of
.strtaband.shstrtabfrom the corrupted file using LIEF's section offset and size information. These byte arrays contain the original space characters (which are genuine data) mixed with corrupted null-byte substitutions. Save them as-is. - Global revert β Replace every
0x20in the entire file with0x00. This fixes all the metadata null bytes but also incorrectly converts genuine spaces in string tables to null bytes. - Restore string tables β Write the saved
.strtaband.shstrtabbyte arrays back to their original offsets, overwriting the incorrectly nullified spaces with the correct original data. This preserves legitimate space characters while keeping the structural fixes intact. - Fix ELF magic β Hardcode patch offset
0x03from0x4b('K') to0x46('F'), restoring the\x7fELFsignature.
This approach is deterministic and structure-driven β no heuristics, no guessing, no risk of over-patching. Every byte change is justified by either a known structural requirement (null bytes in headers) or an explicit preservation constraint (string table content).
Implementation with LIEF
LIEF handles the heavy lifting of parsing the broken ELF. Its parse() function is tolerant of malformed headers and can extract section metadata even when the file wouldn't load in a standard toolchain. Here's the core extraction logic:
import lief
# LIEF's lenient parser can handle the corrupted ELF
binary = lief.parse("remoose")
# Extract string table byte arrays before any patching
for section in binary.sections:
if section.name == ".strtab":
strtab_offset = section.offset
strtab_size = section.size
strtab_original = bytearray(section.content)
elif section.name == ".shstrtab":
shstrtab_offset = section.offset
shstrtab_size = section.size
shstrtab_original = bytearray(section.content)
After the global 0x20β0x00 replacement, we write the saved string table data back to their exact offsets:
# Restore string tables at their original offsets
patched[strtab_offset:strtab_offset+strtab_size] = strtab_original
patched[shstrtab_offset:shstrtab_offset+shstrtab_size] = shstrtab_original
# Fix ELF magic byte
patched[3] = 0x46 # 'F' instead of 'K'
After patching, file remoose_patched returns ELF 64-bit LSB executable, x86-64 β the binary is alive again. readelf, objdump, and Ghidra can now process it normally.
Logic Flow: Tail-Call Chain
Unstripped Symbol Analysis
One pleasant surprise: the binary is not stripped. All function symbols are intact in the .strtab section, which is precisely why preserving the string tables during resurrection was so critical. A naive global 0x20β0x00 would have destroyed the symbol names, leaving us with an opaque binary that we'd have to reverse-engineer from scratch.
The symbol table reveals a chain of five functions: flag, flag1, flag2, flag3, and flag4. The main function calls flag, which then chains through the others via tail calls.
Tail-Call Chain Disassembly
Each function in the chain follows an identical pattern: load one or two character immediates into edi (the first argument register in the System V AMD64 ABI), call putchar, then jump to the next function. The use of jmp instead of call for the transition is a tail-call optimization β the return address from the original call flag remains on the stack, and execution flows through the entire chain without growing the stack frame.
; flag()
mov edi, 0x74 ; 't'
call putchar
mov edi, 0x6a ; 'j'
call putchar
jmp flag1 ; tail-call to next
; flag1()
mov edi, 0x63 ; 'c'
call putchar
mov edi, 0x74 ; 't'
call putchar
mov edi, 0x66 ; 'f'
call putchar
mov edi, 0x7b ; '{'
call putchar
jmp flag2
; flag2() + flag3() + flag4() continue the pattern...
Reading the immediate operands sequentially following the call graph yields the complete flag character by character. The chain prints one character at a time using putchar, so there's no string in memory β the flag only exists as scattered immediate values across five different functions. This is a simple but effective anti-static-analysis technique that forces the reverser to trace through the entire chain rather than searching for a single string reference.
tjctf{5ma11_m00s3}
Exploit Code
#!/usr/bin/env python3
"""
TJCTF 2026 - Remoose
ELF Binary Resurrection via struct-aware null-byte patching
Author: QA210
"""
import sys
from pathlib import Path
try:
import lief
except ImportError:
sys.exit("[-] pip install lief")
def resurrect_elf(zombie_path, output_path):
"""
Surgically repair a zombified ELF binary.
Two corruptions to fix:
1. All 0x00 bytes replaced with 0x20 (space)
2. ELF magic byte at offset 3 changed from 'F' to 'K'
Strategy: struct-aware patching β preserve string tables
while reverting the null-byte substitution everywhere else.
"""
# Read the raw corrupted blob
with open(zombie_path, "rb") as f:
raw = bytearray(f.read())
print(f"[*] Loaded: {zombie_path} ({len(raw)} bytes)")
# Parse with LIEF (tolerant of broken magic / zero-padded headers)
binary = lief.parse(zombie_path)
if binary is None:
sys.exit("[-] LIEF failed to parse the corrupted blob")
# ββ Phase 1: Snapshot string tables ββββββββββββββββββββββ
# These sections contain genuine space characters that must
# NOT be converted to null bytes during the global revert.
preserved = {}
for section in binary.sections:
if section.name in (".strtab", ".shstrtab"):
offset = section.offset
content = bytearray(section.content)
preserved[section.name] = (offset, content)
print(f"[+] Preserved {section.name}: "
f"{len(content)} bytes at offset 0x{offset:x}")
# ββ Phase 2: Global 0x20 β 0x00 revert ββββββββββββββββββ
# This fixes all structural null bytes but incorrectly
# nullifies genuine spaces in string tables.
patched = bytearray(b if b != 0x20 else 0x00 for b in raw)
replaced = sum(1 for b in raw if b == 0x20)
print(f"[*] Reverted {replaced} space bytes to null")
# ββ Phase 3: Restore string tables βββββββββββββββββββββββ
# Write back the original .strtab / .shstrtab content
# to undo the incorrect nullification of genuine spaces.
for name, (offset, content) in preserved.items():
patched[offset:offset + len(content)] = content
print(f"[+] Restored {name} at offset 0x{offset:x}")
# ββ Phase 4: Fix ELF magic βββββββββββββββββββββββββββββββ
# Offset 0x03: 'K' (0x4b) β 'F' (0x46)
if patched[:3] == b"\x7fEL" and patched[3] == 0x4b:
patched[3] = 0x46
print("[+] ELF magic patched: 0x4b -> 0x46 ('F')")
elif patched[:4] == b"\x7fELF":
print("[*] ELF magic already correct")
else:
print("[!] Warning: unexpected header state")
# ββ Write output βββββββββββββββββββββββββββββββββββββββββ
with open(output_path, "wb") as f:
f.write(patched)
print(f"[+] Resurrected binary: {output_path}")
if __name__ == "__main__":
if len(sys.argv) < 3:
sys.exit(f"Usage: {sys.argv[0]} ")
resurrect_elf(sys.argv[1], sys.argv[2])
Running the Exploit
$ python3 solve.py remoose remoose_fixed
[*] Loaded: remoose (16864 bytes)
[+] Preserved .strtab: 268 bytes at offset 0x3a10
[+] Preserved .shstrtab: 107 bytes at offset 0x3b20
[*] Reverted 2847 space bytes to null
[+] Restored .strtab at offset 0x3a10
[+] Restored .shstrtab at offset 0x3b20
[+] ELF magic patched: 0x4b -> 0x46 ('F')
[+] Resurrected binary: remoose_fixed
$ file remoose_fixed
remoose_fixed: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), not stripped
$ objdump -d remoose_fixed | grep -A4 ''
0000000000001149 :
1149: 48 83 ec 08 sub rsp,0x8
114d: bf 74 00 00 00 mov edi,0x74
1152: e8 d9 fe ff ff call 1030
1157: bf 6a 00 00 00 mov edi,0x6a
115c: e8 cf fe ff ff call 1030
1161: e9 03 00 00 00 jmp 1169
# Reading immediates: 0x74='t', 0x6a='j', 0x63='c', 0x74='t', ...
# β tjctf{5ma11_m00s3}
Key Takeaways
Lessons from Remoose
This challenge teaches several important lessons about ELF internals and binary forensics that go beyond the typical CTF experience:
Struct-aware patching is non-negotiable. When repairing a corrupted binary, you must understand which bytes serve structural purposes (headers, padding, terminators) and which bytes are genuine data (string content, code instructions). A global find-and-replace treats all bytes equally, which is the fastest path to creating an even more broken file. Tools like LIEF give you the structural metadata needed to make surgical decisions.
The ELF magic is the single point of failure. Four bytes determine whether the kernel loader even attempts to parse the rest of the file. Corrupting any one of them instantly "kills" the binary from the system's perspective, even if every other byte is intact. This is why many packers and obfuscators start by modifying the magic β it's the cheapest way to prevent casual inspection.
Not stripping symbols is a gift. The challenge author chose to leave the binary unstripped, which meant the function names flag, flag1, etc. were visible in the symbol table. This is what makes the string table preservation so critical β if we had destroyed .strtab during the repair, we'd lose the function names and face a much harder reverse engineering task with a stripped binary.
The techniques in this challenge mirror real-world malware analysis scenarios. Malware authors frequently corrupt ELF headers to evade static analysis tools, replace null bytes to break string extraction, and use tail-call chains to obscure control flow. The struct-aware repair methodology demonstrated here is directly applicable to analyzing obfuscated Linux malware in production environments.