Reverse Engineering Bluetooth Vapes
Post
Cancel

# Reverse Engineering Bluetooth Vapes

For quite a while, I’ve had a Pax 3, a Bluetooth LE-connected vape. It’s a great device, but the iOS app leaves a lot to be desired. Finally, I had enough and decided to set out to figure out how their communication protocol works; with the end goal of implementing my own app to control it.

To be fair, many of these issues likely stem from the fact that Pax hasn’t been able to release an update to their iOS app after Apple banned vaping-related apps quite a while back. Folks who previously downloaded it can continue to use it, and download it on new devices, but it’s no longer listed on the App Store. Despite my complaints about the app (and to be fair, I do like to complain about most things,) I want to be clear that I think Pax Labs made something wonderful that still works as great as the first day that I got it.

Their Android app has received quite a few updates since then, so I am sure it works better, but I don’t have any Android devices, and buying one (and probably getting more annoyed at Android than the buggy iOS app) was out of the question. So of course, as any sane person does, I decided to look into reverse-engineering the app.

Update: After (rather predictably) far too much time, I finally wrote the iOS app that sparked this entire reverse engineering effort. Check out this follow-up post for more information.

# Protocol Reversing

As it turns out, reversing the protocol was significantly easier than I expected, despite a few notable (or rather, annoying) quirks of the protocol implementation and a few roadblocks in actually getting an app to analyze. I relied entirely on static analysis (and a lot of more or less educated guesses…) to reverse the protocol and quite a few little test programs to validate my guesses as I went.

First, I tried to get the iOS app. There seemed to be no way to get at it through my Mac, nor backups. There was a solution using Apple Configurator that worked for other, still listed apps, but since the Pax app had become unlisted, that also was no longer an option. Lastly, there are ways to get the app off the device itself… if the device is jailbroken. Sadly I had no idea where any of my spare iDevices that I could go and jailbreak had gone, so I turned to the Android app as a possible alternative – after all, it’s talking to the same piece of hardware.

## Bluetooth Services

At this point, it occurred to me that since these are Bluetooth LE devices, I could just fire up whatever Bluetooth scanner app I found first online and take a peek at what services and characteristics the device exposed. I didn’t know very much about how BLE works, but a few minutes of reading up on the relationship between centrals and peripherals, as well as services, characteristics, notifications, and so on was all that I needed.

We’ve got the standard system information service (which provides the manufacturer/model string, serial number, etc.) and is of course standardized and documented extensively. But there’s also an unknown service, identified by a full 128-bit UUID (8E320200-64D2-11E6-BDF4-0800200C9A66;) I’ll refer to it as the “Pax Service” from now on. Since there were no other services, I guessed that this was probably how the device itself was controlled. This gave me something to search for in the app binaries since they would also have to store that UUID to discover the service.

The Pax service itself exposes three characteristics: one that’s read-only, one that’s write-only, and lastly, one that is read-notify. Again, I guessed that the read/write characteristics were used to exchange packets between the device and host and that the notify characteristic was used by the device to signal that it had packets available to read from its read endpoint.

## Pax Mobile for Android

Android was a whole new world for me; about the only thing I knew about it was that I couldn’t stand using it a few years ago when I tried an Android phone, that I horribly failed trying to get some Android stuff working in a class once, and that there was a lot of Java – I wrote a lot of Java years ago, and used it in a few classes, but it has been a while since I’ve even touched a Java compiler – so this was certainly going to be a fun little adventure.

It wasn’t as bad as I expected, but it certainly wasn’t high up on my list of “fun” activities. I’m sure a lot of what I did below is overly complicated, or just outright wrong, so I wouldn’t rely on it as a guide of any sort. I had a few friends and coworkers provide me some suggestions and tips along the way, so I suggest finding someone who knows way more about this topic to bother with stupid questions 🙃

Thankfully, getting downloads of Android apps turns out to be significantly easier, with plenty of websites that let you directly download the app’s apk file; which is also just a ZIP file by another name. I’m examining version 4.0.0 of the app; no particular reason, it was where I landed when I scrolled in the list of available versions.

Inside the extracted app binary are some .dex files which actually contain the bytecode of the application. This is roughly translatable back to Java class files by tools like dex2jar; those, in turn, can be run through your favorite Java decompiler for a somewhat tolerable experience1.

With the jar file loaded into a Java decompiler, at first glance, it looked like most of the class names were obfuscated; however, most of the package names were at least partially intact. I figured this would make reversing things more difficult, but thankfully, it didn’t end up being as much of a hindrance as I thought since most relevant code was still in the same class, even if it was just a random single character name.

