// HackTheBox  ·  writeup

Drone Forensics

HackTheBox Hard Pythonpymavlink
DFIR

Scenario

This challenge focuses on reconstructing a UAV pre-arm and mission-control timeline from a single MAVLink telemetry log. We are given only mav.tlog, and our objective is to determine how the autopilot transitioned from boot to mission preparation, why early arming attempts failed, when the motors finally armed, and which autopilot modes appeared in the observable telemetry stream.

Executive Summary

This case is a benign flight reconstruction rather than an intrusion. The single mav.tlog capture shows an ArduPilot copter booting, receiving a flight plan, and failing its first arming attempts because it had no position estimate. Once the EKF took GPS as its source and set its origin, the next arm command succeeded in GUIDED mode. Roughly six minutes later the autopilot moved through LOITER and then RTL. Every answer comes from decoding MAVLink messages (HEARTBEAT, STATUSTEXT, COMMAND_ACK, GPS_RAW_INT) and reading their timestamps.

Artifacts

Timestamp note: Convert the tlog epoch timestamps as UTC (datetime.utcfromtimestamp). Every timestamp question wants YYYY-MM-DD hh:mm:ss, so truncate fractional seconds.

Pre-Arm and Arming

Q1 — At what UTC timestamp did the drone motors successfully arm?

Walk the log for COMMAND_LONG command 400 (MAV_CMD_COMPONENT_ARM_DISARM) and its matching COMMAND_ACK. Most acknowledgements carry a failure result; the one with result 0 is the accepted arm, and it lines up with the autopilot’s Arming motors STATUSTEXT. Convert that message’s timestamp to UTC.

for m in mlog:
    if m.get_type()=='COMMAND_ACK' and m.command==400: print(utc(m._timestamp), m.result)
Click flag to reveal 2026-03-06 11:22:01
Q2 — What flight mode was active at the moment of arming?

The current mode is not a message of its own; it lives in the custom_mode field of the HEARTBEAT. Take the last heartbeat before the Q1 timestamp and map custom_mode through the ArduCopter mode table (4 is GUIDED).

for m in mlog:
    if m.get_type()=='HEARTBEAT' and m._timestamp<=arm_ts: mode=m.custom_mode
print(mode)

Tip: The format hint is six characters. Use the ArduCopter name, not the number.

Click to reveal answer GUIDED
Q3 — How many arming attempts failed before the motors successfully armed?

Failures show up in two places: COMMAND_ACK for command 400 with a non-zero result, and PreArm text messages from the autopilot. Two arm commands were rejected with result 4 (Arm: Need Position Estimate), and one further PreArm: Need Position Estimate message was logged. All three count as failed attempts.

for m in mlog:
    t=m.get_type()
    if t=='COMMAND_ACK' and m.command==400 and m.result!=0: print(utc(m._timestamp), m.result)
    if t=='STATUSTEXT' and 'Arm' in m.text: print(utc(m._timestamp), m.text)

Gotcha: Counting only the rejected arm commands gives 2. The autopilot’s own PreArm message is a failed attempt too.

Click flag to reveal 3
Q4 — How many seconds elapsed from the first telemetry message to the successful arm?

The first message in the file is at 11:21:04. From Q1 the arm is at 11:22:01, so subtract.

print(arm_ts - first_ts)
Click to reveal answer 57
Q13 — What GPS fix_type was the receiver reporting at the moment the motors armed?

Take the last GPS_RAW_INT message received before the Q1 timestamp and read its fix_type. The earlier failures were position-estimate problems, so this is the state that finally let the arm through.

for m in mlog:
    if m.get_type()=='GPS_RAW_INT' and m._timestamp<=arm_ts: fix=m.fix_type
print(fix)
Click flag to reveal 6

Mission Preparation

Q6 — At what UTC timestamp was the flight plan received?

Filter STATUSTEXT for the Flight plan received message and convert its timestamp.

for m in mlog:
    if m.get_type()=='STATUSTEXT' and 'Flight plan' in m.text: print(utc(m._timestamp), m.text)
Click to reveal answer 2026-03-06 11:21:24
Q7 — How many seconds elapsed from flight-plan receipt to the successful arm?

From Q6 the plan arrived at 11:21:24; from Q1 the arm is at 11:22:01. The difference is the answer.

Click flag to reveal 37
Q8 — At what UTC timestamp did "EKF3 IMU0 is using GPS" first appear?

