Custom Packaging CTF Writeup: Decrypting the Custom KCF Container

Barely Tame CTF Player. Debugging Addict. Worshipper of Wi-Fi Signals. Human? Depends on the Ping.
Challenge Description
Category: Forensics
Author: qvipin
Our threat intel team has been tracking KRAMPUS SYNDICATE for months now. Last week, we finally caught a break. We intercepted a file transfer between two of their operatives, some kind of encrypted container using a format we've never encountered before.
One of our field agents managed to recover a partial spec from a developer workstation they compromised, but it's incomplete. Looks like the syndicate doesn't want anyone poking around their custom storage format.
The file was stored as ks2025_ops_final.kcf. Their servers follow the same pattern - ks2025-c2-01, ks2025-stage, etc. Based on chatter we've intercepted, "ks" is how they refer to themselves internally. Seems like they roll new encryption keys every January.
Here's what we know about the format:
| Offset | Size | Field | Notes |
0x00 | 4 | Magic | 4B 43 46 00 ("KCF\0") |
0x04 | 2 | Version | 01 02 (LE) = 0x0201 |
0x06 | 2 | Flags | Bit 0 indicates encryption |
0x08 | 16 | Nonce | Random bytes, likely used in key derivation |
0x18 | 8 | Timestamp | Unix timestamp, little endian |
0x20 | 2 | File count | Number of files in container |
0x22 | 8 | FAT offset | Offset to file allocation table |
0x2A | 4 | FAT size | Size of FAT in bytes |
0x2E | 8 | Data offset | Offset to data region |
0x36 | 8 | Data size | Size of data region |
0x3E | 4 | Checksum | CRC32 of header bytes 0x00 to 0x3D |
Header is 128 bytes. FAT entries are 96 bytes each. Data region starts at 512 byte alignment.
Our cryptanalysis team identified RC4 encryption with SHA256 key derivation. Each encrypted region uses a fresh cipher instance. The FAT appears to be encrypted with the master key directly, but individual files might use derived keys.
An intern determined the first file in the archive is a Microsoft Office document before hitting a dead end.
Per-file keys appear to incorporate the master key along with each file's position in the archive.
Figure out how this thing works and extract whatever's inside.
Hint-1: Master key = SHA256(nonce || timestamp_LE || file_count_LE || identifier). The identifier is 6 lowercase characters (a-z0-9).
Hint-2: Per-file key = SHA256(master_key || file_index || file_offset), truncated by a certain amount. Same endianness conventions apply.
Solution Walkthrough
Initial File Analysis
The challenge provides a ks_operations.kcf file. Initial inspection via file utility returns a false positive, identifying it as a FreeDOS Keyboard Layout. Given the size discrepancy (5.2MB vs typical 150KB) and the provided specification, this is confirmed as a custom Krampus Container Format (KCF).
❯ du -sh ks_operations.kcf
5.2M ks_operations.kcf
❯ file ks_operations.kcf
ks_operations.kcf: FreeDOS KEYBoard Layout collection, version 0x100
Header Parsing and Metadata Extraction
To proceed with decryption, the header metadata must be extracted according to the provided specification. A hex dump of the first 64 bytes reveals the critical parameters.
❯ hexdump -C ks_operations.kcf
00000000 4b 43 46 00 01 02 01 00 b3 71 c7 41 77 fb 3c dc |KCF......q.Aw.<.|
00000010 cc 80 a1 6a 27 73 83 22 00 5b 3b 69 00 00 00 00 |...j's.".[;i....|
00000020 a8 00 80 00 00 00 00 00 00 00 00 3f 00 00 00 40 |...........?...@|
00000030 00 00 00 00 00 00 30 8f 52 00 00 00 00 00 25 78 |......0.R.....%x|
.... SNIP ....
Nonce (16 Bytes): b3 71 c7 41 77 fb 3c dc cc 80 a1 6a 27 73 83 22
Timestamp (8 Bytes): 00 5b 3b 69 00 00 00 00
File Count (2 Bytes): a8 00
FAT Offset (8 Bytes): 00 00 00 00 00 00 00 80
FAT Size (4 Bytes): 00 00 3f 00
Data Offset (8 Bytes): 00 00 00 00 00 00 40 00
Data Size (8 Bytes): 00 00 00 00 00 52 8f 30
The extracted values are mapped below:
| Field | Hex Bytes | Decoded Value (LE) | Interpretation |
| Magic | 4B 43 46 00 | "KCF\0" | Valid KCF header. |
| Version | 01 02 | 2.1 | Version 0x0201. |
| Flags | 01 00 | 1 | Bit 0 set; encryption is enabled. |
| Nonce | b3 71 ... 83 22 | 16 bytes | Used for master key derivation. |
| Timestamp | 00 5b 3b 69 ... | 1765481216 | Epoch time corresponding to Dec 2025. |
| File Count | a8 00 | 168 | 168 total files present. |
| FAT Offset | 80 00 ... | 128 | FAT begins at 0x80. |
| FAT Size | 00 3f 00 00 | 16,128 | FAT region length. |
| Data Offset | 00 40 00 00 ... | 16,384 | Data region begins at 0x4000. |
Understanding the Challenge
1. The Structure
The file is organized like a sandwich:
Header (0 - 128): The metadata we just analyzed.
FAT (File Allocation Table) (128 - 16,256): This is the "Table of Contents." It lists the names, sizes, and positions of the 168 files inside. Critical: The description says the FAT is encrypted. We cannot read the filenames until we unlock this.
Padding (16,256 - 16,384): Empty space to ensure the data starts at a nice, round number (512-byte alignment).
Data Region (16,384 - End): The actual encrypted content of the files.
2. The Lock (Encryption Logic)
The Algorithm: RC4 is a stream cipher. It works by generating a stream of pseudo-random bytes and XORing them with the data. To decrypt, we generate the same stream and XOR it again.
The Key Derivation: The challenge mentions "SHA256 key derivation."
Master key =
SHA256(nonce || timestamp_LE || file_count_LE || identifier)(The identifier is 6 lowercase characters ie. a-z0-9)Per-file key =
SHA256(master_key || file_index || file_offset)(Truncated by a certain amount)
3. The Strategy
- "Intern's Hint": The first file is a Microsoft Office document. Office documents (
.docx,.xlsx) are actually ZIP files. They always start with the magic bytesPK(50 4B).
Master Key Derivation
The master key is derived using SHA256 of the concatenated nonce, timestamp (Little Endian), file count (Little Endian), and a 6-character identifier. Based on the challenge description and file naming patterns (ks2025_ops_final.kcf), the identifier is deduced to be ks2025.
import sys
import hashlib
from Crypto.Cipher import ARC4
identifier = ("ks2025").encode()
NONCE = bytes.fromhex("b371c74177fb3cdccc80a16a27738322")
TIMESTAMP = bytes.fromhex("005b3b6900000000")
FILECOUNT = bytes.fromhex("a800")
KCF_FILE = "ks_operations.kcf"
master_key = hashlib.sha256(NONCE + TIMESTAMP + FILECOUNT + identifier).digest()
print("Master key: ", master_key.hex())
Output: Master key: 95dbdd24af755276432d8b6c06f3151d7c4102a50a98f14a3b68ddb953ac9048
FAT Decryption and Parsing
With the master key obtained, the File Allocation Table (FAT) can be decrypted using RC4. This table contains the metadata for individual files stored in the container.
from Crypto.Cipher import ARC4
import struct
KCF_FILE = "ks_operations.kcf"
FAT_OFFSET = 0x80
FAT_SIZE = 0x3F00
FAT_ENTRY_SIZE = 96
mk_hex = input("[?] Master Key: ").strip()
master_key = bytes.fromhex(mk_hex)
with open(KCF_FILE, "rb") as f:
f.seek(FAT_OFFSET)
fat_enc = f.read(FAT_SIZE)
rc4 = ARC4.new(master_key)
fat_plain = rc4.decrypt(fat_enc)
# write decrypted FAT
with open("fat.dec", "wb") as f:
f.write(fat_plain)
# parse and print FAT entries
file_count = FAT_SIZE // FAT_ENTRY_SIZE
print("\n[+] FAT entries:")
print(" idx | file_offset | file_size")
print("-----+-------------+-----------")
for i in range(file_count):
base = i * FAT_ENTRY_SIZE
file_offset = struct.unpack_from("<Q", fat_plain, base + 0x04)[0]
file_size = struct.unpack_from("<I", fat_plain, base + 0x0C)[0]
print(f"{i:4d} | {file_offset:11d} | {file_size:9d}")
print("\n[✓] FAT decrypted.")
The FAT successfully decrypts, revealing a total of 168 entries. This confirms the master key is correct.
❯ pyexec decrypt_FAT.py
[?] Master Key: 95dbdd24af755276432d8b6c06f3151d7c4102a50a98f14a3b68ddb953ac9048
[+] FAT entries:
idx | file_offset | file_size
-----+-------------+-----------
0 | 0 | 100352
.... SNIP ....
167 | 5368160 | 42442
[✓] FAT decrypted.
Per-File Decryption and Extraction
Each file in the data region is encrypted with a unique key derived from the master key, the file index, and the file's offset within the data region. The derivation uses SHA256(master_key || file_index || file_offset) truncated to 16 bytes.
The following script iterates through all FAT entries, derives the per-file keys, decrypts the RC4 streams, and identifies file types based on magic bytes.
#!/usr/bin/env python3
import struct, hashlib, zlib
from pathlib import Path
from Crypto.Cipher import ARC4
KCF = Path("ks_operations.kcf")
FAT = Path("fat.dec")
OUT = Path("extracted")
FAT_ENTRY = 96
KEY_TRUNC = 16
def ext(b):
return (
".doc" if b.startswith(b"\xD0\xCF\x11\xE0") else
".pdf" if b.startswith(b"%PDF") else
".png" if b.startswith(b"\x89PNG") else
".jpg" if b.startswith(b"\xFF\xD8\xFF") else
".bin"
)
data = KCF.read_bytes()
assert data[:4] == b"KCF\x00"
fc = struct.unpack_from("<H", data, 0x20)[0]
doff = struct.unpack_from("<Q", data, 0x2E)[0]
assert zlib.crc32(data[:0x3E]) & 0xffffffff == struct.unpack_from("<I", data, 0x3E)[0]
fat = FAT.read_bytes()
assert len(fat) // FAT_ENTRY == fc
mk = bytes.fromhex(input("[?] master_key (hex): ").strip())
assert len(mk) == 32
OUT.mkdir(exist_ok=True)
for i in range(fc):
base = i * FAT_ENTRY
foff = struct.unpack_from("<Q", fat, base + 0x04)[0]
fsize = struct.unpack_from("<I", fat, base + 0x0C)[0]
enc = data[doff + foff : doff + foff + fsize]
fk = hashlib.sha256(mk + struct.pack("<I", i) + struct.pack("<Q", foff)).digest()[:KEY_TRUNC]
dec = ARC4.new(fk).decrypt(enc)
(OUT / f"file_{i:03d}{ext(dec)}").write_bytes(dec)
print(f"[+] {i:03d} key={fk.hex()}")
print(f"[✓] extracted {fc} files.")
All 168 file extracted successfully to extracted directory.
❯ pyexec file_extraction.py
[?] Master Key: 95dbdd24af755276432d8b6c06f3151d7c4102a50a98f14a3b68ddb953ac9048
[+] 000 key=dbdc77f7471e4a22d29c7d2e6c296d86
.... SNIP ....
[+] 167 key=20494a9528fdca666679e30e6b544832
[✓] extracted 168 files.
❯ ls extracted
file_000.doc file_034.bin file_068.bin file_102.bin file_136.bin
.... SNIP ....
file_031.bin file_065.bin file_099.bin file_133.bin file_167.bin
As we can clearly see the first file is indeed a Microsoft Document as mention in by Intern, this confirms that the extraction of files is correct.
Flag Discovery
After extracting all 168 files, the contents are searched for the flag format csd{ in the extracted files, identifies the flag within file_137.bin.
❯ cat ./extracted/file_137.bin
OPERATION FROSTBITE - AFTER ACTION REVIEW
==========================================
.... SNIP ....
This operation was conducted under authorization reference:
csd{Kr4mPU5_RE4llY_l1ke5_T0_m4kE_EVeRytH1NG_CU5t0m_672Df}
END OF REPORT
Final Flag
csd{Kr4mPU5_RE4llY_l1ke5_T0_m4kE_EVeRytH1NG_CU5t0m_672Df}



