
记一次 0x2A00/0x0003(GATT Device Name) = UAF028 的「智能语音循环扇」的逆向过程。
TL; DR:
我算一下我用多少时间逆向出来的。
时长:四个小时。
工具链:一部开发者选项打开 HCI,可以 Vol+/Vol- 按下去产生全设备 Debug File 的小米手机,一个 ChatGPT Codex。
方式方法:
1、打开微信用手机号注册他们的小程序
2、绑定风扇(扫描列表直接显示设备蓝牙 MAC 感谢助攻)加入列表。
3、进入设备控制页面,一边录屏记录顺序一边操作。
4、导出设备 Debug File,然后把这些喂给 Codex 分析。
5、再按照 3-4 补充其他按钮的 HCI。
最后四个小时除了厂商绑定(小程序认可这个风扇是他们的,这里有小程序发的 Challenge/Response 模型)之外的所有控制项(左右/上下摆风,风速,氛围灯,三种模式…)和 BLE KeepAlive 都出来了,甚至可以写个 PyQt 用电脑操作。
因为他本来就不是以联网智能为卖点,它只有蓝牙连接。主要还是用语音识别控制。
0、为什么是这个风扇
今日的近场通信笑话。
2026 年 5 月的你的母亲:看这个全新的循环扇,可以语音控制。
你:(拿着吊牌)还可以用小程序。(打开小程序)(配对中)没连 Wi-Fi?
那这不是现代近场科技排除法吗?
毕竟,对于大多数手机而言:
phone = {“Wi-Fi”, “Bluetooth”}
然后这个设备的宣告是:
device = {“Bluetooth”}
common = phone & device
{“Bluetooth”}
那剩下的就是 Bluetooth
当然,现实世界不会这么简单。
因为“智能”这个词经常包含:
云端控制;
局域网控制;
蓝牙控制;
红外控制;
厂商自定义协议。
但为什么我如此确定呢?
今日的 nRF Connect 笑话
后台的 nRF Connect(在我连接风扇那一刻):
“嘿,我发现你打开了这个蓝牙设备,要调试吗?”
你:
🤓👆
谢谢,你帮我确定了一个问题但是……
我不知道它应该怎么说话。
于是问题变成:
这个风扇,到底是怎么和手机说话的?
1、挑战-回应:Exists。实际操作:Not Used。
好的。对于一个蓝牙控制的黑盒风扇,我们能做什么?
第一步:
让这个黑盒自己说话。
不是猜。
不是拆。
而是给它一个输入,然后观察回应。
我们手里有:
一个现成的小程序;
一台 Xiaomi 生产的 Android 手机;
以及:
得到的
/data/misc/bluetooth/logs中的 Bluetooth 调试记录。
我:
(深度思考)
嗯,用户现在想要获得……
🤣
开个玩笑。本文作者不是 DeepSeek。
但是这个流程,确实在类似研究中高度统一:
让机器成为 oracle machine。
我们向系统输入确定的信息(instruction),然后观察它的输出(response)。
形式化一点:
Instruction
|
v
Black Box
|
v
Response
我们不知道内部实现。
但我们可以通过大量输入输出建立模型。
于是我们获得了类似这样的东西:
xxx.btsnoop_hci.log
xxx.btsnoop_hci.log

一开始,我假设:
一个可以被语音控制的现代设备,至少应该会对控制面做一些保护。
所以我花了一些时间研究它的绑定逻辑。
然后发现:
它确实存在 Challenge-Response。


但是,它做的事情不是:
“你有没有权限控制这个风扇?”
而是:
“你是不是官方认可的那个风扇?”
因为挑战-回应模型只在第一次用小程序绑定的时候存在。

这是一个非常微妙的区别。
因为我推测的安全模型是:
微信小程序
|
|
Challenge / Response
|
v
设备控制
看起来像:
“认证之后才能控制。”
实际观察到的模型:
绑定流程
|
v
设备归属确认
---
BLE Control
|
v
Write Characteristic
认证保护的是:
Ownership / Binding
而不是:
Authorization / Command Access
于是产生了一个典型的安全模型 mismatch:
开发者想解决:
“不要让别人绑定我的设备。”
研究者观察: “如果有人已经知道控制协议呢?”
两个问题从本质上不同。
2、于是决定重放。
在刚刚研究「挑战 / 回应」模型的同时,我的 Agent 也已经开始分析手机发送的写入操作。

