蓝牙计量插座协议逆向

蓝牙计量插座协议逆向

逆向分析

因官方只提供了小程序,且保存数据的数量有限制,我准备逆向其协议,实现自动定时同步数据。

image-20250716231001704

使用XposedBLE hook android.bluetooth.BluetoothGatt.writeCharacteristic()

https://github.com/albert2lyu/XposedBLE/blob/master/app/src/main/java/com/wojiaowanghaha/xposedble/BLEHook.java#L21C80-L21C93

1
2
07-16 22:55:22.913 16269 27553 E BLE_HOOK: writeCharacteristic   str :6a 6b 86 85 4e 46 4e 58 a5  bluetoothGattCharacter
istic49535343-8841-43f4-a8d4-ecbe34729bb3

Github搜索”49535343-8841-43f4-a8d4-ecbe34729bb3”,发现这个是蓝牙串口读写相关的,Github上能找到很多读写蓝牙数据的例子。

让Deepseek写了一段Android代码,简单修改后就可以读写蓝牙串口数据。

image-20250716225948408

但数据是加密的,反编译小程序代码,发现密钥和mac相关:

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
function N(l, a) {
c("ymBleWriteLongBuf", s, n, o, a.buf),
function() {
if (!e.CFG_MAC) return m = 0, 0;
let t = e.CFG_MAC.split(":");
m = parseInt(t[5], 16) + parseInt(t[4], 16) + parseInt(t[3], 16) + parseInt(t[2], 16) + parseInt(t[1], 16) + parseInt(t[0], 16), m &= 255, u = parseInt(t[5], 16) + parseInt(t[4], 16) + 10, u &= 65535, d = parseInt(t[5], 16) + 11, d <<= 8, d |= parseInt(t[4], 16), d &= 65535, c("ymBleGetLongBufkey", e.CFG_MAC, m, u, d)
}();
let B = function(e, t) {
let i = [],
s = 0;
e ? (i[s++] = 236, i[s++] = 237, y = 236, h = 237) : (i[s++] = 147, i[s++] = 146, y = 147, h = 146), s += 2;
for (let e = 0; e < t.length; e++) i[s + e] = t[e];
s += t.length;
const n = C(t);
i[s++] = n >> 8 & 255, i[s++] = 255 & n, i[2] = s - 6 >> 8 & 255, i[3] = s - 6 & 255;
for (let e = 0; e < i.length; e++) i[e] ^= m;
return c("ymBleCreateLongBuf", n, i), i
}(l, a.buf),
I = B;
if (r = 1, v = 0, f = 0, B.length > 20)
for (let e = 0; e < B.length; e += 20) e + 20 >= B.length ? (I = B.slice(e, B.length), g({
deviceId: s,
serviceId: n,
characteristicId: o,
value: new Int8Array(I).buffer,
success: e => {
c("ymBleWriteLongBuf success 1", a), t.ymEventOn({
event: "devrecvbuf",
timeout: a.timeout,
success(e) {
wx.stopBluetoothDevicesDiscovery({}), c("ymBleWriteLongBuf 1 devciceId=", e), r = 0, a.success(e)
},
fail(e) {
i = 0, r = 0, "请求响应超时" === e.msg && (e.msg = "设备响应超时"), a.fail(e)
}
})
},
fail: e => {
c("ymBleWriteLongBuf fail 1", a), i = 0, r = 0, a.fail(e)
}
})) : (I = B.slice(e, e + 20), g({
deviceId: s,
serviceId: n,
characteristicId: o,
value: new Int8Array(I).buffer,
success: e => {},
fail: e => (r = 0, i = 0, c("ymBleWriteLongBuf fail 2", a), a.fail(e))
}));
else I = B, g({
deviceId: s,
serviceId: n,
characteristicId: o,
value: new Int8Array(I).buffer,
success: e => {
c("ymBleWriteLongBuf success 3", a), t.ymEventOn({
event: "devrecvbuf",
timeout: a.timeout,
success(e) {
wx.stopBluetoothDevicesDiscovery({}), c("ymBleWriteLongBuf 3 ymEventOn success", e), r = 0, a.success(e)
},
fail(e) {
c("ymBleWriteLongBuf 3 ymEventOn fail", e), i = 0, r = 0, "请求响应超时" === e.msg && (e.msg = "设备响应超时"), a.fail(e)
}
})
},
fail: e => {
c("ymBleWriteLongBuf fail 3", a), r = 0, i = 0, a.fail(e)
}
})
}

DeepSeek分析结果:

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
解密过程说明:
密钥生成:

将MAC地址按:分割成6部分

将每部分从十六进制转为整数

计算所有字节的和,然后取低8位作为解密密钥(sum & 0xFF

数据解密:

对加密缓冲区的每个字节与密钥进行异或操作

异或操作具有自反性:(data ^ key) ^ key = data

返回解密后的字节序列

注意事项:
MAC地址格式:必须符合标准格式 XX:XX:XX:XX:XX:XX(大写或小写)

加密逻辑:此解密基于以下加密特征:

整个数据包(包括帧头、长度字段、数据和校验和)都使用相同密钥异或加密

密钥仅依赖于MAC地址,不随时间变化

数据验证:解密后的数据需要自行验证:

检查帧头(0xEC/0xED0x93/0x94

验证长度字段和校验和(原始代码中的C(t)函数)

协议帧结构参考(解密后):
偏移 字段 长度 说明
0 帧头 2字节 0xEC/0xED0x93/0x94
2 数据长度 2字节 大端格式的有效数据长度
4 有效载荷 N字节 原始应用数据
4+N 校验和 2字节 对有效载荷的校验值
此函数只处理核心解密过程,完整的协议解析需要结合帧结构和校验逻辑实现。

但尝试用mac计算出来的密钥解密后还是不对,一翻折腾后发现读取到的数据中最多的就是0x22,于是用0x22与所有读取到的数据异或,就得到了正确的数据。

1
2
3
4
5
6
#加密数据
6a6b86a62a3f22222257362222a4280bf42b2b30b5432f8e22222210222218a822282331f4a6

6a6b86a62a3f22222257342222a4290bf72b2e30b3432f8e22222210222218a922282331a7a4

6a6b86a62a3f222222573f2222a430083e2b27309f432f8e22222210222218ac222823b34d64

解密后,对比两组数据,就可以分析出各个字段的含义。比如data11.bin中累计电量的数据应该是0x7514,即29972。

image-20250716213435147

最终自动同步方案

创建一个Android APP,打开后自动连接蓝牙插座获取数据,然后将数据传递到http服务器后自动退出App。在使用crontab定期启动Android APP就实现了自动同步数据的目标。