1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
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.
|