Search STATUSTEXT for the EKF3 message and take the first hit. The EKF lines repeat per IMU, so match on the exact text.

for m in mlog:
    if m.get_type()=='STATUSTEXT' and 'EKF3 IMU0 is using GPS' in m.text: print(utc(m._timestamp)); break
Click to reveal answer 2026-03-06 11:21:55
Q9 — How many seconds elapsed from "EKF3 IMU0 origin set" to the successful arm?

Same search with the EKF3 IMU0 origin set text, which is logged at 11:21:30. Subtract from the Q1 arm time at 11:22:01. Note the origin is set before the “using GPS” message from Q8, not after it.

for m in mlog:
    if m.get_type()=='STATUSTEXT' and 'EKF3 IMU0 origin set' in m.text: print(utc(m._timestamp)); break
Click flag to reveal 31

Flight Modes

Q5 — How many distinct autopilot flight modes appear in the telemetry log?

Collect the unique custom_mode values across every HEARTBEAT from the autopilot. Five values appear (0, 3, 4, 5 and 6), which are STABILIZE, AUTO, GUIDED, LOITER and RTL in the ArduCopter table.

print(sorted({m.custom_mode for m in mlog if m.get_type()=='HEARTBEAT'}))
Click to reveal answer 5
Q10 — At what UTC timestamp did the autopilot first switch to LOITER?

LOITER is ArduCopter mode 5. Take the first HEARTBEAT whose custom_mode is 5 and convert its timestamp.

for m in mlog:
    if m.get_type()=='HEARTBEAT' and m.custom_mode==5: print(utc(m._timestamp)); break
Click flag to reveal 2026-03-06 11:27:35
Q11 — At what UTC timestamp did the autopilot first switch to RTL?

Same approach with mode 6, which is RTL.

for m in mlog:
    if m.get_type()=='HEARTBEAT' and m.custom_mode==6: print(utc(m._timestamp)); break
Click to reveal answer 2026-03-06 11:27:47
Q12 — How many seconds elapsed between the first LOITER HEARTBEAT and the first RTL HEARTBEAT?

From Q10 and Q11, the first LOITER heartbeat is at 11:27:35 and the first RTL heartbeat at 11:27:47. The gap is the difference of the two.

Click flag to reveal 12

Attack Summary

This is not an intrusion, so the table covers the flight sequence instead.

PhaseAction
BootFirst telemetry message recorded
Mission preparationFlight plan received
Pre-armArm commands rejected; autopilot reports it needs a position estimate
Pre-armEKF3 origin set, then IMU0 switches to GPS
ArmingArm command accepted in GUIDED mode with GPS fix_type 6
FlightAutopilot enters LOITER, then RTL twelve seconds later

Forensic Timeline

Timestamp (UTC)Event
2026-03-06 11:21:04First telemetry message in mav.tlog
2026-03-06 11:21:24Flight plan received
2026-03-06 11:21:30EKF3 IMU0 origin set
2026-03-06 11:21:55EKF3 IMU0 is using GPS
2026-03-06 11:22:01Motors armed in GUIDED mode
2026-03-06 11:27:35First LOITER heartbeat
2026-03-06 11:27:47First RTL heartbeat

IOCs

TypeValue
Filemav.tlog
ProtocolMAVLink (ArduPilot copter)

No malicious indicators were present in this case.

MITRE ATT&CK

TechniqueIDDescription
NoneN/ABenign telemetry reconstruction; no adversary behaviour to map

Defender Takeaways

Arming failures are visible in three message types. COMMAND_ACK, STATUSTEXT PreArm lines and HEARTBEAT state each tell part of the story, and counting from only one undercounts attempts. Reconstruct from all of them.

Take timestamps from the log, not the ground station. The tlog stamps every message on receipt, so one parser gives a consistent UTC clock across message types. Convert once, as UTC, and reuse it.

Mode changes need a mapping table. HEARTBEAT.custom_mode is a bare integer whose meaning depends on the vehicle type, so a copter log decoded with the wrong table names the wrong modes. Confirm the vehicle type first.

An unexpected LOITER to RTL sequence is a review trigger. Mode transitions after arming are the signal that matters for investigating a flight, whether they came from the operator, a failsafe or an external command.

Conclusion

The drone armed 57 seconds after the log began, only once the EKF had a GPS-backed position, and finished in LOITER then RTL. A single tlog was enough to rebuild the whole sequence, which is the point: MAVLink telemetry holds the evidence if you decode all of its message types.