某蓝牙水表App的分析

学校宿舍淋浴用的蓝牙水表近期迎来了固件更新,我们也被迫迁移到了新的 App, 新 App 不给权限还拒绝启动,甚为讨厌,因此打算拆下 App。

写在前面

由于我并没有充足的经验,本文可以说是我的踩坑记也不为过。因为这个工作并没有得到授权,本文章也会尽可能地避开该 App 的名称。在研究过程中也不会去对蓝牙水表造成物理上的破坏,也不会去 Fuzz 协议。

本文仅供学习交流,不得用于违法行为,所承担法律责任均与本文作者无关。

考虑切入点

  • App 源码
  • 通讯协议
    • 蓝牙通讯协议
    • 网络通讯协议

先从最简单的入手

个人感觉比起研究源码,直接对通讯协议进行分析比较快一些。

这里可以获取两种不同的通讯,分别是服务器到 App 的通讯,一种是 App 到蓝牙水表的通讯。比较靠谱的则是 App 到蓝牙水表的通讯。

蓝牙通讯

那么首先我打算把 App 到蓝牙水表之间的通讯拉出来研究一下。Android 设备可以在开发者设置中启用 HCP snooping log。启用 HCI snooping log,打开 App 完成一个完整的使用周期,拖出 log 即可。

我的设备并不会将捕获的报文写入到 /sdcard,我需要生成一个 bugreport 才能够拿到捕获的报文。使用 adb bugreport 即可获取。捕获的报文可以在 FS/data/misc/bluetooth/logs 中得到。

使用 Wireshark 打开 btsnoop_hci.log,一番查找后,我们可以看到对应的 traffic:

这里我们需要学习以下这个 App 使用的协议栈,在 Wireshark 中我们可以清楚看到该 App 使用了 Bluetooth Attribute Protocol。

关于 ATT 与 GATT, 可以参考这一篇文献: Bluetooth: ATT and GATT

我们完全可以在这个 capture 文件内提取到对应的 traffic。

但在我写这篇文章前,我并没有仔细看这个 capture file,导致我认为对应的 traffic 没有被捕获下来,就有了接下来的步骤。

寻找 API 域名

由于这个软件使用了 SSL Pinning,我们无法轻易的使用手机端的抓包工具进行,也无法直接丢给 burpsuite 之类的软件。想到了好久之前在路由器上抓包的方法。

刚好我的路由器有安装tcpdump,我们不妨使用plink连接至路由器,启用 tcpdump 将捕获的 traffic 写至 stdout,再管道至 wireshark

如: plink -ssh -i .\.ssh\main.ppk -batch <username>@192.168.100.1 "/usr/sbin/tcpdump -U -w - host 192.168.100.34"| "C:\Program Files\Wireshark\Wireshark.exe" -k -i -

这样就会与路由器建立一个 SSH 连接,同时启动 tcpdump 将捕获的数据回传到 Wireshark,不妨直接 filter 下 DNS 如下图:

除此之外,我们也可以去看一下 TLS 握手过程中的 Client Hello 的 SNI 来确定这是不是我们要找的域名,如下图:

应该就是这个 API 域名了,很明显,还是得想办法解决 SSL Pinning 来抓包。

研究源码

那就拿包过来拆,我们首先获取对应的 APK 包,直接丢进 jadx-gui,看一眼:

感觉这个包有点少,而且目录下还有 mpaas ,感觉有点像是 flutter 的开发方式,但是查了一圈文档后发现并非如此。于是搜索了下自己感觉比较可疑的包名 qihoo.util,发现是使用了产品 360加固保

解决加固

于是自己就去 Google 了一下,发现可以使用 frida 搭配对应的脚本将真正的 dex 文件解出来。

参考 [原创]Frida-Apk-Unpack 脱壳工具

刚好自己手上有一台 root 设备,安装上应用,禁用掉 Magisk Hide, 启用 frida-server, 执行命令 frida -U --no-pause -l dexDump.js -f com.<REDACTED>

对应的 dexDump.js 请参考 GuoQiang1993/Frida-Apk-Unpack

可获得以下几个文件:

这次可以使用 jadx,进行反编译了,最后获得结构如下:

寻找 HTTP API 列表

通过浏览对应的 HTTP API 列表,并没有找到蓝牙水表相关的 API,因此决定继续深挖下,发现其使用了 mpaas 小程序。

找到用于 SSL Pinning 的代码

首先查看该 App 所使用的 HTTP Client,发现 Retrofit + OkHttp3

App 的 SSL Pinning

先解决掉 OkHttp3 的 SSL Pinning,但这个代码很明显跟我们找到的代码不太一样,这个代码是返回 boolean 的,而我们找到的代码是通过抛异常的,简单修改下 return value 为 void。

https://techblog.mediaservice.net/2020/11/android-okhttp3-4-2-certificate-pinning-bypass-for-frida-and-brida/

但貌似还是无法正常劫持,因此决定再努努力,发现直接将我们的证书加入到 trust store 是一个更好的解决方案。

https://codeshare.frida.re/@pcipolloni/universal-android-ssl-pinning-bypass-with-frida/

这样下来,我们可以正常劫持 HTTP 请求了,但我们还是打不开小程序,只好继续深挖。

小程序的 SSL Pinning

