ctf-agent 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +131 -0
- package/Dockerfile +192 -0
- package/README.md +522 -0
- package/agents/ctf-analyzer.md +58 -0
- package/agents/ctf-controller.md +151 -0
- package/agents/ctf-speedrun.md +74 -0
- package/bin/ctf-agent.js +119 -0
- package/docker-compose.yml +13 -0
- package/mcp_config.json +19 -0
- package/package.json +44 -0
- package/references/ctf-triage-ladder.md +68 -0
- package/references/exploit-databases.md +575 -0
- package/references/llm-safety-and-policy-compliance.md +173 -0
- package/references/multi-agent-orchestration-and-policy-routing.md +278 -0
- package/references/security-events-and-intelligence.md +566 -0
- package/references/version-matrix.md +59 -0
- package/rules/ctf-execution-rules.md +44 -0
- package/rules/ctf-safety-framing-rules.md +140 -0
- package/rules/flag-validation-rules.md +25 -0
- package/scripts/__init__.py +1 -0
- package/scripts/ctf-tools.lock +205 -0
- package/scripts/ctf_init.py +1026 -0
- package/scripts/ctf_update.py +583 -0
- package/scripts/ctfd_client.py +81 -0
- package/scripts/cve_lookup.py +453 -0
- package/scripts/extract_flags.py +155 -0
- package/scripts/install_as_agent.py +278 -0
- package/scripts/install_ctf_tools.sh +603 -0
- package/scripts/parallel_triage.py +395 -0
- package/scripts/prompt_policy_sanitizer.py +283 -0
- package/scripts/scope_guard.py +444 -0
- package/scripts/skill_validator.py +504 -0
- package/scripts/workspace_cleaner.py +141 -0
- package/skills/ctf-ai-ml/SKILL.md +120 -0
- package/skills/ctf-ai-ml/adversarial-ml.md +623 -0
- package/skills/ctf-ai-ml/llm-attacks.md +487 -0
- package/skills/ctf-ai-ml/model-attacks.md +422 -0
- package/skills/ctf-crypto/SKILL.md +320 -0
- package/skills/ctf-crypto/advanced-math.md +798 -0
- package/skills/ctf-crypto/classic-ciphers.md +651 -0
- package/skills/ctf-crypto/ecc-attacks.md +347 -0
- package/skills/ctf-crypto/exotic-crypto-2.md +380 -0
- package/skills/ctf-crypto/exotic-crypto.md +528 -0
- package/skills/ctf-crypto/historical.md +113 -0
- package/skills/ctf-crypto/lattice-and-lwe.md +524 -0
- package/skills/ctf-crypto/modern-ciphers-2.md +563 -0
- package/skills/ctf-crypto/modern-ciphers-3.md +453 -0
- package/skills/ctf-crypto/modern-ciphers.md +649 -0
- package/skills/ctf-crypto/prng-attacks.md +257 -0
- package/skills/ctf-crypto/prng.md +664 -0
- package/skills/ctf-crypto/rsa-attacks-2.md +792 -0
- package/skills/ctf-crypto/rsa-attacks.md +487 -0
- package/skills/ctf-crypto/stream-ciphers.md +390 -0
- package/skills/ctf-crypto/zkp-and-advanced.md +456 -0
- package/skills/ctf-forensics/3d-printing.md +121 -0
- package/skills/ctf-forensics/SKILL.md +379 -0
- package/skills/ctf-forensics/disk-advanced.md +497 -0
- package/skills/ctf-forensics/disk-and-memory.md +491 -0
- package/skills/ctf-forensics/disk-recovery.md +699 -0
- package/skills/ctf-forensics/linux-forensics.md +511 -0
- package/skills/ctf-forensics/network-advanced.md +583 -0
- package/skills/ctf-forensics/network.md +645 -0
- package/skills/ctf-forensics/peripheral-capture.md +287 -0
- package/skills/ctf-forensics/signals-and-hardware.md +713 -0
- package/skills/ctf-forensics/steganography.md +694 -0
- package/skills/ctf-forensics/stego-advanced-2.md +475 -0
- package/skills/ctf-forensics/stego-advanced.md +481 -0
- package/skills/ctf-forensics/stego-image.md +691 -0
- package/skills/ctf-forensics/windows.md +625 -0
- package/skills/ctf-malware/SKILL.md +181 -0
- package/skills/ctf-malware/c2-and-protocols.md +274 -0
- package/skills/ctf-malware/pe-and-dotnet.md +108 -0
- package/skills/ctf-malware/scripts-and-obfuscation.md +449 -0
- package/skills/ctf-misc/SKILL.md +498 -0
- package/skills/ctf-misc/bashjails.md +323 -0
- package/skills/ctf-misc/ctfd-navigation.md +465 -0
- package/skills/ctf-misc/dns.md +255 -0
- package/skills/ctf-misc/encodings-advanced.md +504 -0
- package/skills/ctf-misc/encodings.md +431 -0
- package/skills/ctf-misc/games-and-vms-2.md +254 -0
- package/skills/ctf-misc/games-and-vms-3.md +690 -0
- package/skills/ctf-misc/games-and-vms-4.md +229 -0
- package/skills/ctf-misc/games-and-vms.md +529 -0
- package/skills/ctf-misc/linux-privesc.md +333 -0
- package/skills/ctf-misc/pyjails.md +671 -0
- package/skills/ctf-misc/rf-sdr.md +91 -0
- package/skills/ctf-osint/SKILL.md +198 -0
- package/skills/ctf-osint/geolocation-and-media.md +464 -0
- package/skills/ctf-osint/social-media.md +312 -0
- package/skills/ctf-osint/web-and-dns.md +341 -0
- package/skills/ctf-pwn/SKILL.md +214 -0
- package/skills/ctf-pwn/advanced-exploits-2.md +579 -0
- package/skills/ctf-pwn/advanced-exploits-3.md +598 -0
- package/skills/ctf-pwn/advanced-exploits-4.md +590 -0
- package/skills/ctf-pwn/advanced-exploits-5.md +119 -0
- package/skills/ctf-pwn/advanced-exploits.md +773 -0
- package/skills/ctf-pwn/advanced.md +326 -0
- package/skills/ctf-pwn/field-notes.md +245 -0
- package/skills/ctf-pwn/format-string.md +694 -0
- package/skills/ctf-pwn/heap-fsop.md +285 -0
- package/skills/ctf-pwn/heap-techniques-2.md +333 -0
- package/skills/ctf-pwn/heap-techniques.md +513 -0
- package/skills/ctf-pwn/kernel-bypass.md +421 -0
- package/skills/ctf-pwn/kernel-techniques.md +366 -0
- package/skills/ctf-pwn/kernel.md +636 -0
- package/skills/ctf-pwn/overflow-basics.md +611 -0
- package/skills/ctf-pwn/rop-advanced.md +725 -0
- package/skills/ctf-pwn/rop-and-shellcode.md +659 -0
- package/skills/ctf-pwn/sandbox-escape.md +313 -0
- package/skills/ctf-reverse/SKILL.md +163 -0
- package/skills/ctf-reverse/anti-analysis-ctf.md +204 -0
- package/skills/ctf-reverse/anti-analysis.md +693 -0
- package/skills/ctf-reverse/field-notes.md +376 -0
- package/skills/ctf-reverse/languages-compiled.md +666 -0
- package/skills/ctf-reverse/languages-platforms.md +592 -0
- package/skills/ctf-reverse/languages.md +553 -0
- package/skills/ctf-reverse/patterns-ctf-2.md +397 -0
- package/skills/ctf-reverse/patterns-ctf-3.md +797 -0
- package/skills/ctf-reverse/patterns-ctf.md +670 -0
- package/skills/ctf-reverse/patterns-runtime.md +274 -0
- package/skills/ctf-reverse/patterns.md +572 -0
- package/skills/ctf-reverse/platforms-hardware.md +387 -0
- package/skills/ctf-reverse/platforms.md +664 -0
- package/skills/ctf-reverse/tools-advanced-2.md +421 -0
- package/skills/ctf-reverse/tools-advanced.md +407 -0
- package/skills/ctf-reverse/tools-dynamic.md +679 -0
- package/skills/ctf-reverse/tools-emulation.md +319 -0
- package/skills/ctf-reverse/tools.md +573 -0
- package/skills/ctf-web/SKILL.md +153 -0
- package/skills/ctf-web/auth-and-access-2.md +82 -0
- package/skills/ctf-web/auth-and-access.md +783 -0
- package/skills/ctf-web/auth-infra.md +321 -0
- package/skills/ctf-web/auth-jwt.md +186 -0
- package/skills/ctf-web/client-side-advanced.md +739 -0
- package/skills/ctf-web/client-side.md +529 -0
- package/skills/ctf-web/cves.md +373 -0
- package/skills/ctf-web/field-notes.md +482 -0
- package/skills/ctf-web/node-and-prototype.md +200 -0
- package/skills/ctf-web/server-side-2.md +337 -0
- package/skills/ctf-web/server-side-advanced-2.md +559 -0
- package/skills/ctf-web/server-side-advanced-3.md +125 -0
- package/skills/ctf-web/server-side-advanced-4.md +480 -0
- package/skills/ctf-web/server-side-advanced.md +378 -0
- package/skills/ctf-web/server-side-deser.md +443 -0
- package/skills/ctf-web/server-side-exec-2.md +799 -0
- package/skills/ctf-web/server-side-exec.md +457 -0
- package/skills/ctf-web/server-side.md +629 -0
- package/skills/ctf-web/sql-injection.md +790 -0
- package/skills/ctf-web/web3.md +374 -0
- package/skills/ctf-writeup/SKILL.md +90 -0
- package/skills/solve-challenge/SKILL.md +269 -0
- package/skills.json +16 -0
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
# CTF Forensics - Network (Advanced)
|
|
2
|
+
|
|
3
|
+
For USB/HID/Bluetooth peripheral capture analysis (mouse/pen drawing recovery, keyboard scan codes, LED Morse exfiltration, RFCOMM reassembly), see [peripheral-capture.md](peripheral-capture.md). For basic network forensics, see [network.md](network.md).
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
- [Packet Interval Timing-Based Encoding (EHAX 2026)](#packet-interval-timing-based-encoding-ehax-2026)
|
|
7
|
+
- [NTLMv2 Hash Cracking from PCAP (Pragyan 2026)](#ntlmv2-hash-cracking-from-pcap-pragyan-2026)
|
|
8
|
+
- [TCP Flag Covert Channel (BearCatCTF 2026)](#tcp-flag-covert-channel-bearcatctf-2026)
|
|
9
|
+
- [DNS Query Name Last-Byte Steganography (UTCTF 2026)](#dns-query-name-last-byte-steganography-utctf-2026)
|
|
10
|
+
- [DNS Trailing Byte Binary Encoding (UTCTF 2026)](#dns-trailing-byte-binary-encoding-utctf-2026)
|
|
11
|
+
- [Multi-Layer PCAP with XOR + ZIP (UTCTF 2026)](#multi-layer-pcap-with-xor--zip-utctf-2026)
|
|
12
|
+
- [Brotli Decompression Bomb Seam Analysis (BearCatCTF 2026)](#brotli-decompression-bomb-seam-analysis-bearcatctf-2026)
|
|
13
|
+
- [SMB RID Recycling via LSARPC (Midnight 2026)](#smb-rid-recycling-via-lsarpc-midnight-2026)
|
|
14
|
+
- [Timeroasting / MS-SNTP Hash Extraction (Midnight 2026)](#timeroasting--ms-sntp-hash-extraction-midnight-2026)
|
|
15
|
+
- [ICMP Payload Steganography with Byte Rotation (HackIM 2016)](#icmp-payload-steganography-with-byte-rotation-hackim-2016)
|
|
16
|
+
- [Packet Reconstruction via Checksum Validation (Break In 2016)](#packet-reconstruction-via-checksum-validation-break-in-2016)
|
|
17
|
+
- [dnscat2 Traffic Reassembly from DNS PCAP (BSidesSF 2017)](#dnscat2-traffic-reassembly-from-dns-pcap-bsidessf-2017)
|
|
18
|
+
- [Unreferenced PDF Objects with Hidden Pages (SharifCTF 7 2016)](#unreferenced-pdf-objects-with-hidden-pages-sharifctf-7-2016)
|
|
19
|
+
- [RDP Session Decryption via Extracted PKCS12 Key (HITB 2017)](#rdp-session-decryption-via-extracted-pkcs12-key-hitb-2017)
|
|
20
|
+
- [RADIUS Shared Secret Cracking (UConn CyberSEED 2017)](#radius-shared-secret-cracking-uconn-cyberseed-2017)
|
|
21
|
+
- [RC4 Stream Identification in Shellcode PCAP (CODE BLUE 2017)](#rc4-stream-identification-in-shellcode-pcap-code-blue-2017)
|
|
22
|
+
- [ICMP Ping Time-Delay Covert Channel (DefCamp 2018)](#icmp-ping-time-delay-covert-channel-defcamp-2018)
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Packet Interval Timing-Based Encoding (EHAX 2026)
|
|
27
|
+
|
|
28
|
+
**Pattern (Breathing Void):** Large PCAPNG with millions of packets, but only a few hundred on one interface carry data. The signal is in the **timing gaps** between identical packets, not their content.
|
|
29
|
+
|
|
30
|
+
**Identification:** Challenge mentions "breathing", "void", "silence", or timing. PCAP has many interfaces but only one has interesting traffic. Packets are identical but spaced at two distinct intervals.
|
|
31
|
+
|
|
32
|
+
**Decoding workflow:**
|
|
33
|
+
```python
|
|
34
|
+
from scapy.all import rdpcap
|
|
35
|
+
|
|
36
|
+
packets = rdpcap('challenge.pcapng')
|
|
37
|
+
|
|
38
|
+
# 1. Filter to the right interface (e.g., interface 2)
|
|
39
|
+
# tshark: tshark -r challenge.pcapng -Y "frame.interface_id == 2" -T fields -e frame.time_epoch
|
|
40
|
+
|
|
41
|
+
# 2. Compute inter-packet intervals
|
|
42
|
+
times = [float(pkt.time) for pkt in packets if pkt.sniffed_on == 'interface_2']
|
|
43
|
+
intervals = [times[i+1] - times[i] for i in range(len(times)-1)]
|
|
44
|
+
|
|
45
|
+
# 3. Identify binary mapping (two distinct interval values)
|
|
46
|
+
# E.g., 10ms → 0, 100ms → 1 (threshold at ~50ms)
|
|
47
|
+
threshold = 0.05 # 50ms
|
|
48
|
+
bits = [0 if dt < threshold else 1 for dt in intervals]
|
|
49
|
+
|
|
50
|
+
# 4. May need to prepend a leading 0 bit (first interval has no predecessor)
|
|
51
|
+
bits = [0] + bits
|
|
52
|
+
|
|
53
|
+
# 5. Convert bits to bytes (MSB-first)
|
|
54
|
+
data = bytes(int(''.join(str(b) for b in bits[i:i+8]), 2)
|
|
55
|
+
for i in range(0, len(bits) - 7, 8))
|
|
56
|
+
print(data.decode(errors='replace'))
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Key insight:** When identical packets appear on a single interface with only two practical interval values, it's almost certainly binary encoding via timing. The content is noise — the signal is in the gaps. Filter by interface and count unique intervals first.
|
|
60
|
+
|
|
61
|
+
**Scale tip:** Large PCAPs (millions of packets) often have the signal in a tiny subset. Triage with `tshark -q -z io,phs` to find which interface has the fewest packets — that's likely the data carrier.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## NTLMv2 Hash Cracking from PCAP (Pragyan 2026)
|
|
66
|
+
|
|
67
|
+
**Pattern ($whoami):** SMB2 authentication in packet capture.
|
|
68
|
+
|
|
69
|
+
**Extraction:** From NTLMSSP_AUTH packet, extract: server challenge, NTProofStr, and blob.
|
|
70
|
+
|
|
71
|
+
**Brute-force with known password format:**
|
|
72
|
+
```python
|
|
73
|
+
import hashlib, hmac
|
|
74
|
+
from Crypto.Hash import MD4
|
|
75
|
+
|
|
76
|
+
def try_password(password, username, domain, server_challenge, blob, expected_proof):
|
|
77
|
+
nt_hash = MD4.new(password.encode('utf-16-le')).digest()
|
|
78
|
+
identity = (username.upper() + domain).encode('utf-16-le')
|
|
79
|
+
ntlmv2_hash = hmac.new(nt_hash, identity, hashlib.md5).digest()
|
|
80
|
+
proof = hmac.new(ntlmv2_hash, server_challenge + blob, hashlib.md5).digest()
|
|
81
|
+
return proof == expected_proof
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## TCP Flag Covert Channel (BearCatCTF 2026)
|
|
87
|
+
|
|
88
|
+
**Pattern (pCapsized):** Suspicious TCP packets with chaotic flag combinations (FIN+SYN, SYN+RST+PSH+URG, etc.). The 6 TCP flag bits encode base64 characters.
|
|
89
|
+
|
|
90
|
+
**Decoding:**
|
|
91
|
+
```python
|
|
92
|
+
from scapy.all import rdpcap, TCP
|
|
93
|
+
|
|
94
|
+
pkts = rdpcap('capture.pcap')
|
|
95
|
+
suspicious = [p for p in pkts if TCP in p and p[TCP].dport == 5748]
|
|
96
|
+
|
|
97
|
+
# Map 6-bit flag value to base64 alphabet
|
|
98
|
+
b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
|
99
|
+
encoded = ''.join(b64[p[TCP].flags & 0x3F] for p in suspicious)
|
|
100
|
+
|
|
101
|
+
import base64
|
|
102
|
+
flag = base64.b64decode(encoded).decode()
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Key insight:** TCP has 6 standard flag bits (FIN, SYN, RST, PSH, ACK, URG) = values 0-63, matching the base64 alphabet exactly. Unusual flag combinations on otherwise normal-looking packets indicate covert channel usage. Filter by destination port or source IP to isolate the channel.
|
|
106
|
+
|
|
107
|
+
**Detection:** Packets with nonsensical flag combinations (e.g., FIN+SYN simultaneously). Consistent destination port. Packet count is a multiple of 4 (base64 alignment).
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## DNS Query Name Last-Byte Steganography (UTCTF 2026)
|
|
112
|
+
|
|
113
|
+
**Pattern (Last Byte Standing):** PCAP with DNS queries where data is encoded in the last byte of each query name.
|
|
114
|
+
|
|
115
|
+
**Identification:** Many DNS queries to unusual or sequential subdomains. The meaningful data is NOT in the query name itself but in the final byte/character of each name.
|
|
116
|
+
|
|
117
|
+
**Decoding workflow:**
|
|
118
|
+
```python
|
|
119
|
+
from scapy.all import rdpcap, DNS, DNSQR
|
|
120
|
+
|
|
121
|
+
packets = rdpcap('last-byte-standing.pcap')
|
|
122
|
+
|
|
123
|
+
data = []
|
|
124
|
+
for pkt in packets:
|
|
125
|
+
if pkt.haslayer(DNSQR):
|
|
126
|
+
qname = pkt[DNSQR].qname.decode(errors='replace').rstrip('.')
|
|
127
|
+
if qname:
|
|
128
|
+
data.append(qname[-1]) # Last character of query name
|
|
129
|
+
|
|
130
|
+
# Reconstruct message from last bytes
|
|
131
|
+
message = ''.join(data)
|
|
132
|
+
print(message)
|
|
133
|
+
# May need additional decoding (hex, base64, etc.)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Variants:**
|
|
137
|
+
- Last byte of each subdomain label (split on `.`)
|
|
138
|
+
- Specific character position (first, Nth, last)
|
|
139
|
+
- Hex-encoded bytes across multiple queries
|
|
140
|
+
- Subdomain labels as base32/base64 chunks (DNS tunneling)
|
|
141
|
+
- **Trailing byte after DNS question structure** (see below)
|
|
142
|
+
|
|
143
|
+
**Key insight:** DNS exfiltration often hides data in query names. When queries look random but follow a pattern, extract specific character positions. The "last byte" pattern is simple but effective — each query contributes one byte to the message.
|
|
144
|
+
|
|
145
|
+
**Detection:** Large number of DNS queries to a single domain, queries with no legitimate purpose, sequential or patterned subdomain names.
|
|
146
|
+
|
|
147
|
+
### DNS Trailing Byte Binary Encoding (UTCTF 2026)
|
|
148
|
+
|
|
149
|
+
**Pattern (Last Byte Standing variant):** Each DNS query packet contains a single extra byte appended AFTER the standard DNS question structure (after the null terminator + Type A + Class IN fields). The extra byte is `0x30` ('0') or `0x31` ('1'), encoding one bit per packet.
|
|
150
|
+
|
|
151
|
+
**Decoding workflow:**
|
|
152
|
+
```python
|
|
153
|
+
from scapy.all import rdpcap, DNS, DNSQR, Raw
|
|
154
|
+
|
|
155
|
+
packets = rdpcap('challenge.pcap')
|
|
156
|
+
|
|
157
|
+
bits = []
|
|
158
|
+
for pkt in packets:
|
|
159
|
+
if pkt.haslayer(DNSQR):
|
|
160
|
+
# Get raw DNS payload
|
|
161
|
+
raw = bytes(pkt[DNS])
|
|
162
|
+
# Standard DNS question ends at: header(12) + qname + null(1) + type(2) + class(2)
|
|
163
|
+
qname = pkt[DNSQR].qname
|
|
164
|
+
expected_len = 12 + len(qname) + 1 + 2 + 2 # +1 for leading length byte
|
|
165
|
+
if len(raw) > expected_len:
|
|
166
|
+
trailing = raw[expected_len:]
|
|
167
|
+
for b in trailing:
|
|
168
|
+
bits.append(chr(b)) # '0' or '1'
|
|
169
|
+
|
|
170
|
+
# Convert bit string to ASCII (MSB-first, 8-bit chunks)
|
|
171
|
+
bitstring = ''.join(bits)
|
|
172
|
+
flag = ''.join(chr(int(bitstring[i:i+8], 2)) for i in range(0, len(bitstring) - 7, 8))
|
|
173
|
+
print(flag)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Key insight:** Data is hidden not in the DNS query name but in extra bytes padding the packet after the question record. Wireshark hex inspection reveals non-standard packet lengths. Each trailing byte represents ASCII '0' or '1', forming a binary stream that decodes to the flag.
|
|
177
|
+
|
|
178
|
+
**Detection:** DNS packets slightly larger than expected for their query name. Hex dump shows `0x30`/`0x31` bytes after the Class IN field (`00 01`). Consistent query domain across all packets.
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Multi-Layer PCAP with XOR + ZIP (UTCTF 2026)
|
|
183
|
+
|
|
184
|
+
**Pattern (Half Awake):** PCAP with multiple protocol layers hiding data. Requires protocol-aware extraction, XOR decryption with a key found in-band, and merging parallel data streams.
|
|
185
|
+
|
|
186
|
+
**Detailed workflow:**
|
|
187
|
+
|
|
188
|
+
1. **Inspect HTTP streams** for instructions or hints (e.g., "mDNS names are hints", "Not every TCP blob is what it pretends to be")
|
|
189
|
+
2. **Identify fake protocol streams:** A TCP stream labeled as TLS may actually contain a raw ZIP file (PK magic bytes `50 4b`). Check raw hex of suspicious streams
|
|
190
|
+
3. **Extract XOR key from mDNS:** Look for mDNS TXT records (e.g., `key.version.local`) containing the XOR key
|
|
191
|
+
4. **XOR-decrypt** the extracted data using the mDNS key
|
|
192
|
+
5. **Merge parallel datasets** using printability as selector
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
import string
|
|
196
|
+
from scapy.all import rdpcap, Raw, DNS, DNSRR
|
|
197
|
+
|
|
198
|
+
packets = rdpcap('half-awake.pcap')
|
|
199
|
+
|
|
200
|
+
# 1. Extract XOR key from mDNS TXT record
|
|
201
|
+
xor_key = None
|
|
202
|
+
for pkt in packets:
|
|
203
|
+
if pkt.haslayer(DNSRR):
|
|
204
|
+
rr = pkt[DNSRR]
|
|
205
|
+
if b'key' in rr.rrname.lower():
|
|
206
|
+
xor_key = int(rr.rdata, 16) # e.g., 0xb7
|
|
207
|
+
|
|
208
|
+
# 2. Extract fake TLS stream (look for PK header in raw TCP data)
|
|
209
|
+
# Use Wireshark: tcp.stream eq N → Export raw bytes
|
|
210
|
+
# Or extract with scapy by filtering the right stream
|
|
211
|
+
|
|
212
|
+
# 3. XOR-decrypt two datasets from ZIP contents
|
|
213
|
+
def xor_decrypt(data, key):
|
|
214
|
+
return bytes(b ^ key for b in data)
|
|
215
|
+
|
|
216
|
+
p1 = xor_decrypt(stage1_data, xor_key)
|
|
217
|
+
p2 = xor_decrypt(stage2_data, xor_key)
|
|
218
|
+
|
|
219
|
+
# 4. Merge using printability: take the printable character from each position
|
|
220
|
+
flag = ''.join(
|
|
221
|
+
chr(p1[i]) if chr(p1[i]) in string.printable and chr(p1[i]).isprintable()
|
|
222
|
+
else chr(p2[i])
|
|
223
|
+
for i in range(len(p1))
|
|
224
|
+
)
|
|
225
|
+
print(flag)
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
**Key insight:** When a PCAP contains two XOR-decoded byte arrays of equal length where neither alone produces readable text, merge them character-by-character using printability as the selector — take whichever byte at each position is a printable ASCII character. The XOR key is often hidden in an in-band protocol like mDNS TXT records rather than requiring brute-force.
|
|
229
|
+
|
|
230
|
+
**Indicators:**
|
|
231
|
+
- HTTP stream with meta-instructions ("not every TCP blob is what it pretends to be")
|
|
232
|
+
- TCP stream with mismatched protocol dissection (Wireshark shows TLS but raw bytes contain PK/ZIP headers)
|
|
233
|
+
- mDNS queries for suspicious service names (e.g., `key.version.local`)
|
|
234
|
+
- Two data files of identical length in extracted archive
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## Brotli Decompression Bomb Seam Analysis (BearCatCTF 2026)
|
|
239
|
+
|
|
240
|
+
**Pattern (Cursed Map):** HTTP download of a file that decompresses to gigabytes (decompression bomb). The flag is sandwiched between two bomb halves at a seam in the compressed data.
|
|
241
|
+
|
|
242
|
+
**Identification:** Compressed data shows a repeating block pattern (e.g., 105-byte period). One block breaks the pattern — the flag is at this discontinuity.
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
import brotli
|
|
246
|
+
|
|
247
|
+
with open('flag.txt.br', 'rb') as f:
|
|
248
|
+
data = f.read()
|
|
249
|
+
|
|
250
|
+
# Find the repeating block size
|
|
251
|
+
block_size = 105 # Determined by comparing adjacent blocks
|
|
252
|
+
for i in range(0, len(data) - block_size, block_size):
|
|
253
|
+
if data[i:i+block_size] != data[i+block_size:i+2*block_size]:
|
|
254
|
+
seam_offset = i + block_size
|
|
255
|
+
break
|
|
256
|
+
|
|
257
|
+
# Decompress only the anomalous block
|
|
258
|
+
dec = brotli.Decompressor()
|
|
259
|
+
result = dec.process(data[seam_offset:seam_offset+block_size])
|
|
260
|
+
# Flag is in the decompressed output
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
**Key insight:** Decompression bombs use highly repetitive compressed data. The flag breaks this repetition, creating a detectable anomaly in the compressed stream. Compare adjacent fixed-size blocks to find the discontinuity, then decompress only that region — no need to decompress the entire multi-gigabyte output.
|
|
264
|
+
|
|
265
|
+
**Detection:** File with extreme compression ratio (MB → GB), HTTP Content-Encoding: br, or file identified as Brotli. Tools hang or OOM when trying to decompress.
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## SMB RID Recycling via LSARPC (Midnight 2026)
|
|
270
|
+
|
|
271
|
+
**Pattern (UntilTime):** PCAP with SMB2 authentication followed by RPC calls over `\pipe\lsarpc`. The attacker enumerates Active Directory accounts by iterating RIDs (Relative Identifiers) through LSARPC functions.
|
|
272
|
+
|
|
273
|
+
**Identification:** SMB2 session setup with multiple authentication attempts (null session, Guest, random username), followed by RPC bind to LSARPC and repeated `LsaLookupSids` calls with incrementing RIDs.
|
|
274
|
+
|
|
275
|
+
**Wireshark analysis:**
|
|
276
|
+
```bash
|
|
277
|
+
# Filter SMB2 authentication attempts from attacker IP
|
|
278
|
+
tshark -r capture.pcapng -Y "ip.src == 198.51.100.16 && smb2.cmd == 1"
|
|
279
|
+
|
|
280
|
+
# Look for LSARPC RPC calls
|
|
281
|
+
tshark -r capture.pcapng -Y "dcerpc.cn_bind_to_str contains lsarpc"
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
**RPC call sequence:**
|
|
285
|
+
1. `LsaOpenPolicy` — opens a policy handle on the target
|
|
286
|
+
2. `LsaQueryInformationPolicy` — extracts the domain SID (e.g., `S-1-5-21-...`)
|
|
287
|
+
3. `LsaLookupSids` — resolves SIDs to account names by iterating RIDs (1000, 1001, 1002, ...)
|
|
288
|
+
|
|
289
|
+
**Key insight:** Guest account authentication (often enabled by default) grants enough access to enumerate domain accounts via LSARPC. The attacker constructs SIDs by appending incrementing RIDs to the domain SID and calling `LsaLookupSids` for each. Valid accounts return their name; invalid RIDs return errors. This technique is called **RID cycling** or **RID brute-forcing**.
|
|
290
|
+
|
|
291
|
+
**Detection indicators:**
|
|
292
|
+
- Multiple `LsaLookupSids` requests with sequential RIDs
|
|
293
|
+
- Guest authentication success followed by RPC pipe connection
|
|
294
|
+
- High volume of LSARPC traffic from a single source
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## Timeroasting / MS-SNTP Hash Extraction (Midnight 2026)
|
|
299
|
+
|
|
300
|
+
**Pattern (UntilTime):** After enumerating valid machine account RIDs via RID recycling, the attacker sends NTP requests with those RIDs to extract HMAC-MD5 authentication material from the domain controller's MS-SNTP responses.
|
|
301
|
+
|
|
302
|
+
**Background:** Microsoft's MS-SNTP extends standard NTP with Netlogon authentication in Active Directory environments. The client places a domain RID in the NTP `Key Identifier` field (4 bytes, little-endian). The domain controller responds with an HMAC-MD5 signature derived from the machine account's NTLM hash — leaking crackable authentication material.
|
|
303
|
+
|
|
304
|
+
**Wireshark extraction:**
|
|
305
|
+
```bash
|
|
306
|
+
# Filter NTP traffic from attacker
|
|
307
|
+
tshark -r capture.pcapng -Y "ntp && ip.src == 10.16.13.13" -T fields -e udp.payload
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
**Convert Key Identifier to RID:**
|
|
311
|
+
```bash
|
|
312
|
+
# NTP Key Identifier is 4 bytes, little-endian
|
|
313
|
+
echo "<key_id_hex>" | sed 's/\(..\)/\1 /g' | awk '{print "0x"$4$3$2$1}' | xargs printf "%d\n"
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
**NTP response payload structure (68 bytes):**
|
|
317
|
+
|
|
318
|
+
| Offset | Length | Field |
|
|
319
|
+
|--------|--------|-------|
|
|
320
|
+
| 0-47 | 48 | Salt (NTP header + extensions) |
|
|
321
|
+
| 48-51 | 4 | Key Identifier (RID, little-endian) |
|
|
322
|
+
| 52-67 | 16 | HMAC-MD5 crypto-checksum |
|
|
323
|
+
|
|
324
|
+
**Hash reconstruction for Hashcat (mode 31300):**
|
|
325
|
+
```python
|
|
326
|
+
import sys
|
|
327
|
+
from struct import unpack
|
|
328
|
+
|
|
329
|
+
def to_hashcat_form(hex_payload):
|
|
330
|
+
data = bytes.fromhex(hex_payload.strip())
|
|
331
|
+
salt = data[:48]
|
|
332
|
+
rid = unpack('<I', data[-20:-16])[0]
|
|
333
|
+
md5hash = data[-16:]
|
|
334
|
+
return f"{rid}:$sntp-ms${md5hash.hex()}${salt.hex()}"
|
|
335
|
+
|
|
336
|
+
if len(sys.argv) != 2:
|
|
337
|
+
print("Usage: python sntp_to_hashcat.py <hex_payload>")
|
|
338
|
+
sys.exit(1)
|
|
339
|
+
|
|
340
|
+
print(to_hashcat_form(sys.argv[1]))
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
**Cracking with Hashcat:**
|
|
344
|
+
```bash
|
|
345
|
+
# Mode 31300 = MS-SNTP (Timeroasting)
|
|
346
|
+
hashcat -m 31300 -a 0 -O hashes.txt rockyou.txt --username
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
**Example hash format:**
|
|
350
|
+
```text
|
|
351
|
+
1108:$sntp-ms$d7d0422d66705c6189c1d20aed76baa4$1c0111e900000000000a09314c4f434ced4c979d652b89f1e1b8428bffbfcd0aed4ca3bbb1338716ed4ca3bbb133cf3a
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
**Key insight:** MS-SNTP responses from domain controllers leak HMAC-MD5 authentication material tied to machine account NTLM hashes. Unlike Kerberoasting (which targets service accounts), Timeroasting targets **machine accounts** whose passwords are often weak or predictable (e.g., lowercase hostname). Any valid RID triggers a response — no special privileges required beyond network access to the DC's NTP service (UDP 123).
|
|
355
|
+
|
|
356
|
+
**Full attack chain:**
|
|
357
|
+
1. Authenticate to SMB as Guest
|
|
358
|
+
2. Enumerate valid RIDs via LSARPC RID recycling
|
|
359
|
+
3. Send MS-SNTP requests with discovered RIDs
|
|
360
|
+
4. Extract HMAC-MD5 hashes from NTP responses
|
|
361
|
+
5. Crack offline with Hashcat mode 31300
|
|
362
|
+
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
## ICMP Payload Steganography with Byte Rotation (HackIM 2016)
|
|
366
|
+
|
|
367
|
+
Data hidden in ICMP echo request/reply payloads with byte-level rotation encoding:
|
|
368
|
+
|
|
369
|
+
```python
|
|
370
|
+
from scapy.all import rdpcap, ICMP
|
|
371
|
+
|
|
372
|
+
packets = rdpcap('challenge.pcap')
|
|
373
|
+
icmp_data = b''
|
|
374
|
+
for pkt in packets:
|
|
375
|
+
if pkt.haslayer(ICMP) and pkt[ICMP].type == 8: # Echo request
|
|
376
|
+
icmp_data += bytes(pkt[ICMP].payload)
|
|
377
|
+
|
|
378
|
+
# Apply byte rotation (Caesar cipher on bytes)
|
|
379
|
+
SHIFT = 42
|
|
380
|
+
decoded = bytes((b - SHIFT) % 256 for b in icmp_data)
|
|
381
|
+
|
|
382
|
+
# Result may be base64-encoded
|
|
383
|
+
import base64
|
|
384
|
+
plaintext = base64.b64decode(decoded)
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
**Key insight:** ICMP payloads are often ignored by analysts focused on TCP/UDP. Check for non-standard payload sizes or non-zero data in ICMP packets. Common encoding layers: byte rotation -> base64 -> shell commands.
|
|
388
|
+
|
|
389
|
+
---
|
|
390
|
+
|
|
391
|
+
## Packet Reconstruction via Checksum Validation (Break In 2016)
|
|
392
|
+
|
|
393
|
+
Reconstruct corrupted/incomplete packets by using protocol checksums as validation:
|
|
394
|
+
|
|
395
|
+
1. **Identify missing bytes** from packet structure analysis (Ethernet, IP, TCP headers)
|
|
396
|
+
2. **Brute-force missing values** and validate against:
|
|
397
|
+
- IP header checksum (16-bit ones' complement)
|
|
398
|
+
- TCP checksum (includes pseudo-header)
|
|
399
|
+
3. **Extract data** from reconstructed payload
|
|
400
|
+
|
|
401
|
+
```python
|
|
402
|
+
import struct
|
|
403
|
+
|
|
404
|
+
def ip_checksum(header_bytes):
|
|
405
|
+
"""Compute IP header checksum"""
|
|
406
|
+
words = struct.unpack('!' + 'H' * (len(header_bytes) // 2), header_bytes)
|
|
407
|
+
s = sum(words)
|
|
408
|
+
while s >> 16:
|
|
409
|
+
s = (s & 0xFFFF) + (s >> 16)
|
|
410
|
+
return ~s & 0xFFFF
|
|
411
|
+
|
|
412
|
+
# Brute-force missing byte to match expected checksum
|
|
413
|
+
for candidate in range(256):
|
|
414
|
+
header = header_template[:missing_offset] + bytes([candidate]) + header_template[missing_offset+1:]
|
|
415
|
+
if ip_checksum(header) == 0: # Valid checksum sums to 0
|
|
416
|
+
print(f"Missing byte: 0x{candidate:02x}")
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
**Key insight:** Protocol checksums constrain missing data. For single missing bytes, brute-force is instant. For multiple missing bytes, use TCP sequence numbers and MAC/IP header structure to reduce the search space.
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
## dnscat2 Traffic Reassembly from DNS PCAP (BSidesSF 2017)
|
|
424
|
+
|
|
425
|
+
**Pattern (dnscap):** Extract data tunneled via dnscat2 from a DNS pcap. Decode base32 subdomain labels from DNS queries, strip the 9-byte dnscat2 protocol header from each chunk, deduplicate retransmitted packets by comparing consecutive queries, then reassemble the payload (e.g., PNG image).
|
|
426
|
+
|
|
427
|
+
```python
|
|
428
|
+
from scapy.all import rdpcap, DNSQR
|
|
429
|
+
|
|
430
|
+
packets = rdpcap('capture.pcap')
|
|
431
|
+
domain = '.skullseclabs.org.'
|
|
432
|
+
prev = None
|
|
433
|
+
data = b''
|
|
434
|
+
|
|
435
|
+
for p in packets:
|
|
436
|
+
if not p.haslayer(DNSQR):
|
|
437
|
+
continue
|
|
438
|
+
qname = p[DNSQR].qname.decode()
|
|
439
|
+
if domain not in qname:
|
|
440
|
+
continue
|
|
441
|
+
# Strip domain, join hex-encoded labels
|
|
442
|
+
labels = qname.replace(domain, '').split('.')
|
|
443
|
+
chunk = bytes.fromhex(''.join(labels))
|
|
444
|
+
chunk = chunk[9:] # strip 9-byte dnscat2 header
|
|
445
|
+
if chunk == prev:
|
|
446
|
+
continue # skip retransmission
|
|
447
|
+
prev = chunk
|
|
448
|
+
data += chunk
|
|
449
|
+
|
|
450
|
+
with open('extracted.png', 'wb') as f:
|
|
451
|
+
f.write(data)
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
**Key insight:** dnscat2 encodes data in DNS query subdomain labels (hex or base32). Each query carries a 9-byte header (session ID, sequence, acknowledgment). Retransmissions are common — deduplicate by comparing consecutive payloads. The reassembled stream may contain files (PNG, documents) identifiable by magic bytes.
|
|
455
|
+
|
|
456
|
+
---
|
|
457
|
+
|
|
458
|
+
## Unreferenced PDF Objects with Hidden Pages (SharifCTF 7 2016)
|
|
459
|
+
|
|
460
|
+
**Pattern (Strange PDF):** A PDF contains objects not referenced by the page tree. To reveal hidden content: (1) examine raw PDF objects with `qpdf --show-xref` or a text editor, (2) identify unreferenced content stream objects, (3) modify the `/Kids` array in the Pages object to include hidden page references, (4) increment the `/Count` value, (5) re-render the PDF to display previously hidden pages containing flag data.
|
|
461
|
+
|
|
462
|
+
```bash
|
|
463
|
+
# List all objects in the PDF
|
|
464
|
+
qpdf --show-xref suspicious.pdf
|
|
465
|
+
|
|
466
|
+
# Find pages object and hidden content objects
|
|
467
|
+
strings suspicious.pdf | grep -E '/Type /Page|/Contents|/Kids'
|
|
468
|
+
|
|
469
|
+
# Manual fix: edit PDF to add hidden page references
|
|
470
|
+
# Change: /Kids [1 0 R] -> /Kids [1 0 R 5 0 R]
|
|
471
|
+
# Change: /Count 1 -> /Count 2
|
|
472
|
+
# Rewrite xref table or use qpdf --linearize to fix offsets
|
|
473
|
+
qpdf --linearize modified.pdf fixed.pdf
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
**Key insight:** PDF viewers only render pages reachable from the `/Pages` tree root. Unreferenced objects are invisible but still present in the file. Check object cross-references: any content stream object not in `/Kids` may contain hidden data. `mutool clean -d` and `qpdf --show-object N` help inspect individual objects.
|
|
477
|
+
|
|
478
|
+
---
|
|
479
|
+
|
|
480
|
+
## RDP Session Decryption via Extracted PKCS12 Key (HITB 2017)
|
|
481
|
+
|
|
482
|
+
PCAP contains a PKCS12 (.p12/.pfx) file transmitted over UDP. Extract the private key from the PKCS12 container, then load it into Wireshark to decrypt the RDP session and recover transmitted data.
|
|
483
|
+
|
|
484
|
+
```bash
|
|
485
|
+
# Extract private key from PKCS12 (no cert, no passphrase protection)
|
|
486
|
+
openssl pkcs12 -in cert.p12 -out key.pem -nocerts -nodes
|
|
487
|
+
|
|
488
|
+
# In Wireshark: Edit > Preferences > Protocols > TLS > RSA keys list
|
|
489
|
+
# Add entry: IP=<rdp_server_ip>, Port=3389, Protocol=tpkt, Key file=key.pem
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
**Key insight:** PKCS12 files in network captures provide the private key needed to decrypt encrypted RDP sessions in Wireshark. Look for .p12/.pfx file transfers (often in UDP or FTP streams) before the RDP session begins.
|
|
493
|
+
|
|
494
|
+
---
|
|
495
|
+
|
|
496
|
+
## RADIUS Shared Secret Cracking (UConn CyberSEED 2017)
|
|
497
|
+
|
|
498
|
+
Extract the RADIUS authenticator hash from a PCAP using `radius2john.pl`, crack the shared secret with john, then enter the cracked secret in Wireshark to decrypt obfuscated password fields.
|
|
499
|
+
|
|
500
|
+
```bash
|
|
501
|
+
# Extract hash for john
|
|
502
|
+
perl radius2john.pl capture.pcap > radius_hash.txt
|
|
503
|
+
john radius_hash.txt --wordlist=rockyou.txt
|
|
504
|
+
|
|
505
|
+
# Wireshark: Edit > Preferences > Protocols > RADIUS > Shared Secret = <cracked_secret>
|
|
506
|
+
# RADIUS Access-Request packets will now show decrypted User-Password fields
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
`radius2john.pl` is part of the JohnTheRipper jumbo package (`src/radius2john.pl`).
|
|
510
|
+
|
|
511
|
+
**Key insight:** RADIUS uses MD5(shared_secret + authenticator + password) for password obfuscation — cracking the shared secret via john exposes all credentials in the capture. The shared secret is typically a short dictionary word.
|
|
512
|
+
|
|
513
|
+
---
|
|
514
|
+
|
|
515
|
+
## RC4 Stream Identification in Shellcode PCAP (CODE BLUE 2017)
|
|
516
|
+
|
|
517
|
+
A backdoor sends 32 bytes of `/dev/urandom` as an RC4 key, then encrypts all subsequent traffic. Identify RC4 by the characteristic 256-byte KSA (Key Scheduling Algorithm) table initialization pattern visible in the shellcode. Extract the key from the first 32 bytes of the TCP stream and decrypt the remainder.
|
|
518
|
+
|
|
519
|
+
```python
|
|
520
|
+
from scapy.all import rdpcap, TCP
|
|
521
|
+
|
|
522
|
+
packets = rdpcap('capture.pcap')
|
|
523
|
+
stream = b''
|
|
524
|
+
for pkt in packets:
|
|
525
|
+
if TCP in pkt and pkt[TCP].payload:
|
|
526
|
+
stream += bytes(pkt[TCP].payload)
|
|
527
|
+
|
|
528
|
+
# First 32 bytes = RC4 key (from /dev/urandom)
|
|
529
|
+
key = stream[:32]
|
|
530
|
+
ciphertext = stream[32:]
|
|
531
|
+
|
|
532
|
+
# RC4 decryption
|
|
533
|
+
def rc4(key, data):
|
|
534
|
+
S = list(range(256))
|
|
535
|
+
j = 0
|
|
536
|
+
for i in range(256):
|
|
537
|
+
j = (j + S[i] + key[i % len(key)]) % 256
|
|
538
|
+
S[i], S[j] = S[j], S[i]
|
|
539
|
+
i = j = 0
|
|
540
|
+
out = []
|
|
541
|
+
for byte in data:
|
|
542
|
+
i = (i + 1) % 256
|
|
543
|
+
j = (j + S[i]) % 256
|
|
544
|
+
S[i], S[j] = S[j], S[i]
|
|
545
|
+
out.append(byte ^ S[(S[i] + S[j]) % 256])
|
|
546
|
+
return bytes(out)
|
|
547
|
+
|
|
548
|
+
plaintext = rc4(key, ciphertext)
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
**Key insight:** RC4 in shellcode is identifiable by the 256-byte permutation table initialization loop (KSA). The key is typically the first N bytes transmitted over the connection before encrypted data begins. Look for a fixed-length initial burst followed by encrypted traffic.
|
|
552
|
+
|
|
553
|
+
---
|
|
554
|
+
|
|
555
|
+
See also: [network.md](network.md) for basic network forensics techniques (tcpdump, TLS/SSL decryption, Wireshark, port scanning, SMB3 decryption, credential extraction, 5G protocols).
|
|
556
|
+
|
|
557
|
+
---
|
|
558
|
+
|
|
559
|
+
## ICMP Ping Time-Delay Covert Channel (DefCamp 2018)
|
|
560
|
+
|
|
561
|
+
**Pattern:** An attacker exfiltrates data inside ICMP echo replies by modulating the server's response time. Latency under 200 ms encodes "ignore" (frame), 200–1000 ms encodes binary `0`, and >1000 ms encodes binary `1`. Reconstruct the data by pairing each request with its reply (matching `icmp.ident`/`icmp.seq`) and converting the time delta to bits.
|
|
562
|
+
|
|
563
|
+
```python
|
|
564
|
+
from scapy.all import rdpcap, ICMP
|
|
565
|
+
pkts = rdpcap("broken_tv.pcap")
|
|
566
|
+
pairs = {}
|
|
567
|
+
for p in pkts:
|
|
568
|
+
if ICMP in p and p[ICMP].type == 8: # echo request
|
|
569
|
+
pairs[p[ICMP].seq] = p.time
|
|
570
|
+
bits = []
|
|
571
|
+
for p in pkts:
|
|
572
|
+
if ICMP in p and p[ICMP].type == 0: # echo reply
|
|
573
|
+
dt = p.time - pairs[p[ICMP].seq]
|
|
574
|
+
if dt < 0.2: # <200 ms: filler
|
|
575
|
+
continue
|
|
576
|
+
bits.append("1" if dt > 1.0 else "0")
|
|
577
|
+
data = int("".join(bits), 2).to_bytes(len(bits)//8, "big")
|
|
578
|
+
print(data)
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
**Key insight:** ICMP timing covert channels split a continuous latency distribution into discrete bins. The two thresholds matter more than the exact values: any bimodal "fast vs slow" distribution flanked by a "filler" region lets the receiver self-clock. Detect this channel by plotting the histogram of `reply_time - request_time` for all ICMP pairs — legit traffic forms a single Gaussian, covert traffic shows clear modes.
|
|
582
|
+
|
|
583
|
+
**References:** DefCamp CTF Qualification 2018 — Broken TV, writeup 11415
|