文章目录
近年来,硬件钱包在区块链行业中,被视为相对安全的私钥存储方式之一。私钥生成后,会存储在硬件钱包的安全芯片(Secure Element)中,仅用于对外部传入的信息进行签名。这种无法联网的封闭式硬件设计,配合禁止导出私钥的软件机制,构建起了强大的保护屏障。
然而,这种安全架构也有其“软肋” — — 硬件钱包高度依赖外部客户端软件和通信通道。一旦这些外围环节被篡改,攻击者便可实施“中间人攻击”(MITM),在用户毫无察觉的情况下替换信息,造成资产损失。
什么是中间人攻击
中间人攻击是指攻击者秘密地介入通信双方之间,拦截、篡改或伪造双方的通信内容,而通信双方误以为他们在直接交互。
这种攻击常见于网络监听、数据伪造、身份盗用和地址替换等场景。举个生活中的例子:你寄给朋友一封重要信件。一个“坏邮差”在途中截获,将信件内容进行替换,然后重新封好寄出。朋友收到信件后,此时获取的信息因为被邮差替换,已经不再准确了。
在这个例子中,邮差扮演了中间人的角色,通过替换信件内容实施了中间人攻击。
在区块链系统中,由于大多数公链转账只需要收款地址,而不需要额外的诸如姓名的验证。这种场景的中间人攻击更容易得手,且造成的损失往往更大,更难追回。

硬件钱包通信流程
市面上主流的硬件钱包大致采用四种通信方式:
1.USB:最常见且稳定,通过数据线实现双向传输
2.蓝牙:低功耗蓝牙 BLE,常用于移动端
3.二维码:airgap,较为新型的方式,物理隔离,通过互相扫码达到通信的目的
4.NFC:近场通信,暂时较少使用
其中,USB 与蓝牙最为常见。
无论采用哪种方式,通信流程大致相同:
1.“钱包端”发起连接,例如浏览器插件钱包或者手机 APP 钱包
2.向“硬件钱包”发送请求,例如获取硬件内部的地址,发起签名等等
3.请求经上述“通信通道”送达硬件设备
4.设备完成处理并返回响应
5.“钱包端”接收并显示结果
尽管硬件钱包自身是安全的“保险库”,但通信过程却依赖客户端、通信通道这一“邮差链条”。一旦“邮差”遭到劫持或者作恶,传递的数据信息就可能早已被调包。
这正是“达摩克利斯之剑”悬于硬件钱包之上的威胁——中间人攻击。

基于恶意脚本的中间人攻击案例
声明
1.所有代码和操作均发生在 2023 年 9 月。
2.所有演示基于当时公开版本的官方软件,具体版本信息已在文中注明。
3.所涉及的攻击方式,已于当时向相关团队披露并确认。
4.本文所有内容仅供学习与安全研究之用,不对由此引发的任何行为或后果承担责任。
Trezor 通信的基本流程
Trezor 是一种硬件钱包,用于安全地存储加密货币私钥,常通过 USB 与电脑连接,以防止私钥暴露在网络环境中。
在 Trezor 与 Metamask 浏览器钱包交互时,用户可以选择通过 Trezor Bridge 实现通信,其基本工作流程如下:
1.用户安装 Trezor Bridge 后,程序会在本地启动一个 HTTP 服务,监听 21325 端口;
2.当 Metamask 尝试连接 Trezor 硬件钱包时,会弹出一个位于 https://connect.trezor.io 的网页,以加载 Trezor 提供的 JavaScript SDK;
3.Trezor SDK 会尝试将经过 Protobuf 序列化后的数据发送至本地的 Trezor Bridge HTTP 服务;
4.Trezor Bridge 接收到请求后,会通过 libusb 库查找符合指定 PID 和 VID 的本地 Trezor 设备,并进行数据通信;
5.一旦检测到本地已安装 Trezor Bridge,所有来自 Trezor SDK 的序列化数据将会通过本地服务器进行中转与传输,而不再使用 WebUSB 通信方式。
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实现开机自启和进程守护。
可参考:https://github.com/trezor/trezord-go/blob/e0f400d59a04280cfbc8a3a9802af968ecacd902/release/macos/flat-install/install.pkg/payload/Library/LaunchAgents/com.bitcointrezor.trezorBridge.trezord.plist
<?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 公钥,并在此基础上,通过变更派生路径序号,在本地计算出一系列地址。
这一过程中不涉及任何硬件端的确认或用户提示,为中间人攻击提供了可行的入口。
如果本地的 Trezor Bridge 被恶意软件控制,整个通信链路中便存在一个“坏邮差”——攻击者可以将其伪装为中转代理,拦截并篡改所有与 Trezor 设备之间传输的数据。此时,无论是发送给硬件的数据,还是从硬件返回的数据,都可能被修改。
结果就是:用户界面所展示的信息可能与硬件真实数据不符。如果软件流程存在疏漏,或用户未仔细核对硬件设备上的信息确认提示,中间人攻击就有可能成功实施,进而造成资产安全风险。
攻击测试
1.安装 Trezor Suite v23.8.1、Trezor Bridge v2.0.27 和 Metamask v11.0.0
2.准备两台 Trezor Model T:一台按正常流程操作,另一台用于中间人攻击测试
3.测试机器是否运行正常:两台设备在 Trezor Suite 和 Metamask 中均能正确读取地址
4.执行恶意脚本替换 trezord 进程,设备在 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 读取公钥阶段,hexBodyStr(请求体)和 hexRes(返回值)在多次调用中保持一致,即使操作时间不同,只要是对同一设备进行操作,所用参数与返回结果也完全相同。
通过对日志进行筛选分析,发现 Metamask 在读取 ETH 地址时,实际调用的是 SDK 中的 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
根据日志比对,003700000000 和 000b0000001f08ac8080800808bc80808008088080808008080008002207426974636f696e 等数据,均为发送、接收报文的序列化结果。
经过重放测试,在信息构建过程中并没有时间戳等参数,这说明通信过程没有用上像时间戳、随机数这样的防重放机制,也就是攻击者可以用旧数据伪造新结果。
攻击步骤:
1.记录日志:抓住一次真实与硬件交互的请求和响应数据(hex 格式);
2.写死在代码里:在本地 Trezor Bridge 中写死这些数据;
3.编译恶意版 Bridge,加上守护进程(后台运行),引导用户安装;
4.当用户用 Metamask 连接硬件时,拦截本该发给硬件的请求,直接返回提前准备好的响应;
5.Metamask SDK 反序列化响应内容,以为是硬件发来的,实际上是伪造数据;
6.攻击者可以控制地址、公钥等关键数据,而用户界面却看不到异常。

