Luoingly's Space

SHCTF 2023「山河」网络安全技能挑战赛 Blockchain 方向个人题解

October 30, 2023 · Legacy Blog

blockchain signin

Can you make the isSolved() function return true?

[1] - Create an account which will be used to deploy the challenge contract
[2] - Deploy the challenge contract using your generated account
[3] - Get your flag once you meet the requirement
[4] - Show the contract source code

不难看出,题目给了一个合约(Contract,而且是每个选手独立提供),需要进行操作让合约的 isSolved 函数返回真值。我们先看一下合约的源代码:

// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.9;

contract Greeter {
    string greeting;

    constructor(string memory _greeting) {
        greeting = _greeting;
    }

    function greet() public view returns (string memory) {
        return greeting;
    }

    function setGreeting(string memory _greeting) public {
        greeting = _greeting;
    }

    function isSolved() public view returns (bool) {
        string memory shctf = "welC0meToSHCTF2023";
        return
            keccak256(abi.encodePacked(shctf)) ==
            keccak256(abi.encodePacked(greeting));
    }
}

不难看出,本题不需要攻击合约,只要能成功调用 setGreeting 函数将合约中变量 greeting 的值设为 welC0meToSHCTF2023,就可以让 isSolved 函数返回真值,进而完成题目得到 Flag。接下来从基础开始,先生成一个钱包:

# create_wallet.py
from web3 import Account
import json

account = Account.create()
wallet_info = {
    "address": account.address,
    "private_key": account._private_key.hex(),
}

print(f"[WALLET] 已生成新的钱包:{wallet_info['address']}")

WALLET_FILE = "wallet_info.json"
with open(WALLET_FILE, "w") as file:
    json.dump(wallet_info, file, indent=4)

print(f"[WALLET] 钱包信息已保存到 {WALLET_FILE} 文件。")

当然再写些基础步骤:读取钱包、连接到节点和查询余额

# read_wallet.py
from web3 import Account
import json

WALLET_FILE = "wallet_info.json"
with open(WALLET_FILE, "r") as file:
    wallet_info = json.load(file)

wallet_private_key = wallet_info["private_key"]
wallet_account = Account.from_key(wallet_private_key)
wallet_address = wallet_info["address"]

print(f"[WALLET] 读取了一个钱包,其地址为: {wallet_address}")
# read_wallet.py
from web3 import Web3
from web3.middleware import geth_poa_middleware

RPC_NODE = "http://101.37.81.166:10001"
w3 = Web3(Web3.HTTPProvider(RPC_NODE))
w3.middleware_onion.inject(geth_poa_middleware, layer=0)

print(f"[NETWORK] 已连接到 RPC 服务器:{RPC_NODE}")

block_number = w3.eth.block_number

print(f"[NETWORK] 当前网络最新块号:{block_number}")
# check_balance.py
from read_wallet import wallet_address
from connect_chain import w3

balance = w3.eth.get_balance(wallet_address)

print(f"[APP] 账户 {wallet_address} 的余额: {balance} wei")

先在题目中选择第一个选项,生成题目 Token 和合约部署钱包:

[+] deployer account: 0xD3d76f9dE2cC9B70eC85939828d42aff3327ee39
[+] token: v4.local.p0KqGlvGj72XlK9Jhimk76yVTipT-rw05_RT-QSy15zbCmZfiV6Z3H_iaUnOIuRw25F2725hxdtaa9k7bYjRdCPnNP_0l-2gyn1JNysyF3EGsmV3Tw5fzHgmsrOpBgRsiDx3-1pb7tnEwC1IeeEJpcqP6_aHJQ4jeiC0y35lQnpTQg.R3JlZXRlcg
[+] please transfer more than 0.001 test ether to the deployer account for next step

Token 是题目用来鉴别身份的,和以太坊无关。接下来我们去题目给的水龙头(那是一个网站)里面搞点钱:将自己刚刚生成的钱包和题目给的合约部署钱包的地址分别填入,然后领点钱。当然也可以码个领钱代码:

# get_faucet.py
from read_wallet import wallet_address
import requests

ETH_FAUCET = "http://101.37.81.166:10002"
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
    AppleWebKit/537.36 (KHTML, like Gecko) \
    Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60"

