We dived into a specific packet-type last time, but couldn’t extract each and every byte purpose. However, we were able to identify one important thingy: 0x00-0x01. Those two bytes indicate the packet’s content. Hence, now we are capable of assigning each sent packet (operator to drone) to a specific group, based on their unique identifier. This will hopefully create a better overview and will facilitate our future reverse-engineering process. In order to group packets, we came up with a .NET 5.0 console application, capable of reading .pcap files and extracting the most important bits.
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Tako.CRC;
public class NetworkPacket : IEqualityComparer<NetworkPacket>
{
public NetworkPacket(byte[] data) => Data = data;
public byte[] Data { get; init; }
// 14 bytes
public byte[] Ethernet => Data[0..14];
// 20 bytes
public byte[] IP => Data[14..34];
// 8 bytes
public byte[] UDP => Data[34..42];
public byte[] Payload => Data[42..];
public static string ToHexString(byte data) => ToHexString(new byte[] { data });
public static string ToHexString(byte[] data) => string.Join(" ", data.Select(d => $"0x{d.ToString("X2")}"));
public string Src => $"{(uint)Data[26]}.{(uint)Data[27]}.{(uint)Data[28]}.{(uint)Data[29]}";
public bool Equals(NetworkPacket x, NetworkPacket y) => x.Data.SequenceEqual(y.Data);
public int GetHashCode([DisallowNull] NetworkPacket obj) => HashCode.Combine(obj.Data);
}
public class DjiPacket
{
private const EnumCRCProvider _crc_16_provider = EnumCRCProvider.CRC16Kermit;
private static CRCManager _crc_manager = new CRCManager();
public DjiPacket(byte[] data) : this(new NetworkPacket(data)) { }
public DjiPacket(NetworkPacket networkPacket) => NetworkPacket = networkPacket;
public NetworkPacket NetworkPacket { get; init; }
public byte Id => NetworkPacket.Payload[0];
public byte Length => NetworkPacket.Payload[0];
public byte ProtocolVersion => NetworkPacket.Payload[1];
public byte[] Session => NetworkPacket.Payload[2..4];
public static byte[] CRC_16(byte[] data) => _crc_manager.CreateCRCProvider(_crc_16_provider).GetCRC(data).CrcArray;
}
Grouping packets based on their Id leads to following taxonomy:
Identifier | Payload length | Occurrence |
---|---|---|
0x1E | 30 | 177 |
0x21 | 33 | 205 |
0x22 | 34 | 104 |
0x23 | 35 | 13 |
0x24 | 36 | 4 |
0x25 | 37 | 27 |
0x26 | 38 | 6 |
0x28 | 40 | 10 |
0x29 | 41 | 4 |
0x2A | 42 | 20 |
0x2B | 43 | 9 |
0x2D | 45 | 4 |
0x2E | 46 | 1 |
0x2F | 47 | 2 |
0x30 | 48 | 4 |
0x32 | 50 | 6 |
0x38 | 56 | 685 |
0x39 | 57 | 1 |
0x3C | 60 | 1 |
0x3D | 61 | 2 |
0x3E | 62 | 4 |
0x4D | 77 | 1 |
0x51 | 81 | 1 |
0x79 | 121 | 6 |
To avoid yet another deep dive into the bits and bytes of each and every listed packet type, we try to provoke specific types of packets to be sent. E.g. moving the drone around its own axis should result in a “rotate” packet type request. As we don’t know which packet type relates to which input action, we require a statistic based approach. We setup two scenarios. 1.) Connect the drone for 20 seconds and provide zero input. 2.) Connect the drone and constantly move the drone in a circle for 20 seconds. Then we should see a higher occurance of the rotation packet type. In order to exclude unrelated packets, we apply following filter within wireshark:
ip.src == 192.168.12.139 && udp && !dns && !icmp && !mdns
Identifier | Payload length | Occurrence |
---|---|---|
0x1E | 30 | 986/351 |
0x21 | 33 | 54/31 |
0x22 | 34 | 92/40 |
0x25 | 37 | 30/20 |
0x2A | 42 | 38/1 |
0x2C | 44 | 2/1 |
0x2E | 46 | 6/4 |
0x32 | 50 | 17/17 |
0x36 | 54 | 5/1 |
0x38 | 56 | 360/1175 |
0x3A | 58 | 4/1 |
0x3E | 62 | 14/2 |
0x40 | 64 | 4/2 |
0x44 | 68 | 4/2 |
0x46 | 70 | 2/1 |
0x4C | 76 | 10/3 |
0x54 | 84 | 5/3 |
0x56 | 86 | 1/1 |
0x5E | 94 | 2/2 |
0x48 | 72 | 0/1 |
0x60 | 96 | 0/1 |
Packets only occurring on idle control mode haven’t been listed within the table above, as they are likely not relevant. Upon a brief comparison, the packet type 0x38 seems to be of our interest. Conveniently its size is unique across all packets, such that we can investigate within wireshark directly.
Address | Bytes | Findings |
---|---|---|
0x00 | 1 | Packet Identifier / Packet Length – All packets with an ethernet frame-length of 72 bytes start with the static byte sequence of: 0x38 |
0x01 | 1 | Protocol Version |
0x02 – 0x03 | 2 | Session Identifier – Value retrieved by Handshake at position 0x02 – 0x03 |
0x04 – 0x05 | 2 | Padding – Zero bits |
0x06 – 0x07 | 2 | ??? |
0x08 | 1 | Mirror – See 0x0A |
0x09 | 1 | Mirror – See 0x0B |
0x0A – 0x0B | 2 | Drone Variable – Will be updated by drone (See Fig. Drone update) 0xC0 0x85 0x6C 0x21 0x68 0x0B 0x02 0x69 0xINIT 0xINIT 0xNEW_VAL 0xNEW_VAL Initial value obtained after first Packet 0x30 0x08 = 0x0A 0x09 = 0x0B |
0x0C – 0x0D | 2 | Padding – Zero bits |
0x0E – 0x11 | 4 | ??? – Changes rarely Variable shared by packets 0x1e, 0x20, 0x22, 0x24, 0x26, 0x28, 0x2a, 0x2c, 0x30, 0x3a, 0x3c, 0x3e, 0x40, 0x42, 0x44, 0x46, 0x4a, 0x4c, 0x50, 0x52, 0x58 Initial value obtained after first Packet 0x30 0x0E = 0x08 0x0F = 0x09 0x10 = 0x08 0x11 = 0x09 |
0x12 – 0x13 | 2 | Padding – Zero bits |
0x14 – 0x15 | 2 | Dependent Variable – Will be updated after sending Packet 0x1e, 0x21 0x22, 0x24, 0x25, 0x29, 0x2a, 0x2b, 0x2e, 0x32 0x14 = 0x08 0x15 = 0x09 Initial value obtained after first Packet 0x30 Assumption, 0x14 and 0x15 are variables carried by many packets, including 0x38 |
0x16 – 0x17 | 2 | Dependent Variable – Will be updated after sending Packet 0x1e, 0x21, 0x22, 0x24, 0x25, 0x28, 0x29, 0x2b, 0x2e, 0x30, 0x32 0x16 = 0x0A 0x17 = 0x0B Initial value obtained after first Packet 0x24 Assumption: 0x16 and 0x17 are variables carried by many packets, including 0x38. |
0x18 – 0x23 | 12 | ???* – { 0x00, 0x00, 0x00, 0x00, 0x1A, 0x00, 0x55, 0x1A, 0x04, 0xB1, 0x02, 0x09 } |
0x24 – 0x25 | 2 | Exclusive Counter – Little Endian |
0x26 – 0x2D | 8 | ???* – { 0x40, 0x01, 0x02, 0x00, 0x00, 0x04, 0x20, 0x00 } |
0x3E – 0x3F | 2 | Rotation direction – Left: { 0x0D, 0x05 } Right: { 0xF3, 0x0A } Idle: { 0x01 0x08 } |
0x30 – 0x34 | 5 | Padding – Zero bits |
0x35 | 1 | Operator -> Drone: 0x06 Drone -> Operator: 0x01 |
0x36 – 0x37 | 2 | CRC-16 KERMIT – from 0x24 to incl. 0x35, Little Endian byte order |
* Might be: ACK/CMD/Type/Encryption/..?
The hard part of the reverse-engineering was by far the CRC checksum. The last two bytes did never equal, hence it had to be some kind of CRC. However, since the CRC’s poly-function, final XOR,… could be chosen arbitrary but fixed within the handshake, we had to bruteforce our way to the result. However, the bruteforce-crc didn’t return any useable values and thus, lead to the conclusion that the CRC might not cover the full payload. As the first 8 bytes yield the same content within each rotation-control packet, the CRC might not include the whole header of the payload. Therefore, we removed bit by bit as long as the CRC hasn’t been found. We concluded, that the bytes from 0x00 – 0x23 represent the header of the packet and isn’t refelcted within the payload’s CRC function.
The drone did only respond once to a rotation packet with the 0x35 byte set to 0x01. Hence, rotation packets do not require an acknowledgement from the drone. Assumption: the byte at position n – 2 might indicate whether the packet requires an acknowledgment. (n = payload-length)
Next up: DJI Protocol – Day 5 – Progress Report
[…] those new features we were able to extend our findings on the packet 0x38. As our findings are limited to our current knowledge, we do require to inspect other packets to […]
[…] already got some hints and clues within our last packet inspection about a possible drone-rotation command. However, the reverse-engineering process is quite intense […]
[…] Next up: DJI Protocol – Day 4 – Progress Report […]
[…] Altitude + Rotation […]