【问题标题】:How to jump to / call arbitrary memory in Rust如何在 Rust 中跳转/调用任意内存
【发布时间】:2021-05-20 09:05:00
【问题描述】:

我正在尝试构建一个执行程序生成的 x86 指令的 JIT。我想我已经创建了一个有效的 x86 字节代码片段,应该打印“Hello World”,但我不知道如何调用它。

我将指向向量开头的指针转换为 void 函数并调用它:

fn main() {
    let msg: &[u8] = b"Hello World\0";

    let mut byte_codes: Vec<u8> = Vec::with_capacity(1000);

    // Move width into edx
    byte_codes.extend_from_slice(&[0xba, msg.len() as u8, 0, 0, 0]);
    
    // Msg to write
    byte_codes.push(0xb9);
    byte_codes.extend_from_slice(&(msg.as_ptr() as u64).to_be_bytes());
    
    // File descriptor and sys call
    byte_codes.extend_from_slice(&[0xbb, 0x01, 0, 0, 0]);
    byte_codes.extend_from_slice(&[0xb8, 0x04, 0, 0, 0]);
    
    // Sys call
    byte_codes.extend_from_slice(&[0xcd, 0x80]);

    // Return
    byte_codes.push(0xc3); 

    let func_ptr = byte_codes.as_ptr();
    unsafe {
        let func: fn() -> () = func_ptr.cast::<fn() -> ()>().read();
        func();
    }
}

执行返回:

error: process didn't exit successfully: `target\debug\run-bytecode.exe` (exit code: 0xc0000005, STATUS_ACCESS_VIOLATION)

除去返回调用以外的所有字节码也会导致同样的错误。

我不确定这个错误是什么意思。字节码有问题还是我的函数转换不正确?如何让它打印“Hello World”?

【问题讨论】:

  • 您可能需要在调用它之前将内存设置为可执行。请参阅此处针对 Linux 平台的 mprotect 调用:github.com/jonathandturner/rustyjit/blob/master/src/main.rs
  • 我预计主要问题是W^X / DEP:在大多数现代系统中,页面可以可写或可执行。因此,当您生成要执行的代码时,您需要更改页面的保护模式。这样做是特定于平台的。
  • 您可能遇到的另一个问题是 ABI:您已经断言您正在调用的代码是 Rust 函数。这意味着 Rust 编译器会期望在另一端有一个 Rust 可调用的,而你不知道这意味着什么,因为 Rust 没有稳定的 ABI。因此,您需要制作您的“函数”,使其尊重“C”ABI,并且您需要通过转换为extern fn告诉 rustc,并且您可能需要指定ABI 明确地例如默认的"C" abi 在 Windows 和 Unices 以及 x86、x64 和 ARM 上有所不同。
  • 如果你在夜间使用,内联和全局汇编都有宏。此外,在 ABI 说明中,extern "System" 可能值得一看。

标签: assembly rust x86 jit


【解决方案1】:

这是一个可行的版本:

use memmap::MmapMut;

fn main() {
    let msg: &[u8] = b"Hello World\0";

    let mut byte_codes: Vec<u8> = Vec::with_capacity(1000);

    // Move width into edx
    byte_codes.extend_from_slice(&[0xba, msg.len() as u8, 0, 0, 0]);

    // Msg to write
    byte_codes.push(0xb9);
    byte_codes.extend_from_slice(&(msg.as_ptr() as u32).to_le_bytes());

    // File descriptor and sys call
    byte_codes.extend_from_slice(&[0xbb, 0x01, 0, 0, 0]);
    byte_codes.extend_from_slice(&[0xb8, 0x04, 0, 0, 0]);

    // Sys call
    byte_codes.extend_from_slice(&[0xcd, 0x80]);

    // Return
    byte_codes.push(0xc3);

    let mut m = MmapMut::map_anon(byte_codes.len()).unwrap();
    m.clone_from_slice(&byte_codes);
    let m = m.make_exec().unwrap();
    let func_ptr = m.as_ptr();
    unsafe {
        let func: extern "C" fn() = std::mem::transmute(func_ptr);
        func();
    }
}

有几件事需要解决:

  1. 看起来 byte_codes 是 32 位 x86 Linux 代码,因此需要使用类似 cargo run --target i686-unknown-linux-gnu 的代码运行
  2. 因为它是 32 位代码,我们想将 msg.as_ptr() 转换为 u32
  3. x86 是 little-endian 所以我们想使用.to_le_bytes()
  4. func_ptr.cast::&lt;fn() -&gt; ()&gt;().read() 不会转换为函数指针,它会将byte_codes 的前 4/8 字节转换为函数指针。
  5. 使用 extern "C" fn() 确保 Rust 知道正确的 ABI
  6. 我们使用 memmap crate 创建内存,我们可以使用make_exec() 将其标记为可执行文件。

【讨论】:

  • 另一个您没有提到的错误:EBX 在 i386 System V ABI 中保留调用(与所有其他 x86 和 x86-64 调用约定一样)。但是0xbb, 0x01, 0, 0, 0mov ebx, 1(int 0x80 Linux 32 位 ABI 的第一个参数),我没有看到此代码保存/恢复 EBX(例如在堆栈上)。如果是,那么在int 0x80C3 ret 之间会有一个pop ebx
猜你喜欢
  • 2016-11-10
  • 2019-05-09
  • 2022-08-21
  • 1970-01-01
  • 2023-04-06
  • 2023-01-08
  • 1970-01-01
  • 2011-01-16
  • 2013-05-05
相关资源
最近更新 更多