EVM 虚拟机在解析合约的字节码时,依赖的是ABI的定义,从而去识别各个字段位于字节码的什么地方。关于ABI,可以阅读这个文档:https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI
这里以 transfer 方法为例,一般 ERC-20 token 标准的代币都会实现 transfer 方法,这个方法在 ERC-20 标准中的定义为:function transfer(address to, uint tokens) public returns (bool success);
第一个参数是发送代币的目的地址,第二个参数是发送token的数量。当我们调用 transfer 函数向某个地址发送 N 个 ERC-20 代币的时候,交易的 input 数据分为 3 个部分:
32字节,是需要传输的代币数量,这里是1*10^18 GNT
0000000000000000000000000000000000000000000000000de0b6b3a7640000
所有这些加在一起就是交易数据:a9059cbb000000000000000000000000abcabcabcabcabcabcabcabcabcabcabcabcabca0000000000000000000000000000000000000000000000000de0b6b3a7640000
当调用 transfer 方法提币时,如果允许用户输入了一个短地址,通常是交易所这里没有做处理,比如没有校验用户输入的地址长度是否合法。
如果一个以太坊地址如下,注意到结尾为0,比如:0x1234567890123456789012345678901234567800,当我们将后面的 00 省略时,EVM 会从下一个参数的高位拿到 00 来补充,这就会导致一些问题了。
这时,token 数量参数其实就会少了 1个字节,即 token 数量左移了一个字节,使得合约多发送很多代币出来。我们看个例子:
pragma solidity ^0.4.23;
contract NPToken {
mapping(address => uint) balances;
event Transfer(address indexed_from, address index_to, uint256 _value);
constructor () public {
balances[tx.origin] = 10000;
}
function sendCoin(address to, uint amount) returns (bool sufficient) {
if (balances[msg.sender] < amount) return false;
balances[msg.sender] -= amount;
balances[to] += amount;
Transfer(msg.sender, to, amount);
return true;
}
function getBalance(address addr) constant returns (uint) {
return balances[addr];
}
}
这里调用 sendCoin 方法时,传入的参数如下:
0x90b98a11
00000000000000000000000062bec9abe373123b9b635b75608f94eb8644163e
0000000000000000000000000000000000000000000000000000000000000002
这里的0x90b98a11
是 method 的 hash值,第二个是地址,第三个是 amount 参数。当我们调用sendCoin
方法的时候,传入地址0x62bec9abe373123b9b635b75608f94eb8644163e
,并且把这个地址最后的“3e”丢掉,即扔掉末尾的一个字节,参数就变成了:
0x90b98a11
00000000000000000000000062bec9abe373123b9b635b75608f94eb86441600
^^
00000000000000000000000000000000000000000000000000000000000002
缺失的 1个字节,EVM 会把 amount 的高位的一个字节的 0填充到 address 部分,这样使得amount 向左移位了 1个字节,即向左移位 8。这样,amount 就成了2 << 8 = 512。
(1)首先需要生成一个 ETH 的靓号,这个账号末尾为 2个0,使用一些跑号工具就可以做到,比如MyLinkToken 工具,可以很轻易跑出末尾两个 0的。笔者写这篇文章时 MyLinkToken 已正式改名为 “币合钱包”,且最新版本为 2.4.3.0,下载地址:https://bbs.bihe.one/thread-322.htm,打开钱包后,选择“靓号生成器选项卡”
(2)找一个交易所钱包,该钱包里 token 数量为 256000;
(3)往这个钱包发送 1000 个币;
(4)然后再从这个钱包中提出 1000 个币,当然这时候写地址的时候把最后两个 0 去掉,如果交易所并没有校验用户填入的以太坊地址,则 EVM 会把所有函数的参数一起打包,会把 amount 参数的高位 1 个字节吃掉;
(5)这三个参数会被传入到 msg.data 中,然后调用合约的 transfer 方法,这时 amount 由于高位的 1 个字节被吃掉了,因此 amount = amount << 8,即扩大了 256 倍,这样就把 25600 个币全部提出来了。
针对这个漏洞,说实话以太坊有不可推卸的责任,因为 EVM 并没有严格校验地址的位数,并且还擅自自动补充消失的位数。此外,交易所在提币的时候,需要严格校验用户输入的地址,这样可以尽早在前端就禁止掉恶意的短地址。