It's Christmas morning. Everyone is gathered around the tree, eager to open presents. We go around a couple times until we get to the big boxes. It's time to open mine!

I take off the wrapping paper slowly to reveal what's inside: a "Propel Ultra-X + WiFi" – a drone! I thank my grandmother for the gift and make a silly remark about how I always wanted one but they were always too expensive to justify it.

Upon closer inspection, it seems this model is able to be controlled via an app on iOS or Android devices – neat! Frustratingly, I open the horribly packaged drone and play around with it to get used to the controls. I attach the included phone holder to the controller and download the app. I notice that there are several other apps made by the developer and they all seem to be the same thing, but with slightly different graphics... odd, but I'm having fun so I dismiss it.

Through the app, you can view a live video feed from the drone as well as control it – typical drone stuff. I wasn't satisfied with this, though. I needed to discover how it worked.

Sniffing the Packets

When I got home from my grandparents' house, my first inclination was to download the wonderful Packet Capture app (which was removed from the Play Store for some reason?), launch the drone app on my phone and sniff the packets. Surely, this will reveal the communication between the drone and app.

Well, sort of...

Packets captured by the Packet Capture app.

The packets above were over TCP port 7060. This is the video stream. Similar packets were captured over TCP port 8060. However, neither of these ports were used to control the drone!

Using lewei_cmd as a starting point, I found this blog post and discovered that the actual drone commands were sent via UDP packets to port 50000 on the drone. Apparently, the app I was using couldn't capture UDP packets, only TCP. So I searched for another way...

I looked for another app that was capable of capturing UDP packets from other apps on the system – some claimed to be able to, but they were broken and didn't work with my device for some reason (probably because they were made when Android Gingerbread was still very much a thing).

Ok, so capturing packets on-device is out of the question here... let's try doing it externally. I fired up Wireshark on my MacBook, connected to the same network that the phone was connected to and... nothing!

Well, let's try using monitor mode. It worked, sort of! It yielded just a few UDP packets between the 2 devices, but not consistently enough to tinker with them, so I needed to find yet another way.

After fiddling with my WiFi Pineapple, trying out different things like network bridging, monitor mode on the Pineapple itself, and many more things, I felt defeated. There was one more thing I hadn't tried: my iPhone. One would think Android would be better to capture packets on – especially a rooted device like I have! However, it turns out there is a tool on macOS called rvictl that lets you create a virtual interface on your Mac that simply mirrors network traffic that goes through your iPhone.

And sure enough, creating that virtual interface and capturing packets on Wireshark showed the UDP packets I was looking for this entire time. Eureka! I wrote a quick filter to only capture the packets between the phone and drone and got to work. It took around 3 hours to get to this point.

UDP packets being sent from my iPhone to the drone.

The Messages

I learned a few things from inspecting these UDP messages:

  • They are always 8 bytes long.
  • They always start with 66 and end in 99.
  • They are sent constantly in rapid succession while the app is open.

The next step was to change some things in the app to see how the messages themselves changed. So I started taking some notes as I fiddled with knobs and buttons in the app (you can read the notes here – the information there is probably incorrect). Here is a summary of what I found:

  • 1st byte – Header: 66
  • 2nd byte – Left/right movement (0-254, with 128 being neutral)
  • 3rd byte – Forward/backward movement (0-254, with 128 being neutral)
  • 4th byte – Throttle (elevation) (0-254, with 128 being neutral)
  • 5th byte – Turning movement (0-254, with 128 being neutral)
  • 6th byte – Reserved for commands (0 = no command)
  • 7th byte – Checksum (XOR of bytes 2, 3, 4, and 5)
  • 8th byte – Footer: 99

These bytes (represented in hex) are used to represent certain commands:

  • 01 – Auto Take-Off/Land
  • 80 – Calibrate Gyro
  • 40 – Unlock Motor

To send a command, the app sets the 6th and 7th byte to the corresponding command number for approximately 1 second.

For example, if you wanted to tell the drone to take off, you would send these messages for around 1 second:

66 80 80 80 80 01 01 99

Or if you wanted the drone to move right:

66 a8 80 80 80 00 40 99

Ok, but why?

You can do cool stuff with a programmable drone!

I have created a sample library for this drone with the source code available here. It should work for any Chinese white-labeled drone this is based off of.

There's also autodrone – a piece of software that lets you script these drones so you can make predictable programs. Here's an example:

What's next?

Some ideas:

  • Web-based interface for controlling the drone
  • Twitch Flies Drones
  • Tracking and following objects with OpenCV
  • ??? (your idea here)

If you have one of these drones, have fun!

If you don't, I still hope you enjoyed reading this post.