response = requests.post(
    f"{ETH_FAUCET}/api/claim",
    headers={
        'Accept': '*/*',
        'User-Agent': USER_AGENT
    }, data={
        'address': wallet_address
    })

print(f"[FAUCET] {response.text}")

import check_balance

领完钱后就可以在题目中选择第二个选项部署合约,可以得到合约的地址和部署合约这场交易的交易哈希:

[+] contract address: 0x248C6B9118A7065653B74FFF4d1bEF4142d4De87
[+] transaction hash: 0xda4f4ee6263e1142bdd7ea6e6978b3d176a82132854968120b0245ad16f62cac

写一些代码用于根据哈希查询交易的详细信息,可以获取一些基础信息:

from connect_chain import w3

transaction_hash = "0xda4f4ee6263e1142bdd7ea6e6978b3d176a82132854968120b0245ad16f62cac"
transaction = w3.eth.get_transaction(transaction_hash)

if transaction:
    print(f"[APP] 查询到交易 {transaction_hash} 的相关信息如下")
    print(f"[APP] Chain Id: {transaction['chainId']}")
    print(f"[APP] Block Number: {transaction['blockNumber']}")
    print(f"[APP] Sender Address: {transaction['from']}")
    print(f"[APP] Receiver Address: {transaction['to']}")
    print(f"[APP] Transfer Value: {transaction['value']} wei")
    print(f"[APP] Gas Price: {transaction['gasPrice']} wei")
    print(f"[APP] Gas Limit: {transaction['gas']}")
    print(f"[APP] Nonce: {transaction['nonce']}")
    # print("[DEBUG]", transaction.__dict__.items())
else:
    print(f"[APP] 交易 {transaction_hash} 不存在或尚未被确认。")

可以查询到链 ID 为 15643

[APP] 查询到交易 0xda4f4ee6263e1142bdd7ea6e6978b3d176a82132854968120b0245ad16f62cac 的相关信息如下
[APP] Chain Id: 15643
[APP] Block Number: 61591
[APP] Sender Address: 0xD3d76f9dE2cC9B70eC85939828d42aff3327ee39
[APP] Receiver Address: None
[APP] Transfer Value: 0 wei
[APP] Gas Price: 1000000007 wei
[APP] Gas Limit: 341762
[APP] Nonce: 0

到了这一步,所有准备工作都完成了,直接根据合约代码生成出合约的 ABI(将其保存到 contract_abi.json 中),然后编写合约操作的代码:

[{
    "constant": true,
    "inputs": [],
    "name": "greet",
    "outputs": [{
        "name": "",
        "type": "string"}],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
}, {
    "constant": false,
    "inputs": [{
        "name": "_greeting",
        "type": "string"}],
    "name": "setGreeting",
    "outputs": [],
    "payable": false,
    "stateMutability": "nonpayable",
    "type": "function"
}, {
    "constant": true,
    "inputs": [],
    "name": "isSolved",
    "outputs": [{
        "name": "",
        "type": "bool"}],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
}]
from read_wallet import wallet_address, wallet_private_key
from connect_chain import w3
import json

contract_address = "0x248C6B9118A7065653B74FFF4d1bEF4142d4De87"

with open("contract_abi.json", "r") as abi_file:
    contract_abi = json.load(abi_file)

contract = w3.eth.contract(address=contract_address, abi=contract_abi)

print(f"[CONTRACT] 加载了一个合约,其地址为:{contract_address}")

def send_transaction(transaction, private_key):
    signed_transaction = w3.eth.account.sign_transaction(
        transaction, private_key)
    transaction_hash = w3.eth.send_raw_transaction(
        signed_transaction.rawTransaction)

    print(f"[TRANSACTION] 发送了一个交易,其哈希为:{transaction_hash.hex()}")
    print(f"[TRANSACTION] 正在等待交易确认......")

    w3.eth.wait_for_transaction_receipt(transaction_hash)

    print(f"[TRANSACTION] 交易已被确认。")
    return transaction_hash

def get_greeting():
    return contract.functions.greet().call()

