再谈重入攻击
简介
前几天无意间看到一篇名为Most common smart contract bugs of 2020的报告,进去认真看了一下,发现提到的一个重入攻击挺有意思的。就来复现一下。整个过程中也碰到了一些有意思的点。很遗憾的是,在即将写完这篇文章的时候,我在实际的审计过程中也遇到了相似的代码实现。
代码分析
先来看一段代码
1 | function update() { |
分析发现这一段代码的功能是:
- 获取用户的存款数额
- 使用
safeTransferETH
函数发送用户的存款 - 把用户的存款数额置为零
一般的审计会认为这段代码是安全的,因为其使用的是safeTransferETH
一般意义上是安全的函数。
但是让我们跟入safeTransferETH
函数去看一下
1 | function safeTransferETH(address to, uint256 value) internal { |
熟悉重入攻击的人一下子就能发现,这段代码十分的不安全。不了解的可以看这一篇文章
尝试攻击
按照之前的攻击思路,完善一下受害合约,顺便写一下攻击合约
1 | pragma solidity ^0.8.0; |
第一次尝试
进行一些初始化之后
然后我们调用attack1函数进行攻击
很遗憾的交易失败,我进行了debug调试
发现成功的调用了攻击合约的fallback函数,此时我认为,是因为攻击合约的存款和受害合约的balance不成正比,导致最后一次的call调用返回了失败导致整体的回退。
第二次尝试
这次我将攻击合约的存款与受害合约的balance调整为10:1,再次攻击,依旧失败了
第三次尝试
这一次我想到了solidity 0.8.9的更新版本允许我们定义下面类型的fallback函数
1 | fallback(bytes calldata) external payable returns(bytes memory) { |
当call调用之后,我们自行定义返回值,使得返回值的第一个字节为1也就等价于true。不知道我们自定义的返回值是否会对bool success产生影响,如果可以的就可以使得我们的重入不会在最后一次失败。
再次失败,进过调试发现我们自定义的返回值对bool success并未产生影响。再次翻阅call调用的返回值的定义,发现 call函数的返回值为true或者false。 只有当能够找到此方法并执行成功后,会返回true,而如果不能够找到此函数或执行失则会返回false。因此我刚刚的方法不能奏效。
第四次尝试
经过进一步调试发现,未限制重入次数的情况下,总会在最后一次call失败,如果我们能控制重入的次数应该就可以实现,保证每一次都是成功的,就可以实现交易不会失败。
稍微改善代码
1 | uint i; |
启动attack
攻击成功!
总结
本次漏洞发生的原因在于错误的利用被认为是安全的函数代码库,而在实际的审计过程中大家往往会忽略这种函数的审查,误以为其是安全的。其实一进入该函数的实现,就能轻而易举的发现问题,也算是人性的弱点吧。本来想展示实际业务中的审计到的代码,但是由于业务保密性要求,很遗憾不能展示。