#+TITLE: Dollar Bin Reverse Engineering #+DATE: <2021-12-24 Fri 09:42> #+TAGS: writeup hardware reverse-engineering tc32 radare2 java 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 capabilities[fn:1]; 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 [[https://www.gnu.org/proprietary/][proprietary software]], so the only options I was seriously considering were those on [[https://gadgetbridge.org/][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 [[https://drewdevault.com/2019/11/20/China.html][ethical implications]] of supporting a totalitarian state's economy). Moreover, I was somewhat capable of reading and writing 汉语, so the [[https://en.wikipedia.org/wiki/Xiaomi_Mi_Band_2][Xiaomi Mi Band 2]] fit the bill. I took to Ebay to purchase one, finding a listing for 10.99 USD with [[https://www.npr.org/2018/08/23/641140144/unraveling-the-mystery-behind-international-shipping-rates][free shipping]]. I ordered it, and things were okay. That is, until the package arrived. #+CAPTION: Clearly _not_ the Mi Band 2. [[./dollar-bin-reverse-engineering-m2.jpg]] 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. #+BEGIN_QUOTE 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. #+END_QUOTE I'm antipathetic toward anyone trying to slight me, so I threatened to file a complaint with Ebay. #+BEGIN_QUOTE 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. #+END_QUOTE 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 [[https://en.wikipedia.org/wiki/COVID-19_pandemic][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. - [[https://toobnix.org/videos/watch/75d1d813-fc9e-415b-9c4a-d6e26faba94a][Friday Night Junk Hacking]] - [[https://toobnix.org/w/rm1nVU4dzikxxkjQ8Dc69M][Friday Night Junk Hacking 2: Electric Boogaloo]] As usual, the project began with reconnaissance. There _was_ software to interface with the watch, I just refused to install it on my cellphone[fn:2]. To figure out how to talk to the watch, the path of least resistance was to reverse engineer that software. [[https://play.google.com/store/apps/details?id=com.uthink.ring][It was an Android app.]] * Reverse Engineering Android Apps Most software using the Android SDK is written in [[https://en.wikipedia.org/wiki/Java_(programming_language)][Java]], a language which runs on a [[https://en.wikipedia.org/wiki/Virtual_machine][process virtual machine]]. This means that Java code doesn't run "on the processor"[fn:5], 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 days[fn:3]. Hence, the [[https://en.wikipedia.org/wiki/Dalvik_(software)][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 instructions[fn:3]. 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 [[https://source.android.com/devices/tech/dalvik/dex-format][Dalvik executable format]][fn:4]. 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 [[https://en.wikipedia.org/wiki/Zip_(file_format)][ZIP archive]] with a specific structure and some signatures. #+BEGIN_SRC prog $ 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 #+END_SRC 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 [[https://github.com/JesusFreke/smali][smali]]. But I know how to use technology, so I used [[https://github.com/skylot/jadx][JADX]] to recover something closer to Java source code. There are [[https://github.com/ashishb/android-security-awesome#readme][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 [[https://en.wikipedia.org/wiki/Strip_(Unix)][non-stripped ELF]]. #+BEGIN_SRC java 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; ... #+END_SRC I'd been saved 90% of the reverse engineering work. * An Introduction to Bluetooth Low Energy [[./dollar-bin-reverse-engineering-manual.jpg]] The manual that came with the watch left much to be desired, but it did at least tell me that [[https://en.wikipedia.org/wiki/Bluetooth_Low_Energy][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 "[[https://en.wikipedia.org/wiki/Bluetooth][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 [[https://en.wikipedia.org/wiki/MAC_address][MAC address]], and any particular characteristic, service, etc. that a BLE server exposes is an *attribute*, which is identified by a [[https://en.wikipedia.org/wiki/UUID][UUID]]. There are tools to enumerate the available attributes, [[https://f-droid.org/en/packages/org.ligi.blexplorer/][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. [[./dollar-bin-reverse-engineering-mac.jpg]] 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 [[https://developer.android.com/reference/android/bluetooth/BluetoothGattCharacteristic][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. #+BEGIN_SRC java 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"); #+END_SRC 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: #+BEGIN_SRC java 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); } } #+END_SRC If you're averse to Java, the bottom-line is that the vibrate feature[fn:6] 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 [[https://en.wikipedia.org/wiki/Alibaba_Cloud][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 [[https://github.com/aliyun/ossutil][ossutil]], which is like [[https://aws.amazon.com/cli/][awscli]] for Aliyun, so we were able to list off the buckets with a couple shell commands. #+BEGIN_SRC prog ~ $ ./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 #+END_SRC 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. #+BEGIN_SRC prog $ 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 #+END_SRC 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. #+BEGIN_SRC sh $ 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 #+END_SRC 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=: #+BEGIN_SRC 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++; } } ... } #+END_SRC So we want to find a branch dependent on =BluetoothLeService.sOtaType= being =1=. There's one hidden deep in =MainActivity.java=[fn:7]: #+BEGIN_SRC java 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%"); } } } #+END_SRC This brings us to the conveniently named =TelinkOtaTask=. #+BEGIN_SRC java public class TelinkOtaTask extends AsyncTask { 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); } } #+END_SRC 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. #+BEGIN_SRC java 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)})); } } } #+END_SRC The bottom-line is that we're breaking the firmware image into 16-byte blocks and attaching a [[https://en.wikipedia.org/wiki/Cyclic_redundancy_check][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. #+BEGIN_SRC java 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())); } #+END_SRC 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.[fn:9] #+BEGIN_SRC prog 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 #+END_SRC 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. #+CAPTION: Before and after flashing firmware. [[./dollar-bin-reverse-engineering-firmware-mod.jpg]] * 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 [[https://gitter.im/nRF51822-Arduino-Mbed-smart-watch/Lobby?at=5b62115c854add6cee148651][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 [[http://wiki.telink-semi.cn/doc/ds/DS_TLSR8232-E_Datasheet%20for%20Telink%20BLE%20SoC%20TLSR8232.pdf][TLSR8232]], and the MCU is likely to be the [[https://github.com/rgov/Ghidra_TELink_TC32][TC32]][fn:8]. 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 [[https://github.com/OnTheWayJohn/ble_lt_sdk][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. [fn:1] 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 [[https://www.gwern.net/Zeo][Zeo]]. [fn:2] My primary concern was the software being non-free, but I also voiced concerns about privacy in the live stream. There are [[https://reports.exodus-privacy.eu.org/en/reports/com.uthink.ring/latest/][three separate analytics platforms]] tracking users of the application. [fn:3] ref: [[https://stackoverflow.com/questions/2719469/why-is-the-jvm-stack-based-and-the-dalvik-vm-register-based][Stack Overflow: Why is the JVM stack-based and the Dalvik VM register-based?]] [fn:4] ref: [[https://source.android.com/devices/tech/dalvik/gc-debug][Android Open Source Project: Debugging ART Garbage Collection]] [fn:5] Of course, Java is pervasive, so we have cursed things such as [[https://en.wikipedia.org/wiki/Jazelle][Jazelle]] which /do/ execute bytecode on hardware. [fn:6] 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. [fn:7] If you're unfamiliar with the Android SDK, "activities" compose the UI. As in, this snippet is in the UI code. [fn:8] There have apparently been /some/ [[https://hackaday.com/2020/11/17/custom-firmware-for-cheap-bluetooth-thermometers/][reverse engineering efforts]] involving the TC32. [fn:9] 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.