何を言っているのだかわからないかもしれないが、これはつまりQEMUのまねごとをRustで実現したいという話。 QEMUではTCGによって変換されたゲストの機械語をホストに変換する際、TCGを機械語に変換したものをメモリ上に配置し、これを機械語とみなして実行するという猛烈な荒業をやってのける。
qemu/include/tcg/tcg.h
#ifdef HAVE_TCG_QEMU_TB_EXEC uintptr_t tcg_qemu_tb_exec(CPUArchState *env, uint8_t *tb_ptr); #else # define tcg_qemu_tb_exec(env, tb_ptr) \ ((uintptr_t (*)(void *, void *))tcg_ctx->code_gen_prologue)(env, tb_ptr) #endif
上記のプログラムがQEMUのTCG実行の最もキモとなる部分である。このcode_gen_prologue
のメモリ領域にTCGから変換された機械語が積み上げられており、これを実行することでホストの機械語がそのまま実行される。
qemu/tcg/i386/tcg-target.inc.c
/* Generate global QEMU prologue and epilogue code */ static void tcg_target_qemu_prologue(TCGContext *s) { int i, stack_addend; ... /* Reserve some stack space, also for TCG temps. */ stack_addend = FRAME_SIZE - PUSH_SIZE; tcg_set_frame(s, TCG_REG_CALL_STACK, TCG_STATIC_CALL_ARGS_SIZE, CPU_TEMP_BUF_NLONGS * sizeof(long)); /* Save all callee saved registers. */ for (i = 0; i < ARRAY_SIZE(tcg_target_callee_save_regs); i++) { tcg_out_push(s, tcg_target_callee_save_regs[i]); } #if TCG_TARGET_REG_BITS == 32 tcg_out_ld(s, TCG_TYPE_PTR, TCG_AREG0, TCG_REG_ESP, ...
このカラクリは、メモリ領域にデータを置いて、実行許可を与えてしまえばすべてプログラムになりうるという非常に単純な原理によって実現されている。分かってしまえば単純な方式だ。
では、これをRustで実現したい。このためにはどのようにすればよいのだろう?
まず検索してみると、以下のようなStackOverFlowが見つかった。
なるほど、mmap
を使えば実現できそうだ。ファイルとして機械語を表現するかどうかはともかく、メモリ領域を確保してそこに機械語を埋め込めば、その領域のプログラムを実行できるはずだ。
unsafe fn reflect(instructions: &[u8]) { let map = match MemoryMap::new( instructions.len(), &[ // MapOption::MapAddr(0 as *mut u8), // MapOption::MapOffset(0), // MapOption::MapFd(fd), MapOption::MapReadable, MapOption::MapWritable, MapOption::MapExecutable, // MapOption::MapNonStandardFlags(libc::MAP_ANON), // MapOption::MapNonStandardFlags(libc::MAP_PRIVATE), ], ) { Ok(m) => m, Err(e) => panic!("Error: {}", e), }; std::ptr::copy(instructions.as_ptr(), map.data(), instructions.len()); let func: unsafe extern "C" fn() -> u8 = mem::transmute(map.data()); let ans = func(); println!("ans = {:x}", ans); }
map
変数により命令を格納するメモリ領域を確保し、copy
により命令をコピーする。そしてfunc()
としてこれをC言語の関数として取り扱うらしい?これを実行するという訳だ。
試しに、命令として戻り値レジスタrax
に即値を与えて戻るだけの命令列を渡してみよう。
fn main() { let shellcode: [u8; 8] = [ 0x48, 0x83, 0xc7, 0x0a, // add 0xa, %rdi 0x48, 0x89, 0xf8, // mov %rdi, %rax 0xc3]; // retq unsafe { reflect(&shellcode); } }
8ビット8個で構成されるx86の命令を渡した。即値0xaをraxに渡して、戻るだけである。これを実行してみよう。
$ cargo run
ans = a
お、いいね。戻り値が正しく取得できた。