合约小白初试薅羊毛

区块链安全 5年前 (2020) admin
887 0 0

何为空投?

合约小白初试薅羊毛

在token发行的过程中,为增加人气发行方可能会选择空投,即在一定时间窗口和投放总量的条件下免费给参与地址发送一定数量的token[1]。

何为薅羊毛?

合约小白初试薅羊毛
    一般来说,发行方会对已经发放token的地址都进行记录,因此每个地址只能得到一定数量的token,记为token_limit。但是贪心的用户可以通过创建很多的新账户,那么每个账户都将得到token_limit,然后再调用发行方合约中的transfer函数给指定账户转账,最终将获得大量的token,这也称为薅羊毛。

但是还有一点需要注意的是,手动申请账户往往比较麻烦,因为账户发起交易需要签名,而且每个账户都需要一些以太币,否则就没有gas去调用发行方合约中的transfer函数转账了。所以攻击者往往采用更加精妙的方法:在合约中创建新的合约账户。即,首先部署一个攻击合约,然后调用其中的函数动态生成临时合约。因为动态生成的临时合约都是未领取过token的账户,因此能够自动获得空投份额token_limit。最后在临时合约的构造函数中直接调用发行方合约中的transfer函数给指定账户转账即可。在这一过程中,攻击者只需要给攻击账户发送以太币就有足够的gas调用攻击合约,并且还能够控制临时合约的生成数量[1]。完整的攻击过程如下图所示[2]:

合约小白初试薅羊毛

薅羊毛实战

合约小白初试薅羊毛

下面以BCTF中的Fake3D题目为例,简单说明薅羊毛过程。

合约代码如下:

