Trust But Verify
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 read-only • copy allowedcapinfos 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:
capinfosreports a 4.1 MBpcapngwith 6201 packets over about 20.6 seconds.- Host
192.168.1.7downloadsindex.htmlandgame.exefrom192.168.1.5:8080. - The server banner is
SimpleHTTP/0.6 Python/3.12.12. - Exported HTTP objects are
index.html,favicon.ico, andgame.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 = 6880bytes
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 read-only • copy allowed#!/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 read-only • copy allowedff a2 44 79 12 3a 5c 94
It drops and launches:
text read-only • copy allowed%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:
- output of
systeminfo - directory listing of
C:\Program Files - directory listing of
C:\Program Files (x86) - contents of
C:\flag.txt
It formats them as one plaintext report:
text read-only • copy allowed=== 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)CryptImportKeywith an AES-256PLAINTEXTKEYBLOBCryptSetKeyParam(KP_IV, iv)CryptEncryptover the full report- raw ciphertext sent with
send
Recovered values:
text read-only • copy allowedAES-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 read-only • copy allowed#!/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 Filesdirectories - contents of
C:\flag.txt
The flag appears in the EXTRA section:
text read-only • copy allowed=== EXTRA === DUCKERZ{REDACTED}