# 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 `tcpdump` on 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

```bash
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  ← suspect
```

`192.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:

```bash
tshark -r ghost_operator.pcap -Y "udp" -T fields -e data | head -1 | cut -c1-2
# fd
```

`0xFD` 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.

![MAVLink v2 packet structure](/breizhctf-2026/ghost-operator/packet_mavlink_v2.jpg)

### 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.

```bash
pip install scapy pymavlink
```

```python
from 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, ...} ← confirmed
```

- `FENCE_ENABLE = 0` → the drone can now fly anywhere
- `FS_THR_ENABLE = 0` → no automatic return-to-launch if GCS signal is lost
- `SYSID_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}                           ← ACCEPTED
```

> Note: 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 bytes
- `packets = 4` — number of `ENCAPSULATED_DATA` packets
- `payload = 80` — useful bytes per packet (the rest is `0xFE` padding)

```
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 padding
```

The first two bytes of the assembled stream are `0x1f 0x8b` — the **gzip magic number**:

```python
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}`**

