TraceBash 2026 — The Silent Echo Writeup

A tiny capture that looks like DNS noise, a decoy ICMP flood and a fake SSH session. The real message is smuggled in two header fields nobody reads: the IP identification field of ICMP echoes and the TCP window size of an HTTP flow.

3 min read
ibrahimsql
535 words

TraceBash 2026 — The Silent Echo

A network-forensics challenge about covert channels: everything visible is a decoy, and the flag lives in header fields you normally ignore.

  • Event: TraceBash 2026
  • Category: Forensics
  • Artifact: the_silent_echo.zip → challenge.pcap
  • Flag format: TBCTF{...}

1. Three conversations, all suspicious

The capture is only 106 packets. Sorting it by protocol gives three independent stories:

DNS   25 packets   lookups for ubuntu.com, github.com, stackoverflow.com, microsoft.com, google.com
ICMP  32 packets   192.168.1.111 <-> 10.0.0.222, echo request/reply, payload "AAAA..."
TCP   49 packets   two HTTP flows and one "SSH" flow

Each one is a red herring dressed as normal traffic. The ICMP payload is 32 bytes of the letter A in every single packet — a "silent echo" that carries nothing. The DNS queries resolve nothing and use transaction id 0. And the "SSH" flow on port 22 is not SSH at all:

SSH-2.0-OpenSSH_8.2p1\r\nTm90aGluZyBoZXJlLCBrZWVwIGxvb2tpbmcu\n

That trailing line is base64:

>>> base64.b64decode("Tm90aGluZyBoZXJlLCBrZWVwIGxvb2tpbmcu")
b'Nothing here, keep looking.'

A decoy that literally tells you to keep looking. So the payloads are empty — the data must be in the headers.

2. Two HTTP flows that are not what they seem

Text
GET /api/v1/status HTTP/1.1          -> 10.0.0.80:80
    User-Agent: Mozilla/5.0 ID-Check/1.0
    X-Window-Trace: enabled
 
GET /downloads/ubuntu-22.04.iso      -> 10.0.0.88:80
    (response: 2304 bytes of pure zeros)

The ubuntu-22.04.iso download is 18 segments of zeroed bytes — no filesystem, no data, another decoy. But its request stream is not: the client 192.168.1.122 and the header X-Window-Trace: enabled are the hint. Both custom headers name the covert channels: the IP ID and the TCP window.

3. Channel one — the IP identification field

Every ICMP echo request has a random-looking IP id. It is not random. The high byte of the 16-bit id is a character:

Python
from scapy.all import rdpcap, IP, ICMP
 
pk = rdpcap("challenge.pcap")
part1 = "".join(chr(p[IP].id >> 8) for p in pk
                if ICMP in p and p[ICMP].type == 8)
# 'TBCTF{h1dd3n_1n_'

Sixteen requests, sixteen characters, each hidden in the upper byte of a field most tooling displays only as a decimal squiggle.

4. Channel two — the TCP window size

The HTTP client in the second flow never sends its advertised window at a "normal" value. Its ACKs all sit just above the default 8192; subtract that base and the remainder is an ASCII code:

Python
part2 = "".join(chr(p[TCP].window - 8192) for p in pk
                if IP in p and TCP in p and p[IP].src == "192.168.1.122"
                and p[TCP].dport == 80 and p[TCP].window != 8192)
# 'w1nd0w_4nd_1p_1d!}'

The server side answers with a constant window of 65535, so only the client's varying window matters. Stitching the two channels together closes the flag.

5. Reassembling the message

print(part1 + part2)
# TBCTF{h1dd3n_1n_w1nd0w_4nd_1p_1d!}

6. Flag

TBCTF{h1dd3n_1n_w1nd0w_4nd_1p_1d!}

The whole point of a covert channel is that the header is metadata, so nobody looks at it. Here the packet bodies were a deliberate parade of noise, and the answer was split across two fields — the IP id of the ICMP echoes and the window of the HTTP client — that most capture tools summarise into a single column you never scroll to.

---
Share this post:

What do you think?

React to show your appreciation

Related Posts