白帽故事 · 2025年3月25日 0

利用 API 和硬件黑客技术攻克数百万台智能电子秤

前言

国外白帽小哥在一次度假期间,注意到酒店健身房的一台电子秤屏幕上有一个奇怪的图标。

这是一个 WiFi 图标,人们决定将体重计连接到互联网是一个好主意,通过亚马逊搜索,可以看到大量设备带有 WiFi 或蓝牙连接功能,其中许多都带有可疑且相似的移动应用程序。

file

事实上,许多应用程序都是由同一家 OEM 开发的,即使它们是由不同的 OEM 开发,代码库也略有不同,但快速浏览一下相关的 Android 应用程序就会发现,其中许多应用程序都使用了相同的通用库,例如 com.qingniu.heightscale

file

file

虽然 BLE 协议相关的代码很有趣,我们能够找到通过蓝牙与这些设备通信的正确操作码,但其中大多数都已被 openScale 项目逆向并记录了下来。

因此试图找出近距离物理接触的本地漏洞利用并不是一件很有趣的事情。

深入研究

如果你的目标是破解所有设备,而不仅仅是一台设备,那么一个关键目标就是设备的关联流程。

例如,当你第一次购买智能设备并将其从包装盒中取出时,你通常需要登录移动应用程序并扫描二维码或通过蓝牙与设备配对,完成后,你的用户账户才能与物理设备关联起来。

这可能是一个棘手的过程,从工厂开始,每个设备都需要一个唯一的设备标识符/密钥,这样你在扫描设备 A 的二维码时就不会意外地与另一个设备 B 配对。

最不安全的方法是使用静态字符串,例如 UUID、MAC 地址或序列号,虽然这些可能可以作为标识符 ,但作为身份验证密钥并不安全。
(即使它们可能是随机生成的,很难被暴力破解,但如果它们被泄露,要撤销它们将非常困难)

相对安全的选择是生成加密密钥,例如公钥/私钥对,这仍会使其成为物理内存提取的目标,而且如果密钥生成过程存在某些薄弱环节,攻击者仍有可能为任何设备生成任意密钥。

传统的解决方案是依靠良好的老式公钥基础设施和证书架构,这样就可以轻松撤销受损的证书。

典型流程如下:

  1. 用户安装移动应用程序并使用其用户帐户登录
  2. 通过该应用程序,用户连接到硬件设备
  3. 硬件设备的密钥被发送到手机应用程序
  4. 移动应用程序将用户的密钥(例如会话令牌)和设备的密钥都发送到服务器
  5. 服务器确认密钥的真实性并将用户帐户与硬件设备关联起来
  6. 用户成功通过互联网远程控制硬件设备并获取数据

看起来合理,那么可能会出现什么问题呢?

OEM 中的 SQL 注入 (宝塔 WAF 绕过)

OEM 一开始就犯了错误,你甚至无需购买实体设备,就可以枚举移动应用程序上可用的 API 端点,包括一个有趣的 api/ota/update 端点。

多亏了 Android 移动应用程序反编译的 Java 代码,我们能够轻松重建所需的 JSON 主体参数,然而,即使输入正确,似乎也没能获得更多‘惊喜’。

但是在探索 API 端点时,白帽小哥发现有几个端点会受到基本的 SQL 注入攻击。

有趣的是,该服务器使用了一种名为 Baota Cloud WAF (BT-WAF) ,它比白帽小哥之前遇到的许多典型 WAF 要强大得多。

特别是 /api/device/getDeviceInfo 端点允许查找设备的序列号,该序列号被该制造商用作标识符和身份验证密钥 。

序列号本身用于 /api/device/bindv2 端点,该端点将绑定或关联请求用户的帐户与序列号引用的设备!“序列号”本身是一个随机生成的 MAC 地址,存储在设备上。

下面是易受攻击端点的初始Payload:

{
  "serialnumber":"'001122334455"
}

这里没什么可利用的,如果有第二个注入点,也许能够设计出更精巧的Payload,经过反复尝试,白帽小哥最终找到了 BT-WAF 的绕过方法:

{
  "serialnumber":"'or\n@@version\nlimit 1\noffset 123#"
}