def set_greeting(new_greeting: str, wallet_address: str, private_key: str):
    transaction = contract.functions.setGreeting(new_greeting).build_transaction({
        "chainId": 15643,
        "maxFeePerGas": w3.to_wei(1.000000014, 'gwei'),
        "maxPriorityFeePerGas": w3.to_wei(1, 'gwei'),
        "gas": 0x200000,
        "value": w3.to_wei(0, 'ether'),
        "nonce": w3.eth.get_transaction_count(wallet_address),
    })

    return send_transaction(transaction, private_key)

def is_solved():
    return contract.functions.isSolved().call()

if __name__ == "__main__":
    greeting = get_greeting()
    print(f"[APP] 当前问候语: {greeting}")

    new_greeting = "welC0meToSHCTF2023"
    set_greeting(new_greeting, wallet_address, wallet_private_key)
    print(f"[APP] 设置新问候语: {new_greeting}")

    greeting = get_greeting()
    print(f"[APP] 当前问候语: {greeting}")

    solved = is_solved()
    print(f"[APP] 是否已解决: {solved}")

程序运行后就完成了问题,连接上题目后通过第三个选项得到 Flag:

[+] flag: SHCTF{1T_Ls_a_ESS3N7Iais_8IOCkCh4IN_ChaLLEn6E_Are_you_Ready_7O_DIve_In}

贪玩蓝月

先连上题目扒一下合约:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.4.25;

contract greedyBlueMoon {
    mapping(address => uint) private shards;
    mapping(address => uint) private newcomer;
    uint private paralysisRing;
    address owner;
    uint80 private rates;
    uint8 private bonus;
    uint16 private price;
    uint8 private loot;
    bytes10 private password;
    mapping(address => uint) private attackingType;
    mapping(address => uint) private waitTime;

    constructor(uint _bonus, uint _price, uint _rates, 
                uint _loot, bytes10 _password) public {
        owner = msg.sender;
        bonus = uint8(_bonus);
        price = uint16(_price);
        rates = uint80(_rates);
        loot = uint8(_loot);
        password = _password;
    }

    function newcomerBundle() public {
        assert(newcomer[msg.sender] == 0);
        shards[msg.sender] += bonus;
        newcomer[msg.sender] = 1;
    }
    function buyShards() public payable {
        uint yourMoney = msg.value / rates;
        shards[msg.sender] += yourMoney;
    }
    function fightMob() public {
        waitTime[msg.sender] = now + 1 minutes;
        attackingType[msg.sender] = 1;
    }
    function collectingMobLoot() public {
        require((waitTime[msg.sender] < now), "Mob is alive");
        require(attackingType[msg.sender] == 1);
        attackingType[msg.sender] = 0;
        shards[msg.sender] += 10;
    }
    function fightBoss() public {
        waitTime[msg.sender] = now + 52 weeks;
        attackingType[msg.sender] = 2;
    }
    function collectingBossLoot() public {
        require((waitTime[msg.sender] < now), "Boss is alive");
        require(attackingType[msg.sender] == 2);
        attackingType[msg.sender] = 0;
        paralysisRing += 1;
    }
    function transfersOfItems(address to, uint value) public {
        assert(shards[msg.sender] >= value);
        shards[msg.sender] -= value;
        shards[to] += value;
    }
    function redeemingParalyzingRing(bytes10 _key) public {
        require((shards[msg.sender] >= price), "Please use the money power");
        require(
            (keccak256(abi.encodePacked(password)) ==
                keccak256(abi.encodePacked(_key))),
            "Wrong Password."
        );
        shards[msg.sender] -= price;
        paralysisRing += 1;
    }
    function isSolved() public view returns (bool) {
        require(paralysisRing >= 1);
        return true;
    }
}

