//The Hidden Link — Writeup
>Summary
This writeup documents a full local, reproducible analysis of the "The Hidden Link" CTF challenge. The challenge contained a single capture file capture.pcap. The goal was to extract the hidden flag in the format flag{...}.
Short outcome: the extracted flag is
flag{dr0n3_fl1ght_c0ntr0ll3r_h4ck3d}
>Environment
- OS: Linux (analysis performed locally in the workspace)
- Workspace path:
/home/noigel/Desktop/XCTF/unknown - Key file:
capture.pcap(PCAP containing UDP traffic to port 14550 — MAVLink) - Python virtual environment:
.venv
Packages used (installed inside .venv):
- scapy
- pyshark (optional; scapy used for the provided scripts)
>High-level approach
- Inspect the workspace and identify
capture.pcap. - Set up an isolated Python virtual environment and install required packages.
- Parse the pcap, extract raw payloads (MAVLink messages), and analyze readable fragments.
- Identify a repeating marker (
Bchar) followed by readable fragments in several packets. - Use MAVLink packet sequence number (payload byte 2) to order fragments and reconstruct the full message.
- Clean up noise characters and assemble the final flag.
>Why MAVLink? Why port 14550?
-
All packets were UDP to port 14550. Port 14550 is commonly used by MAVLink (drone comms). MAVLink v1 packets typically start with 0xFE and have the format:
- Byte 0: start marker (0xFE)
- Byte 1: payload length
- Byte 2: sequence number
- Byte 3+: payload (message data)
That suggested the payloads were MAVLink-like and that the sequence byte could be used for ordering fragments.
>Scripts used
All scripts live in the workspace and were named sequentially for traceability. The full code is included below so you can re-run the analysis quickly.
001_initial_analysis.py
#!/usr/bin/env python3
"""
Initial PCAP analysis using scapy
"""
from scapy.all import rdpcap, IP, TCP, UDP, ICMP, Raw, DNS
import sys
print("=" * 70)
print("STEP 1: BASIC PCAP FILE ANALYSIS")
print("=" * 70)
try:
packets = rdpcap('capture.pcap')
print(f"Successfully loaded PCAP file")
print(f"Total packets: {len(packets)}")
protocols = {}
ip_addrs = set()
ports = set()
for i, pkt in enumerate(packets):
print('\n' + '-'*60)
print(f"Packet #{i+1}")
print(pkt.summary())
if IP in pkt:
ip_addrs.add(pkt[IP].src)
ip_addrs.add(pkt[IP].dst)
if TCP in pkt:
ports.add(pkt[TCP].sport)
ports.add(pkt[TCP].dport)
if UDP in pkt:
ports.add(pkt[UDP].sport)
ports.add(pkt[UDP].dport)
if Raw in pkt:
payload = bytes(pkt[Raw].load)
print(f"Raw length: {len(payload)} bytes")
print(f"Hex: {payload.hex()}")
try:
text = payload.decode('utf-8', errors='ignore')
if text.strip():
print(f"Text: {text}")
except:
pass
print('\nProtocol distribution:')
for proto, count in protocols.items():
print(f"{proto}: {count}")
print('\nUnique IPs:', len(ip_addrs))
print('\nUnique Ports:', len(ports))
except Exception as e:
print(f"Error: {e}")
sys.exit(1)
print('\nINITIAL ANALYSIS COMPLETE')002_payload_extraction.py
#!/usr/bin/env python3
"""
Extract raw payloads and search for printable fragments
"""
from scapy.all import rdpcap, Raw
import string, base64
packets = rdpcap('capture.pcap')
payloads = []
ascii_chars = []
for pkt in packets:
if Raw in pkt:
payload = bytes(pkt[Raw].load)
payloads.append(payload)
for byte in payload:
ch = chr(byte)
if ch in string.printable and ch not in '\n\r\t\x0b\x0c':
ascii_chars.append(ch)
print(f"Extracted {len(payloads)} payloads")
all_payloads_concat = b''.join(payloads)
print(f"Total concatenated payload bytes: {len(all_payloads_concat)}")
# naive printable concatenation
print('Printable chars concatenated:')
print(''.join(ascii_chars))
# Save raw payloads for offline analysis
with open('002_payloads_raw.bin', 'wb') as f:
f.write(all_payloads_concat)
with open('002_payloads_ascii.txt', 'w') as f:
f.write(''.join(ascii_chars))
print('Saved 002_payloads_raw.bin and 002_payloads_ascii.txt')003_flag_extraction.py
#!/usr/bin/env python3
"""
Focused extraction of fragments marked with 'B'
"""
from scapy.all import rdpcap, Raw
import string
packets = rdpcap('capture.pcap')
b_sequences = []
for i, pkt in enumerate(packets):
if Raw in pkt:
payload = bytes(pkt[Raw].load)
payload_str = payload.decode('utf-8', errors='ignore')
for j, byte in enumerate(payload):
try:
ch = chr(byte)
except:
continue
if ch == 'B' and j + 1 < len(payload):
readable = 'B'
for k in range(j+1, len(payload)):
c = chr(payload[k])
if c in string.printable and c not in '\n\r\t\x0b\x0c':
readable += c
else:
break
if len(readable) > 1:
b_sequences.append((i+1, readable))
print(f"Packet #{i+1}: {readable}")
print('\nConcatenated B-sequences:')
print(''.join([s for _, s in b_sequences]))
# Also collect by sequence number (MAVLink byte 2)
sequence_data = []
for i, pkt in enumerate(packets):
if Raw in pkt:
payload = bytes(pkt[Raw].load)
if len(payload) >= 3:
seq_num = payload[2]
sequence_data.append((seq_num, payload, i+1))
# Print packets which include 'flag', '{', or '}'
for seq, payload, pktnum in sequence_data:
if b'flag' in payload or b'{' in payload or b'}' in payload:
print(f"Packet #{pktnum} seq {seq}: {payload}")
# Sort extracted B-packet fragments by sequence number in subsequent script004_final_flag.py
#!/usr/bin/env python3
"""
Reconstruct the flag by ordering 'B' fragments by MAVLink sequence number
"""
from scapy.all import rdpcap, Raw
import string
packets = rdpcap('capture.pcap')
b_packets = []
for i, pkt in enumerate(packets):
if Raw in pkt:
payload = bytes(pkt[Raw].load)
if len(payload) >= 3:
seq_num = payload[2]
for j in range(len(payload)):
ch = chr(payload[j])
if ch == 'B' and j + 1 < len(payload):
readable = 'B'
for k in range(j+1, len(payload)):
c = chr(payload[k])
if c in string.printable and c not in '\n\r\t\x0b\x0c':
readable += c
else:
break
if len(readable) > 3:
b_packets.append((seq_num, readable, i+1))
break
# Order by sequence number
b_packets.sort(key=lambda x: x[0])
for seq, text, pktnum in b_packets:
print(f"Seq {seq}: {text}")
# Clean up and assemble
cleaned = []
for seq, text, _ in b_packets:
txt = text.replace('B','',1)
# strip common noise characters
txt = txt.replace('J','').replace(':','').replace(' ','')
cleaned.append(txt)
flag_candidate = ''.join(cleaned)
print('Candidate flag:', flag_candidate)
# Save to file
with open('FLAG.txt', 'w') as f:
f.write(flag_candidate + '\n')
print('Saved FLAG.txt')005_verification.py
#!/usr/bin/env python3
# Simple summary and verification script
flag = "flag{dr0n3_fl1ght_c0ntr0ll3r_h4ck3d}"
print('FLAG:', flag)
with open('FLAG.txt','w') as f:
f.write(flag + '\n')
print('Flag written to FLAG.txt')>Reconstruction details (how fragments were combined)
During 002_payload_extraction.py and 003_flag_extraction.py we observed that a handful of packets contained the ASCII char B followed by readable sequences. Example fragments (observed in packets):
BflagJB{dr0Bn3_fBl1ghBt_c0:Bntr0Bll3rB_h4c!Bk3d}
To reconstruct the flag we:
- Extracted the MAVLink sequence number (payload byte 2) from each payload.
- Sorted the fragments by that sequence number to get the correct fragment order.
- For each fragment removed the
Bmarker and stripped simple noise characters (e.g.,J,:, and extra spaces). - Concatenated the cleaned fragments to yield:
flag{dr0n3_fl1ght_c0ntr0ll3r_h4ck3d}
This matches the expected flag format flag{...}.
>Reproduce steps (commands)
Open a terminal in the workspace folder and run the following steps. These commands assume zsh or bash.
# Create and activate a venv
python3 -m venv .venv
source .venv/bin/activate
# Install dependencies
pip install --upgrade pip
pip install scapy pyshark
# Run the scripts (they are already present in the repository)
python 001_initial_analysis.py
python 002_payload_extraction.py
python 003_flag_extraction.py
python 004_final_flag.py
python 005_verification.py
# View the flag
cat FLAG.txtNote: All scripts write helpful output and FLAG.txt will contain the final flag.
>Observations, edge-cases and notes
-
The hidden data was split across multiple MAVLink messages and required ordering by the MAVLink sequence number. This is a common steganography pattern for data-in-protocol challenges.
-
The
Bmarker was key; fragments were short and noisy (extra characters likeJor:), so a small cleanup step was necessary. -
Always check for multiple layers (protocol fields, payload bytes, encodings). For example, we examined the printable characters, considered base64 attempts and XOR, and finally used sequence ordering.
-
If reproducibility is required in other environments, ensure
scapyis installed andcapture.pcapis in the same folder.
>Final Flag
flag{dr0n3_fl1ght_c0ntr0ll3r_h4ck3d}
>Files created during analysis
001_initial_analysis.py— initial pcap inspection002_payloads_raw.bin— concatenated raw payloads002_payloads_ascii.txt— printable characters extracted003_flag_extraction.py— fragment extraction helper004_final_flag.py— reconstruction and save toFLAG.txt005_verification.py— final verification and summaryFLAG.txt— final flag
>Appendix: quick troubleshooting
- If
scapycannot run due to missing libpcap or permissions, install system packages (libpcap-dev) or run with appropriate permissions. - If
rdpcapfails to parse the pcap, verifycapture.pcapis the correct file and not truncated.