diff options
| author | Jakob L. Kreuze <zerodaysfordays@sdf.org> | 2024-07-06 18:57:41 -0400 |
|---|---|---|
| committer | Jakob L. Kreuze <zerodaysfordays@sdf.org> | 2024-07-06 18:57:41 -0400 |
| commit | 6f095e2429b67be5e8357cc73b93a714e760d160 (patch) | |
| tree | ebbe87bfdb8a16efca4fef4dffea9a0794b23bbb /haunt/posts/dollar-bin-reverse-engineering.org | |
| parent | 3757dd21a133966d706c2a56701ed1069698b7bb (diff) | |
Initial org-reader re-implementation
Diffstat (limited to 'haunt/posts/dollar-bin-reverse-engineering.org')
| -rw-r--r-- | haunt/posts/dollar-bin-reverse-engineering.org | 751 |
1 files changed, 751 insertions, 0 deletions
diff --git a/haunt/posts/dollar-bin-reverse-engineering.org b/haunt/posts/dollar-bin-reverse-engineering.org new file mode 100644 index 0000000..dbf7ee7 --- /dev/null +++ b/haunt/posts/dollar-bin-reverse-engineering.org @@ -0,0 +1,751 @@ +#+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<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); + } +} +#+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. |