阅读合约代码可以得知,完成题目的要求是得到一个 Paralysis Ring,而做到这一点有两种途径,一个是通过 fightBoss 以及 collectingBossLoot 这两个函数,击杀 Boss 获得物品,不过这其中需要等待 52 周时间(也就是约一年),显然等不起。另一个途径是使用一定数量的 shards 通过 redeemingParalyzingRing 函数直接购买物品,当然这里还需要一个密码,但是这些内容统统是在合约构建的过程中传到链上的,而题目又会给我们创建合约的交易的 Hash,所以直接去这个交易的 input 里面找(相关基础代码在上一题的题解中好像有,不重复写了,constructor 的参数在整个 input 的最后,每个参数占 32 个字节,一共传入 5 个参数,所以直接找尾 160 字节):

   bonus 0000000000000000000000000000000000000000000000000000000000000042 66
   price 0000000000000000000000000000000000000000000000000000000000008f3a 36666
   rates 0000000000000000000000000000000000000000000000000666666666666666 461168601842738790
    loot 000000000000000000000000000000000000000000000000000000000000000a 10
password 007368637466326f323300000000000000000000000000000000000000000000 shctf2o23

直接购买 Paralysis Ring 的价格高得离谱(相对于通过 buyShards 函数使用以太币购买 shards 来说),优先考虑开小号刷新手大礼包(bonus 高达 66,只需要开约 555 个小号即可)然后将 shards 转给大号:

from read_wallet import wallet_address, wallet_private_key
from connect_chain import w3
from web3 import Account
from tqdm import tqdm
from time import sleep
import json

contract_address = "0x7446E2F30E2C8A8AB665eB9413E8A60c062dA53C"

with open("contract_abi.json", "r") as abi_file:
    contract_abi = json.load(abi_file)

contract = w3.eth.contract(address=contract_address, abi=contract_abi)

print(f"[CONTRACT] 加载了一个合约,其地址为:{contract_address}")

def create_wallet(wallet_address: str, private_key: str) -> str:
    account = Account.create()
    print(f"[WALLET] 已生成新的钱包:{account.address}")

    amount_in_wei = w3.to_wei('0.1', 'ether')
    nonce = w3.eth.get_transaction_count(wallet_address)
    transaction = {
        "chainId": 17069,
        'to': account.address,
        'value': amount_in_wei,
        "gas": 0x200000,
        'gasPrice': w3.to_wei('100', 'gwei'),
        'nonce': nonce,
    }
    signed_transaction = w3.eth.account.sign_transaction(
        transaction, private_key)
    tx_hash = w3.eth.send_raw_transaction(signed_transaction.rawTransaction)
    print(f'[TRANSFER] Transaction sent: {tx_hash.hex()}')
    w3.eth.wait_for_transaction_receipt(tx_hash.hex())

    balance = w3.eth.get_balance(account.address)
    print(f"[APP] 账户 {account.address} 的余额: {balance} wei")

    return (account.address, account._private_key.hex())

def send_transaction(transaction, private_key):
    signed_transaction = w3.eth.account.sign_transaction(
        transaction, private_key)
    transaction_hash = w3.eth.send_raw_transaction(
        signed_transaction.rawTransaction)

    print(f"[TRANSACTION] 发送了一个交易,其哈希为:{transaction_hash.hex()}")
    print(f"[TRANSACTION] 正在等待交易确认......")

    w3.eth.wait_for_transaction_receipt(transaction_hash)

    print(f"[TRANSACTION] 交易已被确认。")

    return transaction_hash

def newcomer_bundle(wallet_address: str, private_key: str):
    transaction = contract.functions.newcomerBundle().build_transaction({
        "chainId": 17069,
        "maxFeePerGas": w3.to_wei(1.000000014, 'gwei'),
        "maxPriorityFeePerGas": w3.to_wei(1, 'gwei'),
        "gas": 0x200000,
        "value": w3.to_wei(0, 'ether'),
        "nonce": w3.eth.get_transaction_count(wallet_address),
    })
    return send_transaction(transaction, private_key)

def transfers_of_items(to_address: str, wallet_address: str, private_key: str):
    transaction = contract.functions.transfersOfItems(to_address, 0x42).build_transaction({
        "chainId": 17069,
        "maxFeePerGas": w3.to_wei(1.000000014, 'gwei'),
        "maxPriorityFeePerGas": w3.to_wei(1, 'gwei'),
        "gas": 0x200000,
        "value": w3.to_wei(0, 'ether'),
        "nonce": w3.eth.get_transaction_count(wallet_address),
    })
    return send_transaction(transaction, private_key)

