A Stray Drone Writing in the Sky with Its Flight Path | Connect the Dots | STARPWN 2026
🌐 中文版:繁體中文
In the same Prismantir drone-swarm world, this is the challenge with the shortest story but the most memorable payoff.
150 million bytes of raw telemetry, and at the very end you open an image to find a drone writing letters in the sky with its own flight path.
What the challenge is about
While known for their big eye in the sky, Prismantir huge drone fleet is also a behemoth to keep in control. Every night, each drone patrols its assigned block, following its route with clockwork precision. But tonight, one unit broke the formation. Can you trace where did it go?
In plain terms: Prismantir’s huge drone fleet patrols its assigned city blocks every night, following routes with clockwork precision. But tonight one unit broke formation, and the task is to find where it went. You get a single file: PRISM_S03_B10-30_20260830.raw, about 149.7 MB.
First, confirm the file format
Looking at the header with xxd:
1 | 00000000: fd1d 0000 7a01 01fd 0000 0641 7264 7543 ....z......ArduC |
Two things jump out immediately: the plaintext string ArduCopter V4.6.1, which is the ArduPilot drone firmware’s version string.
And every record starts with 0xFD, the MAVLink v2 frame start marker.
Manually peeling the first packet by the MAVLink v2 header format matches perfectly: payload length, sysid=1, compid=1, msgid=253 (STATUSTEXT), content "ArduCopter V4.6.1 (57591cb8)".
So this file is a raw MAVLink v2 stream with no extra wrapping. Unlike a typical .tlog, there’s no per-packet timestamp outer layer here, just MAVLink packets written one right after another.
pymavlink is too slow, so hand-roll a parser again
First I tried pymavlink:
1 | from pymavlink import mavutil |
It technically works, but parsing a 150 MB file this way, with pure Python walking the full ardupilotmega dialect lookup table, is slow enough to make you question your life choices.
Since MAVLink v2’s wrapping is itself simple (each field is at a fixed position, and the payload length is in the second byte), I just wrote a minimal parser:
1 | import struct |
Because this capture is a clean, uninterrupted stream, walking it linearly needs no resync at all, and all 4,158,449 messages parse in about 3 seconds, more than an order of magnitude faster than the general library. The rule of thumb here: if the standard library becomes the bottleneck and the protocol format is simple, it’s worth hand-rolling a dedicated parser.
Find the fleet
Tallying sysid (MAVLink’s marker for which vehicle sent a message) reveals 10 different drones (sysid 1–10).
Each sends roughly the same proportion of message types (HEARTBEAT, GPS_RAW_INT, GLOBAL_POSITION_INT, ATTITUDE, STATUSTEXT), perfectly matching the “a huge fleet patrolling simultaneously” scenario.
The STATUSTEXT contents are all just standard ArduPilot auto-mission logs (disarm, takeoff, fly to waypoint…), and no drone’s text log hides anything unique, so the information must be in the coordinates.
Plot each drone’s coordinates
Parse GLOBAL_POSITION_INT (msgid 33) to get each drone’s coordinates:
1 | if msgid == 33 and len(payload) >= 28: |
(MAVLink stores lat/lon as int32 scaled by 1e7, so divide back.)
The coordinates fall around latitude 36.11–36.16, longitude -115.19 to -115.32, which is Las Vegas on the map, and makes sense since this CTF is tied to DEF CON.
Overlay all 10 drones’ tracks (longitude, latitude; one colour per drone) on a single plot and the answer jumps out:

Nine drones’ tracks are tight, boxy street-grid patterns, each confined to its assigned rectangular block, matching the challenge’s “patrol your block precisely every night.” Only the red one (sysid 4) completely ignores block boundaries, sweeping across others’ turf in big loops, its shape clearly different from the other 9. That’s the one that broke formation.
It should be obvious by now: sysid 4 spells out BVLOS:

But those four letters aren’t the flag themselves.
Flag
BVLOS is a very common acronym in drone regulation: Beyond Visual Line Of Sight, meaning flying a drone beyond the operator’s unaided visual range. It fits the scenario well:
a drone that flew off to where no one could see it. Assembling the flag per the given example format (each word capitalized, underscore-separated):
starpwn{Beyond_Visual_Line_Of_Sight}
Takeaways
- Judge a file’s format by more than its extension. An undocumented
.rawfile can be identified and worked on just from the leading0xFDmagic byte and the plaintext firmware version string. - When a general library hits a wall on large data, hand-rolling the protocol parse is often well worth it. MAVLink v2’s header is fixed-length and self-describing (the payload length is in the second byte), so a linear walk with
struct.unpack_fromis more than an order of magnitude faster than the full dialect library. sysidis the most natural way to split multiple vehicles mixed into one stream. When a capture might contain more than one vehicle, tallysysidfirst.- The challenge name “Connect the dots” is literal. Spelling text with a flight path is a real-world amateur stunt (like Strava art), and taking the name as a “how to solve” hint rather than just flavour points straight at the solution: plot the ordered coordinate points and read them with your eyes.





