使用 Rust 编写转发 DLL

本文实现一个转发 DLL:version.dll 转发到系统的 version.dll 上。另外要注意,本文代码仅供参考,不一定可以运行,需要做一定的修改,或直接查看最终实现:https://github.com/hamflx/forward-dll

首先用下面的命令查看系统的 version.dll 导出函数:

C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise>dumpbin /exports c:\windows\system32\version.dll
Microsoft (R) COFF/PE Dumper Version 14.29.30136.0
Copyright (C) Microsoft Corporation.  All rights reserved.


Dump of file c:\windows\system32\version.dll

File Type: DLL

  Section contains the following exports for VERSION.dll

    00000000 characteristics
    927B71E6 time date stamp
        0.00 version
           1 ordinal base
          17 number of functions
          17 number of names

    ordinal hint RVA      name

          1    0 00001080 GetFileVersionInfoA
          2    1 00002190 GetFileVersionInfoByHandle
          3    2 00001DF0 GetFileVersionInfoExA
          4    3 00001040 GetFileVersionInfoExW
          5    4 00001010 GetFileVersionInfoSizeA
          6    5 00001E00 GetFileVersionInfoSizeExA
          7    6 00001050 GetFileVersionInfoSizeExW
          8    7 00001060 GetFileVersionInfoSizeW
          9    8 00001070 GetFileVersionInfoW
         10    9 00001E10 VerFindFileA
         11    A 00002360 VerFindFileW
         12    B 00001E20 VerInstallFileA
         13    C 00002F80 VerInstallFileW
         14    D          VerLanguageNameA (forwarded to KERNEL32.VerLanguageNameA)
         15    E          VerLanguageNameW (forwarded to KERNEL32.VerLanguageNameW)
         16    F 00001020 VerQueryValueA
         17   10 00001030 VerQueryValueW

  Summary

        1000 .data
        1000 .pdata
        2000 .rdata
        1000 .reloc
        1000 .rsrc
        3000 .text

C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise>

然后我们拿 GetFileVersionInfoSizeA 举例来写一个实例函数:

#![allow(unused)]
fn main() {
static mut RealGetFileVersionInfoSizeA: usize = 0;

#[no_mangle]
pub extern "system" fn GetFileVersionInfoSizeA() -> u32 {
    unsafe {
        std::arch::asm!(
            "jmp rax",
            in("rax") RealGetFileVersionInfoSizeA,
            options(nostack)
        );
    }
    1
}
}

这个函数其实也不能叫函数,因为它不会返回,直接跳转到目标函数地址,这个目标函数地址需要在 DllMain 中通过 LoadLibraryGetProcAddress 进行赋值:

#![allow(unused)]
fn main() {
#[no_mangle]
pub extern "system" fn DllMain(_inst: isize, reason: u32, _: *const u8) -> u32 {
    if reason == 1 {
        let version_module = load_library("c:\\windows\\system32\\version.dll");
        unsafe { RealGetFileVersionInfoSizeA = get_proc_address(version_module, "GetFileVersionInfoSizeA") };
    }
    1
}
}

如果每个函数都这么写,那是相当的麻烦,因此,我们可以写一个宏,并把加载目标 dll 的真实地址封装到结构体的方法里面,这样在 DllMain 时直接调用即可:

#![allow(unused)]
fn main() {
#[macro_export]
macro_rules! forward_dll {
    ($lib:expr, $name:ident, $($proc:ident)*) => {
        static mut $name: forward_dll::DllForwarder<{ forward_dll::count!($($proc)*) }> = forward_dll::DllForwarder {
            lib_name: $lib,
            target_functions_address: [
                0;
                forward_dll::count!($($proc)*)
            ],
            target_function_names: [
                $(stringify!($proc),)*
            ]
        };
        forward_dll::define_function!($name, 0, $($proc)*);
    };
}

#[macro_export]
macro_rules! define_function {
    ($name:ident, $index:expr, ) => {};
    ($name:ident, $index:expr, $proc:ident $($procs:ident)*) => {
        #[no_mangle]
        pub extern "system" fn $proc() -> u32 {
            unsafe {
                std::arch::asm!(
                    "jmp rax",
                    in("rax") $name.target_functions_address[$index],
                    options(nostack)
                );
            }
            1
        }
        forward_dll::define_function!($name, ($index + 1), $($procs)*);
    };
}

/// DLL 转发类型的具体实现。该类型不要自己实例化,应调用 forward_dll 宏生成具体的实例。
pub struct DllForwarder<const N: usize> {
    pub target_functions_address: [usize; N],
    pub target_function_names: [&'static str; N],
    pub lib_name: &'static str,
}

impl<const N: usize> DllForwarder<N> {
    /// 将所有函数的跳转地址设置为对应的 DLL 的同名函数地址。
    pub fn forward_all(&mut self) -> ForwardResult<()> {
        let load_module_dir = "C:\\Windows\\System32\\";
        let module_full_path = format!("{}{}", load_module_dir, self.lib_name);
        let module_handle = get_module_handle(module_full_path.as_str())?;

        for index in 0..self.target_functions_address.len() {
            let addr_in_remote_module =
                get_proc_address_by_module(module_handle, self.target_function_names[index])?;
            self.target_functions_address[index] = addr_in_remote_module as *const usize as usize;
        }

        Ok(())
    }
}

forward_dll::forward_dll!(
    "C:\\Windows\\system32\\version.dll",
    DLL_VERSION_FORWARDER,
    GetFileVersionInfoA
    GetFileVersionInfoByHandle
    GetFileVersionInfoExA
    GetFileVersionInfoExW
    GetFileVersionInfoSizeA
    GetFileVersionInfoSizeExA
    GetFileVersionInfoSizeExW
    GetFileVersionInfoSizeW
    GetFileVersionInfoW
    VerFindFileA
    VerFindFileW
    VerInstallFileA
    VerInstallFileW
    VerLanguageNameA
    VerLanguageNameW
    VerQueryValueA
    VerQueryValueW
);

// 在 DllMain 中调用:
// unsafe { DLL_VERSION_FORWARDER.forward_all() };
}

这就完成了 version.dll 的转发。可参考通过该方法实现的一个小工具:https://github.com/hamflx/huawei-pc-manager-bootstrap

法二

如果我们不希望通过 DllMain 来初始化怎么办?我们可以在跳板函数里面加载目标函数地址,为了保证寄存器和栈上数据的状态,我们单独写一个加载函数,并从跳板里面调用过去,由编译器来帮我们保证寄存器的状态。

通过该函数拿到目标函数地址后将其返回,那么目标函数地址就存储在 rax 上,然后再跳转到 rax 上:

#![allow(unused)]
fn main() {
pub extern "system" fn $proc() -> u32 {
    unsafe {
        std::arch::asm!(
            "push rcx",
            "push rdx",
            "push r8",
            "push r9",
            "push r10",
            "push r11",
            options(nostack)
        );
        std::arch::asm!(
            "sub rsp, 28h",
            "call rax",
            "add rsp, 28h",
            in("rax") forward_dll::default_jumper,
            in("rcx") std::concat!($lib, "\0").as_ptr() as usize,
            in("rdx") std::concat!(std::stringify!($proc), "\0").as_ptr() as usize,
            options(nostack)
        );
        std::arch::asm!(
            "pop r11",
            "pop r10",
            "pop r9",
            "pop r8",
            "pop rdx",
            "pop rcx",
            "jmp rax",
            options(nostack)
        );
    }
    1
}
}

然后我们实现一个 forward_dll::default_jumper 方法:

#![allow(unused)]
fn main() {
/// 默认的跳板,如果没有执行初始化操作,则进入该函数。
pub fn default_jumper(
    lib_name: *const u8,
    func_name: *const u8,
) -> usize {
    let module_handle = unsafe { LoadLibraryA(lib_name) };
    if module_handle != 0 {
        let addr = unsafe { GetProcAddress(module_handle, func_name) };
        // 这里调用了 FreeLibrary 释放目标模块,实际使用需要在其他地方持有目标模块的句柄,防止被释放。
        unsafe { FreeLibrary(module_handle) };
        return addr.map(|addr| addr as usize).unwrap_or(exit_fn as usize);
    }

    exit_fn as usize
}
}