最终的 SQL 注入是: SELECT * FROM devices WHERE serial = 'INJECTION'or\n@@version\nlimit 1\noffset 123#' 。这里有两个关键的绕过Gadgets:

  1. @@version 总是计算结果为真,可以用来代替 1=1
  2. \n 换行符可以代替空格来分隔语句

有了这个注入点,就可以泄露任何设备的设备信息,包括用作身份验证密钥的序列号!

事实证明,通过增加 offset ,这个数字超过了二十万台设备。

在 Withings WBS06 上获取串行调试 SHELL

当白帽小哥开始研究其它设备时,他偶然发现了 Withings Body 体重秤。

与其它设备类似,它具有 WiFi 和蓝牙连接功能以及定制的移动应用程序,这是一个更加知名的品牌。

通过应用程序的 API 提取固件进行进一步分析相对容易,然而与路由器等使用的更复杂的固件不同,这些固件通常包含完整的文件系统和 Linux 操作系统,这是一个裸机 ARM 固件。

虽然我们可以逆向裸机 ARM,但有经验的专家会告诉你,这是一件非常困难的事。

尽管如此,白帽小哥还是决定尝试了一下,按照 Barun 的博客文章 “分析 Ghidra 中的裸机固件二进制文件” 进行操作,获得了所需的一些大致信息,其中包括通过 FCC 认证文件中的内部图片确定 WBS06 的微控制器型号,并设置正确的内存映射。

吸引白帽小哥注意力的是一对散落的字符串,难道是……一个SHELL?

Connection Manager Shell Command
Usage:
  wifi <wifi_sync_flags>
            Attempts a Wifi sync with the given flags.
            wifi_sync_flags is a combination of the following flags:
                0x01 (allow update), 0x02 (store DbLib), 0x04 (send DbLib), 0x08 (send 
                rawdata),
                0x10 (send wlog), 0x20 (send events), 0x40 (send extras)
  wifi_no_update <wifi_sync_flags>
            Attempts a Wifi sync, no update allowed (even if set in flags).
  wifi_update <wifi_sync_flags>
            Attempts a Wifi sync, allows update if available (even if not set in 
            flags).
  bt        Attempts a Bluetooth sync
  do   Attempts a Wifi/Cellular sync and fallback to Bluetooth if it fails.

智能电子称为什么要装SHELL?经过一番调查,白帽小哥偶然发现了另一位研究人员在 Reddit 上发布的一篇帖子 ,他已经弄清楚了早期型号 WBS05 上的 UART 引脚。

看起来很简单,白帽小哥开始兴奋地在 WBS06 尝试,最大的线索是 WBS06 底部也有相同的三个孔,分别对应 Tx、Rx 和 GND UART 引脚,FCC文档中的内部图片充分证实了这一点。

file

file

然而白帽小哥的努力却失败了,尽管用逻辑分析仪计算出了正确的波特率,但串行连接一直返回乱码。

经过几个小时的痛苦尝试,白帽小哥意识到可能是廉价 CP2102 USB 转 TTL 转换器导致了这个问题,更换了更加可靠的 FT232 终于得到了小哥需要的结果。

file

终于有了一个调试 SHELL,我们可以探索设备上存储的所有数据,包括证书、密钥等等!

虽然这很令人兴奋,但实际意义却不大——“破解”你已经拥有的设备,这没什么大不了。

‘打破’用户-设备关联

为了真正测试远程向量,白帽小哥需要充分了解设备如何向 API 服务器进行身份验证并执行用户-设备关联。

例如, connection_manager wifi命令将尝试连接到 API 服务器并带有详细的调试日志。

shell>connection_manager wifi

