从零逆向研究一款 BLE 智能插座纯命令行控制的思路与探索本文记录了对一款市售 BLE 智能插座易微联 eWeLink 系的逆向研究过程。仅限个人设备学习和安全研究目的不提供完整可用代码请勿用于控制他人设备。一、研究动机市面上的 BLE 智能插座大多依赖厂商 App 控制用户无法通过命令行或脚本自动化。笔者的需求很简单用电脑定时开关显示器电源但不想每次都掏手机点 App。于是产生了一个问题能不能脱离 App用电脑直接控制答案是可以但需要走一段逆向的路。二、设备信息项目信息设备类型BLE 智能插座纯蓝牙非 Wi-Fi控制方式厂商 AppReact Native 应用通信方式纯 BLE 广播非 GATT 连接关键特征这是一款纯 BLE 设备不走云端所有控制都在本地蓝牙射频范围内完成。这为脱离 App 控制提供了理论基础。三、研究思路总览APK 反编译 → JS 逆向 → 协议结构分析 → 加密算法分析 → Linux 蓝牙发包 → 闭环验证 → 常驻服务整体思路分四个阶段逆向阶段反编译 App定位 BLE 协议代码分析报文结构和加密算法发包阶段在 Linux 上用蓝牙底层工具发送构造好的广播验证阶段用 DDC/CI 检测显示器通断电实现闭环控制部署阶段Linux 常驻服务 Windows 客户端脚本四、逆向阶段4.1 确定反编译目标这类厂商的 App 通常是 React Native 应用核心逻辑不在 Java 层而是打包在 JS bundle 里。判断方法很简单解压 APK 后看assets/目录下有没有index.android.bundle。如果有说明业务逻辑在 JS 里classes*.dex只是原生模块的壳。4.2 反编译 Hermes 字节码React Native 新版默认使用 Hermes 引擎bundle 是 Hermes 字节码不是明文 JS。需要用专用反编译工具如 hermes-dec将字节码还原为可读的 JS 伪代码。反编译后会得到一个巨大的 JS 文件几百万行所有业务逻辑都在里面。提示Hermes 反编译的输出不是完美源码变量名会被替换但函数逻辑、字符串常量、数值常量都保留足够分析。4.3 定位协议代码在反编译输出中搜索关键词可以定位到与 BLE 广播相关的模块。关键线索包括搜索BluetoothLeAdvertiser、startAdvertising→ 找到发包入口搜索getManufactureID、companyId→ 找到厂商 AD 构造搜索encrypt、xor、crc→ 找到加密和校验逻辑搜索toggle、turnOn、turnOff→ 找到命令字定义通过这种方式可以锁定两到三个核心模块分别负责广播报文构造和设备控制逻辑。4.4 分析报文结构通过阅读报文构造函数通常是一个_createBytes或类似名称的方法可以还原出完整的报文结构。这类 BLE 广播控制报文通常是这样的布局┌─────────────────────────────────────────────┐ │ 固定头 (数字节) │ ├─────────────────────────────────────────────┤ │ 加密载荷 (十几字节) │ │ ├─ 版本号 │ │ ├─ 防重放计数器 │ │ ├─ 设备地址 │ │ ├─ 命令数据 (内层加密后) │ │ └─ 随机数 │ ├─────────────────────────────────────────────┤ │ CRC 校验 (2字节, 小端序) │ └─────────────────────────────────────────────┘具体偏移和长度需要从代码中逐字节确认不能猜。4.5 分析加密算法关键发现这类低功耗 BLE 设备通常不用 AES而是用简单的 XOR 加密。原因是BLE 广播每次发送都要重新加密设备端 MCU 算力有限AES 太重。XOR 方案在足够隐蔽的前提下也能达到一定安全性。典型模式是双层 XOR明文 → [内层: 单字节随机数 XOR] → [外层: 固定密钥循环 XOR] → 密文内层用随机生成的单字节rand对命令数据逐字节 XORrand本身附在报文里传输但也会被外层加密外层用一段固定长度的密钥对整个明文做循环 XOR密钥提取固定密钥通常硬编码在 JS 模块中以数组形式定义。可以通过搜索常量定义、交叉验证找到。具体密钥值本文不公开。安全提示如果厂商所有同型号设备共用一个固定密钥那这个密钥一旦泄露所有人设备都暴露。这本身是一个值得向厂商报告的安全问题。4.6 分析 CRC 校验校验算法一般是 CRC16 的某种变种。通过阅读代码中的 CRC 函数可以确认初始值如0x5555不是标准0x0000或0xFFFF多项式如0x1021CCITT 标准计算范围对加密前的明文计算不是密文字节序可能先算大端再交换为小端这些细节都需要从代码中逐个确认差一个位就完全不同。4.7 命令字通过分析turnOn()、turnOff()、setTimer()等函数可以提取出命令字功能命令字说明翻转开关~0x10toggle 语义无独立开/关定时器~0x18下发定时规则清码/解绑~0x13解除绑定关键发现turnOn()和turnOff()内部调用的是同一个toggle()函数产生的报文完全一样。设备收到后自行切换状态控制器不知道当前是开还是关。具体命令字数值本文略去请自行从代码中确认。4.8 防重放机制设备会记录最后一次接受的count值滚动计数器拒绝任何count 该值的报文。这意味着如果手机最后用的 count 是 41你发 count8 的包会被拒绝你必须发一个 count 41 的包才能被接受一旦你的包被接受手机之前缓存的低 count 包也会失效实战经验PC 接管控制后count 从一个较高值如 200起跳每次递增并持久化避免与手机冲突。五、发包阶段为什么需要 Linux5.1 Windows 的困境在 Windows 上尝试了多种方案方案结果原因WinRTBluetoothLEAdvertisementPublisher失败无法设为可连接广播ADV_INDraw HCI legacy 命令失败返回 Command Disallowedraw HCI Extended 命令失败返回错误码核心原因这类插座只认可连接广播ADV_IND 特定 Flags。Windows 的蓝牙 API 抽象层太高无法控制广播类型和原始 Flags 字节。Windows 能发的都是非连接广播被插座直接丢弃。5.2 Linux 方案在 Linux 上可以使用btmgmtBlueZ 管理工具直接控制蓝牙控制器发送广播# 初始化btmgmt--index0power on btmgmt--index0le on btmgmt--index0connectable on# 清旧实例btmgmt--index0rm-adv1# 发送广播关键命令btmgmt--index0add-adv-c\-d31字节AD数据的hex\-D持续秒数1关键参数参数含义-cconnectable发 ADV_IND 可连接广播-d31 字节完整 AD 数据Flags Manufacturer AD payload-D持续时间秒到时自动停止AD 数据结构┌───────────────────┬──────────────────────────────┬─────────────────┐ │ Flags AD (3字节) │ Manufacturer AD (4N字节) │ Payload │ │ 02 01 06 │ 1b ff companyId 2字节 │ 加密报文 │ └───────────────────┴──────────────────────────────┴─────────────────┘Flags 字段02 01 06表示 LE General Discoverable BR/EDR Not Supported这是 BLE 设备的标准 Flags插座固件会检查这个。5.3 关键踩坑尝试结果根因不带-c非连接广播插座不认插座固件要求 ADV_IND不带 Flags AD插座不认固件检查 Flags 字段带 Flags 但不带-c超限btmgmt 自动加 Flags 导致超过 31 字节raw HCI 命令错误码控制器拒绝直接操作结论必须用btmgmt add-adv -c发完整的 31 字节 AD缺一不可。5.4 bluetoothd 冲突问题实测发现bluetoothd守护进程会和btmgmt抢控制权导致广播发出去了但设备不响应。解决方案发广播前先停bluetoothd重置hci0再初始化btmgmtsystemctl stop bluetooth hciconfig hci0 down hciconfig hci0 up btmgmt--index0power on btmgmt--index0le on btmgmt--index0connectable on每次发广播都执行一遍这个初始化序列确保控制器状态干净。六、验证阶段闭环检测6.1 为什么需要验证因为命令是 toggle 语义发一次广播只是翻转状态不知道翻到哪边了。必须检测实际效果。6.2 DDC/CI 检测如果插座控制的是显示器可以用 DDC/CIDisplay Data Channel Command Interface通过 I2C 总线读取显示器的 VCP Feature0xD6Power Modepower_mode 值含义1on2standby3suspend4off (soft)5off (hard)能读到1/2/3 通电读不到 断电断电后 I2C 无应答。Python 库monitorcontrol封装了这个操作frommonitorcontrolimportget_monitorsforminget_monitors():withm:pmm.get_power_mode()# 1on, 2standby, 3suspend, 4/5off6.3 闭环逻辑off 命令: 1. 检测当前显示器状态 2. 已断电 → 跳过避免 toggle 反送上电 3. 已通电 → 发 BLE 广播 → 等待 ~2s → DDC/CI 检测 4. 断电成功 → 返回 5. 仍通电 → 重试最多 3 次 6. 3 次都没断 → 报错退出不假装成功 on 命令: 同上等待 ~5s显示器启动需要时间设计原则off 命令必须保证断电。如果广播没被插座收到射频干扰、距离太远重试机制会再发。3 次都没断就明确报错。七、部署阶段常驻服务7.1 架构┌──────────────────┐ ┌──────────────────┐ │ Windows 端 │ HTTP │ Linux 端 │ │ 客户端脚本 │ ────── │ 常驻 daemon │ │ │ :8421 │ │ │ • 发 off/on 命令 │ │ • 构造 BLE 报文 │ │ • DDC/CI 验证 │ ────── │ • btmgmt 发广播 │ │ • 重试逻辑 │ JSON │ • systemd 常驻 │ └──────────────────┘ └──────────────────┘ │ │ BLE 广播 ▼ ┌──────────────┐ │ BLE 智能插座 │ └──────────────┘ │ ▼ ┌──────────────┐ │ 显示器/负载 │ └──────────────┘7.2 Linux 端 daemon用 Pythonhttp.server监听一个端口收到 HTTP 请求后构造报文并调btmgmt发广播。为什么不用每次 SSH因为 SSH 连接开销大~200ms常驻 HTTP 请求只要几 ms。而且 systemd 管理更可靠。部署为 systemd 服务实现开机自启[Unit] DescriptionBLE Plug Daemon Afterbluetooth.target [Service] ExecStart/usr/bin/python3 /opt/ble_daemon/ble_daemon.py Restartalways RestartSec3 [Install] WantedBymulti-user.target资源占用内存 ~20MB空闲 CPU 0%socket 阻塞等待不轮询。7.3 Windows 端客户端用户入口脚本支持off/on/status命令。HTTP 请求注意一个坑Python 标准库urllib默认用 HTTP/1.1 keep-alivedaemon 的BaseHTTPRequestHandler处理完不主动关连接urlopen会等到超时。解决方案用原始 socket 发 HTTP/1.0 请求daemon 处理完自动关连接importsocket,jsondefhttp_request(host,port,method,path,timeout30):ssocket.create_connection((host,port),timeouttimeout)reqf{method}{path}HTTP/1.0\r\nHost:{host}\r\n\r\ns.sendall(req.encode())databwhileTrue:chunks.recv(4096)ifnotchunk:breakdatachunk s.close()bodydata.split(b\r\n\r\n,1)[1]returnjson.loads(body)7.4 状态指示灯可以用 Scroll Lock LED 作为脚本执行状态指示——执行时亮结束灭。实现注意GetKeyState在控制台程序中读不到真实键盘 toggle 状态依赖消息队列改用GetAsyncKeyState读硬件状态。开灯时记录初始状态关灯时恢复到初始值避免脚本结束后灯还亮着。7.5 手机控制daemon 加上 GET 接口后手机浏览器直接访问http://linux-ip:8421/off就能控制。可以做个简单网页收藏到手机桌面快捷方式。注意手机版没有 DDC/CI 验证只发广播。如果需要发了一定要断电的保障还是要用 Windows 端带闭环验证的命令。八、定时器协议除了 toggle还分析了定时器命令。这类 BLE 插座的定时规则通过广播写入插座固件手机关机后插座本地 RTC 执行。定时数据的结构大致是定时器 randData: [0] 固定 [1] cmdtimer [2] timerId 定时器 ID [3] repeatType 0不重复, 1每天 [4] actionBitmask 动作位掩码多通道编码 [5] hours 距目标时间小时差 [6] minutes1 距目标时间分钟差1 [7] enable 0 或 1多通道插座的 actionBitmask 用位编码每通道 2 位表示 on/off/keep。具体数值本文略去请自行从代码中确认。九、总结研究方法论这次研究的核心方法论确定代码位置React Native App 的逻辑在 JS bundle 不在 Java 层反编译工具链Hermes 字节码需要专用反编译器关键词搜索定位startAdvertising、encrypt、crc等逐字节确认结构报文每个字节的含义和偏移都要从代码确认不能猜交叉验证JS 层和 dex 层的常量应一致不一致说明找错了发包环境Windows 蓝牙 API 太高必须用 Linux 底层工具闭环验证toggle 语义必须有外部验证手段确定方向关键技术发现控制通道是纯 BLE 广播不是 GATT 连接——无需配对加密是双层 XOR不是 AES——MCU 算力限制count 防重放——必须单调递增ADV_IND Flags是硬性要求——非连接广播被丢弃Windows 无法直发——API 抽象层不支持控制广播类型toggle 语义——靠外部检测确定方向安全建议如果厂商使用全局固定密钥这是一个设计缺陷。建议每设备独立密钥基于设备地址派生使用 AES-CCM 等标准认证加密增加设备端对控制器的认证免责声明本文仅记录对个人设备的安全研究过程不提供完整可用代码和密钥。请勿将文中方法用于控制他人设备。如厂商认为本文内容涉及安全问题欢迎联系删除。本文不提供完整密钥、完整可运行代码仅作为技术思路分享。