Skip to main content

Command Palette

Search for a command to run...

Custom Packaging CTF Writeup: Decrypting the Custom KCF Container

Updated
8 min read
Custom Packaging CTF Writeup: Decrypting the Custom KCF Container
N

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:

OffsetSizeFieldNotes
0x004Magic4B 43 46 00 ("KCF\0")
0x042Version01 02 (LE) = 0x0201
0x062FlagsBit 0 indicates encryption
0x0816NonceRandom bytes, likely used in key derivation
0x188TimestampUnix timestamp, little endian
0x202File countNumber of files in container
0x228FAT offsetOffset to file allocation table
0x2A4FAT sizeSize of FAT in bytes
0x2E8Data offsetOffset to data region
0x368Data sizeSize of data region
0x3E4ChecksumCRC32 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:

FieldHex BytesDecoded Value (LE)Interpretation
Magic4B 43 46 00"KCF\0"Valid KCF header.
Version01 022.1Version 0x0201.
Flags01 001Bit 0 set; encryption is enabled.
Nonceb3 71 ... 83 2216 bytesUsed for master key derivation.
Timestamp00 5b 3b 69 ...1765481216Epoch time corresponding to Dec 2025.
File Counta8 00168168 total files present.
FAT Offset80 00 ...128FAT begins at 0x80.
FAT Size00 3f 00 0016,128FAT region length.
Data Offset00 40 00 00 ...16,384Data region begins at 0x4000.

Understanding the Challenge

1. The Structure

The file is organized like a sandwich:

  1. Header (0 - 128): The metadata we just analyzed.

  2. 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.

  3. Padding (16,256 - 16,384): Empty space to ensure the data starts at a nice, round number (512-byte alignment).

  4. 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 bytes PK (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}

Advent of CTF'25

Part 15 of 15

A structured walkthrough of all the challenges I solved from CyberStudents’ Advent of CTF 2025. This series documents each day’s puzzle with precise methodology, technical breakdowns, and reproducible exploitation steps.

Start from the beginning

The Mission Begins: Cryptography CTF Writeup

Challenge Description Category: CryptographyAuthor: qvipin You step into the North Pole Security Operations Room for your first official briefing. Before you can even say a word, an elf swivels around in his chair and looks directly at you with a gri...