if __name__ == "__main__":
    error_count = 0
    for i in tqdm(range(100)):
        print()
        print(f"[BUMP] Counter: {i-error_count}")
        try:
            tmp_addr, tmp_key = create_wallet(
                wallet_address, wallet_private_key)
            newcomer_bundle(tmp_addr, tmp_key)
            transfers_of_items(wallet_address, tmp_addr, tmp_key)
        except ValueError:
            print("[BUMP] Error Catched.")
            error_count += 1
            sleep(5)

刷完之后直接兑换:

from read_wallet import wallet_address, wallet_private_key
from connect_chain import w3
import json

contract_address = "0x7446E2F30E2C8A8AB665eB9413E8A60c062dA53C"

with open("contract_abi.json", "r") as abi_file:
    contract_abi = json.load(abi_file)

contract = w3.eth.contract(address=contract_address, abi=contract_abi)

print(f"[CONTRACT] 加载了一个合约,其地址为:{contract_address}")

def send_transaction(transaction, private_key):
    signed_transaction = w3.eth.account.sign_transaction(
        transaction, private_key)
    transaction_hash = w3.eth.send_raw_transaction(
        signed_transaction.rawTransaction)
    print(f"[TRANSACTION] 发送了一个交易,其哈希为:{transaction_hash.hex()}")
    print(f"[TRANSACTION] 正在等待交易确认......")

    transfer_receipt = w3.eth.wait_for_transaction_receipt(transaction_hash)

    print("[DEBUG]", transfer_receipt)
    print(f"[TRANSACTION] 交易已被确认。")

    return transaction_hash

def buy_shards(value: int, wallet_address: str, private_key: str):
    transaction = contract.functions.buyShards().build_transaction({
        "chainId": 17069,
        "maxFeePerGas": w3.to_wei(1.000000014, 'gwei'),
        "maxPriorityFeePerGas": w3.to_wei(1, 'gwei'),
        "gas": 0x200000,
        "value": w3.to_wei(value, 'ether'),
        "nonce": w3.eth.get_transaction_count(wallet_address),
    })
    return send_transaction(transaction, private_key)

def redeeming_paralyzing_ring(key: str, wallet_address: str, private_key: str):
    transaction = contract.functions.redeemingParalyzingRing(
        bytes(key.rjust(10, '\0'), 'utf-8')).build_transaction({
            "chainId": 17069,
            "maxFeePerGas": w3.to_wei(1.000000014, 'gwei'),
            "maxPriorityFeePerGas": w3.to_wei(1, 'gwei'),
            "gas": 0x200000,
            "value": w3.to_wei(0, 'ether'),
            "nonce": w3.eth.get_transaction_count(wallet_address),
        })
    return send_transaction(transaction, private_key)

def is_solved():
    return contract.functions.isSolved().call()

def transfers_of_items(amount: int, to_address: str, wallet_address: str, private_key: str):
    transaction = contract.functions.transfersOfItems(to_address, amount).build_transaction({
        "chainId": 17069,
        "maxFeePerGas": w3.to_wei(1.000000014, 'gwei'),
        "maxPriorityFeePerGas": w3.to_wei(1, 'gwei'),
        "gas": 0x200000,
        "value": w3.to_wei(0, 'ether'),
        "nonce": w3.eth.get_transaction_count(wallet_address),
    })
    return send_transaction(transaction, private_key)

if __name__ == "__main__":
    # buy_shards(10, wallet_address, wallet_private_key).hex()
    print(redeeming_paralyzing_ring("shctf2o23",
          wallet_address, wallet_private_key).hex())
    # transfer = transfers_of_items(366660, wallet_address,
    #                               wallet_address, wallet_private_key).hex()
    transfer_receipt = w3.eth.get_transaction_receipt(transfer)

    print("[DEBUG]", transfer_receipt.__dict__.items())

Flag:SHCTF{AirDrop_Hunting_1s_v3ry_w0ndeRful_bUt_ESsEnT1AIs}

Tags: #CTF #Writeup #Blockchain #SHCTF

This article is authored by luoingly and licensed under CC BY-NC 4.0

Permalink: https://luoy.ing/posts/shctf-2023-blockchain-writeup/