首页
社区
课程
招聘
对某嵌入式设备声波配网的研究
2023-12-5 00:05 8394

对某嵌入式设备声波配网的研究

2023-12-5 00:05
8394

前言

img

在一年前笔者研究过一款嵌入式设备,当时就对其声波配网的逻辑感到十分新奇,但后面专注于漏洞挖掘就没有深入研究

最近在研究一些诸如sub-ghz、蓝牙、射频等无线通信的东西,于是通过声波传递信息的方式又激发了我研究的兴趣,所以就有了这篇文章,简要记录一下我的一些发现和心得

硬件分析

将主控芯片上的散热片去除后,我们可以通过放大镜读取主控芯片上的丝印:

img

使用的是lngenic t20

芯片的官方网站:Ingenic-北京君正集成电路股份有限公司

音频功放为XPT4890:

img

具体的音频接收是通过设备上的咪头,咪头接收到声波后产生电信号,然后主控芯片对电信号进行处理

音频分析

首先采集一段配网时候的音频进行分析

通过aux公对公线,能直接从手机的耳机孔输出音频到电脑上,通过windows自带的录音软件或者OBS就可以采集

img

音频处理

查阅资料发现声波配网和声音的频率有很大关系,所以这一步主要是通过脚本提取声波的频率特征

这里使用librosa这个库,但是它不支持m4a,只支持对wav进行处理,所以首先需要将m4a转换为wav

m4a转wav

转换使用的是pydub这个库,它对音频的转换要安装FFmpeg:

Windows 10系统下安装FFmpeg教程详解_ffmpeg window10-CSDN博客

下载链接:https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip

把bin目录添加到环境变量就行了:

img

转换代码:

1
2
3
4
5
6
7
8
9
10
import librosa
from pydub import AudioSegment
import matplotlib.pyplot as plt
import numpy as np
input_file = './声波配网信息.m4a'
output_file = './声波配网信息.wav'
 
# 将M4A文件转换为WAV格式
audio = AudioSegment.from_file(input_file, format='m4a')
audio.export(output_file, format='wav')

频谱生成

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
audio_path = './声波配网信息.wav'
audio_data, sample_rate = librosa.load(audio_path)
 
spectrogram = librosa.stft(audio_data)
 
plt.figure(figsize=(12, 8))
plt.title('Spectrogram')
plt.xlabel('Time')
plt.ylabel('Frequency')
plt.imshow(librosa.amplitude_to_db(abs(spectrogram), ref=np.max),
           origin='lower', aspect='auto', cmap='coolwarm')
plt.colorbar(format='%+2.0f dB')
plt.tight_layout()
plt.show()

效果:

img

提取频率数组

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 使用librosa进行频谱分析,并将能量值转换为分贝单位
spectrogram = librosa.amplitude_to_db(librosa.stft(audio_data), ref=np.max)
 
# 设置阈值,只记录强度大于-10dB的频率
threshold = -10
 
# 初始化空的频率数组
frequencies = []
 
# 遍历每个时间帧
for i in range(spectrogram.shape[1]):
    # 获取当前时间帧的能量值
    frame = spectrogram[:, i]
    # 获取强度大于阈值的频率索引
    freq_indices = np.where(frame > threshold)[0]
    # 将符合条件的频率添加到频率数组中
    frequencies.extend(freq_indices)
 
# 去除重复的频率,并按升序排列
frequencies = sorted(set(frequencies))
 
print(frequencies)

效果:

img

尝试特殊的wifi和密码

当wifi的名称是8个1,密码是32个1的情况

img

发现有一部分和上面的图片是重叠的,而且这张图片的频率分布确实更平整了,说明确实和频率是有关系的

但仍然看不出什么东西,需要具体逆向分析一下通信过程中传输协议是什么样的

通信细节分析

通信概要

整个通信大体可以概括为:

设备配套的app将wifi的ssid和passwd通过声波这种媒介传递到设备中,设备接收到信息后连接上对应的wifi并访问运营商的云端服务器,并给予用户连接成功/失败的反馈

接收方

在固件中能看到一个名为"connect_wifi.sh"的sh脚本,脚本主要的作用是通过wpa_supplicant.conf中的内容去连接wifi

img

wpa_supplicant.conf的格式如下:

img

搜索存在"wpa_supplicant.conf"的elf可以发现有如下调用:

img

在"ipcam"这个elf中可以看到如下内容:

img

那么可以确定是ipcam这个elf对声波进行识别,并把内容保存到"wpa_supplicant.conf"中,然后在"daemon"中执行"connect_wifi.sh"连接wifi

该函数会被当作参数传递到"vr_setRecognizerListener"中进行处理:

img

img

然后就到了c++的魅力时刻:

img

这里应该是某个结构体的虚表跳转,那么想继续从固件里看出东西的话就很困难了

但其实可以看出有很多地方都有调试的打印,而且这个设备是有UART调试串口的,所以应该可以通过调试打印的信息定位后续处理的位置

遗憾的是,调试口被笔者焊坏掉了(QAQ):

img

而且从板子上飞线出来也不是很好飞

所以暂时放弃对接收方的研究,去逆向一下发送方(app)的逻辑

发送方app的逆向分析

app脱壳

对app进行解压,确定不是flutter架构:

img

Pkid查壳发现是《梆梆》加固:

img

用jadx直接打开也能看到比较明显的加固特征

img

通过frida dexdump脱壳试试:

1
frida-dexdump -FU

img

能成功dump下来59个dex,并且每一个dex都可以通过JEB进行查看了:

img

定位关键dex

首先需要从59个dex中定位到哪些是我们需要的

我们可以让app运行到声波配网的页面:

img

然后通过adb shell执行"dumpsys activity top | grep ACTIVITY"这行命令

就可以列出顶层的activity,也就是需要尤为关注的内容:

img

通过"Everthing"或者其它字符串搜索工具搜索"SoundWaveGuideActivity",能够定位到如下dex

img

用JEB对该dex进行简单的逆向可以知道整个流程应该是在"soundWaveGuide.control"中进行的

img

首先输入的SSID和passwd会被分别存入this.F和this.G:

img

然后到下面这个地方会对传入的SSID和passwd进行处理然后通过"play"方法进行播放

img

"play"和"encodeString"方法都不是在这个dex实现的,同样可以通过搜关键字符串的形式进行查找,最后在16.dex里找到了"play"方法的具体实现:

img

并且可以发现这里用的是一个原生库的函数:

img

直接检索"play"发现有很多的原生库都能匹配:

img

好在这个app在运行的时候会有一些关键的log,能够通过"adb logcat"进行打印获取:

img

通过搜索"play text"最终定位了具体的原生库文件:

img

对原生库文件进行分析

原生库文件一样是用c++写的,直接逆向分析也会遇到虚表跳转

img

好在对于安卓有frida这种神器,可以打印具体的内存信息获取虚表跳转的地址

但是要hook的地方实在是太多了,基本上是一步一hook非常麻烦

好在app的开发者偷懒了,通过在github上搜原生库中的一个函数名"slg_gen"就能直接搜到一个名为"SmartBoy"的项目:

img

和原生库中的一模一样,而且其相关调用链也高度一致,也能在项目中搜到adb logcat的打印字符串:

img

项目地址:https://github.com/npnet/SmartBoy/tree/af0403fc7151f22381b0d0da7df3812f64ea5b3a

更为夸张的是,笔者在项目的"main.c"里发现了如下内容:

img

好家伙,直接不演了,黑盒测试变成白盒了

img

CSDN链接:http://blog.csdn.net/softlgh

通过CSDN的链接我们能下载下来linux、windows、macos、android这几个主流平台的声波配网的全部demo,这里面显然就包括了android的源码

在源码里我们能看到熟悉的原生库,以及熟悉的代码

img

img

只不过demo的baseFreq是16000,而app里的是4000:

img

至此,我们的逆向工作其实已经可以结束了,因为我们完成了逆向的最高境界——逆出源码(笑)

后续的工作更多的就是对传输的协议(对ssid+passwd字符串的处理)的分析了

协议分析

分析协议主要是分析"play"的字符串长啥样

官方app提供了设备的日志,从日志里能看见对传入声波的解析后的内容:

img

主要是ssid psd phone这三个东西,猜测"play"的字符串就和这三个东西有关

在JEB反编译的java代码中可以看见这三个内容经过了两次处理:

img

第一次处理是对三个字符串的拼接,这里就不赘述

第二次处理是对第一次拼接的字符串进行编码:

img

hexChar是一个数组:

img

在encodeData里会把传入的字符串分为8类:

