The background for this project is a lesson in avoiding dishonest vendors. Two years ago, I was looking to purchase a smart watch with sleep tracking capabilities1; I've always had difficulty sleeping and wanted a way of finally quantifying that difficulty. One of my requirements was the ability to pull data off of the watch without the use of proprietary software, so the only options I was seriously considering were those on Gadgetbridge's "supported devices" list. At the time, I was still in high school, and still awed by the affordability of consumer electronics on websites such as AliExpress (woefully unaware of the ethical implications of supporting a totalitarian state's economy). Moreover, I was somewhat capable of reading and writing 汉语, so the Xiaomi Mi Band 2 fit the bill. I took to Ebay to purchase one, finding a listing for 10.99 USD with free shipping. I ordered it, and things were okay. That is, until the package arrived.
What appeared outside my garage was not what I ordered. I gave the vendor the benefit of the doubt, thinking that it may have been a mistake, and explained that they had sent me the wrong product.
Hi [my Ebay username],
Thank you for your message. Sincerely sorry for your inconvenience.
Please kindly konw [sic] that they are the same kind product and all the functions are the same [sic]. In order to protect your interest, we suggest that we issue $5 USD refund without returning the item and you can keep this item and try to use it. If it is suit for you and please feel free to give us a positive feedback. If it is still not your favor, please kindly do NOT leave any feedback.
Please kindly let us know if you agree.
If you need further assistance or inquiry, please feel free to contact us.
I'm antipathetic toward anyone trying to slight me, so I threatened to file a complaint with Ebay.
Hi [my Ebay username],
We feel sorry to know that you have received your parcel but the watch you received is not the same as the listing in our store.
In order to protect your interest, we suggest that we issue a full refund without returning the item and you can keep this item and try to use it. If it suit for you and please feel free to give us a positive feedback. If it is still not your favor, please kindly do NOT leave any feedback.
Please kindly let us know if you agree.
They did give me a refund, so I got the watch for free. But it was unusable to me. I put it aside, noting its liberation as a project for another time.
Years later, I decided that finally reverse engineering the smart watch would be a nice quarantine activity to share with my friends through the magic of live streaming. The project is over now, but the recordings are available on PeerTube.
As usual, the project began with reconnaissance. There was software to interface with the watch, I just refused to install it on my cellphone2. To figure out how to talk to the watch, the path of least resistance was to reverse engineer that software. It was an Android app.
Reverse Engineering Android Apps
Most software using the Android SDK is written in Java, a language which runs on a process virtual machine. This means that Java code doesn't run "on the processor"3, but instead in an interpreter-like program known as a "virtual machine" (VM). Source code is still compiled, but the target is a fairly high-level "bytecode" rather than the machine code that would be output by a C compiler.
Virtual machines can be quite fast, but the performance characteristics of the
Java VM were deemed unsuitable for the sorts of phones on the market in
Android's early days4. Hence, the Dalvik virtual machine was developed: a
comparable process virtual machine with a register-based architecture (the Java
VM is stack-based) and fewer virtual machine instructions4. Java bytecode
and Dalvik bytecode are nearly isomorphic; the latter can be thought of as an
optimistic post-processing of the former. The compilation process for an Android
app is, conceptually, using the Java compiler to obtain JVM bytecode for the
app's sources, and then feeding that bytecode into dx
to obtain Dalvik bytecode.
Nowadays, the Dalvik VM is no more, but the techniques for reverse engineering
Dalvik bytecode are still relevant as modern Android runtimes still use the
Dalvik executable format5.
A quick rundown of the process for reverse engineering android apps: an Android APK, like you'd get from F-Droid or the Play Store, is just a ZIP archive with a specific structure and some signatures.
$ file com.uthink.ring.426.apk com.uthink.ring.426.apk: Zip archive data, at least v0.0 to extract, compression method=deflate $ unzip -l com.uthink.ring.426.apk | grep classes.dex 8685980 00-00-1980 00:00 classes.dex
All of the code is in one or more classes.dex
files. You can, if you're a
caveman (or faced with some seriously obfuscated code), unzip the APK and dump
the .dex
file into radare2 or smali. But I know how to use technology, so I used
JADX to recover something closer to Java source code.
There are more tools out there. But in this case, I didn't need to reach for
anything besides JADX because the source code was unobfuscated. R.java
, the
table of references to application resources, was as rich as the symtab
of a
non-stripped ELF.
package com.uthink.ring; public final class R { public static final class anim { public static final int abc_fade_in = 2130771968; public static final int abc_fade_out = 2130771969; public static final int abc_grow_fade_in_from_bottom = 2130771970; public static final int abc_popup_enter = 2130771971; public static final int abc_popup_exit = 2130771972; public static final int abc_shrink_fade_out_from_bottom = 2130771973; public static final int abc_slide_in_bottom = 2130771974; public static final int abc_slide_in_top = 2130771975; ...
I'd been saved 90% of the reverse engineering work.
An Introduction to Bluetooth Low Energy
The manual that came with the watch left much to be desired, but it did at least tell me that Bluetooth Low Energy (BLE) was being used to communicate with the wearer's smartphone. Not well that, despite the name, BLE is a different protocol from what's normally called "Bluetooth".
The premise of BLE is that one device acts as a "server" to which "clients" can connect and request characteristics: essentially, some packet of data from the server, such as the number of steps counted by the watch. There are a few other concepts (services, descriptors) in BLE, but they largely wrap around the concept of characteristics.
A BLE server is identified by a MAC address, and any particular characteristic, service, etc. that a BLE server exposes is an attribute, which is identified by a UUID. There are tools to enumerate the available attributes, BLExplorer being the one I used initially.
In our case, the MAC address of the watch is conveniently available from the user interface… for some reason.
Though, if this weren't the case, it would be easy enough to run hcitool lescan
.
Furthermore, none of the characteristics require authentication to read from, so I was half-way towards my goal of being able to pull data off of the watch. What was left to do was make sense of the data I was reading.
I headed to the Android Developer Docs to find the interfaces involved with
reading BLE characteristics. It was sufficient to grep
for references to
BluetoothGattCharacteristic. The files of interest are BluetoothLeService.java
(~6k LoC) and UpdateImage.java
(~500 LoC). The former gives us names for all of
the characteristics we can read from the watch.
public static final UUID BLE_AUDIO_CMD = UUID.fromString("0000af01-0000-1000-8000-00805f9b34fb"); public static final UUID BLE_AUDIO_DATA = UUID.fromString("0000af02-0000-1000-8000-00805f9b34fb"); public static final UUID BLE_AUDIO_DESCRIPTION = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"); public static final UUID BLE_AUDIO_SERVICE = UUID.fromString("0000af00-0000-1000-8000-00805f9b34fb"); public static final UUID MAXSCEND_OTA_CMD = UUID.fromString("0000FD02-0000-1000-8000-00805F9B34FB"); public static final UUID MAXSCEND_OTA_DATA = UUID.fromString("0000FD01-0000-1000-8000-00805F9B34FB"); public static final UUID MAXSCEND_OTA_DESCRIPTION = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"); public static final UUID MAXSCEND_OTA_SERVICE = UUID.fromString("0000FD00-0000-1000-8000-00805F9B34FB"); public static final UUID TELINK_SPP_DATA_OTA = UUID.fromString("00010203-0405-0607-0809-0a0b0c0d2b12"); public static final UUID TELINK_SPP_DATA_OTA_SERVICE = UUID.fromString("00010203-0405-0607-0809-0a0b0c0d1912"); public static final UUID WERUN_SERVICE = UUID.fromString("0000fee7-0000-1000-8000-00805f9b34fb"); public static final UUID YOHO_BATTERY_INFO = UUID.fromString("0000cc03-0000-1000-8000-00805f9b34fb"); public static final UUID YOHO_CHARACTERISTIC_CONFIGURATION = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"); public static final UUID YOHO_CONTROL = UUID.fromString("0000cc06-0000-1000-8000-00805f9b34fb"); public static final UUID YOHO_DEVICE_INFO = UUID.fromString("0000cc02-0000-1000-8000-00805f9b34fb"); public static final UUID YOHO_REALTIME_DATA = UUID.fromString("0000cc04-0000-1000-8000-00805f9b34fb"); public static final UUID YOHO_SERVICE = UUID.fromString("0000cc00-0000-1000-8000-00805f9b34fb"); public static final UUID YOHO_SYNC_DATA = UUID.fromString("0000cc05-0000-1000-8000-00805f9b34fb"); public static final UUID YOHO_USER_INFO = UUID.fromString("0000cc01-0000-1000-8000-00805f9b34fb");
While this isn't enough to use them (for example, YOHO_CONTROL
is clearly the
entry point to a multitude of functionality), the JADX output is readable enough
that behavior is easily determined. Take this excerpt from
BluetoothLeService.java
as an example:
public static void setVibrate(BluetoothGatt bluetoothGatt, boolean z) { BluetoothGattService service; BluetoothGattCharacteristic characteristic; Log.i(TAG, "setVibrate()"); if (bluetoothGatt != null && \\ (service = bluetoothGatt.getService(YOHO_SERVICE)) != null && \\ (characteristic = service.getCharacteristic(YOHO_CONTROL)) != null) { if (z || ((Boolean) SPUtils.get(sContext, Constant.HAS_BT, false)).booleanValue()) { characteristic.setValue(new byte[]{1, 1}); } else { characteristic.setValue(new byte[]{1, 0}); } bluetoothGatt.writeCharacteristic(characteristic); } }
If you're averse to Java, the bottom-line is that the vibrate feature6 is
configured by sending a packet to YOHO_CONTROL
where the first byte is 1
and the
second byte is whether or not to enable vibration.
Perhaps that's a bit mundane. If you're more interested by the acronym "OTA" appearing in this context, you're not alone.
Striking Oil
It wasn't long until I came across a bunch of plaintext API secrets for Aliyun, which is apparently China's answer to Amazon Web Services. The keys were for their S3-equivalent (OSS: Object Storage Service), which I needed my friend Luis to explain to me as I was an AWS virgin until a few months ago. S3 (and OSS) are key-value databases. The database is divided into buckets. It's a fairly simple way of storing chunks of data "in the cloud", and the format for keys makes apparent the comparison to a file system.
Luis also found ossutil, which is like awscli for Aliyun, so we were able to list off the buckets with a couple shell commands.
~ $ ./ossutil64 ls CreationTime Region StorageClass BucketName 2018-05-15 09:43:06 +0000 UTC oss-cn-beijing Standard oss://android-update 2019-11-15 08:25:54 +0000 UTC oss-cn-beijing Standard oss://mcube-osm 2018-04-28 06:31:06 +0000 UTC oss-cn-beijing Standard oss://mcube-ota Bucket Number is: 3 1.727812(s) elapsed ~ $ ./ossutil64 ls oss://android-update LastModifiedTime Size(B) StorageClass ETAG ObjectName 2018-05-24 06:03:42 +0000 UTC 0 Standard D41D8CD98F00B204E9800998ECF8427E oss://android-update/Bingo Sport/ 2018-05-18 09:31:37 +0000 UTC 0 Standard D41D8CD98F00B204E9800998ECF8427E oss://android-update/DJObewegt/ 2018-05-15 09:49:26 +0000 UTC 0 Standard D41D8CD98F00B204E9800998ECF8427E oss://android-update/l8star/ 2018-05-15 09:43:33 +0000 UTC 0 Standard D41D8CD98F00B204E9800998ECF8427E oss://android-update/yoho/ Object Number is: 4 1.716424(s) elapsed ~ $ ./ossutil64 ls oss://mcube-ota LastModifiedTime Size(B) StorageClass ETAG ObjectName 2020-11-04 03:06:57 +0000 UTC 0 Standard D41D8CD98F00B204E9800998ECF8427E oss://mcube-ota/Dialog/ 2020-11-04 03:07:07 +0000 UTC 0 Standard D41D8CD98F00B204E9800998ECF8427E oss://mcube-ota/Dialog/MP1612/ 2021-03-23 03:06:34 +0000 UTC 0 Standard D41D8CD98F00B204E9800998ECF8427E oss://mcube-ota/Dialog/MP1613/ 2021-07-02 02:43:31 +0000 UTC 281396 Standard 91F5B5CA01AE0953C225E71C1B145153 oss://mcube-ota/Dialog/MP1613/mc_band.8F.64.09.00.img 2021-03-18 07:22:23 +0000 UTC 0 Standard D41D8CD98F00B204E9800998ECF8427E oss://mcube-ota/Dialog/MP1615/ 2021-07-01 02:53:01 +0000 UTC 286676 Standard 6893E93A305B0DA4FEC5E30E0B67E598 oss://mcube-ota/Dialog/MP1615/mc_band.8F.64.0F.01.img 2021-07-26 03:06:29 +0000 UTC 0 Standard D41D8CD98F00B204E9800998ECF8427E oss://mcube-ota/GuangJi/ 2021-07-26 03:10:52 +0000 UTC 92392 Standard A69DD1D52C5A714507CA4E18705E02B5 oss://mcube-ota/GuangJi/GM121Q1UI_V7B_6B_04_30.bin 2020-04-29 08:27:35 +0000 UTC 0 Standard D41D8CD98F00B204E9800998ECF8427E oss://mcube-ota/Habit+/ 2020-01-10 09:50:03 +0000 UTC 0 Standard D41D8CD98F00B204E9800998ECF8427E oss://mcube-ota/JT/ 2020-01-10 09:50:15 +0000 UTC 119876 Standard EC902EEA38076E9132978E77CE1D72F9 oss://mcube-ota/JT/FACTORY_JT_R7_0.96_HRS3300_V6E_73_00_00.bin 2020-01-10 09:50:15 +0000 UTC 117568 Standard A072ED4E40E2E255670EEDC16D4FB595 oss://mcube-ota/JT/FACTORY_JT_R9_1.0_HRS3300S_V6B_72_00_00.bin 2020-01-10 09:50:15 +0000 UTC 168023 Standard CE29DA4EE5E2829A43B9B8C03D016284 oss://mcube-ota/JT/JT_R3_0.66_HRS3300S_V4A_72_00_00.bin 2020-01-10 09:50:15 +0000 UTC 107564 Standard 1827A73A6778C25C231DE716B5479E60 oss://mcube-ota/JT/JT_R5_0.96_96X96_HRS3300_V4F_6E_00_00.bin 2020-06-17 10:10:47 +0000 UTC 0 Standard D41D8CD98F00B204E9800998ECF8427E oss://mcube-ota/M2/ 2020-07-23 15:01:22 +0000 UTC 111616 Standard DB7815961B514B0637A398FB1CC679DD oss://mcube-ota/M2/M2_E_IPE167_V41_7E_00_32.bin 2020-07-23 15:01:22 +0000 UTC 111616 Standard E784EB22FDB99F943C9790D935D341ED oss://mcube-ota/M2/M2_GS_IPG67_V41_7E_00_33.bin 2018-11-19 09:54:26 +0000 UTC 0 Standard D41D8CD98F00B204E9800998ECF8427E oss://mcube-ota/Maxsend/ 2019-11-12 10:29:47 +0000 UTC 0 Standard D41D8CD98F00B204E9800998ECF8427E oss://mcube-ota/Maxsend/GM115/ 2019-11-12 10:33:05 +0000 UTC 0 Standard D41D8CD98F00B204E9800998ECF8427E oss://mcube-ota/Maxsend/GM115/0.96S/ 2019-11-12 10:38:06 +0000 UTC 96940 Standard E34860D829252CB9F7FB3EA94F5C32C4 oss://mcube-ota/Maxsend/GM115/0.96S/GM115_0.96S_V82_62_00_20.bin 2019-12-26 10:18:31 +0000 UTC 96100 Standard 1D979392E3E040319EFDF59BE90CC82D oss://mcube-ota/Maxsend/GM115/0.96S/GM115_V82_63_00_24.bin 2019-11-12 10:30:33 +0000 UTC 0 Standard D41D8CD98F00B204E9800998ECF8427E oss://mcube-ota/Maxsend/GM115/7735BOE/ 2019-11-12 10:30:51 +0000 UTC 96940 Standard BB4945F2B255C2D97725F30E840C91B9 oss://mcube-ota/Maxsend/GM115/7735BOE/GM115_7735BOE_V82_62_00_21.bin 2019-12-26 10:19:10 +0000 UTC 96100 Standard 111EE153248CA0A09CBCE84ED58C2A6C oss://mcube-ota/Maxsend/GM115/7735BOE/GM115_V82_63_00_25.bin 2020-05-14 11:33:04 +0000 UTC 100564 Standard BA066102FA168C7FCFD434DEB8BCBE9D oss://mcube-ota/Maxsend/GM115/GM115_CEUI_0.96S_MC34XX_HRS3300_JJ_V82_66_00_36.bin 2020-05-14 11:33:20 +0000 UTC 100584 Standard D88180FA87B24E934D2CBDDE9E051006 oss://mcube-ota/Maxsend/GM115/GM115_CEUI_BOE_0.96S_MC34XX_HRS3300_JJ_V82_66_00_37.bin 2020-08-19 08:50:57 +0000 UTC 0 Standard D41D8CD98F00B204E9800998ECF8427E oss://mcube-ota/Maxsend/GM120/ 2020-08-19 08:52:20 +0000 UTC 116852 Standard 63B9FD299CCFA9D47C22DF79ACD9B712 oss://mcube-ota/Maxsend/GM120/GM120M_V8D_63_00_21.bin 2020-07-27 11:44:29 +0000 UTC 0 Standard D41D8CD98F00B204E9800998ECF8427E oss://mcube-ota/Maxsend/GM127B0/ 2020-07-27 11:45:29 +0000 UTC 115772 Standard 34C740D1B7C6E1C732B96306FC2F3917 oss://mcube-ota/Maxsend/GM127B0/GM127_V8C_6D_00_21.bin 2019-12-10 14:44:46 +0000 UTC 0 Standard D41D8CD98F00B204E9800998ECF8427E oss://mcube-ota/Maxsend/M3/ 2020-05-15 11:13:43 +0000 UTC 102428 Standard 8032B2E58825204E9264B434BB57F684 oss://mcube-ota/Maxsend/M3/M3_7735BOE_0.96S_MC34XX_HRS3300_SC7R30_JJ_V80_6C_00_25.bin 2020-08-06 03:39:21 +0000 UTC 101856 Standard D4633331413CA3992E836257023271EC oss://mcube-ota/Maxsend/M3/M3_HSD_0.96S_V80_6E_00_24.bin 2019-11-09 08:39:37 +0000 UTC 0 Standard D41D8CD98F00B204E9800998ECF8427E oss://mcube-ota/Maxsend/M4/ 2020-05-15 11:14:19 +0000 UTC 102656 Standard 3B6DEE0CA2399446B60499F7C0681B3D oss://mcube-ota/Maxsend/M4/M4_UI3_7735BOE_0.96S_MC34XX_HRS3300_SC7R30_JJ_V81_69_00_25.bin 2020-05-15 13:31:52 +0000 UTC 102636 Standard 2F8CD37A50EB9F3D68DB250EC2A80C98 oss://mcube-ota/Maxsend/M4/M4_UI3_7735BOE_0.96S_MC34XX_HRS3300_SC7R30_JJ_V81_69_00_27.bin 2020-05-15 11:14:19 +0000 UTC 102636 Standard FEE888D1CCA800FD2275B748410477E2 oss://mcube-ota/Maxsend/M4/M4_UI3_HSD_0.96S_MC34XX_HRS3300_SC7R30_JJ_V81_69_00_24.bin 2020-05-15 13:31:52 +0000 UTC 102636 Standard 02B36A5DD5E7154EC163FA20B0BF0FD0 oss://mcube-ota/Maxsend/M4/M4_UI3_HSD_0.96S_MC34XX_HRS3300_SC7R30_JJ_V81_69_00_26.bin 2020-06-01 07:52:49 +0000 UTC 0 Standard D41D8CD98F00B204E9800998ECF8427E oss://mcube-ota/Maxsend/gm116m/ 2020-06-01 07:59:42 +0000 UTC 100420 Standard F2F8A68725BA5B78039011B87F8D1C1B oss://mcube-ota/Maxsend/gm116m/GM116M_EARTHUI_1.3S_MC34XX_HRS3300_JJ_V8A_60_00_24.bin 2020-08-12 05:46:13 +0000 UTC 99920 Standard F36E3E2BC232D48497C717A6CFF0F315 oss://mcube-ota/Maxsend/gm116m/GM116M_SF1049A_V3D_64_00_23.bin 2019-04-08 01:50:48 +0000 UTC 0 Standard D41D8CD98F00B204E9800998ECF8427E oss://mcube-ota/force-upgrade/ 2020-06-01 08:02:51 +0000 UTC 1528 Standard 9D0DB298389CF3296ADFD459C18CB2C1 oss://mcube-ota/force-upgrade/force upgrade.txt 2020-08-04 08:17:16 +0000 UTC 206740 Standard BBF5D1BCC6845E63AE084DF75CDD4312 oss://mcube-ota/mc_band.8F.61.0A.20.img Object Number is: 45 6.872844(s) elapsed ~ $ ./ossutil64 ls oss://mcube-osm LastModifiedTime Size(B) StorageClass ETAG ObjectName 2019-11-15 08:27:09 +0000 UTC 0 Standard D41D8CD98F00B204E9800998ECF8427E oss://mcube-osm/andromeda/ 2019-11-15 08:27:20 +0000 UTC 153824 Standard DC701B0A89718AFE223C62DE10139A33 oss://mcube-osm/andromeda/andromeda2_ble_1.0.0.bin Object Number is: 2 1.736465(s) elapsed
I swear these buckets were populated with more entries when I was streaming. Anyway, this is when we realized that this is an elaborate scheme. Whatever organization is behind the watch I have actually makes several kinds of smart watches, and several distinct apps for interfacing with them.
The name on the box is a hint that we care about either
M2_E_IPE167_V41_7E_00_32.bin
or M2_GS_IPG67_V41_7E_00_33.bin
, which are
deceptively similar.
$ radiff2 M2* File size differs 111617 vs 111638 Buffer truncated to 111617 byte(s) (21 not compared) 0x000065f4 32 => 33 0x000065f4 0x0000c1b0 32 => 33 0x0000c1b0 0x0000d9ac 32 => 33 0x0000d9ac 0x0001a876 452d49504531 => 47532d495047 0x0001a876 0x0001b27f 452d49504531 => 47532d495047 0x0001b27f 0x0001b2a0 452d49504531 => 47532d495047 0x0001b2a0
But turns out that we're not interested in those. The four hexadecimal bytes in each filename corresponds to a device identifier that's spit out by OTA characteristic.
$ sudo gatttool -I [ ][LE]> connect A4:C1:7A:56:82:90 Attempting to connect to A4:C1:7A:56:82:90 Connection successful [A4:C1:7A:56:82:90][LE]> characteristics handle: 0x0002, char properties: 0x12, char value handle: 0x0003, uuid: 2b120008-0600-072a-0100-050200042a00 handle: 0x0004, char properties: 0x02, char value handle: 0x0005, uuid: 0708090a-0b0c-0d2b-1200-080600072a01 handle: 0x0007, char properties: 0x06, char value handle: 0x0008, uuid: 00010203-0405-0607-0809-0a0b0c0d2b12 handle: 0x000b, char properties: 0x08, char value handle: 0x000c, uuid: 0000fec7-0000-1000-8000-00805f9b34fb handle: 0x000d, char properties: 0x20, char value handle: 0x000e, uuid: 0000fec8-0000-1000-8000-00805f9b34fb handle: 0x0010, char properties: 0x02, char value handle: 0x0011, uuid: 0000fec9-0000-1000-8000-00805f9b34fb handle: 0x0012, char properties: 0x32, char value handle: 0x0013, uuid: 0000fea1-0000-1000-8000-00805f9b34fb handle: 0x0015, char properties: 0x2a, char value handle: 0x0016, uuid: 0000fea2-0000-1000-8000-00805f9b34fb handle: 0x0019, char properties: 0x0a, char value handle: 0x001a, uuid: 0000cc02-0000-1000-8000-00805f9b34fb handle: 0x001b, char properties: 0x12, char value handle: 0x001c, uuid: 0000cc03-0000-1000-8000-00805f9b34fb handle: 0x001e, char properties: 0x12, char value handle: 0x001f, uuid: 0000cc04-0000-1000-8000-00805f9b34fb handle: 0x0021, char properties: 0x1a, char value handle: 0x0022, uuid: 0000cc05-0000-1000-8000-00805f9b34fb handle: 0x0024, char properties: 0x08, char value handle: 0x0025, uuid: 0000cc06-0000-1000-8000-00805f9b34fb [A4:C1:7A:56:82:90][LE]> char-read-uuid 0000cc02-0000-1000-8000-00805f9b34fb handle: 0x001a value: 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 [A4:C1:7A:56:82:90][LE]> char-read-hnd 0x001a Characteristic value/descriptor: 00 00 00 00 00 00 00 00 00 00 00 21 41 6b 00 00 00 00 00 00 [A4:C1:7A:56:82:90][LE]> char-read-uuid 0000cc02-0000-1000-8000-00805f9b34fb handle: 0x001a value: 00 00 00 00 00 00 00 00 00 00 00 21 41 6b 00 00 00 00 00
In our case, we want something with the filename ..._V41_xx_00_21.bin
(note
indices 11-14 in the byte array above). I have such a file saved from when I was
initially doing the reverse engineering for this project,
LD702A_DY_0.42_MC34XX_EM70XX_HRS3300_JJ_V41_72_00_21.bin
, but this doesn't
appear in the listing from ossutil
from today. I suspect the organization has
since nuked several firmware images.
Flashing Firmware
I was determined to flash custom firmware to this device. There are several
characteristics whose name contains "OTA", but only the TELINK_*
ones are
advertised by my watch. The first clue, in BluetoothLeService.java
:
public void onServicesDiscovered(BluetoothGatt bluetoothGatt, int i) { ... int i3 = 0; while (true) { if (i3 >= services.size()) { break; } UUID uuid2 = services.get(i3).getUuid(); if (uuid2.toString().equals(BluetoothLeService.TELINK_SPP_DATA_OTA_SERVICE.toString())) { BluetoothLeService.sOtaType = 1; break; } else if (uuid2.toString().equals(BluetoothLeService.MAXSCEND_OTA_SERVICE.toString())) { BluetoothLeService.sOtaType = 2; break; } else { BluetoothLeService.sOtaType = 0; i3++; } } ... }
So we want to find a branch dependent on BluetoothLeService.sOtaType
being 1
.
There's one hidden deep in MainActivity.java
7:
public void onReceive(Context context, Intent intent) { ... else if (action.equals(Constant.ACTION_OTA_CONFIRMED)) { int intValue = ((Integer) SPUtils.get(MainActivity.this, Constant.DEV_BATT_PERCENTAGE, 0)).intValue(); int intValue2 = ((Integer) SPUtils.get(MainActivity.this, Constant.DEV_BATT_STATUS, 3)).intValue(); if (intValue >= 50 || intValue2 != 3) { SPUtils.put(MainActivity.this, "has_weather", false); if (BluetoothLeService.sOtaType == 1) { if (TelinkOta.getBytesCount() != 0) { new TelinkOtaTask().execute(new Void[0]); } } else if (BluetoothLeService.sOtaType == 2) { BluetoothLeService.setMxdCmdCharNotify(BluetoothLeService.getBluetoothGatt()); } } else { new AlertDialog.Builder(MainActivity.this) .setMessage(R.string.force_ota_failed) .setNegativeButton(R.string.ok, (DialogInterface.OnClickListener) null) .create() .show(); Log.i(MainActivity.TAG, "can not upgrade firmware while battery is less than 50%"); } } }
This brings us to the conveniently named TelinkOtaTask
.
public class TelinkOtaTask extends AsyncTask<Void, Void, Void> { long delay = 100; public TelinkOtaTask() { } /* access modifiers changed from: protected */ public void onPreExecute() { super.onPreExecute(); MainActivity.this.initOTAProgressDialog(1); MainActivity.this.getWindow().addFlags(128); } /* access modifiers changed from: protected */ public Void doInBackground(Void... voidArr) { for (int i = 0; i < TelinkOta.getBlockCount() + 3; i++) { if (i != 0) { if (i == 1) { BluetoothLeService.writeOtaCharacteristic(new byte[]{1, -1}); SystemClock.sleep(1000); } else if (i == TelinkOta.getBlockCount() + 2) { BluetoothLeService.writeOtaCharacteristic(TelinkOta.getEndCmd()); TelinkOta.close(); SystemClock.sleep(1000); MainActivity.this.unboundAllMSDevice(); } else { if (TelinkOta.getCurrentCount() % 100 == 0) { SystemClock.sleep(0); } BluetoothLeService.writeOtaCharacteristic(TelinkOta.getBlock(TelinkOta.getCurrentCount())); TelinkOta.currentCountPlus(); SystemClock.sleep(this.delay); publishProgress(new Void[0]); } } } return null; } /* access modifiers changed from: protected */ public void onProgressUpdate(Void... voidArr) { super.onProgressUpdate(voidArr); MainActivity.this.otaProgressDialog.incrementProgressBy(1); } /* access modifiers changed from: protected */ public void onCancelled() { super.onCancelled(); } /* access modifiers changed from: protected */ public void onCancelled(Void voidR) { super.onCancelled(voidR); } }
The code is using this com.uthink.ring.update.TelinkOta
class to chunk the
firmware image, and write it to the OTA characteristic one block at a time. We
begin the transfer by writing 0x01ff
, and end by writing TelinkOta.getEndCmd()
.
The TelinkOta
class is short enough that I can drop the listing here.
package com.uthink.ring.update; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Arrays; public class TelinkOta { public static final int BLOCK_LENGTH = 16; public static final int CODE_SIZE_LENGTH = 4; public static final int CODE_SIZE_START = 24; private static final boolean DEBUG = false; private static final String TAG = TelinkOta.class.getSimpleName(); public static final boolean USE_CB = false; public static int blockCount; public static byte[] bytes; public static int bytesCount; public static int currentCount; public static void setFile(InputStream inputStream) { try { bytesCount = inputStream.available(); blockCount = (int) Math.ceil((double) (((float) bytesCount) / 16.0f)); bytes = new byte[bytesCount]; inputStream.read(bytes); inputStream.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (NullPointerException e2) { e2.printStackTrace(); } catch (IOException e3) { e3.printStackTrace(); } } public static void setFile(byte[] bArr) { bytesCount = bArr.length; blockCount = (int) Math.ceil((double) (((float) bytesCount) / 16.0f)); bytes = bArr; } public static int getBytesCount() { return bytesCount; } public static int getBlockCount() { return blockCount; } public static int getCurrentCount() { return currentCount; } public static void currentCountPlus() { currentCount++; } public static int getCodeSize() { return ByteBuffer.wrap(Arrays.copyOfRange(bytes, 24, 28)).order(ByteOrder.LITTLE_ENDIAN).getInt(); } public static byte[] getEndCmd() { byte[] bArr = new byte[6]; bArr[0] = 2; bArr[1] = -1; int i = blockCount; bArr[2] = (byte) ((i - 1) & 255); bArr[3] = (byte) (((i - 1) >> 8) & 255); bArr[4] = (byte) (bArr[2] ^ 255); bArr[5] = (byte) (bArr[3] ^ 255); return bArr; } public static byte[] getBlock(int i) { byte[] bArr; byte[] bArr2 = new byte[0]; byte[] bArr3 = {(byte) (i & 255), (byte) ((i >> 8) & 255)}; try { if (i == blockCount - 1) { int i2 = bytesCount % 16; if (i2 == 0) { int i3 = i * 16; bArr = Arrays.copyOfRange(bytes, i3, i3 + 16); } else { int i4 = i * 16; byte[] copyOfRange = Arrays.copyOfRange(bytes, i4, i4 + i2); byte[] bArr4 = new byte[(16 - i2)]; for (int i5 = 0; i5 < bArr4.length; i5++) { bArr4[i5] = -1; } bArr = concatByteArrays(copyOfRange, bArr4); } } else { int i6 = i * 16; bArr = Arrays.copyOfRange(bytes, i6, i6 + 16); } byte[] concatByteArrays = concatByteArrays(concatByteArrays(bArr2, bArr3), bArr); int CRC_16 = CRC_16(byteToUnsignedChar(concatByteArrays)); byte[] concatByteArrays2 = concatByteArrays(concatByteArrays, new byte[]{(byte) (CRC_16 & 255), (byte) ((CRC_16 >> 8) & 255)}); printByteToHex(concatByteArrays2); return concatByteArrays2; } catch (NullPointerException unused) { return bArr2; } } public static int CRC_16(char[] cArr) { char[] cArr2 = {0, 40961}; int i = 0; char c = 65535; while (i < cArr.length) { char c2 = cArr[i]; char c3 = c; for (int i2 = 0; i2 < 8; i2++) { c3 = cArr2[(c3 ^ c2) & 1] ^ (c3 >> 1); c2 = (char) (c2 >> 1); } i++; c = c3; } return c; } public static void close() { bytesCount = 0; blockCount = 0; currentCount = 0; bytes = null; } public static byte[] concatByteArrays(byte[] bArr, byte[] bArr2) { byte[] bArr3 = new byte[(bArr.length + bArr2.length)]; System.arraycopy(bArr, 0, bArr3, 0, bArr.length); System.arraycopy(bArr2, 0, bArr3, bArr.length, bArr2.length); return bArr3; } public static char[] byteToUnsignedChar(byte[] bArr) { char[] cArr = new char[bArr.length]; for (int i = 0; i < cArr.length; i++) { cArr[i] = (char) (bArr[i] & 255); } return cArr; } public static void printByteToHex(byte[] bArr) { StringBuilder sb = new StringBuilder(); for (byte b : bArr) { if (sb.length() > 0) { sb.append(':'); } sb.append(String.format("%02x", new Object[]{Byte.valueOf(b)})); } } }
The bottom-line is that we're breaking the firmware image into 16-byte blocks
and attaching a CRC-16 to each one. The code's already there, so I hacked
together a little main
function to perform the chunking for a file of my
choosing.
public static void main(String[] args) { if (args.length != 1) { System.err.printf("usage: TelinkOta [IMAGE]\n"); return; } try { FileInputStream f = new FileInputStream(args[0]); setFile(f); } catch (FileNotFoundException e) { e.printStackTrace(); return; } System.out.println(printByteToHex(new byte[]{1, -1})); for (int i = 0; i < TelinkOta.getBlockCount(); i++) { byte[] block = TelinkOta.getBlock(i); System.out.println(printByteToHex(block)); } System.out.println(printByteToHex(TelinkOta.getEndCmd())); }
This spits out each "packet" as a line of hexadecimal digits. I can then use some Emacs magic to turn said lines into shell commands, producing an extremely cursed shell script to flash a hard-coded firmware image to the device.8
gatttool -b A4:C1:7a:56:82:90 --char-write-req --handle=0x0008 --value=01ff gatttool -b A4:C1:7a:56:82:90 --char-write-req --handle=0x0008 --value=00000e800103000000004b4e4c54000288006365 gatttool -b A4:C1:7a:56:82:90 --char-write-req --handle=0x0008 --value=01007680000000000000cc9e0100000000007423 gatttool -b A4:C1:7a:56:82:90 --char-write-req --handle=0x0008 --value=020031083209320a910202ca085004b1fa878c26 gatttool -b A4:C1:7a:56:82:90 --char-write-req --handle=0x0008 --value=03002008c06b210885061f08c06b200885063504 gatttool -b A4:C1:7a:56:82:90 --char-write-req --handle=0x0008 --value=040000a02009200a910202ca085004b1fa873b7f gatttool -b A4:C1:7a:56:82:90 --char-write-req --handle=0x0008 --value=05001f09200a910202ca085004b1fa871b090552 gatttool -b A4:C1:7a:56:82:90 --char-write-req --handle=0x0008 --value=06001d08084001b048403fa31bf31b58a5abd4be gatttool -b A4:C1:7a:56:82:90 --char-write-req --handle=0x0008 --value=070012c11fa2050b060812f302da02d3830271fc gatttool -b A4:C1:7a:56:82:90 --char-write-req --handle=0x0008 --value=0800fbc1040b88a21a40fe87c0460080800095a0 gatttool -b A4:C1:7a:56:82:90 --char-write-req --handle=0x0008 --value=090000868000020680001009110a110b9a02592c
At this point, I hadn't reverse-engineered the firmware image, so I attempted
changing one of the ASCII strings that radare2
could find, with little
consideration to what might happen.
Where I Would Have Gone Next
I found being able to flash unauthenticated firmware to be a humorous attack vector. How about a worm for that $5 watch you got at the gas station?
I came across a Gitter conversation revealing I'm not the first to try to reverse engineer this watch. Ah well. They figured out that the SoC is likely based on TLSR8232, and the MCU is likely to be the TC329. That saved me from having to hammer my watch into pieces.
I couldn't find much information on either, so if I were going to reverse engineer the firmware, I would have to reverse engineer the SDK to figure out things like the image load address. The project's on the shelf for now because of that. It might be a fun project to reverse engineer the SoC/MCU, and I'd certainly learn a lot, but that would be more work than I want to put into this – I don't have an immediate interest in firmware reverse engineering at the moment.
Footnotes:
This was before I learned how the sleep tracking features on these sorts of products work: not well. In retrospect, I should've saved up for an EEG device like the now-discontinued Zeo.
My primary concern was the software being non-free, but I also voiced concerns about privacy in the live stream. There are three separate analytics platforms tracking users of the application.
Of course, Java is pervasive, so we have cursed things such as Jazelle which do execute bytecode on hardware.
When I was first working on this, the damn vibrate feature woke me up at five in the morning, trying to tell me that the watch was low on battery.
If you're unfamiliar with the Android SDK, "activities" compose the UI. As in, this snippet is in the UI code.
In this case, I'm referring to the characteristic by its handle rather than by its UUID. My uninformed understanding is that the handle is a sort of "short" identifier used for the same purpose as the UUID. I'm doing it this way because gatttool
only let me write to this particular characteristic if I used a handle.
There have apparently been some reverse engineering efforts involving the TC32.
I managed to RE the apps for the watches, but couldn't really find anything useful. There are some APIs which get OTA info but the with the current params of the post request they just end up giving useless information. I tried setting the version to something older and still the same result. What would be your pointers for this?