具体分析结果可以在 Repo 中查看。
本文不打算完整展开每一个字段,而希望用更简单的方式描述这个过程:
我们不是破解了一个协议,而是在学习一个协议。
在排除了绑定流程带来的干扰后,我们进入实际操作阶段。
尝试一次重放。
根据前面的分析:
App 在发送控制指令时,会维护一个递增的序列号。
类似:
Command A:
seq = 01
Command B:
seq = 02
Command C:
seq = 03
于是问题出现:
在真正写入 GATT 时,这个序列号到底重要吗?
或者:
它只是 App 自己维护的状态?
……
因为按照经验,这类序列号往往意味着:
-
防止重放攻击(Replay Attack);
-
保持通信状态同步;
-
或者参与某种校验计算。
因此,在真正把数据写入 GATT 之前,我已经做好了和各种安全措施斗智斗勇的准备:
序列号(Sequence Number);
XOR 混淆;
Session Token;
或者某种一次性校验值。
……
结果。
发了一个包过去。
氛围灯亮了。
……
再发一个旧包过去。
风扇也摇头了。
……
它根本不在意包里的序列号。

更让我意外的是,设备回读(Notify / Read)得到的状态数据,与抓包时官方 App 收到的原始数据流完全一致。
也就是说:
写入没有反重放(Replay Protection);
回读没有额外混淆(Obfuscation);
控制平面就是一个相当直接的 GATT 协议。
我盯着 tshark 和我的 Codex 看了好一会儿。
?
我原本已经做好了和序列号、XOR,甚至各种奇奇怪怪的校验算法斗智斗勇的心理准备。
结果你告诉我:
GATT 控制平面基本就是明文协议?
🤣
后来想了很久,我才意识到:
也许问题不在于它“没有安全”。
而在于——
它采用的是另一套安全模型。
这个风扇没有 Wi-Fi。
没有云端。
没有公网接口。
它始终假设:
能和它通信的人,就已经站在它附近。
于是,它面对的威胁模型更接近于:
一台传统红外遥控风扇。
区别只是:
以前:
红外遥控器 ↓ 风扇
现在:
手机 ↓ BLE ↓ 风扇
控制介质发生了变化。
安全假设却基本保持一致。
3、Differential Analysis
如果要给这一阶段起一个名字,我认为 Differential Analysis(差分分析) 是比较准确的。
前面的工作告诉了我们:
可以发送控制指令;
可以重放;
风扇会产生稳定的回应。
接下来要解决的问题只有一个:
每一个 bit,到底代表什么?
放在几年前,这一步通常意味着大量的人眼工程。
先抓包。
然后:
00 01 03 17
00 01 04 18
在 WireShark 盯着它们看半天。
直到用 ASCII 或者其他方式发现:
哦,第三个 byte 变了。
……
但现在是 2026 年。
于是流程变成了:
- 将 Android Bug Report 压缩包交给 Agent。
- 让 Agent 自动生成 grep、tshark 等命令,从 BTSnoop 中提取相关数据包。
- 根据 Prompt 提供的信息,例如:
- 风扇的 MAC 地址;
- 操作顺序;
- 哪些操作是成对出现(例如打开/关闭、开始/停止);
- 已观察到的循环规律;
先构建出一张初始的数据对应关系表。
特别的,这时候我们得到的还不是答案。
而是一份待验证的假设。
随后进入真正重要的阶段:
Human in the Loop。
AI 会提出:
「这个 byte 很可能对应风速。」
我负责:
「好,那我只改变风速,再抓一次。」
AI 又会提出:
「这个字段似乎和摆风有关。」
我继续:
「那就只操作摆风。」