通过一番寻找,结合 mpaas 小程序 API 锁定了 com.alipay.mobile.nebulaconfig.util.H5BizPluginList.java,这个文件应该就是注册小程序的 js 组件了,其中两个文件引起了我的关注:

  • com.alipay.mobile.nebulaappproxy.plugin.tinyapp.TinyTlsWhiteListPlugin
  • com.alipay.mobile.nebulaappproxy.plugin.tinyapp.TinyAppRequestPlugin

TinyAppRequestPlugin 找到了突破点,但发现其还是在使用 TrustManager。

只好跑一下 frida-trace,发现其使用了 UC 的 WebView,遂决定 dig a little bit deeper。

frida-trace -U -j “*!certificate/isu” -f com.xiaolian.prometheus

https://github.com/ytsutano/axmldec/

axmldec -o output.xml AndroidManifest.xml

https://stackoverflow.com/questions/51174130/certificate-pinning-generate-sha256-pinning-key-from-certificate-crt-file

openssl s_client -connect api.<REDACTED>.com:443 -showcerts

然后利用工具生成这个证书的哈希值,写个脚本比对下。发现什么东西也没匹配到。最后决定放弃了。

不知道 HTTP Canary 是什么原理,能够抓到需要的数据,等有机会再去处理吧。

小程序源码的研究

我们可以通过 app 的通讯获取到小程序的 package。直接解压即可得一下文件。

我们可以在 index.worker.js 中找到小程序的核心代码,通过代码我们可以逆向出业务流程。

我们不妨将其内 Unicode 编码后的字符串恢复出来,结合捕获的 traffic,总结其流程如下:

  1. 获取可用小程序
  2. 获取用户余额
  3. 获取支付方式
  4. 获取上次使用蓝牙水表地点
  5. 获取上次使用使用服务及状态
  6. 获取用户预支付限额
  7. 获取设备列表
  8. 检查设备状态
  9. 获取设备连接细节
  10. 处理设备通讯若干次 (该请求会出现多次 直到设备关阀)
  11. 获取订单
  12. 支付订单

我们根据源码内容,可知一次正常操作中与水表通讯共有以下过程:

  1. 连接 (CONNECT)
  2. 更新费率 (UPDATE_RATE)
  3. 开阀 (OPEN_VALVE)
  4. 关阀 (CLOSE_VALVE)
  5. 结算 (CHECKOUT)

研究蓝牙协议

通过阅读源码可知,所有与蓝牙水表的通讯都是由服务端进行处理与下发的,我们只需要写程序去跑一个完整的业务流程就好了,认证部分看起来使用了 JWT,直接根据主程序的业务逻辑跑就好了。前段时间比较忙,就咕了两个星期。不过在写到这里时 (2021-10-07),我已经打算走读了。学校洗澡水水质可能有问题,洗几次澡头发就变超级柴,对头发不好。在之前我也做了一部分协议分析,这里就分享下我的思路吧。

我首先进行了大量采样,获取了大量跑完一个洗澡流程的数据。对着这些数据尝试找到其规律,通过规律对协议细节进行还原。

这里看到总体长度基本固定,排除 TLV 模型。

根据规律推断如下:

所有的通讯都有一个三字节的头,字段分别如下:

1
| Source | OP | Flags |

Source: 1 byte, 0xa7 客户端到蓝牙水表, 0xa8 蓝牙水表到客户端
OP: 0x01 CONNECT, 02 OPEN VALVE, 04 CLOSE VALVE, 05 UPDATE RATE, 07 CHECKOUT, 08 RESUME & UPDATE RATE
Flags: 0x00 后面没有数据, 0x08 后面有更多数据

每条消息最后一字节为校验位: 计算方式为 前面所有单字节之和去摸 0x100

其中 CONNECT 包的 Request 为:

1
2
3
     a7     |      01    |     08      |  aa bb cc dd  | 00 00 00 00 | 00
Server OP:CONNECT More Data UNKNOWN Stays zero
考虑到隐私问题,这里改掉这里的变量,抹去校验位

Response 为

1
2
3
     a8     |      01    |     08      |     11 ff     |    00 f4    |     00 00 00 00 00 00    | 00
Server OP:CONNECT More Data Counter 1 Counter 2 UNKNOW (Stays constant)
Counter 为小端序 这里考虑到隐私问题,抹掉后面的常量以及校验位 这里的 Counter 第一个每次会加 0x02 第二个每次会减去 0x02

后面的 UPDATE_RATE 的 Request, OPEN_VALVE 的 Request, CLOSE_VALUE 的 Request, CHECKOUT 的 Request 和 Response 都是 8 字节,随机性特别大的数据。

其中,CLOSE_VALVE 的 Response 是 16 字节 随机性较大的数据。

这里合理怀疑是做了加密,而第一个 Request 中的随机数据即为其密钥或者 IV。

这里根据可能的 block size,选出了可能用到的算法:

  • Blowfish
  • TEA
  • RC2
  • More…

但具体是哪个,是难以判断的。

最后,我认为这个协议有重放攻击的可能。我 CONNECT 部分的数据重放后第二次拿到的数据是跟好几天前我拿到的数据是完全一致的。

为什么我要逆向这个 App

我觉得这个 App 很不可思议,明明与业务无关的权限也要,于是打算研究下。抓到登录包的时候我已经被恶心到了。

这里附上代码佐证: