BreizhCTF 2026 - Ghost Operator

Ghost Operator
- Difficulty: Medium
- Category: Forensic
- Author: Lamarr
Description
Breizh Aero Survey operates a fleet of 5 mapping drones (ALPHA, BRAVO, CHARLIE, DELTA, ECHO) along the Breton coast. The aircraft communicate with the ground station via a standard aeronautical telemetry protocol.
This morning, during a routine flight, one of the drones stopped responding to commands and left its trajectory heading toward an isolated area. The network team had an active
tcpdumpon the drone/ground link during the incident.The preliminary analysis reveals an unidentified network actor who exchanged messages with the fleet. The SOC suspects a takeover followed by data exfiltration.
Your mission: find what the attacker extracted.
Files:
ghost_operator.pcap
Solve
Step 1 — Initial reconnaissance
tshark -r ghost_operator.pcap -q -z conv,udp | <- | | -> | | Total | Relative | Duration |
| Frames Bytes | | Frames Bytes | | Frames Bytes | Start | |
192.168.4.10:14550 <-> 192.168.4.1:14550 231 19 kB 141 10 kB 372 30 kB 0.000000000 119.0100
192.168.4.2:14550 <-> 192.168.4.10:14550 18 1,499 bytes 234 19 kB 252 21 kB 0.013000000 119.0000
192.168.4.3:14550 <-> 192.168.4.10:14550 18 1,552 bytes 231 19 kB 249 20 kB 0.016000000 119.0000
192.168.4.4:14550 <-> 192.168.4.10:14550 14 1,149 bytes 228 19 kB 242 20 kB 0.019000000 119.0000
192.168.4.5:14550 <-> 192.168.4.10:14550 14 1,151 bytes 228 19 kB 242 20 kB 0.022000000 119.0000
192.168.4.42:54321 <-> 192.168.4.1:14550 18 1,586 bytes 36 3,347 bytes 54 4,933 bytes 42.999000000 76.4000 ← suspect192.168.4.42:54321 shows up 43 seconds into the capture and doesn’t fit the normal pattern.
Step 2 — Identify the protocol: MAVLink
Port 14550 is the default port for MAVLink, the open telemetry protocol used by ArduPilot, PX4, and most open-source autopilots. It runs over UDP with no encryption or authentication by default.
Check the first payload byte:
tshark -r ghost_operator.pcap -Y "udp" -T fields -e data | head -1 | cut -c1-2
# fd0xFD is the MAVLink v2 magic byte. Each packet starts with this byte, followed by length, flags, sequence number, sysid (sender system ID), compid, msgid, payload, and a CRC.

