通过API与硬件黑客技术攻陷数百万智能体重秤

本文详细分析了智能体重秤的用户设备关联流程中的安全漏洞,包括SQL注入绕过BT-WAF、UART调试shell获取以及用户设备关联逻辑缺陷,揭示了如何远程控制数百万台设备的技术细节。

通过API与硬件黑客技术攻陷数百万智能体重秤

Mar 24, 2025 · 2580 words · 13 minute read

悄悄说:我正在与No Starch Press合作出版一本书!我写了《从零日到零日:漏洞研究实战指南》,面向希望进入漏洞研究领域的新手。从代码审查到逆向工程再到模糊测试,我不仅讲解了“做什么”,还深入探讨了“如何做”来寻找零日漏洞——这些都是我希望从一开始就能学到的东西。现已可在包括亚马逊在内的所有常规渠道进行早期访问,2025年6月正式发布!

为什么只攻击一个设备,当你可以攻击所有设备?通过逆向工程并发现智能体重秤用户设备关联流程中的漏洞,我成功控制了数百万台联网健康设备。硬件安全和Web安全是现代智能设备安全的两个重要方面,学会攻击两者可以带来令人印象深刻且可怕的结果。这篇博客文章从头到尾介绍了攻击联网智能设备的基础知识,重点关注用户设备关联的关键工作流程。

联网的……体重秤??? 🔗

假期期间,我注意到酒店健身房体重秤屏幕上有一个奇怪的图标。是一个WiFi图标。我惊恐地意识到,人们现在认为将体重秤连接到互联网是个好主意(安息吧,垃圾物联网)。当我在亚马逊上查看时,注意到大量可用的选项都支持WiFi或蓝牙连接,许多还配有可疑相似的移动应用。

事实上,许多都是由同一家原始设备制造商(OEM)生产的。即使它们是由代码库略有不同的不同OEM制造的,快速查看相关的Android应用也会发现,许多应用使用了相同的通用库,例如com.qingniu.heightscale,大概是因为从头编写兼容库的工作量要小得多。

虽然与BLE协议相关的代码很有趣,让我能够找出通过蓝牙与这些设备通信的正确操作码,但其中大部分已经被openScale项目逆向工程并记录了下来。无论如何,尝试找出需要近距离物理接触的本地漏洞并不是很有趣。

我们需要更深入 🔗

如果你的目标不仅仅是攻击一个设备,而是所有设备,那么一个关键目标就是用户设备关联流程。例如,当你第一次购买智能设备并开箱时,通常需要登录移动应用并扫描二维码或通过蓝牙与设备配对。完成后,你在制造商Web服务上的用户账户现在与物理设备关联。

这是一个难以保护的过程。从工厂开始,每个设备都需要一个唯一的设备标识符/密钥,这样在扫描设备A的二维码时就不会意外地与设备B配对。最不安全的方法是使用静态字符串,如UUID、MAC地址或序列号。虽然这些作为标识符可能没问题,但作为认证密钥并不安全。即使它们可能是随机生成的,因此难以暴力破解,但如果泄露,撤销它们将非常困难。

更安全的选择是生成加密密钥,如公钥/私钥对。这仍然使其成为物理内存提取的目标,如果密钥生成过程在某些方面较弱,攻击者仍可能为任何设备生成任意密钥。传统的解决方案是依赖良好的公钥基础设施和证书架构,允许轻松撤销受损证书。

因此,典型流程如下:

  1. 用户安装移动应用并使用其用户账户登录。
  2. 通过应用,用户连接到硬件设备。
  3. 硬件设备的密钥发送到移动应用。
  4. 移动应用将用户的密钥(例如会话令牌)和设备的密钥发送到服务器。
  5. 服务器确认密钥的真实性,并将用户账户与硬件设备关联。
  6. 用户现在可以通过互联网远程控制和从硬件设备获取数据。

看起来合理。会出什么问题呢?

OEM中的SQL注入(BT-WAF绕过) 🔗

相关的OEM一开始就跌倒了。甚至不需要购买物理设备,我就枚举了移动应用上可用的API端点,包括一个有趣的api/ota/update端点。我以为这将允许我获取固件以进一步了解设备。多亏了Android移动应用的反编译Java代码,我能够轻松重建所需的JSON主体参数。然而,即使输入正确,制造商似乎也没有太多更新可以分享。

