HILLarious
XESXOR8/23/20263 min read
#reversing#htb#n/a
HILLarious
Platform: HackTheBox | Category: Reversing | Difficulty: Medium | Author: D3v0o0Nu11 | Date: 2026-08-01
Description
Blackink-corp's server was hit by ransomware. An executable encrypted all files of a new secret project. We have the malware (ransom) plus three encrypted files (secret.txt.enc, document.txt.enc, notes.txt.enc). Recover the project data.
Solution Approach
Core idea: UPX-packed ELF (UPX 5.20) — unpack first with upx -d before any RE. Custom ransomware file header: SNAR magic + uint64 LE timestamp + uint32 plaintext length + uint32 flags, then ciphertext.
Steps
unzipthe challenge zip → getransom(ELF) + three.encfiles.file ransom→ ELF 64-bit PIE, statically linked;stringsshows UPX packer signatures →upx -d→ unpacked 14.6KB dynamically-linked ELF.stringson unpacked binary →Usage: %s encrypt <file>, header magicSNAR,.encsuffix.- Disassemble (radare2/ghidra): main reads the file, gets length,
time(NULL)→ buildsSNARheader with timestamp + length. encryptwrapper: FNV-1a64 hash over the 8 timestamp bytes, XOR with 0xdeadbeefcafebabe → seed; LCG chain derives the 2x2 Hill matrix; then fcn.000013d0 does per-pair(a*p0+b*p1, c*p0+d*p1)mod 256; then an 8-byte repeating XOR with seed LE bytes.- Write decrypt.py: parse header → seed → un-XOR → invert Hill → truncate → recover all three files.
Key code
import struct
A, B, C = 0x5851f42d4c957f2d, 0x6c576fac43fd007c, 0x14057b7ef767814f
FNV_BASIS, FNV_PRIME, XOR_MASK = 0xcbf29ce484222325, 0x100000001b3, 0xdeadbeefcafebabe
def fnv1a64(b: bytes) -> int:
h = FNV_BASIS
for x in b:
h = ((h ^ x) * FNV_PRIME) & 0xFFFFFFFFFFFFFFFF
return h
def decrypt(data: bytes) -> bytes:
ts = struct.unpack("<Q", data[4:12])[0]
plen = struct.unpack("<I", data[12:16])[0]
enc = data[20:]
seed = fnv1a64(struct.pack("<Q", ts)) ^ XOR_MASK
ks = seed.to_bytes(8, "little")
enc = bytes(c ^ ks[i % 8] for i, c in enumerate(enc))
n1 = (seed * A + B) & 0xFFFFFFFFFFFFFFFF
n2 = (n1 * A + C) & 0xFFFFFFFFFFFFFFFF
n3 = (n2 * A + C) & 0xFFFFFFFFFFFFFFFF
n4 = (n3 * A + C) & 0xFFFFFFFFFFFFFFFF
m00, m01 = (n1 | 1) & 0xFF, n2 & 0xFF
m10, m11 = n3 & 0xFF, (n4 | 1) & 0xFF
det = (m00 * m11 - m01 * m10) & 0xFF
dinv = pow(det, -1, 256)
inv = [[(dinv * m11) & 0xFF, (dinv * (-m01)) & 0xFF],
[(dinv * (-m10)) & 0xFF, (dinv * m00) & 0xFF]]
pt = bytearray()
for i in range(0, len(enc), 2):
o0, o1 = enc[i], enc[i + 1]
pt += bytes(((inv[0][0] * o0 + inv[0][1] * o1) & 0xFF,
(inv[1][0] * o0 + inv[1][1] * o1) & 0xFF))
return bytes(pt[:plen])
Flag
REDACTED
Lessons Learned
- UPX-packed ELF (UPX 5.20) — unpack first with
upx -dbefore any RE. - Custom ransomware file header:
SNARmagic + uint64 LE timestamp + uint32 plaintext length + uint32 flags, then ciphertext. - The timestamp in the header IS the key seed → deterministic decryption, no key recovery needed.
- Seed = FNV-1a64(timestamp bytes) XOR 0xdeadbeefcafebabe.
- Byte repeating XOR keystream = seed LE bytes.
- X2 Hill cipher over Z_256: matrix derived from an LCG chain on the seed (n1=seedA+B; n2..n4 each nC), constants A=0x5851f42d4c957f2d, B=0x6c576fac43fd007c, C=0x14057b7ef767814f. Note TWO different LCG constants (B then C).
- Determinant must be odd → force odd by OR-ing m00/m11 with 1, and if det is even, add 2 to m00. Odd det ⇔ invertible mod 256.
- Decryption: un-XOR first, then per 2-byte block apply inverse matrix mod 256 (det inverse via
pow(det, -1, 256)), truncate to header length. - Plaintext is padded to even length with a single 0x01 byte before Hill-encrypt.
- Validate the RE by round-trip: encrypt a test file with the real binary, decrypt with the script, compare.
- Dummy lure files (notes/document) contained decoy text; the flag was in secret.txt.