img

在encodeType里对字符串的每一位分为4类:

img

img

其中is64Char其实是对前三个的汇总,然后多了"-"(45)和""(95),其实就是把"+/"换成"-"的base64字符串

仔细分析encodeType可以做出如下归纳:

1
2
3
4
5
6
7
8
type0:全是数字(1111)
type1:前面是小写后面是数字(aa11)
type2:前面是除数字外的base64后面全是数字的(_A11)
type3:含有非base64的(@#$%)
type4:前面全是数字然后出现不是数字的base64的(11_a)
type5:全是小写(aaaa)
type6:只要是由base64组成的(a-_b)
type7:全是大写(AAAA)

看上去有些重叠,比如type6涵盖了除type3的所有。这是因为它返回的时候也是有先后顺序的:

img

对于8种不同的type的详细encode,感兴趣的师傅可以查阅一下csdn上的源码,这里挑两个比较有意思的

首先是ssid和passwd全是数字的情况,通过下面这个js脚本可以打印encode前后的字符串(这里是对csdn上demo的hook):

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
function hook_java(){
     
    Java.perform(function(){
         
        Java.use("voice.encoder.DataEncoder").encodeSSIDWiFi.implementation = function(ssid,wifi){
            console.log("java hooked");
            console.log(ssid);
            console.log(wifi);
            var enc = this.encodeSSIDWiFi(ssid,wifi);
            return enc;
        }
        Java.use("voice.encoder.DataEncoder").encodeData.implementation = function(a1,a2,a3){
            //console.log("encode hooked");
            let str = String.fromCharCode.apply(null, a1);
            console.log("string before encode:"+str);
            var enc = this.encodeData(a1,a2,a3);
            return enc;
        }
         
        Java.use("voice.encoder.VoicePlayer").play.overload('java.lang.String', 'int', 'int').implementation = function(a1,a2,a3){
            //console.log("play hooked");
            console.log("string after encode:"+a1);
            //console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()));
            this.play(a1,a2,a3);
            return;
        }  
    });
}
hook_java();

img

前面这个"2"是ssid的2 | [16&(len-1)]>>4得到的,"7"是ssid-1

2,3的两个"0"是type,表示ssid和passwd都是全数字,而全数字的encode就直接返回原来的字符串

然后是无规律的base64字符串的情况:

img

一开始我也很好奇这里为啥不是"6"而是"c",一看源码发现它乘了个2:

img

感觉这个设计也挺奇怪的

以passwd为例,用cyberchef encode也能进行验证:

img

至此,对于协议层面的分析我们也能告一段落了

后续的工作以及收尾

原生库中的内容其实主要是对编码后的字符进行播放,这涉及的是一个比较庞大的框架,以及涉及很多的信号与系统的原理

而对于我们安全研究人员,其实更关心的是如何控制发送的数据,然后这些数据传入到设备后有没有可能产生漏洞,所以对声波和字符的对应关系笔者就浅尝辄止了

而在知道如何编码ssid和wifi以及手上有so文件的情况下,我们可以编写一个demo一样的app,调用原生库函数来向目标设备发送配网信息,所以对声波的分析就没有再进行下去了(其实是笔者在经历长时间被c++虚函数的折磨后啥都没有发现的借口罢了23333)

总结

通过这次的研究之旅,体验了从硬件、音频、设备固件、app、原生库这几个不同的层面对一个设备进行分析并最终成功破解了声波通信的协议,达到了可以发送符合协议的声波数据的目标,可谓收获良多

最后还要感谢void*爷和oacia爷对于本篇提到的安卓逆向的指导ORZ


[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。

最后于 2023-12-5 00:05 被Nameless_a编辑 ,原因:
收藏
点赞13
打赏
分享
最新回复 (4)
雪    币: 19349
活跃值: (28971)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
秋狝 2023-12-5 11:13
2
1
感谢分享
雪    币: 508
活跃值: (4295)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
bwner 1 2023-12-5 16:02
3
0
感谢分享
雪    币: 283
活跃值: (3079)
能力值: ( LV5,RANK:75 )
在线值:
发帖
回帖
粉丝
囧囧 2023-12-6 09:54
4
0
感谢分享
雪    币: 26
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
wx_培森 2024-2-20 11:23
5
0
感谢分享
游客
登录 | 注册 方可回帖
返回