整个过程不断循环:
提出假设
↓
人工设计实验
↓
重新抓包
↓
AI 更新模型
↓
人工验证
协议就是这样一点一点浮现出来的。
不过,这里也暴露出了目前 Agent 一个很有意思的边界。
很多人会觉得:
「既然 AI 能分析协议,那为什么不直接让它自己完成整个过程?」
实际上,恰恰相反。
真正困难的,并不是分析,而是在真实世界里稳定地完成那些看似机械的操作。
例如:
打开手机。
点击某一个按钮。
等待动画结束。
再点击另一个按钮。
保证每一次实验只改变一个变量。
对于人来说,这几乎不用思考。
但对于今天(2026 年)的 Agent 来说,这反而是最难的部分之一。
它无法真正理解另一个物理设备的状态。
尤其是在 Android 的触摸界面里。
它不知道:
- 哪个按钮到底有没有点中;
- 当前页面是否真的已经切换;
- 风扇现在究竟是一档还是二档;
- 某次实验失败,到底是协议的问题,还是只是手指点偏了、动画没结束。
也就是说,AI 很擅长推理,却很难可靠地感知现实。
所以,这个流程最后变成了一种很有意思的协作方式:
AI 负责处理海量数据、寻找模式、提出假设。
人负责设计实验、操作真实设备,并判断这个假设到底有没有意义。
这其实也是今天许多 Agent 最真实的使用方式。
不是让 AI 替代人,而是让 AI 负责思考,人负责连接真实世界。
4、然后就结束了。
是的。
结束了。
最终得到的成果,并不是一段神奇的 Exploit,也不是一个能够突破认证的工具。
而是一份:
里面记录了这个设备控制协议的行为。
我决定战术性放弃绑定流程的逆向。
原因很简单。
-
根据前面的分析,它不会影响后续的控制交互。
- 更具体来说,从安全模型来看,我一直都站在绑定链路的中间:
官方云服务
│
官方 App
│
我
│
风扇
所以就密码学而言,我破解这个东西的 P/E Ratio,不是很高。
我关心的是:
控制协议是什么。
而不是:
绑定协议为什么这样设计。
更重要的是……
- 我不想为了研究这个问题尝试拆 MCU 然后把风扇拆坏了。
🤣
于是剩下的工作几乎变成了自动化。
让 Agent:
- 根据 Protocol.md 构造 BlueZ 发包;
- 自动测试每一条写入;
- 自动读取设备状态; 我:
- 负责站在旁边,看风扇到底有没有按预期运转。

最后再让 Agent 写一个 PyQt 应用。

整个项目,就这样结束了。
四个小时。
其中,把表格里的所有功能逐项验证,大约花了一个小时。
还是直播的时候完成的。
……
我确实没有预料到事情会这么简单。
但回过头来看这个设备的安全模型,一切又显得十分合理。
整个逆向过程中,真正让我卡住的,反而只有一个字段:
0xFF / 0x00
它最初被标记为:
每 20 秒一次的 KeepAlive。
但当时,我并不确定它真正的意义。
直到后来,Agent 开始出现一种奇怪的现象:
连接成功。
紧接着。
GATT 被设备主动断开。

我最开始以为:
是不是哪里还有认证没有完成?
是不是遗漏了某个字段?
后来……
我决定先去洗个澡。
洗澡的时候,人反而不会盯着十六进制数据。
脑子开始重新整理整个系统。
突然想到:
等等。
如果:
0xFF / 0x00
真的只是 KeepAlive。
而且。
这个风扇本身只是一个非常简单的 BLE 外设。
那么有没有一种可能:
它根本就是单用户机制。
也就是说:
同一时间。
它只允许一个 GATT Client。
回想之前几次失败:
都是我刚刚还在用手机调整设置。
然后立刻切换到 Agent。
时间完全对得上。
于是回去做了一个非常简单的验证实验。
结果证明:
这个推测是正确的。
于是,这一条也被补充进了 Protocol.md。
NaN
很遗憾,本文标题使用了一定程度的 clickbait 表达。
作者本人并非计算机科学专业学生,而是就读于机械工程学院(Mechanical Engineering School,具体名称因学校体系而异)。
本文并非针对特定厂商或产品的安全评价,亦无意对其实现方式进行负面判断。本文希望记录一次实际边缘设备协议分析过程,并讨论当前 IoT 设备安全中的一个趋势:
随着低成本无线通信模块的普及,设备控制面的安全边界正在从传统网络侧逐渐迁移至近场通信协议本身。
在缺少复杂联网功能的设备中,BLE 等近距离通信接口仍然可能成为实际攻击面。
本项目相关代码已开源于 GitHub,供技术交流与研究参考:
https://github.com/iasoc/uaf028_ui_refresh
如对项目或文章内容有任何问题,欢迎通过邮件交流:
本文由 IaSoC 「Let’s Break It Open and See(我就是想看看它怎么工作的)」工作组呈现。 https://tnsc.iasoc.org/security
(╮(╯_╰)╭ 要是那天晚上直播间的人都来点歌我大概要多花两个小时。)
-1、你到底写完这个文没有。
本研究过程中没有循环扇受到伤害。
这个循环扇的 MAC 地址的 OUI 在公共查询列表中不可见生产厂商。
¯\_(ツ)_/¯