对Bigrun Team心率带高级功能的逆向分析
目的
在骑行圈大家应该都早有耳闻:Polar H10是最准的心率带。在互联网上,很多心率带的评测,都会以H10作为标准。
我在去年上半年了解到了Bigrun Team心率带,这个心率带对标H10心率带,并且同样有Polar H10的ECG功能。
Polar H10的ECG功能文档中写到了H10的采样率为130Hz,同时还能够记录加速度传感器的数据,采样率为25Hz、50Hz、100Hz或者200Hz,范围有2G、4G或者8G。
Bigrun Team的心率带在淘宝页面中提到心率带的采样率为1000Hz,我想要通过程序读取到ECG信号,但是Bigrun Team并没有公布蓝牙协议的相关文档,也没有SDK,于是只能自己来逆向了。
步骤
数据准备
使用jadx,反编译apk文件,由于ECTRI并未对代码进行混淆,所以能够轻松获取到程序的源代码
除了对源代码进行分析,同时需要结合手机上的蓝牙抓包,能够更快的定位代码(ECTRI的代码太多了)
这里使用小米手机,在开发者模式中打开hci
,然后使用官方APP进行一些蓝牙操作,最后再adb bugreport xiaomi
即可导出其中的蓝牙日志FS\data\misc\bluetooth\logs\BT_HCI_YYYY_MMDD_hhmmss.cfa.curf
。这个文件可以直接使用Wireshark打开。
蓝牙数据分析和代码分析
初步分析
第一步使用nRF Connect
来初步获取到心率带的相关蓝牙特征
例如可以看到两个未知服务
F000EFE0-0451-4000-0000-00000000B000
F000EFE1-0451-4000-0000-00000000B000
F000EFE3-0451-4000-0000-00000000B0000xFEE7
0xFEC9
0xFEA1
0xFEA2
随后在代码中搜索这两个服务的UUID(或者特征的),即可发现前者用来传输高级数据,后者可能是接入小程序的蓝牙服务(结合搜索0xFEE7,可以看到是微信小程序接入)
再结合属性:
F000EFE1-0451-4000-0000-00000000B000 WRITE, WRITE NO RESPONSE
F000EFE3-0451-4000-0000-00000000B000 NOTIFY
以及代码:
package com.manridy.manbledevice;
......
public class ManConstants {
......
public static final UUID SERVICE_UUID = UUID.fromString("F000EFE0-0451-4000-0000-00000000B000");
public static final UUID TX_CHAR_UUID = UUID.fromString("F000EFE1-0451-4000-0000-00000000B000");
public static final UUID RX_CHAR_UUID = UUID.fromString("F000EFE3-0451-4000-0000-00000000B000");
......
}
可以推断EFE1
用来向设备发送控制指令,而EFE3
将会回应数据
蓝牙抓包分析
使用筛选器筛选出与心率带相关的事件,例如
(_ws.col.def_src == "") || (_ws.col.def_src == "")
即可筛选出手机与心率带之间的通讯
也可以进一步筛选服务的UUID,例如且选中
btatt.service_uuid128 == f0:00:ef:e0:04:51:40:00:00:00:00:00:00:00:b0:00
或者筛选特征
btatt.characteristic_uuid128 == f0:00:ef:e3:04:51:40:00:00:00:00:00:00:00:b0:00
筛选出写入相关的数据包
((_ws.col.def_src == "") || (_ws.col.def_src == "")) && (btatt.opcode == 0x52)
查看写入数据,然后在nRF Connect
程序中重放,就可以分析出各个指令的功能
例如发送以下数据,都只是会在EFE3
中回复一个数据,可以推断为读取配置相关的查询
fc0f600000000000000000000000000000000000
fc0f620000000000000000000000000000000000
fc0f610000000000000000000000000000000000
fc42060000000000000000000000000000000000
fc420b0000000000000000000000000000000000
fc42010000000000000000000000000000000000
然后发送以下数据,都会开启某个数据流(发送后EFE3
会不断恢复大量数据)
fc14040001000000000000000000000000000000
fc00250204234721025500000000000000000000
这些指令都有一个共同点:开头是fc,接下来在代码中搜索0xfc
或者其对应的十进制数-4
。前者没有出现过,后者找到最有可能的代码是com.manridy.manbledevice.bean.type.ManBeanEnum
代码分析
package com.manridy.manbledevice.bean.type;
......
public enum ManBeanEnum {
......
WriteDate(new byte[]{-4, 0}),
SystemWriteSportModel(new byte[]{-4, 15, 96}),
SystemWriteXO(new byte[]{-4, 15, 97}),
SystemWriteSR(new byte[]{-4, 15, 98}),
ReadEcgInfoSpeed(new byte[]{-4, ManConstants.ReadEcgInfo, 6}),
ReadEcgInfoSignal(new byte[]{-4, ManConstants.ReadEcgInfo, 1}),
ReadEcgAppSR(new byte[]{-4, ManConstants.ReadEcgInfo, 11}),
ReadSportsRealRimeDataHR(new byte[]{-4, 20, 6}),
ReadSportsRealRimeDataRRI(new byte[]{-4, 20, 7}),
ReadGSensor(new byte[]{-4, 15}),
......
}
再结合ManConstants
package com.manridy.manbledevice;
......
public class ManConstants {
......
public static final byte ReadEcgInfo = 66;
......
}
就可以发现这里的byte数组和指令头一一对应
接下来再深挖使用了这个ManBeanEnum
的代码,如com.manridy.manbledevice.bean.read
中的各个类
再搜索com.manridy.manbledevice.bean.read
的各个类名,调查到com.manridy.manbledevice.ManAnalysisUtils
这个类,用来处理接收到的蓝牙数据
同时也可以找到发送数据的类com.manridy.manmodel.device.adapter.send.ManSendDevice
类,可以找到发送指令的方法
疑问
在GSensor处的解析代码存疑
package com.manridy.manbledevice.bean.read;
......
public class ReadGSensorBean extends ManBean<ReadGSensorBean> {
......
public ReadGSensorBean response(String str) {
String replaceAll = str.replaceAll(" ", "");
String substring = replaceAll.substring(8, 12);
String substring2 = replaceAll.substring(12, 16);
String substring3 = replaceAll.substring(16, 20);
String substring4 = replaceAll.substring(16, 20);
String substring5 = replaceAll.substring(20, 24);
String substring6 = replaceAll.substring(24, 28);
int hexStringToDecimal = BleUtils.hexStringToDecimal(substring);
int hexStringToDecimal2 = BleUtils.hexStringToDecimal(substring2);
int hexStringToDecimal3 = BleUtils.hexStringToDecimal(substring3);
int hexStringToDecimal4 = BleUtils.hexStringToDecimal(substring4);
int hexStringToDecimal5 = BleUtils.hexStringToDecimal(substring5);
int hexStringToDecimal6 = BleUtils.hexStringToDecimal(substring6);
setX1Gravity(hexStringToDecimal);
setY1Gravity(hexStringToDecimal2);
setZ1Gravity(hexStringToDecimal3);
setX2Gravity(hexStringToDecimal4);
setY2Gravity(hexStringToDecimal5);
setZ2Gravity(hexStringToDecimal6);
return this;
}
......
}
此处的字符串切片z1和x2一样,不符合常理
通过对加速度数据的分析
0f12 05c6 00ea 0106 ff8a 00ed 0104 ff95 ff28702b
0f12 05c7 00ef 0109 ff8d 00f2 010a ff90 ff28702b
0f12 05c8 00ee 0108 ff8e 00eb 010a ff84 ff28702b
0f12 05cc 00f1 010a ff8f 00f3 010e ff8a 00000000
0f12 05cd 00ed 0106 ff90 00ec 0104 ff93 00000000
0f12 05ce 00eb 0102 ff95 00e5 00fc ff97 00000000
0f12 05de 00ec 0109 ff86 00e2 0100 ff87 01270427
0f12 05df 00eb 010b ff81 00e4 0103 ff85 01270427
0f12 05e0 00ec 010a ff84 00eb 010c ff7e 01270427
0f12 05e1 00ef 010c ff88 00e7 010c ff79 01270427
0f12 05e4 00ee 010d ff82 00e9 0104 ff8d 0c270e27
0f12 05eb 00f4 010f ff89 00f4 010d ff8e 1b287426
0f12 05ec 00f0 010c ff89 00e7 0102 ff8c 1b287426
0f12 05f0 00e9 0100 ff96 00ed 0104 ff95 00000000
0f12 05f1 00ec 0103 ff94 00ef 010a ff8a 00000000
0f12 05f2 00ec 0109 ff86 00ec 0109 ff87 00000000
可以发现这些数据都符合一个格式
0f 12 → 指令头
05c6 → 递增计数器(2字节)
00ea 0106 ff8a → 数据段1(6字节)
00ed 0104 ff95 → 数据段2(6字节)
ff28 702b → 未解析尾部(4字节)
于是我修改了python的解析代码
class GSensorParser:
@staticmethod
def parse_gsensor(data: bytes) -> Dict[str, int]:
if len(data) < 12:
return {}
return {
"x1": bytes_to_signed_short(data[0], data[1]),
"y1": bytes_to_signed_short(data[2], data[3]),
"z1": bytes_to_signed_short(data[4], data[5]),
"x2": bytes_to_signed_short(data[6], data[7]),
"y2": bytes_to_signed_short(data[8], data[9]),
"z2": bytes_to_signed_short(data[10], data[11]),
}
这样能符合常理一些
最后
此心率带有3个模式:APP中是普通,EC和HRV三种,代码中对应的是Sport,ECG和HRV。
这三个模式下,采样10s的数据,获得的ECG总点数都在1250左右
与ReadEcgAppSr读取的125对应
我不禁要问:真的有宣传中的1000Hz采样率吗?