[info][CM] Connection manager request, action = 3, wifi sync flags = 0xffffffff
[VAS] t:15
[info][CM] Start with cnlib action = 3
[VAS] t:15
[CNLIB] Recovered LastCnx from DbLib
[AM] Defuse id 4
[TIME] Current time (timestamp) 0 , 8h 0min 0sec
[TIME] Waking up in 16h 90min 60sec
[TIME] Add random time 0
[AM] Set id 3 at 63060
[AM] Set id 1 at 600
[CNLIB] Try to connect via wifi (1)
[DBLIB][ERASEBANK] Bank 1
[info][DBLIB][SUBSADD] 14 0
[info][CM] Initializ[VAS] t:15
e Wifi
[WIFIM] Request
[WIFIM] init
[VAS] t:15
wifi_chip_enable
bcm43438_request
== Set dcdc_sync ==
bcm43438_request: pwron module
[WIFIMFW] current_fw == FW_2 1
version 1
size 80
[WIFIMFW] wifi_crc: 0
[WIFIMFW] Take current bank
[WIFIMFW] Firmware block 1a8000 : OK
[WIFIMFW] Wifi Offset 21a370, lenght 58d1d
[WWD] HT Clock available in 31 ms
[WWD] mac: a4:7e:fa:19:2c:f6
supported channels: 13
[WIFIM] init OK
[info][CM] Wifi initialized
[WIFIM] join_configured_ap
[VAS] t:15
[WIFIM] ssid = ...
[WIFIM] key  = ...
[WIFIM] WPA key already saved
[WWD] join: ssid=<...>, sec=0x00400004, key=<...>
[WDM] wwdm_join_event_handler: state=1, wifim_err=9, stopped=0
[WDM] wwdm_join_event_handler: state=2, wifim_err=9, stopped=0
[WDM] wwdm_join_event_handler: state=2, wifim_err=0, stopped=1
[WDM] wwdm_join_event_handler: stopped
[WWD] join: wiced_res=0, wifim_res=0
[info][WIFIM] join: attempt #0, rc=0
[info][WIFIM] join: SSID <...> join rc=0 after 1 attempts
[VAS] t:15
[VAS] t:15
[info][WIFIM] join: RSSI=-64
[VAS] t:15
[WIFIM] connect: use static ip
[WIFIM] Interface UP (Status : 0xf)
[WIFIM] netif_up: use DHCP
[WIFIM] Interface UP (Status : 0xf)
[WIFIM] netif_up:
[WIFIM] IP=192.168.0.9
[WIFIM] Mask=255.255.255.0
[WIFIM] Gw=192.168.0.1
[WIFIM] DNS[0]=192.168.0.1
[WIFIM] DNS[1]=0.0.0.0
[WIFIM] connect_cfg_ap: success
[info][CM] Joined configured AP successfully
[VAS] t:15
[info][CM] Store DbLib...
[VAS] t:15
[DBLIB][ERASEBANK] Bank 2
[info][CM] Store DbLib done
[HTT[VAS] t:15

S_CLIENT] Init
[HTTPS_CLIENT] Init
[info][CM] Wslib init successful, carry on
[VAS] t:15

[WS] WsLib_StartSession

[WS] __WsLib_Once
[WS] Https_client browsing <https://wbs06-ws.withings.net/once?appliver=1181&appname=WBS06&apppfm=device>
[HTTPS_CLIENT] New connection or Adress/Security Changed
[HTTPS_CLIENT] Close
[HTTPS_CLIENT] Init
[HTTPS_CLIENT] Handshake started
{"status":0,"body":{"user":[{"userid":...,"screens":[{"id":66,"deactivable_status":6,"src":1,"embid":11,"rk":1}]},...]}}
>
[DBLIB][ERASEBANK] Bank 1
[WS] WSLIB_OK
[WS] Https_client browsing <https://wbs06-ws.withings.net/v2/summary?appliver=1181&appname=WBS06&apppfm=device>
[HTTPS_CLIENT] Socket already opened
[WS] Params <action=getforscale&sessionid=...>
{"status":0,"body":[{...}]}
>
[WS] WSLIB_OK
[USLIB] FLUSH STORED MEASURE
[USLIB] 0 measure(s) flushed
[WS] Https_client browsing <https://wbs06-ws.withings.net/v2/weather?appliver=1181&appname=WBS06&apppfm=device>
[HTTPS_CLIENT] Socket already opened
[WS] Params <action=getforecast&sessionid=...short=1&enrich=t>
...

白帽小哥尝试替换设备上存储的 mTLS 证书,使 WiFi 拦截更容易,但由于服务器拒绝了自签名证书,因此无法按照预期工作。

不过,通过调试日志和从内存中读取各种状态数据,白帽小哥弄清楚了大部分身份验证流程:

  1. 通过蓝牙从移动应用程序接收到 WiFi 凭证后,设备现在可以独立连接到 API 服务器
  2. 设备出示其证书并使用 TLS(mTLS)连接到 API 服务器
  3. API 服务器返回一个 nonce
  4. 设备使用本地私钥对随机数进行签名并将其发送给服务器
  5. API 服务器确认签名有效并返回设备会话令牌
  6. 设备现在可以使用设备会话令牌作为身份验证与 API 服务器进行交互

有趣的是,用户设备关联工作流程可以通过两种方式完成,第一种方式由用户的移动应用程序发起:

  1. 移动应用程序已经拥有用户的会话令牌
  2. 应用程序通过蓝牙获取设备的会话令牌
  3. 应用程序使用 Session-Id: USER_SESSION_TOKEN 向 API 服务器进行身份验证,并发送请求 userid=USER_ID& sessionidtoken=DEVICE_SESSION_TOKEN 。userid 是一个简单的 userid 数字
  4. API 服务器确认 Session-Id 以及 sessionidtoken 有效,然后将 userid 与 DEVICE_SESSION_TOKEN 所属的设备 ID 关联起来

第二种方式,由设备发起:

  1. 设备已有设备会话令牌
  2. 设备通过蓝牙从应用程序获取用户的会话令牌
  3. 设备使用 Session-Id: DEVICE_SESSION_TOKEN 向 API 服务器进行身份验证,并发送请求 deviceid=DEVICE_ID& sessionidtoken=USER_SESSION_TOKEN 。deviceid 是一个简单的递增 deviceid
  4. API 服务器确认 Session-Id 以及 sessionidtoken 有效,然后将 deviceid 与 USER_SESSION_TOKEN 所属的用户 ID 关联起来

以上两种方法都经过了适当的强化和验证;尝试更改第一个流程中的 userid 或第二个流程中的 deviceid 都会失败,因为它们与 Session-Id 会话令牌不匹配。

然而,业务逻辑中有一个致命的缺陷,可以用服务器端验证逻辑的近似值来说明这一点:

if (req.session.isValid) {
  if (!validateSession(req.body.sessionidtoken)) {
    return error
  }

  const targetSession = fetchSession(req.body.sessionidtoken)

  // 用户应用程序发起的流程
  if (targetSession.type === 'device') {
    associate(req.body.userid, targetSession.id)
  // 设备发起的流程
  } else if (targetSession.type === 'user') {
    associate(req.body.deviceid, targetSession.id)
  }
}

假设一个请求,其中 Session-Id 和 sessionidtoken 都是攻击者的用户会话令牌,而 deviceid 设置为他们不拥有的设备,逻辑仍然会认为这是一个设备发起的流程,并且永远不会要求攻击者提供与目标 deviceid 相对应的会话令牌!

相反,代码应该进行额外的验证:

if (req.session.isValid) {
  if (!validateSession(req.body.sessionidtoken)) {
    return error
  }

  const targetSession = fetchSession(req.body.sessionidtoken)

  // //用户应用程序启动的流程以验证该用户与之关联的流程与会话令牌标头匹配
  if (req.body.userid === req.session.id && targetSession.type === 'device') {
    associate(req.body.userid, targetSession.id)
  // // 由设备发起的流程,验证要关联的设备与会话令牌头匹配
  } else if (req.body.deviceid === req.session.id && targetSession.type === 'user') {
    associate(req.body.deviceid, targetSession.id)
  }
}

由于这个错误,根据可用的设备 ID,估计有超过 100 万台潜在设备可以重新关联到攻击者用户帐户。

即使是在假期期间,厂商也很快对漏洞进行了修复:

  • 2024 年 12 月 29 日:向厂商报告漏洞
  • 2025 年 1 月 3 日:报告确认并修复

这表明了厂商对安全的重视。

总结

在入侵硬件时,很难将攻击范围从单个设备扩展到完全远程攻击,用户设备关联是可以绕过许多标准硬件和网络强化控制的关键流程之一,因为漏洞存在于 API 服务器上,而不是设备上。对于优先考虑可用性和易于设置的消费级硬件的产品,这一点尤其值得关注。

以上内容由骨哥翻译并整理。

原文:https://spaceraccoon.dev/pwning-millions-smart-weighing-machines-api-hardware-hacking/