在这个例子中,我们针对性的将数据进行了替换,当接收到 SDK 获取 ETH 公钥的请求时,不再使用硬件返回的结果,而是直接使用上述代码中硬编码的结果。重新通过文档编译后制作二进制文件,同时制作守护进程等攻击必要组件。
准备工作完成后,当用户不慎安装和运行恶意软件后。每当用户使用 Metamask 连接 Trezor 并且发送数据时,通过 bridge 通信的数据不再是硬件读取出来的信息,而是上述代码中硬编码的序列化数据,再基于业务 SDK 的反序列化,读取地址的信息即替换成功。
问题的核心及建议
硬件整体可以作为一个封闭的安全环境,所以所有信息都应以硬件为准。比如你用 Trezor 生成一个 ETH 地址,在安全设计上,应该让硬件设备本身显示出这个地址,让你看清楚,你点“确认”后,软件端(比如 Metamask)才能拿到这个地址。
但是在 Metamask 的产品设计流程当中,在硬件没有任何显示的情况下,直接读取完整地址的明文信息并进行展示。
在 Trezor SDK 中,有一个叫 ethereumGetAddress 的方法,用来读取地址。这个方法有个参数叫:
showOnTrezor: true/false
true:让地址在 Trezor 的屏幕上显示出来,用户按确认按钮,才能返回地址;
false:直接静默返回地址,不提示用户,不显示;
这就涉及到一个权衡:
1.如果每次都要求“显示 + 硬件确认”,那用户体验会很差,尤其是:要批量导入多个地址(比如:钱包地址管理工具、批量收款人场景);用户每次都得点一次确认,体验性差。
2.所以大多数支持硬件钱包的钱包(比如 Metamask、XDEFI、Keystone 等),采用折中方案:默认支持静默读取(不用用户确认);但是在你想“接收资产”或“绑定地址用于转账/授权”时,要求用 showOnTrezor: true,让用户硬件确认一次,确保地址是真的。
Metamask 静默读取地址之后,没有让用户确认,就直接展示了完整地址,那么首次使用的用户,一旦被攻击,就很容易中招:

盲签的威胁,可能比我们想象中的更大
按照这种攻击手段和攻击方式,我们可以得出结论:所有硬件非确定性的信息,都是理论上不安全的。
所有的盲签同理,虽然盲签硬件上有确认,但是这个确认的信息对于用户来说并不可读,所以他也满足硬件非确定性的信息,无法确保不被中间人攻击。
safe 多签钱包的案件就是一个典型,钱包构造的签名信息被服务器恶意 javascript 代码替换,同时因为 ledger 钱包是盲签,导致这一次攻击的发生。
通信通道的中间人攻击,方式众多
除了用户本地有恶意软件以外,一些钱包的通信和构建流程可能涉及到服务器(例如一些钱包 encodeTx 需要通过服务端进行构建),这些都是潜在的攻击对象和被攻击点。
通信通道当然还包括硬件层面,例如不安全的电脑摄像头外设,不安全的 USB 数据线等等。从物理层到应用层,在通信层面,所有环节都有被攻击的可能。
主动提升安全意识,任重而道远
客户端到硬件钱包的完整通信,核心流程理论上是不需要联网的,所以即使有端到端的加密通信,也无法 100% 确保流程安全。
增加端到端的加密之后,对于真正的黑客来说,增加的只是逆向客户端,对通信信道的破解成本。而对于攻击本身,从来都不是难度的较量,而是黑客付出的成本和法律后果的考量。
同时,大部分区块链的产品团队,一般在面对这类中间人攻击的威胁时,会将这部分的优化放在整个迭代中处于低优先级,变相等于允许一部分中间人攻击的风险出现。
所以对于用户来说,更需要主动我们自身提高防范意识。对于重要资产,即使是使用硬件钱包,也最好隔离电脑,不连接不可信的网络,不访问不可信的网页等等等等。
写在最后
硬件钱包的“安全神话”并非来自单一设备本身,而是建立在整个生态系统的协作安全。
当我们忽视客户端的可信性、放松对通信通道的防护,那把“达摩克利斯之剑”便悄然悬于我们的资产之上。
真正的安全,从不止于硬件,而在于每一个你以为“无关紧要”的细节。
Written by:https://zhangzhao.name/the-sword-of-damocles-hanging-over-hardware-wallets-the-hidden-threat-of-man-in-the-middle-attacks-8d10fcd3ef07
983

被折叠的 条评论
为什么被折叠?