contract WinnerList{
    address public owner;
    struct Richman{
        address who;
        uint balance;
    }

    function note(address _addr, uint _valuepublic{
        Richman rm;
        rm.who = _addr;
        rm.balance = _value;
    }

}

contract Fake3D {
    using SafeMath for *;
    mapping(address => uint256)  public balance;
    uint public totalSupply  = 10**18;
    WinnerList wlist;

    event FLAG(string b64email, string slogan);

    constructor(address _addr) public{
        wlist = WinnerList(_addr);
    }

    modifier turingTest({
            address _addr = msg.sender;
            uint256 _codeLength;
            assembly {_codeLength := extcodesize(_addr)}
            require(_codeLength == 0"sorry humans only");
            _;
    }

    function transfer(address _to, uint256 _amountpublic{
        require(balance[msg.sender] >= _amount);
        balance[msg.sender] = balance[msg.sender].sub(_amount);
        balance[_to] = balance[_to].add(_amount);
    }

    //空投函数
    function airDrop(public turingTest returns (bool{
        uint256 seed = uint256(keccak256(abi.encodePacked(
            (block.timestamp).add
            (block.difficulty).add
            ((uint256(keccak256(abi.encodePacked(block.coinbase))))/ (now)).add
            (block.gaslimit).add
            ((uint256(keccak256(abi.encodePacked(msg.sender)))) / (now)).add
            (block.number)
        )));

        if((seed - ((seed / 1000) * 1000)) < 288){
            balance[tx.origin] = balance[tx.origin].add(10);
            totalSupply = totalSupply.sub(10);
            return true;
        }
        else
            return false;
    }

   function CaptureTheFlag(string b64emailpublic{
        require (balance[msg.sender] > 8888);
        wlist.note(msg.sender,balance[msg.sender]);
        emit FLAG(b64email, "Congratulations to capture the flag?");
    }

}

由题目中的两个合约名称WinnerList和Fake3D,容易让人联想到Last Winner(类 Fomo3D)游戏[3],而此题目也确实利用到了游戏中的空投漏洞,可以直接利用上面描述的攻击方式来解题。

在解题之前首先了解一下智能合约中的随机数预测的漏洞[6],下面进行简单展开。

在智能合约中,有漏洞的伪随机数生成器PRNG一般有如下四种类型:

1. 使用区块变量作为熵源的 PRNG

2. 基于过往区块的区块哈希的 PRNG

3. 基于过往区块和私有种子(seed)的区块哈希的 PRNG

4. 易被抢占交易(front-running)的 PRNG

题目中涉及到的为第一种类型,其中所说的区块变量包含如下几种:

(1). block.coinbase 表示当前区块的矿工地址

(2). block.difficulty 表示当前区块的挖掘难度

(3). block.gaslimit 区块内交易的最大限制燃气消耗量

(4). block.number 表示当前区块高度

(5). block.timestamp 表示当前区块挖掘时间


由于以上所有的区块变量都可以被矿工操纵,因此都不能用来作为信息熵源。通俗地来讲就是:如果生成随机数的种子使用到了当前区块的变量,那么这个PRNG就是有漏洞的。因为当攻击者通过其恶意合约调用受害者合约时,两个交易会被打包到一个区块中,其区块变量是一样的,因此恶意合约可以获得生成随机数种子的所有信息,从而实现随机数的预测。

回头看看题目中随机数种子的生成方法如下:

uint256 seed = uint256(keccak256(abi.encodePacked(
            (block.timestamp).add
            (block.difficulty).add
            ((uint256(keccak256(abi.encodePacked(block.coinbase))))/ (now)).add
            (block.gaslimit).add
            ((uint256(keccak256(abi.encodePacked(msg.sender)))) / (now)).add
            (block.number)
        )));

可以看到随机数种子的生成使用到了当前区块的变量block.timestamp、block.difficulty等以及msg.sender。其中的msg.sender虽然不是上述区块变量之一,但是却是用户可以操纵的字段。在以太坊中,一个地址(账户)创建一个合约,而合约地址是可以按照特定规则计算得到的,因此任何人都可以根据已知信息进行推算。从而攻击者可以构造一个类似挖矿的机制,以msg.sender为Nonce来挖矿,最终得到一个满足判断条件if((seed – ((seed / 1000) * 1000)) < 288)的随机数种子,这样一来就能保证每次都能够获得空投奖励了。 还有一点要说明的是空投函数airDrop有如下要求: assembly {_codeLength := extcodesize(_addr)} require(_codeLength == 0, "sorry humans only"); 也就是extcodesize为0。此修改器的作用是用于限制调用方法者智能是普通账户,即无法执行复杂的代码,也无法重入。主要是通过判断地址内extcodesize是否为0来达到判断该地址是否为普通账户的目的的。但是当合约正在执行构造函数并部署时,其extcodesize也为0[4],因此可以通过在动态生成的临时合约的构造方法中直接调用空投函数airDrop来绕过此判断。 攻击逻辑代码如下[5]:

contract Attack {
    function Attack(payable {}
    Pwn pwn;
    function exp(uint256 timespublic {
        //循环创建临时合约
        for(uint i=0;i<times;i++){
            pwn = new Pwn();
        }
    }
    function (payable {
    }
}
contract Pwn {
    function Pwn(payable {
        Fake3D f3d;
        f3d=Fake3D(...);
        //调用空投函数
 //一般来说要判断一下seed,但是题目中调用airDrop不会造成什么损失,所以可以直接调用
        f3d.airDrop();

        if (f3d.balance(this)>=10)
        {
            //转账给指定账户
            f3d.transfer(...,10);
        }
        selfdestruct(...);
    }
    function (payable{
    }
}

这道题后面还需要通过WinnerList合约的地址进行反编译,然后发现对转账地址有所限制,使用爆破的方式得到合法的地址即可,这里就不进行展开了。

参考文章

合约小白初试薅羊毛

1.https://paper.seebug.org/646/

2.https://blog.csdn.net/xq723310/article/details/82779390?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task

3.https://www.freebuf.com/vuls/181486.html

4.https://blog.csdn.net/weixin_38746124/article/details/81904115

5.https://xz.aliyun.com/t/3472#toc-17

6.https://www.freebuf.com/vuls/179173.html    


招新小广告

ChaMd5 ctf组 长期招新

尤其是crypto+reverse+pwn+合约的大佬

欢迎联系[email protected]



合约小白初试薅羊毛

原文始发于微信公众号(ChaMd5安全团队):合约小白初试薅羊毛

版权声明:admin 发表于 2020年3月30日 上午12:00。
转载请注明:合约小白初试薅羊毛 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...