ReversingHidden Investigations CTF 2026
Snapfile
snaptool is a stripped Linux x86-64 Crystal binary that reads a .snap file, validates it, and on success prints its embedded label. Reverse-engineer the 104-byte format spec, forge a valid file, and the binary prints the flag.
What this one teaches
- Locating format-validation logic in a stripped binary by tracing back from the only error string.
- Recognising a wyhash-style checksum loop and reimplementing it to forge valid inputs.
- Verifying that section contents don't affect the flag output (constant XOR keystream), so zero-filled sections suffice.
Task description
A small research-tooling team got tired of shelling out to
tar/zipfor their dataset snapshots, so they rolled their own compact binary container format, the snapfile (.snap), and a Crystal command-line tool, snaptool, that validates and unpacks them.The format was never documented outside the team, and the only thing that leaked is the tool itself: a single stripped release binary. No source, no spec, no sample
.snapfile. If you can hand it a.snapit considers genuine, it will unpack the snapshot’s embedded label, which is the flag.Run it with
./snaptool somefile.snap. It prints the unpacked label on success andinvalid snapfileon any failure. There are no other options and no diagnostics. Linux x86-64 ELF, no runtime dependencies beyond a normal glibc system.
Approach
Find the parser
The only diagnostic string is invalid snapfile (string object at 0x461f70), lea’d at 0x436711 into a no-return “print error + exit” helper (0x436710) called from the real failure site 0x4035e4 (and an exception landing pad 0x403bc2). To locate the format logic directly, I ran the binary in gdb (Docker, ubuntu 24.04 — the handout needs glibc 2.38+), broke on the file read, and scanned the stack for the return address into the top-level function: 0x402c02. The validation runs from there.
Recover the header format
Starting at 0x402c02, the total file must be exactly 104 bytes:
[0:4]="SNAP"[4:6]= BE16 version ==2[6]==0x03,[7]==0x00[8:16]="SNAP-LNG"(checked as a BE64 ==0x534e41502d4c4e47)- A big-endian u32 section table (reader at
0x438800):[16:20]=40,[20:24]=16,[24:28]=56,[28:32]=32,[32:36]=88,[36:40]=8. These are (offset, length) for sections A[40:56], B[56:88], C[88:96];88+8=96is checked. The three section bodies’ contents are not validated.
Reverse the checksum
Bytes [96:104] are a BE64 checksum over file[8:96] (88 bytes) using a wyhash-style loop:
h = 0x1357bdf02468ace1
for b in file[8:96]:
s = (b + h) & 2**64-1
s = (s * 0xd1b54a32d192ed03) & 2**64-1
h = (s >> 29) ^ s
require BE64(file[96:104]) ^ h == 88
=> file[96:104] = BE64(h ^ 88)
Understand the label logic
The tool computes SHA256(file[0:40]) (helper 0x438890 = OpenSSL EVP SHA-256 — libcrypto is linked but no HMAC is used) and uses it as a keystream to build a constant 46-byte buffer (section-A content ends up ignored). Then decrypted = built XOR KEY, where KEY is a 46-byte constant assembled into a global at 0x401ee9. A gate requires SHA256(decrypted) == EXPECTED, a 32-byte digest baked in at 0x401f2b. If it matches, the 46-byte decrypted buffer is printed — this is the flag, embedded in the binary.
Forge and run
Since section A/B/C contents aren’t checked, I built a structurally valid 104-byte file with zero-filled sections and the correct BE64 wyhash checksum:
import struct
def wyh(data):
h = 0x1357bdf02468ace1
M = 0xd1b54a32d192ed03
mask = (1 << 64) - 1
for b in data:
s = (b + h) & mask
s = (s * M) & mask
h = ((s >> 29) ^ s) & mask
return h
def build():
hdr = b"SNAP"
hdr += struct.pack(">H", 2) # version BE16 = 2
hdr += bytes([3, 0])
hdr += b"SNAP-LNG"
hdr += struct.pack(">IIIIII", 40, 16, 56, 32, 88, 8)
assert len(hdr) == 40
body = hdr + bytes(16) + bytes(32) + bytes(8) # zero-filled sections
assert len(body) == 96
h = wyh(body[8:96])
body += struct.pack(">Q", h ^ 88)
assert len(body) == 104
return body
with open("valid.snap", "wb") as f:
f.write(build())
Running the binary on the forged file prints the flag:
$ ./snaptool valid.snap
hiCTF{r3c0nstruct3d_th3_sn4pf1l3_by73_by_by73} (exit 0)
In gdb, the decrypted buffer was identical for different section-A values, confirming the flag is constant regardless of section content.
Lessons
- A stripped Crystal binary still links OpenSSL and has recognizable EVP call patterns — useful landmarks when string-based xrefs are sparse.
- When the checksum is over a known region and the sections themselves are unchecked, forging a valid file is trivial: fill with zeros and compute the expected digest.
- The flag being a constant XOR of two baked-in buffers means it’s extractable statically, but running the binary on a forged input is faster and confirms correctness.
The flagReveal
hiCTF{r3c0nstruct3d_th3_sn4pf1l3_by73_by_by73}This CTF was possibly solved, and the writeup most likely written by an LLM. Apologies for inconsistencies.