Before This:
长久以来,饱受华为蓝牙耳机APP客户端的拉跨使用体验折磨(目前华为耳机的控制APP主要有两个,一个是已经断更许久的智慧音频,另一个是及其臃肿的智慧生活,我就买了一个耳机,你给我上全家生态?)在很久之前就有了重构一个控制APP的想法了,但是奈何实在没有时间,所以耽搁了。大二开学,实习也结束了,今天终于有时间来实现以下这个想法了。不过本篇文章只会讲到比较关键的地方,至于具体的实现方法,后续可能会有帖子讲到。
1. 从哪里入手?
我们想要实现一个耳机控制APP,最主要的两个目标如下:
- 向耳机发送数据,接收耳机的返回数据
- 解析耳机返回的数据,得到我们需要的数据形式
在第一个目标中,由于每个厂商对于发送的数据格式的定义不同,所以我们如果想得到具体的控制数据内容,最简单的方法就是直接逆向现有的官方耳机控制APP,如“智慧音频”、“智慧生活”等,然后进行静态代码分析,找到APP中用来发送(或接收)蓝牙数据到设备的总方法,然后使用Xposed
框架来Hook
目标方法,从而获得对应数据内容,方便后续的模拟测试。
在第二个目标中,相比发送数据到设备来实现对设备的控制,难度要高出一些,你可能需要逆向静态分析在APP接收到设备的数据后,数据的整个解析过程,例如,设备返回的电量数据会是一组数组,字面上来看是毫无意义的,需要查询APP的解析过程,然后进行模拟实现,来获取真正的数值。
2. 逆向哪一个APP ?
本篇文章中,将会对“智慧音频”APP进行逆向分析。
1 | APP Name: 智慧音频 |
- 为什么不选择“智慧生活”APP?
我们这边把主要逆向“智慧音频”APP而不是“智慧生活”APP,一方面是“智慧生活”APP本身体量很大,涉及到的类和方法过于繁多,会严重干扰我们的方法确定和追溯;另一方面,在“智慧生活”APP中,耳机的控制功能是作为一个单独的耳机控制插件出现的,使用时需要单独下载,插件为APK格式,作为一个module嵌入到“智慧生活”主APP中,导致后面HOOK难度增高,其次,我们在研究时需要单独抓取网络下载GET包获取这个插件本体,而且浅浅的分析后,发现该插件和主APP间存在部分方法互相调用,对后面分析也不利。总的来说,还是为了方便。
3. SPP和BLE是什么?
在我们开始逆向工作前,我们不得不先来了解一下蓝牙模块的这两种通讯方式,由于通信方式上的差异,APP中向设备发送数据的实现方法也就截然不同,因此我们需要简单的了解下这两种通信方式。
BLE(Bluetooth Low Energy,蓝牙低功耗)和 SPP(Serial Port Profile,串行端口协议)是两种不同的蓝牙通信方式,主要区别在于它们的设计目的、应用场景和工作方式。
1. BLE(Bluetooth Low Energy)
- 简介:BLE 是一种低功耗的蓝牙技术,设计用于需要间歇性、小数据量传输的设备,例如传感器、可穿戴设备、智能家居设备等。
- 特点:
- 低功耗:BLE 设备在闲置时消耗极低的能量,非常适合需要长时间运行的设备。
- 低数据传输量:BLE 适合传输较小的数据块,通常用于状态信息的传输,如心率、温度等。
- 适用场景:健康监测设备(如心率监测器)、物联网设备(如各家的手环,手表)、跟踪器(果子的AirTag)、智能锁等。
- 通信方式:基于事件驱动和广播模式的通信,主要通过 GATT(Generic Attribute Profile)协议进行数据交互。
2. SPP(Serial Port Profile)
- 简介:SPP 是经典蓝牙的一种协议,用来模拟有线串行通信接口,主要用于在两个设备之间传输大量连续的数据。
- 特点:
- 高数据传输量:SPP 支持相对较高的数据传输速率,适用于需要连续、较大数据流的场景。
- 适用场景:传统的无线串行连接替代方案,如蓝牙耳机、无线打印机、无线POS机、PDA、蓝牙串口模块等。
- 通信方式:模拟传统串口的数据传输方式,通常是点对点的长连接,稳定且持续的传输数据。
3. 总结-两者的区别
特性 | BLE | SPP |
---|---|---|
功耗 | 低 | 较高 |
数据量 | 少 | 多 |
通信方式 | 事件驱动,广播模式 | 点对点,持续连接 |
适用场景 | 可穿戴设备,传感器 | 需要连续数据传输的设备,如蓝牙耳机 |
传输协议 | 操作系统提供的GATT接口 | 串口通讯 |
4. 逆向源码获取方法
在我们了解到这两种蓝牙通讯方式后,我们可以基本确定,蓝牙耳机是使用SPP协议与设备进行通讯,我们需要在“智慧音频”APP中找到传输数据的函数;
我们这边先思考一下,发送与接收数据到蓝牙耳机的方法在哪里?
在一般的蓝牙APP中,通常为了保证代码的简洁性与易读性,会实现一个总的数据传输方法,而我们想要获取设备间的通讯数据,就需要找到相应的数据传输方法;
但是这里就遇到了一个问题,我们作为逆向者,并不知道这个总的数据发送方法具体的方法名称,有的时候可以通过猜测,来直接定位到,不过大部分情况(或者代码被混淆)下,难以直接确定;
思考一下,实际上,APP为用户提供了操作页面,用户点击相应的功能按钮、开关,APP会调用这个按钮对应的后端处理的API接口,总的调用流程大概如下:
所以为了精准的定位这个总的数据传输接口,我们可以从小的功能入手;
使用Jadx-GUI反编译APK,来获取APP的反编译源码;
1. 数据发送部分
我们先从简单的例子入手,比如现在APP需要切换耳机的降噪模式(主动降噪一般简称ANC),我们在Jadx中搜索关键词ANCmode
,位置选择“方法名”,搜索到的结果如下:
在搜索结果中,我们很明显的就发现了一个名为MbbCmdApi
的类,而相对应存在的方法包均为get ··· / set ···
,从中我们可以大胆地猜测,这个类就是我们找的不同功能的后端API接口,打开该类后,证实了这一点;
我们这边取其中一个方法来进行分析:
1 | // 定义方法,接收并传入参数 |
此方法调用另一个重载的方法 getANCModeAndLevel
,并传入 CURRENT_DEVICE
和 iRspListener
作为参数。可以理解为是为了获取当前设备的 ANC 模式和级别,并通过传入的监听器处理结果;
继续分析其调用的方法 getANCModeAndLevel
:
1 | // 定义方法,接收并传入参数 |
此方法通过 sendData 方法向指定的设备发送请求,以获取 ANC 模式和级别,并通过 iRspListener 处理响应;
我们来具体分析一下传入的数据内容:
- 调用
sendData
方法,传入四个参数:str
:传入的字符串参数。MbbAppLayer.getANCModeAndLevel()
:调用MbbAppLayer
类中的getANCModeAndLevel()
方法,可能返回与 ANC 模式和级别相关的数据。FaqConstants.NO_SN
:一个常量,通常用于表示序列号或其他标识符,具体取决于FaqConstants
类的定义。new d4(this, iRspListener)
:创建一个新的d4
对象,并将当前实例 (this
) 和iRspListener
作为参数传递。d4
可能是一个内部类或回调,用于处理数据发送后的响应。
继续分析其调用的方法 sendData
:
1 | // 定义私有方法,接收并传入4个参数 |
在我们大致分析完该方法后,发现该方法为发送数据的关键方法,我们可以右键这个方法,点击“查找用例”,可以发现,基本上该Api类中的所有功能方法都调用了这个方法,我们可以基本确定,这个方法为关键的数据传输方法;
2. 数据接收部分
上面我们成功获取到了数据发送的方法,现在我们来寻找数据接收的方法;
前面在我们找到的数据发送部分中,这个方法所在的类似乎并不是该APP底层的数据通讯类,在APP的底层通讯类中,一般是同时包括发送与接收两个方法的,我们继续向下探索;
1 | // 调用 handlingDevice 的 send 方法,传入相关数据 |
在发送的方法中,调用了send
方法,我们利用Jadx
一路追溯下去,最终找到了SPPManager
类,仔细观察这个类,从命名中也能看得出来是一个关键通讯类,在其中我们发现了我们需要的所有关键方法:write
与receive
。
receive
方法:
1 | // 定义公共方法,传入数组参数 |
write
方法:
1 | // 定义公共方法,传入数组参数 |
至此,我们已经找到了我们所需要的所有关键方法;设备间通讯数据类型为数组,接下来,我们通过Xposed Hook
来获取设备间的通讯数据。
5. 构建Hook程序
在AndroidStudio
中制作一个基本的Hook
程序;
1. 引入Xposed依赖
在settings.gradle.kts
中添加Xposed
的Maven
仓库
1 | dependencyResolutionManagement { |
进入我们app目录下的build.gradle.kts
引入xposed
的依赖
1 | dependencies { |
2. 添加模块作用域(可选)
在LSPosed
中,启用模块需要勾选相应的作用域;我们在res/values
目录下创建一个名叫arrays
的资源文件,添加作用域应用:
1 | <resources> |
3. 声明模块
我们想让LSPosed框架识别到此APP是一个Xposed模块,就需要进行声明;
回到AndroidManifest.xml
文件里,我们将<application ... />
改成以下形式(注意,是改成!就是把结尾的/>
换成> </application>
),修改后的文件如下:
1 |
|
然后在src/main
目录下创建一个文件夹名叫assets
,并且创建一个文件叫xposed_init
,注意,它没有后缀名!
接着我们需要创建一个入口类,名叫MainHook
(或者随便你想取什么名字都行)。
创建好后回到我们的xposed_init
里并用文本文件的方式打开它,输入我们刚刚创建的类的完整路径,如:
1 | com.sakongapps.spp_hooker.MainHook |
4. 编写模块
篇幅有限,我们在此不详细解释Xposed模块其中,该Hook类还是很简单的,可以结合注释加以理解。
其中,示例的MainHook类如下:
1 | class MainHook : IXposedHookLoadPackage { |
在编写完成MainHook
类后,我们前往 Build - Build App Bundle(s) / APK(s) - Build APK(s)
中进行构建APK,完成后右下角会弹出提示,点击Locate即可打开APK的所在文件夹,我们在手机上安装模块,并勾选对应的作用域。
6. 抓取通讯数据
我们将手机连接到电脑上,进行Logcat
抓取,同时需要过滤关键词,然后在手机上启动“智慧音频APP”,即可看到相关的通讯数据了;
这里有一点需要注意,如果想使用Linux
中的grep
过滤命令,我们需要调用Android Shell
中的logcat
,而不是Windows
中的adb logcat
,命令如下:
1 | adb shell "logcat | grep LSPosed-Bridge" |
抓取到的通讯信息如下:
1 | 10-12 14:52:42.887 27980 27980 I LSPosed-Bridge: Loading class com.sakongapps.spp_hooker.MainHook |
7. Final:
在我们可以抓取到通讯数据后,我们就可以通过使用不同功能来捕捉对应通讯数据,这样可以更加方便地定位我们需求功能的通讯命令,但是如果需要更加完整的通讯命令清单,或者后面需要实现对返回数据的解析,就需要继续逆向追溯返回数据的解析部分,后面有时间的话,会继续出相关的文章;
最后感谢您的耐心阅读!
8. Thanks:
- 本文部分引用/借鉴了以下文章,对作者表示感谢: