← Back to Writeups
CUSTOMMediumForensics

Trust But Verify

XESXOR9/2/20265 min read
#forensics#custom#medium#aes_cbc_decryption#http_object_extraction#staged_malware_analysis#static_unpacking#tcp_stream_reassembly#xor_stage_recovery#windows

Trust But Verify

Platform: duckerz | Category: Forensics | Type: Challenge | Difficulty: Medium | OS: Windows | Author: XESXOR | Date: 2026-02-11 | Status: Solved Techniques: aes_cbc_decryption, http_object_extraction, staged_malware_analysis, static_unpacking, tcp_stream_reassembly, xor_stage_recovery

Summary

Task: a PCAP shows a host downloading a fake Snake game and then sending an opaque blob to a suspicious listener. Solution: extract the staged malware logic, recover the AES-256-CBC key and IV from the second stage, and decrypt the exfiltrated report to reveal the leaked data and flag.

Trust But Verify — duckerz CTF

Description

Неизвестный источник, подозрительный файл и ни о чём не подозревающая жертва... Теперь вам кажется что кто-то незнакомый узнал нечто большее о вашем компьютере. Выясните, какие данные подверглись утечке.

English summary: the PCAP captures a victim downloading a suspicious file and then leaking data to an attacker. The goal is to determine what was exfiltrated.

Analysis

1. Initial triage of the capture

The first pass over the PCAP shows a short capture with a clear lure-delivery phase. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

bash
capinfos trust_but_verify.pcapng
tshark -r trust_but_verify.pcapng -Y http.request
tshark -r trust_but_verify.pcapng --export-objects http,objects/

Key observations:

  • capinfos reports a 4.1 MB pcapng with 6201 packets over about 20.6 seconds.
  • Host 192.168.1.7 downloads index.html and game.exe from 192.168.1.5:8080.
  • The server banner is SimpleHTTP/0.6 Python/3.12.12.
  • Exported HTTP objects are index.html, favicon.ico, and game.exe.

The HTML page is only a decoy. It is titled Snake Game: CMD Edition and contains a link to game.exe. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

2. Finding the real exfiltration stream

After the HTTP download, the most suspicious flow is a separate connection:

  • 192.168.1.7:58072 -> 192.168.1.5:31337
  • TCP stream 27
  • one-way upload only
  • 5 payload chunks: 1448 * 4 + 1088 = 6880 bytes

The rebuilt payload was saved as stream27_payload.bin.

Important properties:

  • SHA256: b789a6555691d879d133e05ff2bfc8799ebac75f08c01c68602741bad1bc2cd8
  • entropy: about 7.98
  • size divisible by 16
  • no useful file magic
  • not gzip, zlib, bzip2, or lzma
  • no easy single-byte XOR pattern ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

That is a strong hint that the traffic is encrypted application data rather than a normal file transfer.

Solution

3. Confirm that game.exe is a decoy

The downloaded binary is a PE32+ x64 console executable. Functionally, it presents a Snake game, but that is not its real purpose. Static analysis shows hidden dropper logic inside the packed binary.

The binary uses a modified UPX-like packer, so automatic unpacking is unreliable. A custom unpacker was used to recover the real code:

python
#!/usr/bin/env python3
import struct
from pathlib import Path
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

BASE = 0x140000000
SRC_RVA = 0x15025

def rva_to_off(rva: int) -> int:
    if 0x15000 <= rva < 0x15000 + 0xEE00:
        return 0x200 + (rva - 0x15000)
    if 0x24000 <= rva < 0x24000 + 0x200:
        return 0xF000 + (rva - 0x24000)
    raise ValueError(hex(rva))

class BitReader:
    def __init__(self, data: bytes, off: int):
        self.data = data
        self.i = off
        self.bb = 0
        self.bc = 0

    def getbit(self) -> int:
        if self.bc > 0:
            self.bc -= 1
            return (self.bb >> self.bc) & 1
        self.bb = struct.unpack_from("<I", self.data, self.i)[0]
        self.i += 4
        self.bc = 31
        return (self.bb >> 31) & 1
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

    def read_u8(self) -> int:
        b = self.data[self.i]
        self.i += 1
        return b

def nrv2b_unpack(src: bytes, src_off: int):
    br = BitReader(src, src_off)
    out = bytearray()
    last_m_off = 1
    while True:
        while br.getbit():
            out.append(br.read_u8())
        m_off = 1
        while True:
            m_off = m_off * 2 + br.getbit()
            if br.getbit():
                break
        if m_off == 2:
            m_off = last_m_off
        else:
            m_off = (m_off - 3) * 256 + br.read_u8()
            if m_off == 0xFFFFFFFF:
                break
            m_off += 1
            last_m_off = m_off
        m_len = br.getbit()
        m_len = m_len * 2 + br.getbit()
        if m_len == 0:
            m_len += 1
            while True:
                m_len = m_len * 2 + br.getbit()
                if br.getbit():
                    break
            m_len += 2
        m_len += 1 if m_off > 0xD00 else 0
        pos = len(out) - m_off
        out.append(out[pos])
        pos += 1
        while m_len > 0:
            out.append(out[pos])
            pos += 1
            m_len -= 1
    return bytes(out)
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

root = Path(".")
data = (root / "extracted" / "game.exe").read_bytes()
out = nrv2b_unpack(data, rva_to_off(SRC_RVA))
(root / "extracted" / "game_unpacked.bin").write_bytes(out)

Inside the unpacked code, the hidden logic XOR-decodes an embedded second stage with the repeating 8-byte key:

text
ff a2 44 79 12 3a 5c 94

It drops and launches:

text
%TEMP%\Diagnostics\CHKHKR-Release.exe

So the Snake game is only a lure. The actual theft is performed by the dropped second-stage executable.

4. Recover the second-stage behavior

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

The dropped payload HKR-Release.exe is also packed/protected. After static unpacking and reversing, its core logic becomes clear.

It collects four things:

  1. output of systeminfo
  2. directory listing of C:\Program Files
  3. directory listing of C:\Program Files (x86)
  4. contents of C:\flag.txt

It formats them as one plaintext report:

text
=== SYSTEM INFO ===
%s
=== PROGRAM FILES ===
%s
=== PROGRAM FILES (X86) ===
%s
=== EXTRA ===
%s

The hardcoded destination is 192.168.1.5:31337, which exactly matches the suspicious stream found in the PCAP. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

5. Recover the encryption parameters

Static reversing of the second stage shows standard Windows CryptoAPI usage:

  • provider: Microsoft Enhanced RSA and AES Cryptographic Provider
  • CryptAcquireContextA(..., PROV_RSA_AES=24, CRYPT_VERIFYCONTEXT=0xF0000000)
  • CryptImportKey with an AES-256 PLAINTEXTKEYBLOB
  • CryptSetKeyParam(KP_IV, iv)
  • CryptEncrypt over the full report
  • raw ciphertext sent with send

Recovered values:

text
AES-256 key: aea93868534334d4dae7939ee17582c853ef1c313189efab3c3d2ed4a1af2393
IV:          24f7d076e02a6ff6afd6b9609a61b3f7

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

6. Decrypt the captured exfiltration blob

Once the key and IV are known, the stream can be decrypted directly.

python
#!/usr/bin/env python3
from Crypto.Cipher import AES
from pathlib import Path

key = bytes.fromhex("aea93868534334d4dae7939ee17582c853ef1c313189efab3c3d2ed4a1af2393")
iv = bytes.fromhex("24f7d076e02a6ff6afd6b9609a61b3f7")
ct = Path("stream27_payload.bin").read_bytes()

pt = AES.new(key, AES.MODE_CBC, iv).decrypt(ct)
pad = pt[-1]
if 1 <= pad <= 16 and pt.endswith(bytes([pad]) * pad):
    pt = pt[:-pad]
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

print(pt.decode("cp866", errors="replace"))

This reveals the full stolen report. The important answer to the task question is:

  • system information for host OLEGE_DESKTOP
  • installed software listings from both Program Files directories
  • contents of C:\flag.txt

The flag appears in the EXTRA section:

text
=== EXTRA ===
DUCKERZ{REDACTED}

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