I started by searching for the UUIDs advertised by the device and very quickly confirmed my theory on how the Pax service worked. While the decompiled code isn’t exactly easy to read, it beats raw Java bytecode by a long shot:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package com.pax.peace.a.b; import e.f.b.j; import java.util.UUID; public enum f { INTERNAL_LOG_SERVICE_NOTIFICATION, INTERNAL_SERVICE_NOTIFICATION, LOG_SERVICE_NOTIFICATION, PAX_SERVICE_NOTIFICATION; private final UUID dataReadyCharacteristicUUID; private final UUID readCharacteristicUUID; private final g serviceType; static { UUID uUID1 = UUID.fromString("8E320203-64D2-11E6-BDF4-0800200C9A66"); UUID uUID2 = UUID.fromString("8E320201-64D2-11E6-BDF4-0800200C9A66"); f f1 = new f("PAX_SERVICE_NOTIFICATION", 0, g1, uUID1, uUID2); PAX_SERVICE_NOTIFICATION = f1; // ... } f(g paramg, UUID paramUUID1, UUID paramUUID2) { this.serviceType = paramg; this.dataReadyCharacteristicUUID = paramUUID1; this.readCharacteristicUUID = paramUUID2; } } 

The 8E320203 UUID corresponds to the read-notify characteristic, whereas the 8E320201 UUID corresponds to the read-only characteristic; and with the property names still intact, it does indeed confirm that the notify characteristic indicates data is available to read from the device.

At this point, I had a pretty good idea of how data was exchanged between the device and the app via this service and its characteristics. Since the Pax contains some Nordic Semi part, and BLE is relatively low bandwidth, I figured that the actual protocol is relatively simple. In most cases, this means some common packet header indicating the type of data that follows; essentially just taking some packed structs, reading them out of memory, and sending them over the air.2

### Packet Format

The com.pax.peace package ended up being where I focused my search on, as that was where those first string constants for the characteristic UUIDs were found. With the assumption that the packets had some sort of type field, I searched for constants that define those types. I was rewarded pretty quickly with a file that mapped string constants to integer values; all of these were less than 0xFF so I again assumed that the type was a single byte, likely at the very start of the messages exchanged with the device:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package com.pax.peace.g; import com.pax.peace.a.b.h; import com.pax.peace.a.b.i; import e.f.b.g; public enum y implements i { TEMPERATURE, HEATER_SET_POINT, BATTERY, USAGE; // ... private final int value; static { y y1 = new y("TEMPERATURE", 0, 1); TEMPERATURE = y1; y y2 = new y("HEATER_SET_POINT", 1, 2); HEATER_SET_POINT = y2; y y3 = new y("BATTERY", 2, 3); BATTERY = y3; y y4 = new y("USAGE", 3, 4); USAGE = y4; // ... } y(int paramInt1) { this.value = paramInt1; } } 

I started writing some code at this point to try to communicate with one of my devices. Reading out system info was no problem, and I was even able to register for notifications on the Pax service and read out data… but it seemed to be random garbage. The first byte rarely matched up with one of the message type IDs I discovered. Random data meant there was likely some form of encryption at play. Yaaaaaay…

### Key Derivation

Searching through the decompiled code for any references to key or AES brought up a few hits in the com.pax.peace package, including a few debug strings that alluded to certain single letter fields originally being encryption keys of some kind, as well as some references to an authentication protocol3 – and plenty of functions the decompiler couldn’t deal with, so I figured I was reasonably close to the right place.

I assumed that there’d be some shared key that’s used to transform some device data (I suspected the mysterious 8-byte “system ID” device info characteristic) into the key that’s then used to encrypt the data packets.

After staring at obfuscated Java code for long enough, it became clear that the key derivation algorithm was pretty simple:

1. Read the device serial number, which is 8 characters. Concatenate it to itself to yield a 16 character string.
2. Encode the string as UTF-8 to get 16 bytes. Serial numbers just contain alphanumerics so I don’t think it really matters, but this is what the app does.
3. Encrypt these bytes with the shared key4, using AES-128 in ECB mode.

That key is then stored away to encrypt/decrypt packets exchanged with the device. I’m not sure what the point of this is, since the device’s serial is easily readable through the device info service; if it was to waste a few hours of my time, it certainly worked.

### Data Encryption

While I now had the key that the packets were encrypted with, it still left the question of what algorithm. Trying AES-128 ECB, like used in the key derivation, didn’t read any intelligible data; this meant that there was some IV that was computed or sent with the message. So back into the obfuscated Java code I went in hopes of figuring that one out.

This didn’t really yield anything useful, likely because I have no idea what I’m doing. What I did have, however, was the correctly derived key, and messages read from a device via a little test program; as well as the assumption that the packet was encrypted with that derived key, using AES-128, in some mode that requires an IV. Packets I read from the device were always 32 bytes, which happens to be twice the AES-128 block size, so I also assumed that the IV would either be prepended or appended to the actual data.

Because I had nothing better to do, I just wrote a little Python tool that tried every combination of getting the IV from the start/end of the packet and AES modes supported by the pyCrypto module. Pretty quickly I had this figured out, too: packets are encrypted with AES-128 in OFB mode, using the last 16 bytes of the packet as the IV.

All the above described encryption stuff only applies to the read (8E320201-64D2-11E6-BDF4-0800200C9A66) and write (8E320202-64D2-11E6-BDF4-0800200C9A66) characteristics, which actually exchange useful data. There is however still the third notification characteristic (8E320203-64D2-11E6-BDF4-0800200C9A66) that I haven’t paid much attention to yet. The way this one works is that any time the device has a packet ready to send – usually because some value changed, but it can also be in response to packets sent by the host – a notification is sent to the host.

The value of this notification doesn’t matter, and seems to be discarded by the app; from my testing, however, it’s always the first byte of the packet that’ll be available from the read characteristic. Triggering a read of it in response to the notification seems to be sufficient to make things work.

Notifications work in conjunction with the STATUS_REQUEST message type; its payload is a 64-bit bitfield, with a bit set for each property to read out. When the device receives this message, it’ll generate notifications for all of these properties so that the host can read them out.

# Proof of Concept

Now I knew enough about the protocol to build a little something something to communicate with the device. While there’s Python libraries to do BLE stuff, I opted instead to write a little test app in Swift, mostly since it’s been a little bit since I’ve thrown together a Mac app. Additionally, Apple’s frameworks are, for the most part, pretty damn good – Core Bluetooth was no exception. Just a few lines of code are all that’s necessary to connect to, send, and receive data with the device. Any updates and notifications are delivered through delegate methods.

All of the Pax protocol stuff is hidden away in a base PaxDevice class. There are then device-specific classes that support particular properties unique to the device. For example, this is a simplified example of how the Pax 3 specific device class could be used to configure the device: