硬件钱包上的达摩克利斯之剑:隐秘的中间人威胁


近年来,硬件钱包在区块链行业中,被视为相对安全的私钥存储方式之一。私钥生成后,会存储在硬件钱包的安全芯片(Secure Element)中,仅用于对外部传入的信息进行签名。这种无法联网的封闭式硬件设计,配合禁止导出私钥的软件机制,构建起了强大的保护屏障。

然而,这种安全架构也有其“软肋” — — 硬件钱包高度依赖外部客户端软件和通信通道。一旦这些外围环节被篡改,攻击者便可实施“中间人攻击”(MITM),在用户毫无察觉的情况下替换信息,造成资产损失。

什么是中间人攻击

中间人攻击是指攻击者秘密地介入通信双方之间,拦截、篡改或伪造双方的通信内容,而通信双方误以为他们在直接交互。

这种攻击常见于网络监听、数据伪造、身份盗用和地址替换等场景。在加密资产领域尤其危险,因为攻击者只需替换一个地址,就可能造成巨大的资产损失。

举个生活中的例子:你寄给朋友一封重要信件。一个“坏邮差”在途中截获,将信件内容进行替换,然后重新封好寄出。朋友收到信件后,此时获取的信息因为被邮差替换,已经不再准确了。

在这个例子中,邮差扮演了中间人的角色,通过替换信件内容实施了中间人攻击。

在区块链系统中,由于大多数公链转账只需要收款地址,而不需要额外的诸如姓名的验证。这种场景的中间人攻击更容易得手,且造成的损失往往更大,更难追回。

硬件钱包通信流程:看不见的邮差

市面上主流的硬件钱包大致采用四种通信方式:

  • USB:最常见且稳定,通过数据线实现双向传输
  • 蓝牙:低功耗蓝牙 BLE,常用于移动端
  • 二维码:airgap,较为新型的方式,物理隔离,通过互相扫码达到通信的目的
  • NFC:近场通信,暂时较少使用

其中,USB 与蓝牙最为常见。无论采用哪种方式,通信流程大致相同:

  1. “钱包端”发起连接,例如浏览器插件钱包或者手机 APP 钱包
  2. 向“硬件钱包”发送请求,例如获取硬件内部的地址,发起签名等等
  3. 请求经上述“通信通道”送达硬件设备
  4. 设备完成处理并返回响应
  5. “钱包端”接收并显示结果

尽管硬件钱包自身是安全的“保险库”,但通信过程却依赖客户端、通信通道这一“邮差链条”。一旦“邮差”遭到劫持或者作恶,传递的数据信息就可能早已被调包。

这正是“达摩克利斯之剑”悬于硬件钱包之上的隐秘威胁 — — 中间人攻击。

实战案例:基于恶意脚本的中间人攻击


内容目录
  1. 声明
  2. Trezor 通信的基本流程
  3. Trezor Bridge 的基本信息
  4. 攻击的基本流程
  5. 攻击测试
  6. 代码分析
  7. 与团队的沟通
  8. Metamask 读取完整地址,无需硬件确认
  9. 盲签的威胁,可能比我们想象中的更大
  10. 通信通道的中间人攻击,方式众多
  11. 主动提升安全意识,任重而道远

声明

  • 所有代码和操作均发生在 2023 年 9 月,距本文发布时间(2025 年 6 月)已超过 20 个月。
  • 所有演示基于当时公开版本的官方软件,具体版本信息已在文中注明。
  • 所涉及的攻击方式,已于当时向相关团队披露并确认
  • 本文所有内容仅供学习与安全研究之用,观点仅代表作者本人。
  • 本文不对由此引发的任何行为或后果承担责任。

Trezor 通信的基本流程

Trezor 通过 USB 连接 Metamask 浏览器钱包时可选择通过 Trezor Bridge 进行通信,Trezor Bridge 的基本流程

  • 安装软件之后,会在用户的电脑中,在本地 21325 端口启动 HTTP 服务器
  • Metamask 连接 Trezor 硬件钱包时,会通过弹出 网页来加载 JS SDK
  • Trezor 的 SDK 会尝试的将基于 protobuf 的序列化数据,发送向这个本地服务器
  • Trezor Bridge 接收到数据后,通过 lib-usb 库查找本地满足条件的 PID VID Trezor 设备并传输数据

当检测到本地已安装 Trezor Bridge 时,所有通过 网页,也就是 Trezor SDK 的序列化数据都会经由本地 Trezor Bridge 服务器转发给设备,而不使用 Web USB。

Trezor Bridge 的基本信息

Trezor Bridge 使用 Golang 开发,当前用户版本为 v2.0.27。

从 GitHub 开源仓库可见,v2.0.27 使用 xgo 进行跨平台编译,从而生成 MacOS,Windows 和 linux 的安装包。

以 MacOS 为例,安装时会在 /Applications/Utilities/TREZOR\ Bridge/trezord 目录创建 trezord 服务器二进制文件,并在用户本地创建 com.bitcointrezor.trezorBridge.trezord.plist 文件,通过 KeepAlive 实现开机自启和进程守护。参考链接

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.bitcointrezor.trezorBridge.trezord</string>
<key>KeepAlive</key>
<true/>
<key>ProgramArguments</key>
<array>
<string>sh</string>
<string>-c</string>
<string>/Applications/Utilities/TREZOR\ Bridge/trezord -l $HOME/Library/Logs/trezord.log</string>
</array>
</dict>
</plist>

攻击的基本流程

在 Metamask 连接 Trezor 设备时,立刻就会读取设备内部的 ETH 公钥,然后基于派生地址序号在 Metamask 软件端计算出不同路径的地址。

在这个过程中,没有任何的硬件确认或者提示,这也为一个简单的中间人攻击,提供了可实现的路径。

一旦 Trezor Bridge 被本地恶意软件控制,整个通信链路上就会出现一个“坏邮差”。攻击者便可将其变为一个中转服务器,劫持所有与硬件钱包的通信数据。这样,发送给硬件的数据和从硬件返回的数据都可能被篡改,造成界面显示信息与实际硬件信息不一致,一旦软件流程有漏洞或者硬件上的信息没有认真确认,中间人攻击就能操作成功。

攻击测试

  • 首先安装 Trezor Suite v23.8.1、Trezor Bridge v2.0.27 和 Metamask v11.0.0
  • 准备两台 Trezor Model T:一台按正常流程操作,另一台用于中间人攻击测试
  • 首先展示正常流程:两台设备在 Trezor Suite 和 Metamask 中均能正确读取地址
  • 执行恶意脚本替换 trezord 进程
  • 第一台设备在 Metamask 中仍能正确读取地址,流程正常
  • 第二台设备在 Metamask 中读取的地址被中间人替换,与 Trezor Suite 和硬件显示的地址不一致

代码分析

在 Trezor Bridge 关键位置添加日志标记,记录设备通信过程中的请求体和响应体数据:

if mode != core.CallModeWrite {
hexbodyStr := hex.EncodeToString(binbody)
a.logger.Log(fmt.Sprintf("hexbodyStr, %s", hexbodyStr))
hexres := hex.EncodeToString(binres)
a.logger.Log(fmt.Sprintf("hexres, %s", hexres))
_, err = w.Write([]byte(hexres))
if err != nil {
a.respondError(w, err)
}
}

SDK 读取公钥阶段的 hexbodyStrhexres 在多次调用时,对同一设备的不同时间操作参数和返回结果完全相同。通过日志筛选,发现 Metamask 读取 ETH 地址时的 call 函数调用数据如下:

[28.178293 : 16:14:47] [server/api/api.go 220 server/api.(*api).call] hexbodyStr, 003700000000
[27.843938 : 16:14:47] [server/api/api.go 220 server/api.(*api).call] hexbodyStr, 000b0000001f08ac8080800808bc80808008088080808008080008002207426974636f696e
[27.843954 : 16:14:47] [server/api/api.go 228 server/api.(*api).call] hexres, 000c000000c70a4e080510f7c2896218002220520f8747876da744c2bf49fecc8562017c9be0ecf2f55eae8d9b98360dd72fc1322102bfb03b1c56e5a4bc3085acc16170c91fcb8903413291d6246ccbd0cfe769345b126f78707562364657475054416d5a386f636847554631716261436d4e6569685953636f53774d426177567232324b796666674c4a4635766d536d43364147537062325079565a59417a383337313147456d44486f42504d4b5775634d4a41376f47556831685a364874794646624d503918a3eeeb870d
[27.466270 : 16:14:46] [server/api/api.go 220 server/api.(*api).call] hexbodyStr, 000b0000001d08ac8080800808bc8080800808808080800808002207426974636f696e
[27.466317 : 16:14:46] [server/api/api.go 228 server/api.(*api).call] hexres, 000c000000c80a4f080410cec1b2da0a18002220f107c96b21b14c5a304af43e1187341dd153f4702d82a04fc712bd43b5d2d65332210201694395789981413027ef739b4360316a7d6878b18dcb9bc670a1a2bd8baad9126f7870756236456e76784c3665674737796d5a514b4d3141524e325670794c624e56624b68443168694d346555586333735967576574726e6461415a4b56626375466a523137755a68456a77447366516163387776576f4e78637738694144427466595663366d764b466e624157756318a3eeeb870d
[27.094882 : 16:14:46] [server/api/api.go 220 server/api.(*api).call] hexbodyStr, 001d0000002108ac80808008088180808008088080808008080008001207546573746e65742800
[27.094912 : 16:14:46] [server/api/api.go 228 server/api.(*api).call] hexres, 001e000000460a226d736a586538664d446e4c724a6877515675527332554462574c314d58324c6579581220258c7690ef920a9400c30705156b60dea068193e7d5e7d86f83e24682ca4b7b6
[24.027874 : 16:14:43] [server/api/api.go 220 server/api.(*api).call] hexbodyStr, 0000000000021800
[23.409576 : 16:14:42] [server/api/api.go 220 server/api.(*api).call] hexbodyStr, 003700000000

根据日志比对,003700000000000b0000001f08ac8080800808bc80808008088080808008080008002207426974636f696e 等数据,均为发送、接收报文的序列化结果。

经过重放测试,在信息构建过程中并没有时间戳等参数,可以非常简单轻易的模拟攻击手法,即针对部分发送报文,硬编码返回的结果,进行结果替换:

在这个例子中,我们针对性的将数据进行了替换,当接收到 SDK 获取 ETH 公钥的请求时,不再使用硬件返回的结果,而是直接使用上述代码中硬编码的结果。重新通过文档编译后制作二进制文件,同时制作守护进程等攻击必要组件。

准备工作完成后,当用户不慎安装和运行恶意软件后。每当用户使用 Metamask 连接 Trezor 并且发送数据时,通过 bridge 通信的数据不再是硬件读取出来的信息,而是上述代码中硬编码的序列化数据,再基于业务 SDK 的反序列化,读取地址的信息即替换成功。完整的攻击如上述视频所示。

与团队的沟通

整理好问题及操作之后,我第一时间已与 Metamask 和 Trezor 团队进行了沟通,以下是所有沟通信息及邮件信息

  • Metamask 沟通记录:
  • Trezor 邮件沟通信息:

问题的核心及建议

Metamask 读取完整地址,无需硬件确认

硬件整体可以作为一个封闭的安全环境,所以所有信息都应以硬件为准。但是在 Metamask 的产品设计流程当中,在硬件没有任何显示的情况下,直接读取完整地址的明文信息并进行展示。

Trezor SDK ethereumGetAddress 方法提供了 showOnTrezor 参数,他的作用是读取硬件的地址,并在硬件当中展示,以供用户二次确认。

这是一个产品设计的取舍问题,如果每次都需要用户确认过才能读取地址,那么在批量添加地址的场景用户会非常痛苦。所以在主流的支持硬件钱包的 APP 内,同时支持静默读取地址,但是如果想要接收资产,都需要先进行地址的硬件确认,就是为了防止上述攻击场景出现。

Metamask 静默读取地址之后,没有让用户确认,就直接展示了完整地址,那么首次使用的用户,一旦被攻击,就很容易中招。

图为 OneKey 钱包,静默读取地址,但需要二次确认才会展示完整信息

盲签的威胁,可能比我们想象中的更大

按照这种攻击手段和攻击方式,我们可以得出结论:所有硬件非确定性的信息,都是理论上不安全的

所有的盲签同理,虽然盲签硬件上有确认,但是这个确认的信息对于用户来说并不可读,所以他也满足硬件非确定性的信息,无法确保不被中间人攻击。

safe 多签钱包的案件就是一个典型,钱包构造的签名信息被服务器恶意 javascript 代码替换,同时因为 ledger 钱包是盲签,导致这一次攻击的发生。事件上下文:

通信通道的中间人攻击,方式众多

除了用户本地有恶意软件以外,一些钱包的通信和构建流程可能涉及到服务器(例如一些钱包 encodeTx 需要通过服务端进行构建)。这些都是潜在的攻击对象和被攻击点。

通信通道当然还包括硬件层面,例如不安全的电脑摄像头外设,不安全的 USB 数据线等等。从物理层到应用层,在通信层面,所有环节都有被攻击的可能。

主动提升安全意识,任重而道远

客户端到硬件钱包的完整通信,核心流程理论上是不需要联网的。所以即使有端到端的加密通信,也无法 100% 确保流程安全。

增加端到端的加密之后,对于真正的黑客来说,增加的只是逆向客户端,对通信信道的破解成本。而对于攻击本身,从来都不是难度的较量,而是黑客付出的成本和法律后果的考量。

同时,大部分区块链的产品团队,一般在面对这类中间人攻击的威胁时,会将这部分的优化放在整个迭代中处于低优先级,变相等于允许一部分中间人攻击的风险出现。如上面的团队沟通邮件,如 Metamask 的 bug bounty 界面,就将 MITM 中间人攻击排除在外:

所以对于用户来说,更需要主动我们自身提高防范意识。对于重要资产,即使是使用硬件钱包,也最好隔离电脑,不连接不可信的网络,不访问不可信的网页等等等等。

写在最后

硬件钱包的“安全神话”并非来自单一设备本身,而是建立在整个生态系统的协作安全。

当我们忽视客户端的可信性、放松对通信通道的防护,那把“达摩克利斯之剑”便悄然悬于我们的资产之上。

真正的安全,从不止于硬件,而在于每一个你以为“无关紧要”的细节。