“The Descent” Crackme — Solve Notes
Quick reference for the two-stage crackme (serial_of gate + XOR-encrypted vault).
Overview
The binary has two gates:
- Floor 1 – Sponsorship Gate: prompts for a
Crawler IDand aSponsor Serial. The serial must equal a hash of the ID. - Floor 2 – Sealed Vault: prompts for a
phrase. If correct, prints a flag that was XOR-decrypted from an embedded blob at runtime.
Floor 1 — serial_of()
undefined8 serial_of(byte *param_1, char *param_2)
{
uint local_14 = 0x1505;
for (byte *p = param_1; *p != 0; p++)
local_14 = (uint)*p ^ (local_14 * 0x21); // 0x21 = 33
sprintf(param_2, "%08X", (ulong)local_14);
return 0;
}- This is a djb2-style hash (seed
0x1505, multiplier33), formatted as an 8-digit uppercase hex string. - There’s no secret — the “serial” is just the hash of whatever ID you type.
- Solve strategy: pick any Crawler ID, compute its hash the same way, and supply that hash as the serial.
def serial_of(s: str) -> str:
h = 0x1505
for c in s.encode():
h = (c ^ (h * 0x21)) & 0xFFFFFFFF
return "%08X" % h
print(serial_of("test")) # -> 7C73AF33Example that passes Floor 1:
Crawler ID: test
Sponsor Serial: 7C73AF33
Floor 2 — XOR-encrypted vault
Key derivation
for (i = 0; i < 8; i++)
real_key[i] = obf_key[i] ^ 0x5A;obf_key bytes (from binary): C0 66 2B BF 52 E7 1C 75 → XOR with 0x5A →
real_key = 9A 3C 71 E5 08 BD 46 2F
Decryption routine — unpack()
void unpack(long src, int len, long key, int keylen, long dst) {
for (int i = 0; i < len; i++)
dst[i] = src[i] ^ key[i % keylen];
}Simple repeating-key XOR. Since XOR is self-inverse, decrypting = re-applying the same key.
Decrypting enc_pass (10 bytes → 9-char passphrase + NUL)
Ciphertext: D8 53 03 84 66 C9 05 40 E8 4C
XOR each byte with real_key[i % 8]:
| i | cipher | key | plain |
|---|---|---|---|
| 0 | D8 | 9A | B |
| 1 | 53 | 3C | o |
| 2 | 03 | 71 | r |
| 3 | 84 | E5 | a |
| 4 | 66 | 08 | n |
| 5 | C9 | BD | t |
| 6 | 05 | 46 | C |
| 7 | 40 | 2F | o |
| 8 | E8 | 9A | r |
| 9 | 4C | 3C | p |
Passphrase = BorantCorp
Decrypting enc_flag (48 bytes → flag string)
Same key, same technique, applied to the longer enc_flag blob.
Flag: DBFCTF{the_desperado_club_always_pays_its_debts}
General takeaways / reusable technique
- Always separate the two problems: a “which input passes the gate” check (Floor 1, hash-based) vs. “what secret is hidden in the binary” (Floor 2, XOR-encrypted data). They can use completely unrelated mechanisms even in the same binary.
- Repeating-key XOR is trivially reversible — if you can dump the ciphertext and the key (or how the key is derived, e.g.
obf_key ^ constant), you can decrypt entirely offline without ever running the binary. - Custom hash-check gates (djb2/FNV-style loops with a multiply-XOR pattern) usually don’t need to be “cracked” — you just need to replicate the same arithmetic in Python and pick any input, then compute its correct output.
- Ghidra/decompiler workflow used here:
- Decompile
mainto find the overall flow and prompts. - Decompile helper functions (
serial_of,unpack) referenced frommain. - Dump raw bytes of relevant global data (
obf_key,enc_pass,enc_flag) via the disassembly listing orgdb/objdump. - Reimplement the logic in Python to compute expected inputs / decrypt secrets without needing to brute-force anything at runtime.
- Decompile
# Full reusable solve script
def serial_of(s: str) -> str:
h = 0x1505
for c in s.encode():
h = (c ^ (h * 0x21)) & 0xFFFFFFFF
return "%08X" % h
def unpack(cipher: bytes, key: bytes) -> bytes:
return bytes(c ^ key[i % len(key)] for i, c in enumerate(cipher))
obf_key = bytes.fromhex("C0662BBF52E71C75")
real_key = bytes(b ^ 0x5A for b in obf_key)
enc_pass = bytes.fromhex("D8530384 66C9 0540 E84C".replace(" ", ""))
enc_flag = bytes.fromhex("<fill in all 48 bytes here>")
print(unpack(enc_pass, real_key).rstrip(b"\x00").decode())
print(unpack(enc_flag, real_key).rstrip(b"\x00").decode())Transclude of the-darkest-descent.7z