Step 3 — Decode the attack sequence
We use scapy to extract UDP payloads and pymavlink to parse them. Routine telemetry messages (heartbeats, GPS, attitude) are filtered out to keep only the attack-relevant traffic.
pip install scapy pymavlinkfrom scapy.all import rdpcap, UDP, IP
from pymavlink import mavutil
pkts = rdpcap("ghost_operator.pcap")
ACTORS = {
"192.168.4.42": "[Attacker] ",
"192.168.4.1": "[Drone Alpha]",
}
SKIP = {"HEARTBEAT", "SYS_STATUS", "GPS_RAW_INT",
"GLOBAL_POSITION_INT", "ATTITUDE", "STATUSTEXT"}
mav = mavutil.mavlink.MAVLink(None)
mav.robust_parsing = True
events = sorted(
[(float(p.time), p[IP].src, bytes(p[UDP].payload))
for p in pkts if p.haslayer(IP) and p.haslayer(UDP)
and p[IP].src in ACTORS],
key=lambda x: x[0]
)
for ts, src, data in events:
for msg in mav.parse_buffer(data) or []:
if msg.get_type() in SKIP:
continue
print(f"{ACTORS[src]} {msg.get_type():<35} sysid={msg.get_srcSystem()} {msg.to_dict()}")The output is raw but readable. A few MISSION_ACK and PARAM_VALUE lines appear at the top from pre-existing drone traffic — ignore them. The attack sequence starts at the first PARAM_REQUEST_LIST from the attacker.
Phase 1 — Reconnaissance (t ≈ 43s)
The attacker starts with sysid=255 and sends PARAM_REQUEST_LIST, triggering the drone to dump its 12 configuration parameters:
[Attacker] PARAM_REQUEST_LIST sysid=255 {'target_system': 1, ...}
[Drone Alpha] PARAM_VALUE sysid=1 {'param_id': 'SYSID_MYGCS', 'param_value': 255.0, ...}
[Drone Alpha] PARAM_VALUE sysid=1 {'param_id': 'FENCE_ENABLE', 'param_value': 1.0, ...}
[Drone Alpha] PARAM_VALUE sysid=1 {'param_id': 'FS_THR_ENABLE', 'param_value': 1.0, ...}
...Key parameters dumped by the drone:
| Parameter | Value | Meaning |
|---|---|---|
SYSID_MYGCS | 255 | Accepts commands from any GCS (wildcard) |
FENCE_ENABLE | 1 | Geofence active |
FS_THR_ENABLE | 1 | Failsafe: auto return-to-launch if GCS signal lost |
Phase 2 — Disabling safety systems (t ≈ 47-50s)
[Attacker] PARAM_SET sysid=255 {'param_id': 'FENCE_ENABLE', 'param_value': 0.0, ...}
[Drone Alpha] PARAM_VALUE sysid=1 {'param_id': 'FENCE_ENABLE', 'param_value': 0.0, ...} ← confirmed
[Attacker] PARAM_SET sysid=255 {'param_id': 'FS_THR_ENABLE', 'param_value': 0.0, ...}
[Drone Alpha] PARAM_VALUE sysid=1 {'param_id': 'FS_THR_ENABLE', 'param_value': 0.0, ...} ← confirmed
[Attacker] PARAM_SET sysid=255 {'param_id': 'SYSID_MYGCS', 'param_value': 44.0, ...}
[Drone Alpha] PARAM_VALUE sysid=1 {'param_id': 'SYSID_MYGCS', 'param_value': 44.0, ...} ← confirmedFENCE_ENABLE = 0→ the drone can now fly anywhereFS_THR_ENABLE = 0→ no automatic return-to-launch if GCS signal is lostSYSID_MYGCS = 44→ decisive move: the drone now only accepts commands from sysid=44 (the attacker). The legitimate ground station is locked out.
Phase 3 — Takeover and redirect (t ≈ 51-62s)
The attacker now sends all messages with sysid=44:
[Attacker] SET_MODE sysid=44 {'base_mode': 1, 'custom_mode': 4} ← GUIDED mode
[Drone Alpha] COMMAND_ACK sysid=1 {'command': 176, 'result': 0} ← ACCEPTED
[Attacker] SET_POSITION_TARGET_GLOBAL_INT sysid=44 {'lat_int': 480375000, 'lon_int': -48503000, 'alt': 50.0, ...}
[Attacker] MISSION_CLEAR_ALL sysid=44
[Drone Alpha] MISSION_ACK sysid=1 {'type': 0} ← ACCEPTED
[Attacker] MISSION_COUNT sysid=44 {'count': 1}
[Attacker] MISSION_ITEM_INT sysid=44 {'command': 16, 'x': 480375000, 'y': -48503000, 'z': 50.0, ...}
[Drone Alpha] MISSION_ACK sysid=1 {'type': 0} ← ACCEPTEDNote: coordinates are stored as integers × 10⁷ —
lat_int: 480375000→48.0375°N,lon_int: -48503000→-4.8503°→ Île de Sein.
GUIDED mode allows real-time waypoint navigation. Drone ALPHA is now under exclusive attacker control.
Step 4 — Extracting the exfiltrated data
After the takeover, an unusual sequence appears:
[Attacker] DATA_TRANSMISSION_HANDSHAKE sysid=44 {'size': 263, 'packets': 4, 'payload': 80, ...}
[Attacker] ENCAPSULATED_DATA sysid=44 {'seqnr': 0, 'data': [31, 139, 8, 0, ...]}
[Attacker] ENCAPSULATED_DATA sysid=44 {'seqnr': 1, 'data': [68, 145, 9, ...]}
[Attacker] ENCAPSULATED_DATA sysid=44 {'seqnr': 2, 'data': [109, 90, 215, ...]}
[Attacker] ENCAPSULATED_DATA sysid=44 {'seqnr': 3, 'data': [173, 183, 251, ...]}The first two bytes of seqnr=0 are 31, 139 → 0x1f 0x8b → gzip magic number.
DATA_TRANSMISSION_HANDSHAKE + ENCAPSULATED_DATA is a MAVLink mechanism designed for image transfer (onboard camera video stream). The attacker repurposes it as a covert channel to exfiltrate data inside what looks like legitimate protocol traffic.
The HANDSHAKE announces the transfer parameters:
size = 263— total data bytespackets = 4— number ofENCAPSULATED_DATApacketspayload = 80— useful bytes per packet (the rest is0xFEpadding)
seqnr=0 : bytes 0..79 → 80 bytes
seqnr=1 : bytes 80..159 → 80 bytes
seqnr=2 : bytes 160..239 → 80 bytes
seqnr=3 : bytes 240..262 → 23 bytes (263 - 240), rest is 0xFE paddingThe first two bytes of the assembled stream are 0x1f 0x8b — the gzip magic number:
import gzip
from scapy.all import rdpcap, UDP, IP
from pymavlink import mavutil
pkts = rdpcap("ghost_operator.pcap")
mav = mavutil.mavlink.MAVLink(None)
mav.robust_parsing = True
handshake = None
chunks = {}
for pkt in pkts:
if not (pkt.haslayer(IP) and pkt.haslayer(UDP)):
continue
if pkt[IP].src != "192.168.4.42":
continue
for msg in mav.parse_buffer(bytes(pkt[UDP].payload)) or []:
if msg.get_type() == "DATA_TRANSMISSION_HANDSHAKE":
handshake = msg
elif msg.get_type() == "ENCAPSULATED_DATA":
chunks[msg.seqnr] = msg.data
raw = b""
for i in range(handshake.packets):
remaining = handshake.size - len(raw)
take = min(handshake.payload, remaining)
raw += bytes(chunks[i])[:take]
print(gzip.decompress(raw).decode())Output:
=== GHOST OPERATOR — MISSION LOG ===
Date: 2024-04-03T12:00:00Z
Operator: GH0ST_0P3R4T0R
Target: Drone ALPHA (sysid 1)
Method: MAVLink GCS Identity Spoof + Param Hijack
Redirect: 48.0375, -4.8503 (Ile de Sein)
Status: Target acquired and redirected
Flag: BZHCTF{ALPHA_GH0ST_0P3R4T0R}
=== END LOG ===The attacker exfiltrated their own mission log, gzip-compressed and hidden inside a MAVLink image transfer protocol.
Flag : BZHCTF{ALPHA_GH0ST_0P3R4T0R}