summaryrefslogtreecommitdiff
path: root/org/Dollar Bin Reverse Engineering/dollar-bin-reverse-engineering.org
diff options
context:
space:
mode:
Diffstat (limited to 'org/Dollar Bin Reverse Engineering/dollar-bin-reverse-engineering.org')
-rw-r--r--org/Dollar Bin Reverse Engineering/dollar-bin-reverse-engineering.org846
1 files changed, 595 insertions, 251 deletions
diff --git a/org/Dollar Bin Reverse Engineering/dollar-bin-reverse-engineering.org b/org/Dollar Bin Reverse Engineering/dollar-bin-reverse-engineering.org
index a5604e5..4bb02d9 100644
--- a/org/Dollar Bin Reverse Engineering/dollar-bin-reverse-engineering.org
+++ b/org/Dollar Bin Reverse Engineering/dollar-bin-reverse-engineering.org
@@ -1,23 +1,27 @@
#+TITLE: Dollar Bin Reverse Engineering
-#+DATE:
-#+TAGS: writeup, hardware, reverse-engineering, avr, radare2, java
+#+DATE: <2021-12-24 Fri 09:42>
+#+TAGS: writeup, hardware, reverse-engineering, tc32, radare2, java
-The background for this project is a lesson in recognizing dishonest vendors.
-Two years ago, I was looking at purchasing a smart watch with sleep tracking
-capabilities;[fn:1] I've always had difficulty sleeping, and wanted a way of
+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 [[https://www.gnu.org/proprietary/][proprietary software]], so I was only seriously
-considering what was on [[https://gadgetbridge.org/][Gadgetbridge]]'s "supported devices" list. At the time, I
-was still in high school, so I was still awed by the affordability of consumer
-electronics available on websites such as AliExpress, somewhat capable of
-reading and writing 汉语, and woefully unaware of the [[https://drewdevault.com/2019/11/20/China.html][ethical implications]] of
-supporting a totalitarian state's economy. The Xiaomi Mi Band 2 fit the bill. I
-took to Ebay to purchase one, and found 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 until the package arrived.
+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 project.
+that they had sent me the wrong product.
#+BEGIN_QUOTE
Hi [my Ebay username],
@@ -25,19 +29,19 @@ 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 [sic], 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.
+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 with the gall to believe they can slight me, so I
-threatened to file a complaint with Ebay.
+I'm antipathetic toward anyone trying to slight me, so I threatened to file a
+complaint with Ebay.
#+BEGIN_QUOTE
Hi [my Ebay username],
@@ -45,7 +49,7 @@ 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 [sic], we suggest that we issue a full refund
+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.
@@ -53,113 +57,273 @@ 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 essentially
-unusable to me. I put it away, noting its liberation as a project for another
-time.
-
-If you're observant, you may have noticed that these two messages are
-essentially the same. I suspect that an expectation of complaint-adjacent losses
-is a part of the vendor's business model. Looking back on it now, I should have
-taken the refund and filed the complaint anyway.
+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.
-Regrets aside, 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, and linked below.
+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]]
-* Method of Attack
+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.]]
-The original plan was to write a Gadgetbridge plugin to pull data from the
-watch, which is somewhat less ambitious than what I eventually did. In either
-case, the project began with reconnaissance. There _was_ software to interface
-with the watch, I just refused to install it on my cellphone. To figure out how
-to talk to the watch, the easy route would be to reverse engineer that software.
-It was an Android app.
+* Reverse Engineering Android Apps
-** 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.
-Most software using the Android SDK is written in Java. Java runs on a [[https://en.wikipedia.org/wiki/Virtual_machine][process
-virtual machine]], specifically a stack machine, which was a performance concern
-on the meager phone hardware that was available in Android's early days [1]. So
-the [[https://en.wikipedia.org/wiki/Dalvik_(software)][Dalvik]] virtual machine was developed: a similar process virtual machine with
-a register-based architecture and fewer virtual machine instructions [1].
+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]] [2]. The compilation process for an Android app is,
-essentially, using the Java compiler to obtain JVM bytecode for the app's
-sources, and then feeding that bytecode into =dx= to obtain Dalvik bytecode.
+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].
-Here's a quick rundown: 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. 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 the Java sources.
+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
-There are [[https://github.com/ashishb/android-security-awesome#readme][more tools]].
+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;
-[1]: https://stackoverflow.com/questions/2719469/why-is-the-jvm-stack-based-and-the-dalvik-vm-register-based
-[2]: https://source.android.com/devices/tech/dalvik/gc-debug
+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
-** An Introduction to Bluetooth Low Energy
+I'd been saved 90% of the reverse engineering work.
-# BLE Explorer
+* An Introduction to Bluetooth Low Energy
-# MAC address is accessible from watch
+[[./dollar-bin-reverse-engineering-manual.jpg]]
-# Dongle didn't work first stream, did the second
+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]]".
-# Bluetooth is incompatible with BLE
+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.
-# Service vs characteristic, important later.
+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.
-# AdaFruit article, BlueZ
+In our case, the MAC address of the watch is conveniently available from the
+user interface... for some reason.
-# Grep for BLE from Android devdocs
+[[./dollar-bin-reverse-engineering-mac.jpg]]
-# Callback structure
+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.
-# hcidool lescan
-# gattool
+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.
-** Striking Oil
+#+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
-"Awesome, we hit the gold mine! ... I think"
+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:
-# Luis pointed out ostool, explained AWS to me
+#+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
-# This is when we realized that this is a big scheme, and that this app works for many of these kinds of smart watches
+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.
-# String "MSE-IPE167" in firmware, googling yields http://www.ebooks-cct.com.tw/product_show.php?id=1245
-# This explains the title of this blog post.
+Perhaps that's a bit mundane. If you're more interested by the acronym "OTA"
+appearing in this context, you're not alone.
-# Try to disassemble as AVR in Radare2, it works.
+* Striking Oil
-*** Object Storage
+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.
-# I'm pretty familiar with S3 et al. now, but it was new to me when I Was working on this project. So here's a quick summary.
+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.
-** Flashing Firmware
+#+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
-# Essentially just write a byte array to this one "service"
+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
-# This is a humorous attack vector.
+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
-# Callstack:
+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
-Aliyun.java:
-downloadOtaFile downloads =this.objectKey=
-set in =chkOta=
+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.
-sFWver = 0d.0e.0f.10
+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.
-jakob@Epsilon ~/Streams/Friday Night Junk Hacking 2: Electric Boogaloo/m2-firmware $ radiff2 M2*
+#+BEGIN_SRC prog
+$ radiff2 M2*
File size differs 111617 vs 111638
Buffer truncated to 111617 byte(s) (21 not compared)
0x000065f4 32 => 33 0x000065f4
@@ -168,21 +332,14 @@ Buffer truncated to 111617 byte(s) (21 not compared)
0x0001a876 452d49504531 => 47532d495047 0x0001a876
0x0001b27f 452d49504531 => 47532d495047 0x0001b27f
0x0001b2a0 452d49504531 => 47532d495047 0x0001b2a0
+#+END_SRC
-# Modifying Strings
-
-** Where I Would Have Gone Next
-
-** Why I Gave Up Here
-
-** Addendum: Watching Yourself
-
----
-
-Getting the FW version
+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
-jakob@Epsilon ~ $ sudo gatttool -I
+$ sudo gatttool -I
[ ][LE]> connect A4:C1:7A:56:82:90
Attempting to connect to A4:C1:7A:56:82:90
Connection successful
@@ -201,9 +358,6 @@ handle: 0x001e, char properties: 0x12, char value handle: 0x001f, uuid: 0000cc04
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
-# This is where it's a little weird.
-
-
[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
@@ -212,196 +366,386 @@ Characteristic value/descriptor: 00 00 00 00 00 00 00 00 00 00 00 21 41 6b 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
-
-# Like you have to read the handle first..?
#+END_SRC
-From =mGattCallback.onCharacteristicRead=:
+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
-int intValue = bluetoothGattCharacteristic.getIntValue(17, 11).intValue();
-int intValue2 = bluetoothGattCharacteristic.getIntValue(17, 12).intValue();
-int intValue3 = bluetoothGattCharacteristic.getIntValue(17, 13).intValue();
-int intValue4 = bluetoothGattCharacteristic.getIntValue(17, 14).intValue();
-if (intValue3 < 53) {
- SPUtils.put(BluetoothLeService.sContext, Constant.NEW_FORMULA, false);
-} else if (intValue3 >= 53) {
- SPUtils.put(BluetoothLeService.sContext, Constant.NEW_FORMULA, true);
-}
-String unused = BluetoothLeService.sFWVer = String.format("%02x", new Object[]{Integer.valueOf(intValue2)}) + "." + String.format("%02x", new Object[]{Integer.valueOf(intValue3)}) + "." + String.format("%02x", new Object[]{Integer.valueOf(intValue4)}) + "." + String.format("%02x", new Object[]{Integer.valueOf(intValue)});
-String str2 = BluetoothLeService.TAG;
-StringBuilder sb = new StringBuilder();
-sb.append("sFWVer = ");
-sb.append(BluetoothLeService.sFWVer);
-Log.i(str2, sb.toString());
-if (intValue3 <= 73) {
- boolean unused2 = BluetoothLeService.sNewSync = false;
- String str3 = BluetoothLeService.TAG;
- Log.i(str3, "sNewSync = " + BluetoothLeService.sNewSync);
- Iterator<BluetoothDevice> it = BluetoothLeService.getBoundDevices().iterator();
- loop0:
+public void onServicesDiscovered(BluetoothGatt bluetoothGatt, int i) {
+ ...
+ int i3 = 0;
while (true) {
- z = false;
- while (true) {
- if (!it.hasNext()) {
- break loop0;
- }
- BluetoothDevice next = it.next();
- if (next.getName() != null) {
- if (BluetoothLeService.getConnectedDevice() != null) {
- if (next.getAddress().equals(BluetoothLeService.getConnectedDevice().getAddress())) {
- z = 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++;
}
}
- String str4 = BluetoothLeService.TAG;
- Log.i(str4, "isBonded = " + z);
- if (z) {
- BluetoothLeService.sConnSeq = 2;
- BluetoothLeService.readBattery(bluetoothGatt);
- }
-} else if (intValue3 > 73) {
- boolean unused3 = BluetoothLeService.sNewSync = true;
- String str5 = BluetoothLeService.TAG;
- Log.i(str5, "sNewSync = " + BluetoothLeService.sNewSync);
- BluetoothLeService.sConnSeq = 2;
- BluetoothLeService.readBattery(bluetoothGatt);
-}
-boolean unused4 = BluetoothLeService.sKMD = BluetoothLeService.chkIsKMD(bluetoothGattCharacteristic);
-SPUtils.put(BluetoothLeService.sContext, Constant.IS_KMD, Boolean.valueOf(BluetoothLeService.sKMD));
-boolean unused5 = BluetoothLeService.sMultiple = BluetoothLeService.chkHasMultipleSports(bluetoothGattCharacteristic);
-SPUtils.put(BluetoothLeService.sContext, Constant.HAS_MULTIPLE_SPORTS, Boolean.valueOf(BluetoothLeService.sMultiple));
-boolean unused6 = BluetoothLeService.sHrBpSync = BluetoothLeService.chkHasHrBpSync(bluetoothGattCharacteristic);
-SPUtils.put(BluetoothLeService.sContext, Constant.HAS_HRBP_SYNC, Boolean.valueOf(BluetoothLeService.sHrBpSync));
-boolean unused7 = BluetoothLeService.sWeather = BluetoothLeService.chkWeather(bluetoothGattCharacteristic);
-SPUtils.put(BluetoothLeService.sContext, "has_weather", Boolean.valueOf(BluetoothLeService.sWeather));
-boolean unused8 = BluetoothLeService.sMusic = BluetoothLeService.chkMusic(bluetoothGattCharacteristic);
-SPUtils.put(BluetoothLeService.sContext, Constant.HAS_MUSIC, Boolean.valueOf(BluetoothLeService.sMusic));
-boolean unused9 = BluetoothLeService.sDownladImage = BluetoothLeService.chkDonwloadImage(bluetoothGattCharacteristic);
-SPUtils.put(BluetoothLeService.sContext, Constant.HAS_IMAGE_UPGRADE, Boolean.valueOf(BluetoothLeService.sDownladImage));
-boolean unused10 = BluetoothLeService.sHasBT = BluetoothLeService.chkBT(bluetoothGattCharacteristic);
-SPUtils.put(BluetoothLeService.sContext, Constant.HAS_BT, Boolean.valueOf(BluetoothLeService.sHasBT));
-SPUtils.put(BluetoothLeService.sContext, Constant.HAS_CONTACTS, Boolean.valueOf(BluetoothLeService.chkContacts(bluetoothGattCharacteristic)));
-String upperCase = BluetoothLeService.sFWVer.replace(".", "").toUpperCase();
-if (BluetoothLeService.sDownladImage && (upperCase.equals("81620022") || upperCase.equals("81600023") || upperCase.equals("80620022") || upperCase.equals("81600021") || upperCase.equals("7B610081"))) {
- UpdateImage.getInstance().setFWInfo(80, 160, 114688, 0);
-} else if (BluetoothLeService.sDownladImage && (upperCase.equals("7D610081") || upperCase.equals("7D620080"))) {
- UpdateImage.getInstance().setFWInfo(80, 160, 258048, 0);
-} else if (!BluetoothLeService.sDownladImage) {
- UpdateImage.getInstance().setFWInfo(0, 0, 0, 4);
+ ...
}
-SPUtils.put(BluetoothLeService.sContext, Constant.DEV_INFO_ADDR, BluetoothLeService.getConnectedAddress());
-SPUtils.put(BluetoothLeService.sContext, Constant.FIRMWARE_VERSION, BluetoothLeService.sFWVer);
-Intent intent = new Intent(Constant.ACTION_READ_FIRMWARE_VERSION);
-intent.putExtra(Constant.FIRMWARE_VERSION, BluetoothLeService.sFWVer);
-BluetoothLeService.sContext.sendBroadcast(intent);
#+END_SRC
-So my fw version is =41.6b.00.21=; 0x6b > 73, so we use "new sync".
-It isn't one of the special cases checked at the end, but chkDonwloadImage [sic] is false, so =.setFWInfo(0, 0, 0, 4)=.
+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 setFWInfo(int i, int i2, int i3, int i4) {
- this.WIDTH = i;
- this.HEIGHT = i2;
- this.flashSize = i3;
- this.isWait = false;
- this.dialColor = i4;
+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
-#+BEGIN_SRC java
- public class TelinkOtaTask extends AsyncTask<Void, Void, Void> {
-#+END_SRC
+This brings us to the conveniently named =TelinkOtaTask=.
#+BEGIN_SRC java
-protected void doInBackground() {
- for (int i = 1; i < TelinkOta.getBlockCount() + 3; i++) {
- 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);
+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]);
+ }
}
- 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
-public static void writeOtaCharacteristic(byte[] bArr) {
- BluetoothGattService service;
- BluetoothGattCharacteristic characteristic;
- BluetoothGatt bluetoothGatt = sBluetoothGatt;
- if (bluetoothGatt != null && (service = bluetoothGatt.getService(TELINK_SPP_DATA_OTA_SERVICE)) != null && (characteristic = service.getCharacteristic(TELINK_SPP_DATA_OTA)) != null) {
- characteristic.setValue(bArr);
- sBluetoothGatt.writeCharacteristic(characteristic);
+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
-# Big ziku is big font library
+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;
+ }
-* Story about scamming
+ try {
+ FileInputStream f = new FileInputStream(args[0]);
+ setFile(f);
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ return;
+ }
-* Run-down of the software
-** Decompiling with JADX; dependencies
-** Exodus
-** I discovered the API keys because I was sus about the alibaba dependency
-+* Android Shit+
-+** References (Maybe omit)+
-* Bluetooth Shit
-** Query UUID via BLE, get a response back
-** Found some UUIDs in BluetoothLeService.java
-* Malware
-** 10M downloads?
-* Appendix: Watching Your Own Streams
-** Damn, coming back to this over a year later and I'm impressed
+ 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
-* Commands
+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 shell
-sudo gattool -b A4:C1:7A:56:82:90 -I
+#+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
-Binaries available for OSSutil. Might be interesting to RE and see if anything differs from the Github source code... https://www.alibabacloud.com/help/doc-detail/120075.htm
+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?
-Ghidra oneshot:
-- ASCII strings
+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.
-* Additional Reconnaissance
+[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]].
-** SoC is likely to be based on [[http://wiki.telink-semi.cn/doc/ds/DS_TLSR8232-E_Datasheet%20for%20Telink%20BLE%20SoC%20TLSR8232.pdf][TLSR8232]]
+[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.
-http://wiki.telink-semi.cn/wiki/chip-series/TLSR826x-Series/
+[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?]]
-- Strings
-- Conversation here https://gitter.im/nRF51822-Arduino-Mbed-smart-watch/Lobby?at=5b62115c854add6cee148651
+[fn:4] ref: [[https://source.android.com/devices/tech/dalvik/gc-debug][Android Open Source Project: Debugging ART Garbage Collection]]
-** MCU is likely to be the TC32
+[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.
-https://github.com/rgov/Ghidra_TELink_TC32
+[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.
-** Reverse engineering the SDK to figure out load address
+[fn:7] If you're unfamiliar with the Android SDK, "activities" compose the UI. As in, this snippet is in the UI code.
-https://github.com/OnTheWayJohn/ble_lt_sdk
+[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:1] This was before I learned that the "sleep tracking" feature on these "smart watch" type products tends to be inaccurate. In retrospect, I should've saved up for an EEG device like the now discontinued [[https://www.gwern.net/Zeo][Zeo]].
+[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.