盲签:当屏幕什么有用的都没说,你究竟在批准什么

·阅读 6 分钟·作者:SSP Editorial Team
SSP Academy 封面:盲签与无法阅读的交易批准

盲签:当屏幕什么有用的都没说,你究竟在批准什么

大多数重大加密损失都发生在一个非常具体的瞬间。不是密钥被偷的那一刻,而是有人盯着一条自己看不懂的签名请求,心想大概没问题吧,然后点了批准。

这个行业给它起了个名字:盲签。意思是在你读不懂的数据上落下签名。而多年以来,钱包一直把这当作理所当然。

一块签名屏幕应当做到什么

自我保管的安全承诺建立在一个假设之上:没有你的批准,什么都不会动。二取二多签、硬件钱包、气隙签名器——它们每一个都是把你的意图转换成一枚签名的机器。

这套机器的价值,恰好等于你对自己所批准之事的理解程度。

如果屏幕上写着*「向 0x8f3C… 发送 0.5 ETH」,你的批准就有意义。你把它和自己原本的意图作了比对。如果屏幕上写着「合约交互——数据:0xa22cb465000000000000000000000000d8dA6…0001」*,你的批准就毫无意义。你同意的不是一笔交易,而是一个由十六进制字符组成的长方形。

攻击者很清楚,他们需要你看到的是这两块屏幕中的哪一块。

藏在字节里的东西

一笔 EVM 交易的载荷是 calldata:一个四字节的函数选择器,后面跟着 ABI 编码的参数。对机器完全可读,对人完全不透明。而钱正是从其中少数几个选择器那里流走的。

approve(address,uint256)——选择器 095ea7b3。授予某个合约花费你代币的权限。这不是转账,而是一份长期授权。你签下它时什么也没有移动,而这恰恰是它能蒙混过关的原因。掏空发生在之后,按攻击者的时间表进行。

setApprovalForAll(address,bool)——选择器 a22cb465。NFT 版的对应物,而且更糟:一次签名就把你在该系列中持有的每一枚代币的处置权交给某个操作者,现在的和将来的都算。这里连一个能让你安心的金额字段都没有。

increaseAllowance(address,uint256)——选择器 39509351。在既有额度上再加码。常被忽略,因为它不是那个人人都被叮嘱要盯住的选择器。

transferFrom(address,address,uint256)——选择器 23b872dd。从一个已经授予过额度的地址代币取走。正是这笔签名,最终花掉了先前那次 approve 所许可的东西。

值得内化的规律是:偷走你钱的那笔交易,往往并不是你签下的那一笔。 你签下的是一份权限。盗窃是另一笔交易,在之后发出,而你从头到尾没有见过它。

为什么「无限」是那个要盯住的词

几乎所有的额度滥用都有一个共同点:金额实际上是无穷大。

各种 dapp 之所以索要无限授权,是因为方便——批准一次,此后不再打扰。结果就是用户被训练成习惯性地交出无上限的花费权,而一个显示 115792089237316195423570985008687907853269984665640564039457584007913129639935 的界面,其实什么也没告诉任何人。

这里有一处会让天真的实现栽跟头的细节。「无限授权」的规范取值恰好是 2²⁵⁶−1,因此最直觉的检查就是与这个常量比对。但 dapp 惯常还会使用别的天文数字——最大值的一半、0xff…f0、10¹⁸ × 10³⁸——它们在任何实际意义上都是无限的,却通不过一次精确相等的检验。一条只在精确哨兵值上触发的警告,攻击者只要减一就能绕过。

正确的阈值应当远低于哨兵值,又远高于任何真实数额。SSP 会标记任何大于或等于 2²⁵⁵ 的额度——约 5.8 × 10⁷⁶,比任何可能的 ERC-20 供应量都高出几十个数量级。没有任何合法情形会被误标,而把数值打磨到刚好低于最大值也躲不开这条警告。

这个阈值只适用于授予额度的调用。在一笔普通转账里,「无限」并不是一个有意义的概念——你转的是一个具体数额——因此那里的精确最大值哨兵不予干预。

SSP 的做法

SSP 会在批准界面上把 calldata 解码成平实的语言。被识别出的选择器会按其真实含义呈现:对手方是谁、金额是多少、所授予的权限是否无上限。原始十六进制不再是主要内容——它退居到「高级」区域,留给需要它的人。

有三个设计决定,比解码本身更重要。

解码器只属于展示层。 它绝不改变被签名的内容。批准始终签署原封不动的原始载荷;这个辅助工具只是把先前以原始十六进制显示的字节重新呈现一遍。一个能够改动载荷的解码器将成为新的攻击面,而不是防御——你读到的东西和你签下的东西,必须永远是同一批字节。

它以失败关闭的方式出错。 未知选择器、长度错误、格式损坏的十六进制、非标准的地址填充——任何意料之外的东西都返回空,界面随即回退为一个通用动作,并把原始十六进制放进「高级」。它从不猜测。一个会猜的解码器比没有解码器更糟,因为一份自信而错误的摘要,比看得见的十六进制更危险:十六进制至少诚实地告诉你,你看不懂它。

这种「失败即关闭」的直觉,比处理未知函数走得更深。一个 ABI 编码的地址是十二个零字节加上二十个地址字节;如果那些前导字节里还有别的东西,这个字就不是规范编码,SSP 会把它当作可疑,而不是试图去解释它。布尔值同理——只接受规范编码:全零(假)以及三十一个零后跟 0x01(真)。在一块授予花费权限的屏幕上,非规范编码是一面红旗,而不是一道解析难题。

代币符号与小数位从不猜测。 只有当代币能从设备本地的注册表中被确凿识别时,才会显示人类可读的金额。否则你看到的就是以基本单位表示的原始数字。这是刻意做得不那么漂亮:为一个仅仅自称是 USDC 的合约显示「5.0 USDC」,会把解码器变成一台说谎的机器——而这恰恰是攻击者想要的结果。

接下来是结构性而非表面性的那一部分。在 SSP 中,交易在一处构建,在另一处批准:在浏览器扩展中组装,在你的手机上独立解码并显示,而 SSP Key 会在设备本地重新计算交易哈希,一旦与展示内容不符便拒绝签名。单设备上的盲签意味着只要一块屏幕被攻陷就够了。而在这里,向你展示已解码动作的那块屏幕,和保管第二把密钥的那台设备,是同一台设备——并且它选择验证,而不是信任。

该怎么处理你自己的授权

approvesetApprovalForAll 当作危险的那两个。 让人真正损失金钱的正是这些签名,而它们之所以显得无害,恰恰因为什么都没有移动。

在你能拒绝的时候,拒绝无限授权。 很多 dapp 在你自行设定时是接受有上限金额的。这会带来一点摩擦,但它把你的下行风险限制在你真正打算花的额度之内。

审查你已经授予过的东西。 旧的授权不会过期。你两年前给某个协议的许可至今仍然有效,而如果那个合约日后被攻陷,它就是一条通往你代币的活路。撤销授权很快,而且是自我保管里回报最高的一小时清理工作。

当屏幕什么也没告诉你,那本身就是信号。 如果你的钱包说不清一笔交易在做什么,那是一条信息,而不是一个可以一路点过去的不便。面对读不懂的请求,正确的反应是停下,而不是眯起眼睛。

我们的目标从来不是让你去读十六进制。而是要确保:当你批准某件事时,你和你的钱包对那是什么达成了一致。

分享本文

相关文章