漏洞原理
这个漏洞发生在第三方合约或应用程序调用智能合约时。当将参数传递给智能合约时,参数将根据 ABI 规范进行编码。第三方合约可以发送比预期参数长度短的编码参数(例如,发送一个只有 38 个十六进制字符,即19 个字节长度,而不是标准的 20个字节长度的地址时)。在这种情况下,EVM 将在编码参数的末尾填充 0,以弥补预期的长度。这样,当智能合约不验证输入时,这就会成为一个问题。最明显的例子是,当用户请求提款时,交易所不验证 ERC20 令牌的地址。
安全隐患
转账函数
考虑标准的 ERC20 转账函数接口,注意参数的顺序:
function transfer(address to, uint tokens) public returns (bool success);
EVM 将按照 transfer()
函数指定的顺序对这些参数进行编码,即先地址然后数量。恶意用户可以通过删除以 0 或多个 0 结尾的以太地址的最后一个 0 来构造这种攻击。
短地址
接收用户数据的智能合约允许小于 20 字节的以太坊地址。通常以太坊地址看起来像:
0xc3Bb35818d58FCA0C4943bA98938cb6F46A91700
如果去掉末尾的两个 0 (一个十六进制字节等于0) 会怎样?EVM 会接受它,但在打包的功能参数的末尾添加额外的零。从给出一个短参数的点开始,传递函数参数将移动一个字节,这将移动传递的令牌数量。
攻击过程
假设用户有 100 个代币,但是想要 25600 个,怎么做?
- 生成一个结尾为 0 的以太坊地址。以太坊地址几乎是随机生成的,所以平均需要 256 次尝试。虽然看似有难度,实际上还是有这个可能性的,因为生成地址不需要多少时间。
- 找到一个有 256,00 个代币的兑换钱包,比如交易所。
- 向这个交易所钱包发送 100 个代币,即在我的链外账户内部存入 100 个代币。
- 请求使用我生成的地址提取 1,00 个代币(去掉地址中最后一个 “0” 字节)。
然后会发生什么呢?简单地说,如果目标合约不验证地址,它将把所有内容 “打包” 在一起,并将数量 (最后一个参数) 移动到上一个字节上。当需要68个字节时,实际向传输函数传递了 67 个字节的参数。正确的打包数据长度应该是 68 个字节,其中前4个字节是 transfer()
函数签名,第二个是 32 个字节地址,后面的最后32个字节代表的是 uint256
的令牌数量(它有很多前导零)。
所有这些参数都在 msg.data
下传递调用,在这种攻击中所发生的是,前导零的一个字节被从数量中取出,并给缩短的地址。这将给我们留下与开始时相同的地址,所以发送到这里的代币将是可转账的。
当 EVM 检测到在处理 256 位的数据类型时发生下溢 (小于256位),则在地址的末尾追加 0。这意味着你的金额乘以了 1<<8
或 256
,最重要的是,交易所在他们的内部账本上检查了你的余额。
预防措施
一个比较简单的方法是在合约的转账函数中检查 msg.data
的长度是正确的大小 (68字节)。
contract NonPayloadAttackableToken {
modifier onlyPayloadSize(uint size) {
assert(msg.data.length >= size + 4);
_;
}
function transfer(address _to, uint256 _value) onlyPayloadSize(2 * 32) {
// todo
}
}