4. 短地址攻击:参数编码与Gas消耗的博弈
短地址攻击,这名字听着挺唬人。说白了,就是利用EVM在解析交易数据时的一个小漏洞——嗯,准确说是编码上的不对称性。我最早接触这个坑是在一次审计中,客户说他们的代币转账偶尔会失败,查了半天才发现是前端和合约对地址长度的处理不一致。
为什么会这样?因为以太坊的ABI编码规范里,地址类型固定是20字节。但有些客户端或者钱包,在构造交易数据时,如果传入的地址参数少了几个字节(比如只有19字节),EVM不会直接报错,而是会从下一个参数里“借”字节来补齐。你想想看,这会导致什么后果?
核心问题:短地址攻击的本质是参数编码长度与EVM解析长度不匹配,导致后续参数被错误截断或偏移。
攻击原理:字节级的博弈
我们来看一个典型的ERC20转账函数签名:
function transfer(address to, uint256 value) public returns (bool)
正常调用时,ABI编码是这样的:
0xa9059cbb // 函数选择器(4字节)
0000000000000000000000001234567890abcdef1234567890abcdef12345678 // to地址(32字节,左补零)
0000000000000000000000000000000000000000000000000000000000000064 // value = 100(32字节)
现在假设攻击者构造了一个短地址,只有19字节:
0xa9059cbb
0000000000000000000000001234567890abcdef1234567890abcdef123456 // 只有19字节有效数据
0000000000000000000000000000000000000000000000000000000000000064
EVM解析时,它会按32字节对齐去读。第一个参数to需要32字节,但实际只有19+13=32?不对,它会从下一个参数的开头“借”1个字节过来。结果就是:
- to地址 = 0x1234567890abcdef1234567890abcdef12345600(末尾被补了一个0x00)
- value值 = 0x0000000000000000000000000000000000000000000000000000000000000064 << 8 = 0x6400(左移了8位,变成了一个巨大的数)
你想想看,本来只想转100个代币,结果因为地址短了一个字节,value被左移了8位,变成了25600。这就是攻击的核心——用极小的代价(少传一个字节),让受害者转出远超预期的代币数量。
⚠️ 注意:这个攻击只对某些旧版客户端有效。现代钱包和节点大多已经做了长度校验。但如果你在写合约或者做DApp前端,千万别以为这问题已经绝迹了。
Gas消耗的微妙变化
有意思的是,短地址攻击不仅影响安全性,还会影响Gas消耗。我做过一个实验:
| 参数情况 | 交易数据长度 | Gas消耗(估算) |
|---|---|---|
| 正常地址(20字节) | 68字节 | 约21,000 + 数据费 |
| 短地址(19字节) | 67字节 | 少1字节的calldata费,但... |
| 短地址攻击成功 | 67字节 | 实际转账金额变大,Gas可能更高 |
这里有个反直觉的点:攻击者省了1字节的calldata费用(约16 Gas),但导致value参数被错误解析成一个巨大的数。如果合约内部有根据value值动态计算Gas的逻辑(比如某些代币的转账费用与金额挂钩),那实际Gas消耗反而会飙升。
我个人习惯是在合约里做一层参数校验,而不是依赖前端。因为前端可以绕过,但链上的规则是铁律。
防御方案:从源头堵住漏洞
防御短地址攻击,其实就一句话:永远不要信任传入的原始数据长度。具体做法有几种:
- 在合约内校验地址长度:虽然Solidity 0.8+已经自动做了长度检查,但如果你用汇编或者底层call,还是要手动校验。
- 使用SafeERC20库:OpenZeppelin的SafeERC20在调用transfer时,会检查返回值。虽然它不直接防御短地址,但能避免很多衍生问题。
- 前端强制补零:在构造交易数据时,确保地址参数是20字节。如果用户输入了短地址,前端自动补零到20字节。
// 不安全的做法:直接使用传入的地址
function transfer(address to, uint256 amount) external {
// 如果to是短地址,这里就出事了
_transfer(msg.sender, to, amount);
}
// 安全的做法:在底层调用前校验长度
function safeTransfer(address token, address to, uint256 amount) external {
require(to == address(uint160(to)), "Invalid address length");
// 或者用更直接的方式:
// require(to.code.length == 0 || to.code.length > 0, "Just a dummy check");
// 实际上,address类型在Solidity中永远是20字节,问题出在ABI编码阶段
}
我曾经在一个跨链桥项目里遇到过类似问题。当时用户从其他链跨过来一笔交易,目标地址被截断了。幸好我们在合约里加了一个地址校验:require(uint256(uint160(to)) == uint256(to)),直接把短地址拦在了门外。嗯,那次之后我养成了一个习惯——所有涉及地址参数的外部函数,都要做一次显式的长度校验。
💡 小技巧:如果你在用Hardhat或Foundry做测试,可以手动构造一个短地址的calldata来测试合约的鲁棒性。比如:abi.encodeWithSignature("transfer(address,uint256)", address(0x1234), 100),然后截掉最后一个字节。
知识体系:一张图看懂短地址攻击
下面这张SVG图,把短地址攻击的完整链路画出来了。从攻击者构造数据,到EVM解析出错,再到最终的资金损失,一目了然。
这张图把整个攻击链路拆成了四个阶段:攻击者构造数据、EVM解析、攻击结果、防御方案。最下面还单独拎出了Gas消耗的博弈——你省了1字节的Gas,但可能引发更大的Gas消耗,甚至导致交易失败。说白了,这是一场不对等的博弈,攻击者永远在暗处。
总结一下:短地址攻击虽然老,但它的思想——利用编码长度与解析长度的不一致——在很多新场景下依然适用。比如在Layer2的跨链消息中,如果对地址长度处理不当,同样会出现类似问题。我建议你在写任何涉及参数编码的代码时,都问自己一句:这个参数的长度,我确认过吗?
公众号:蓝海资料掘金营,微信deep3321