不幸的是,在探索API端点时,我发现有几个端点存在基本的SQL注入漏洞。有趣的是,服务器使用了一个名为Baota Cloud WAF(BT-WAF)的中国WAF,它比我之前面对的许多典型WAF要强大得多。特别是,一个/api/device/getDeviceInfo端点允许查找设备的序列号,这些序列号被该制造商用作标识符和认证密钥。序列号本身用于一个/api/device/bindv2端点,该端点将请求用户的账户与序列号引用的设备绑定或关联!“序列号”本身是一个随机生成的MAC地址,存储在设备上。

以下是易受攻击端点的初始负载主体:

1
2
3
{
  "serialnumber":"'001122334455"
}

这里没有太多可操作的空间。如果有第二个注入点,我可能能够制定一个更微妙的跨负载。经过大量试错,我最终成功绕过了BT-WAF:

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

让我们稍微分解一下。如果这注入到像SELECT * FROM devices WHERE serial = 'INJECTION'这样的SQL语句中,最终注入的SQL将是:SELECT * FROM devices WHERE serial = 'INJECTION'or\n@@version\nlimit 1\noffset 123#'。这里有两个关键的绕过小工具:

  • @@version 总是评估为true,可以用于代替更明显的1=1
  • \n 换行符可以打破语句,而不是空格。

有了这个,我现在能够泄露任何设备的设备信息,包括用作认证密钥的序列号!事实证明,通过增加偏移量,设备数量超过二十万台。

在Withings WBS06上获取串行调试Shell 🔗

当我转向研究其他设备时,我遇到了Withings Body体重秤。与其他设备类似,它具有WiFi和蓝牙连接以及自定义移动应用。这是一个更有信誉的品牌,有趣的是,它似乎也与诺基亚Body秤共同品牌。

通过应用的API拉取固件进行进一步分析相对容易。然而,与路由器等使用的更复杂的固件(通常包括完整的文件系统和Linux操作系统)不同,这是一个裸机ARM固件。我希望我能更多地阐述逆向裸机ARM的艺术,但经验更丰富的专家的视频和博客基本上告诉了你同样的事情:这非常困难。

尽管如此,我还是尝试了一下,遵循Barun的《在Ghidra中分析裸机固件二进制文件》博客文章,并得到了我需要知道的一些近似值。这涉及通过其FCC认证文档中的内部图片找出WBS06的微控制器型号,并设置正确的内存映射。

引起我注意的是几个零散的字符串,暗示了一个……shell?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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文档中的内部图片对比确认了这一点。

然而,我的初步努力失败了。尽管用逻辑分析仪正确找出了正确的波特率,我的串行连接一直返回乱码。经过许多小时的痛苦,我意识到我便宜的CP2102 USB转TTL转换器是问题的根源,使用更可靠的FT232终于得到了我需要的结果。

现在我有了一個调试shell,我可以探索设备上存储的所有数据,包括证书、密钥等!当然,虽然这很令人兴奋,但这并没有多大意义——我可以“黑客”一个我已经拥有的设备,没什么大不了的。

破碎的用户设备关联逻辑 🔗

为了真正测试远程向量,我需要完全了解设备如何向API服务器认证自己并执行用户设备关联。

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

  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
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
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服务器返回一个随机数。
  4. 设备使用本地私钥对随机数进行签名,并将其发送到服务器。
  5. API服务器确认签名有效,并返回设备会话令牌。
  6. 设备现在可以使用设备会话令牌作为认证与API服务器交互。

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

  • 移动应用已经拥有用户的会话令牌。
  • 应用通过蓝牙获取设备的会话令牌。
  • 应用使用Session-Id: USER_SESSION_TOKEN向API服务器认证,并发送请求负载userid=USER_ID& sessionidtoken=DEVICE_SESSION_TOKENuserid是一个简单的递增数字。
  • API服务器确认Session-Idsessionidtoken有效,然后将useridDEVICE_SESSION_TOKEN所属的设备ID关联。

第二种方式由设备启动:

  • 设备已经拥有设备会话令牌。
  • 设备通过蓝牙从应用获取用户的会话令牌。
  • 设备使用Session-Id: DEVICE_SESSION_TOKEN向API服务器认证,并发送请求负载deviceid=DEVICE_ID& sessionidtoken=USER_SESSION_TOKENdeviceid是一个简单的递增数字。
  • API服务器确认Session-Idsessionidtoken有效,然后将deviceidUSER_SESSION_TOKEN所属的用户ID关联。

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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-Idsessionidtoken都是攻击者的用户会话令牌,而deviceid设置为他们不拥有的设备。逻辑仍然会认为这是一个设备启动的流程,并且从不要求攻击者提供与目标deviceid对应的会话令牌!花几秒钟用这个思路解析代码。

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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日:报告确认并修复

这证明了他们对安全的重视——漏洞影响每个供应商,但我知道我宁愿

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计