Home > Writeups > Raptor Weekly 2 - ECHELON Network 2 - 4.2 ; EXFIL

Raptor Weekly 2 - ECHELON Network 2 - 4.2 ; EXFIL

Parsing a custom ECP/1.2 protocol capture, deriving a stream key via HMAC-SHA256 from the Tier 4 handshake key, decrypting three of four signing key chunks, and hunting down the missing chunk in a PCAP from two tiers and two days earlier.

4.2 ; EXFIL

Challenge Description

PCAP captured on 2026-03-16 ; one day after the compromise of NODE 07. Something was exfiltrated. Decrypt it. Read it carefully.

Files provided: exfil.pcap

Required: handshake_key from 4.1 ; REMNANT


Overview

The PCAP contains C2 traffic using a custom application protocol (ECP) over TCP port 4433. Each data chunk is encrypted with AES-128-CBC, with per-chunk IVs derived from a base IV. Both the stream key and base IV are derived from the handshake key recovered in 4.1. Three of four chunks are present in the PCAP, the fourth is missing. An ACK packet contains a plaintext field pointing back to the Tier 2 INTERCEPT PCAP.


Step 1: Parse the PCAP

Open in Wireshark (https://www.wireshark.org) and filter by tcp.port == 4433. The TCP stream carries a framed application protocol. Each frame begins with a 4-byte magic value 0x45435000 ("ECP\x00"), followed by type, sequence, length, and payload.

Extract TCP payloads programmatically:

import struct

def parse_pcap(path):
    with open(path, "rb") as f:
        f.read(24)  # skip global header
        packets = []
        while True:
            hdr = f.read(16)
            if len(hdr) < 16: break
            _, _, inc_len, _ = struct.unpack("<IIII", hdr)
            data = f.read(inc_len)
            if len(data) < 54: continue
            ip_ihl   = (data[14] & 0x0f) * 4
            tcp_off  = 14 + ip_ihl
            tcp_doff = (data[tcp_off + 12] >> 4) * 4
            payload  = data[tcp_off + tcp_doff:]
            if payload:
                packets.append(payload)
    return packets

Parse ECP frames from the payloads:

ECP_MAGIC = 0x45435000
frames = []

for payload in packets:
    if len(payload) < 8: continue
    if struct.unpack(">I", payload[:4])[0] != ECP_MAGIC: continue
    msg_type = payload[4]
    seq      = payload[5]
    length   = struct.unpack(">H", payload[6:8])[0]
    body     = payload[8:8+length]
    frames.append({'type': msg_type, 'seq': seq, 'payload': body})

Types present: 0x01 HELLO, 0x02 DATA, 0x03 ACK, 0x04 FIN.


Step 2: Derive the Stream Key

The handshake key from 4.1 is a 32-byte hex string. Derive the stream key and base IV using HMAC-SHA256:

import hmac, hashlib

HANDSHAKE_KEY = bytes.fromhex("bbae799e...")  # from beacon.enc in 4.1
STREAM_KEY    = hmac.new(HANDSHAKE_KEY, b"EXFIL.STREAM.KEY", hashlib.sha256).digest()[:16]
STREAM_IV     = hmac.new(HANDSHAKE_KEY, b"EXFIL.STREAM.IV",  hashlib.sha256).digest()[:16]

Step 3: Decrypt the Data Chunks

Each DATA frame carries an AES-128-CBC encrypted chunk. The IV for chunk N is STREAM_IV XOR N (where N is the sequence byte, zero-padded to 16 bytes):

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

def decrypt_chunk(ct, seq):
    seq_iv = bytes(a ^ b for a, b in zip(STREAM_IV, seq.to_bytes(16, 'big')))
    return unpad(AES.new(STREAM_KEY, AES.MODE_CBC, seq_iv).decrypt(ct), 16)

for frame in frames:
    if frame['type'] == 0x02:  # DATA
        seq       = frame['seq']
        plaintext = decrypt_chunk(frame['payload'], seq)
        print(f"--- chunk {seq} ---")
        print(plaintext.decode())

Three DATA frames are present: sequence 1, 2, and 4. Sequence 3 is absent. Each decrypted chunk contains a DATA field with a hex fragment of the final signing key material, and chunk 4 contains the flag.


Step 4: Find the Missing Chunk

Chunks 1, 2, and 4 are present. Chunk 4 contains the assembly instruction: ASSEMBLY ; CHUNKS 1+2+3+4 ; SIGNING.KEY. Three chunks are in hand. One is not.

There is no pointer. Chunk 3 does not appear anywhere in this PCAP. The question is where it came from.

The payload header in chunk 1 identifies the source node (ECHELON.NODE.07), the operator (HANDLER.ARC), and the timestamp (2026-03-16). The exfiltration happened on 2026-03-16, the day after the compromise. Go back to the earlier captures from that node and audit every field, not just the ones that answered the challenge at the time.

The X-Session-Trace response header in the Tier 2 INTERCEPT PCAP (2026-03-14) contains the value f1e2d3c4b5a69788. That is chunk 3.


Step 5: Assemble the Signing Key

The four chunk DATA values concatenate to form the signing key material passed to 5.1 ; ECHELON:

chunk1.DATA : 7f3d9a1e2b4c8f0a6d5e9c3b7f2a1d8e
chunk2.DATA : 1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d
chunk3.DATA : f1e2d3c4b5a69788  (from Tier 2 PCAP)
chunk4.DATA : 9c8b7a6f5e4d3c2b1a0f9e8d7c6b5a4f

This value, combined with the C2 handshake key from 4.1 and the ACCESS.CERT.KEY from the 3.3 heap artifact, feeds the final challenge.


Key Takeaways

C2 traffic that omits a payload chunk and includes a plaintext reference to another capture is realistic tradecraft misdirection. Operators sometimes deliberately fragment payloads across sessions to complicate forensic reconstruction. The lesson for defenders: every protocol field in every packet in every PCAP is potentially meaningful; partial analysis is not complete analysis.


Flag

ECHELON{exfil_left_a_trace}

< Back to All Writeups