基于虚拟化技术的内存监控与逆向工程(很长)

admin 2025年6月5日03:20:34评论31 views字数 37293阅读124分18秒阅读模式

【翻译】Hypervisors for Memory Introspection and Reverse Engineering

免责声明:本博客文章仅用于教育和研究目的。提供的所有技术和代码示例旨在帮助防御者理解攻击手法并提高安全态势。请勿使用此信息访问或干扰您不拥有或没有明确测试权限的系统。未经授权的使用可能违反法律和道德准则。作者对因应用所讨论概念而导致的任何误用或损害不承担任何责任。

引言

本文探讨基于 Rust 实现的 Windows 内存监控与逆向工程虚拟机监控程序(hypervisor)的设计与实现。我们重点分析两个项目——UEFI 架构的 illusion-rs 和内核驱动架构的 matrix-rs。两者均利用扩展页表(Extended Page Tables, EPT)技术实现无需修改客户机内存的隐蔽控制流重定向。

我们首先确定如何可靠检测系统服务描述表(System Service Descriptor Table, SSDT)在 ntoskrnl.exe 中的初始化完成时机,以确保安全植入钩子(hook)而不引发系统崩溃。Illusion 和 Matrix 在触发和执行重定向机制上有所不同:Illusion 采用单一 EPT 架构,结合 VMCALL 等 VM-exit 指令进行原位补丁(in-place patching),并通过监控陷阱标志(Monitor Trap Flag, MTF)单步执行来安全回放原始字节;Matrix 则采用双 EPT 模型,主 EPT 映射读写内存,次 EPT 重映射包含跳板(trampoline)钩子的仅执行(execute-only)影子页面,利用 INT3 断点和 EPT 违规时的动态 EPTP 切换实现执行流重定向。两种方案都通过 EPT 重映射技术隐藏内联钩子(inline hook),并借助 INT3VMCALL 或 CPUID 等指令触发的 VM-exit 将执行流重定向到攻击者控制的代码(如 shellcode 或处理函数)。

在 hypervisor 开发中,影子分页(shadowing)指创建 hypervisor 控制的客户机内存第二视图。当页面被影子化时,hypervisor 会创建原始页面的副本(称为影子页面),并更新 EPT 将访问重定向到该副本。这使得 hypervisor 能够在不修改原始客户机内存的情况下拦截、监控或重定向内存访问。该技术常用于注入钩子、隐藏修改或细粒度控制执行流。客户机与影子页面保持隔离:客户机认为在访问自身内存,而实际访问由 hypervisor 控制。

我们将演示如何利用仅执行权限捕获指令获取、读写权限捕获访问违规,以及通过影子页面注入跳板重定向。根据上下文环境,使用 VMCALLCPUID 和 INT3 等指令级陷阱实现内存监控和控制流转移。在 Illusion 中,指令回放通过 MTF 单步执行安全恢复被覆盖字节。

尽管这些技术在游戏黑客社区广为人知,但在信息安全领域仍未得到充分利用。本文旨在通过可复现的早期启动阶段和内核模式 EPT 钩接技术实践,弥合这一差距。所有技术均基于公开稳定接口实现,不依赖未文档化内部机制或特权 SDK。

本文采用极简主义方法论,假设读者具备分页机制、虚拟内存及 Intel VT-x/EPT基础概念。虽然部分概念适用于AMD SVM/NPT,但本文仅聚焦 Intel 平台。两个 hypervisor 均完全避免修改客户机内存,保持系统完整性并规避 PatchGuard 等内核保护机制,通过EPT重映射实现对外部函数(如 NtCreateFile 和 MmIsAddressValid)的隐蔽监控。

目录

  • Illusion:基于 UEFI 的 EPT 钩接方案
    • 通过超级调用(hypercall)控制 EPT 钩子
    • 安装钩子载荷(ept_hook_function()
    • 大页内存映射(map_large_page_to_pt()
    • 步骤 1 - 页面拆分(is_large_page() -> split_2mb_to_4kb()
    • 影子页面创建(is_guest_page_processed() -> map_guest_to_shadow_page()
    • 步骤 2 - 代码克隆(unsafe_copy_guest_to_shadow()
    • 步骤 3 - 内联钩子安装
    • 步骤 4 - 撤销执行权限(modify_page_permissions()
    • 步骤 5 - 刷新 TLB 与 EPT 缓存(invept_all_contexts()
    • 步骤 6-7 - 通过 EPT 违规捕获执行(handle_ept_violation()
    • 步骤 8 - 处理 VMCALL 钩子(handle_vmcall()
    • 步骤 9 - MTF 单步执行(handle_monitor_trap_flag()
    • 捕获读写违规(handle_ept_violation()
    • Intel 处理器执行路径可靠性验证(基于 Windows 11 build 26100 的 Binary Ninja 分析)
    • AMD 处理器条件执行路径验证(基于 Windows 11 build 26100 的 Binary Ninja 分析)
    • 初始化阶段设置 IA32_LSTAR MSR 钩子(initialize_shared_hook_manager()
    • 设置内核镜像基址与大小(set_kernel_base_and_size()
    • 检测 ntoskrnl.exe 中 SSDT 加载状态
    • 配置 EPT 钩子(handle_cpuid()
    • 目标解析与钩子分发(manage_kernel_ept_hook()
    • 二级地址转换(SLAT):EPT(Intel)与 NPT(AMD)
    • EPT 钩接技术总览(build_identity()
    • Illusion 执行追踪:概念验证演练
  • Matrix:基于双 EPT 模型的 Windows 内核驱动方案
    • 初始化主次 EPT(virtualize_system()
    • 步骤 1-2 - 创建影子钩子与跳板设置(hook_function_ptr()
    • 步骤 3-6 - 双 EPT 重映射实现影子执行(enable_hooks()
    • 步骤 7 - 配置断点 VM-exit 的 VMCS 控制域(setup_vmcs_control_fields()
    • 步骤 8 - 动态 EPTP 切换处理 EPT 违规(handle_ept_violation()
    • 步骤 9 - 通过断点处理程序重定向执行(handle_breakpoint_exception()
    • 步骤 10 - 通过跳板返回原始函数(mm_is_address_valid()与 nt_create_file()
    • Matrix 执行追踪:概念验证演练
  • 钩子重定向技术:INT3、VMCALL 与 JMP 对比
  • 虚拟机检测向量
  • 附录
    • Matrix(跨逻辑处理器共享 EPT)
    • Illusion(每逻辑处理器私有 EPT 配合 MTF)
    • 客户机辅助钩接模型
    • EPT 钩接模型对比:每核私有 vs 全局共享
  • 结论
    • 技术文章、工具与研究资料
    • 社区研究与启发
    • 特别致谢
    • 规范文档
    • 致谢与参考文献

Illusion:基于 UEFI 的 EPT 钩接方案

Illusion 是专为早期启动阶段内存监控和系统调用钩接设计的 UEFI 架构 hypervisor。作为 matrix-rs 的后续项目,其设计更简洁,结构更优化,专注于非侵入式执行控制。

与采用内核模式双 EPT 共享架构的 Matrix 不同,Illusion 运行于 UEFI 固件环境,采用每逻辑处理器(logical processor)独立 EPT 实现影子分页和执行流转移。部分 hypervisor 会扩展此设计——例如为不同执行阶段或进程上下文维护多个 EPT,或实现每逻辑处理器 EPT 隔离以增强控制。Illusion 通过仅执行影子页面结合 VMCALL 和 MTF 单步执行实现内存监控,在保持早期启动可见性的同时支持用户态 CPUID 超级调用的运行时控制。与其他 EPT 钩接技术类似,该架构在设计、可维护性、复杂性和检测风险间存在权衡,本文不做深入探讨。

下图展示了 illusion-rs 中 EPT 钩接内核内存的技术实现。虽然示例应用于早期启动阶段,但相同逻辑也可通过用户态信号触发启用/禁用。

基于虚拟化技术的内存监控与逆向工程(很长)

图 1: Illusion UEFI hypervisor 中基于 EPT 的函数钩接控制流

下文将详细解析图中各步骤。

初始化阶段设置 IA32_LSTAR MSR 钩子(initialize_shared_hook_manager()

为解析 Windows 内核的物理/虚拟基址和大小,我们拦截对 IA32_LSTAR MSR 寄存器的写操作。该寄存器存储系统调用处理程序地址(Windows 设置为内核模式分发器 KiSystemCall64)。当发生 WRMSR 触发的 VM-exit 时,我们检查 MSR ID 是否匹配 IA32_LSTAR,若匹配则提取 MSR 值并逆向扫描内存定位 ntoskrnl.exe 的 PE 镜像 MZ 签名,从而确定其虚拟基址。拦截 IA32_LSTAR 的目的并非修改系统调用行为,而是为了在早期启动阶段可靠提取内核加载基址——由于 Windows 总是在早期启动阶段写入该 MSR 来设置 KiSystemCall64,因此这是可靠的锚点。

需注意这不是内联钩子,而是由 MSR 写操作触发的 VM-exit 拦截。以下代码展示 hypervisor 初始化阶段如何配置 IA32_LSTAR 拦截:

Code Reference (hook_manager.rs)

trace!("Modifying MSR interception for LSTAR MSR write access");hook_manager    .msr_bitmap    .modify_msr_interception(msr::IA32_LSTARMsrAccessType::WriteMsrOperation::Hook);

处理 WRMSR 访问 IA32_LSTAR 的 VM-exit 流程([handle_msr_access()]:解除钩子并调用 [set_kernel_base_and_size()])

当内核早期初始化阶段由 IA32_LSTAR 钩子触发 WRMSR 指令导致的 VM-exit 时,handle_msr_access()函数将执行以下操作:首先解除对该 MSR 的钩子拦截,随后调用 set_kernel_base_and_size() 来解析内核镜像的基地址与大小。

Code Reference (msr.rs)

if msr_id == msr::IA32_LSTAR {    trace!("IA32_LSTAR write attempted with MSR value: {:#x}", msr_value);    hook_manager.msr_bitmap.modify_msr_interception(        msr::IA32_LSTAR,        MsrAccessType::Write,        MsrOperation::Unhook,    );    hook_manager.set_kernel_base_and_size(msr_value)?;}

在系统启动的这个阶段,内核已完全解析系统调用入口点(KiSystemCall64)。我们使用其地址作为扫描基准,通过逆向内存扫描定位 PE 镜像的起始位置(MZ 头),进而计算出 ntoskrnl.exe 的物理基址。

设置内核镜像基址与大小(set_kernel_base_and_size()

我们将 MSR 值传递给 set_kernel_base_and_size 函数,该函数内部调用 get_image_base_address 逆向扫描内存以定位 MZIMAGE_DOS_SIGNATURE)头。随后使用 pa_from_va_with_current_cr3 通过 guest 的 CR3 寄存器将虚拟基址转换为物理地址,最后调用 get_size_of_image 从 OptionalHeader.SizeOfImage 字段获取 ntoskrnl.exe 的大小。这些操作本质上具有不安全性,因此必须确保传入正确的参数值——否则可能导致系统崩溃。

Code Reference (hook_manager.rs)

self.ntoskrnl_base_va = unsafe { get_image_base_address(guest_va)? };self.ntoskrnl_base_pa = PhysicalAddress::pa_from_va_with_current_cr3(self.ntoskrnl_base_va)?;self.ntoskrnl_size = unsafe { get_size_of_image(self.ntoskrnl_base_pa as _).ok_or(HypervisorError::FailedToGetKernelSize)? } as u64;

检测 ntoskrnl.exe 中 SSDT 加载完成时机

在对 NtCreateFile 等内核函数实施基于 EPT 的钩子(hook)之前,必须确保 Windows 内核已完成系统服务描述表(System Service Descriptor Table, SSDT)的初始化。否则会引入竞态条件:若过早应用钩子,当 hypervisor 尝试通过 SSDT 的系统调用号解析函数地址时(该机制仅在函数未包含于 ntoskrnl.exe 导出表时作为备用方案),可能指向无效内存区域,导致系统崩溃。通过对 ntoskrnl.exe 内部执行路径的分析,我们发现了一个位于 SSDT 初始化之后、同时仍处于内核设置早期阶段的可靠锚点,可用于监控其他软件对这些函数的调用。

对 KiInitializeKernel(负责每个处理器内核初始化的核心例程)的分析表明,该函数通过调用 KeCompactServiceTable 完成 SSDT 的最终配置。自此之后安装钩子操作变得安全。然而仍需寻找可靠且可重复的触发点——理想情况下应是 KeCompactServiceTable 调用后立即发生的无条件 VM-exit 事件。

基于虚拟化技术的内存监控与逆向工程(很长)

图 2:通过 Binary Ninja 逆向分析 KiInitializeKernel() 中观察到的 KeCompactServiceTable() 与 KiSetCacheInformation() 调用序列,确认 SSDT 初始化后的执行流程。

此时 KiSetCacheInformation 的作用凸显。该函数在 SSDT 设置完成后立即被调用,并触发包含 CPUID 指令的标准化执行序列。在 Intel 处理器上,KiSetCacheInformation 会调用 KiSetStandardizedCacheInformation,后者通过执行 cpuid(4, 0)指令查询缓存拓扑信息。CPUID 指令在 Intel 处理器上会无条件触发 VM-exit,在 AMD 处理器上则根据拦截配置可能触发 VM-exit,这为同步 EPT 钩子安装提供了可靠且确定性的时机点。这使得 CPUID 成为无需客户机协作即可同步状态转换或触发早期 hypervisor 逻辑的理想指令。

基于虚拟化技术的内存监控与逆向工程(很长)

图 3:通过 Binary Ninja 观察到的 KiSetCacheInformation() 与 KiSetCacheInformationAmd()调用路径,两者均在 SSDT 设置后通过执行 CPUID 指令进入 KiSetStandardizedCacheInformation()

从历史版本看,Intel 系统采用 KiSetCacheInformation -> KiSetCacheInformationIntel -> KiSetStandardizedCacheInformation 调用链。在最新的 Windows 10/11构建版本中,中间层 KiSetCacheInformationIntel 的调用已被移除——Intel 平台现由 KiSetCacheInformation 直接调用 KiSetStandardizedCacheInformation

Intel 处理器执行路径可靠性验证(基于 Windows 11 build 26100 的 Binary Ninja 逆向分析)

KiInitializeKernel-> KeCompactServiceTable-> KiSetCacheInformation-> KiSetStandardizedCacheInformation-> cpuid(4, 0)

AMD 处理器执行路径的条件性验证(基于 Windows 11 build 26100 的 Binary Ninja 逆向分析)

  • 若 CPUID(0x80000001).ECX 寄存器第 22 位(TopologyExtensions)被置位:
KiInitializeKernel-> KeCompactServiceTable-> KiSetCacheInformation-> KiSetCacheInformationAmd-> KiSetStandardizedCacheInformation-> cpuid(0x8000001D, 0)

该位表示处理器支持 CPUID(0x8000001D) 指令(以标准化方式枚举缓存与拓扑信息)。若该位未置位,操作系统必须回退使用 0x80000005/0x80000006 指令。

• 其他情况(未启用 TopologyExtensions 支持的回退路径):

KiInitializeKernel-> KeCompactServiceTable-> KiSetCacheInformation-> KiSetCacheInformationAmd-> cpuid(0x80000005) and cpuid(0x80000006)

虽然在测试的 Windows 构建版本中,Intel 平台的 cpuid(4, 0) 和 AMD 平台的 cpuid(0x8000001D, 0) 指令会在 SSDT 初始化完成后立即执行,但本 hypervisor 却错误地使用了 cpuid(2, 0) 作为触发条件。这个错误源于早期开发阶段的遗留问题——cpuid(0x2) 并不属于 KiSetCacheInformation() 的标准执行路径,也无法作为 SSDT 初始化完成的确定性判断指标。该指令在测试环境的启动过程中偶然表现出可靠性,使其在当时被视为"足够有效"的解决方案。由于项目已停止主动维护,代码保持现状未作修正——但对于需要投入生产环境的使用者而言,正确的实现路径应选择挂钩 cpuid(4, 0) 或 cpuid(0x8000001D)

EPT 钩子设置机制 (handle_cpuid())

CPUID 指令在系统启动初期会多次执行,可能导致冗余的 VM-exit 事件。为避免重复的钩子安装操作,hypervisor 采用 has_cpuid_cache_info_been_called 状态标志进行控制。该钩子只需在 SSDT 初始化完成后执行一次,这种设计使其成为简单可靠的时间同步标记。

Code Reference (cpuid.rs)

match leaf {    leaf if leaf == CpuidLeaf::CacheInformation as u32 => {        trace!("CPUID leaf 0x2 detected (Cache Information).");        if !hook_manager.has_cpuid_cache_info_been_called {            hook_manager.manage_kernel_ept_hook(                vm,                crate::windows::nt::pe::djb2_hash("NtCreateFile".as_bytes()),                0x0055,                crate::intel::hooks::hook_manager::EptHookType::Function(                    crate::intel::hooks::inline::InlineHookType::Vmcall                ),                true,            )?;            hook_manager.has_cpuid_cache_info_been_called = true;        }    }}

这确保我们仅在 SSDT(System Service Descriptor Table)初始化完成后才应用 EPT(Extended Page Table)函数钩子,并保证后续的 CPUID 指令调用不会重复触发钩子逻辑。

目标解析与钩子分发 (manage_kernel_ept_hook())

让我们解析该函数的核心功能:manage_kernel_ept_hook 负责在内核函数(如 NtCreateFile)上安装或移除扩展页表(EPT)钩子。其实现逻辑如下:

该函数接收经过哈希处理的函数名和系统调用号作为输入参数,首先尝试通过 get_export_by_hash 解析目标函数的虚拟地址——该方法会检查 ntoskrnl.exe 的导出表。若解析失败,则通过系统服务描述符表(SSDT)使用系统调用号进行二次解析。

当 enable == true 时,调用 ept_hook_function() 通过影子内存(shadowing)机制安装钩子并修改 EPT 权限(具体实现细节将在后续章节详述)。若 enable == false,则调用 ept_unhook_function() 恢复原始内存映射并解除函数钩子。

Code Reference (hook_manager.rs)

pub fn manage_kernel_ept_hook(    &mut self,    vm: &mut Vm,    function_hash: u32,    syscall_number: u16,    ept_hook_type: EptHookType,    enable: bool,) -> Result<(), HypervisorError> {    let action = if enable { "Enabling" } else { "Disabling" };    debug!("{} EPT hook for function: {:#x}", action, function_hash);    trace!("Ntoskrnl base VA: {:#x}"self.ntoskrnl_base_va);    trace!("Ntoskrnl base PA: {:#x}"self.ntoskrnl_base_pa);    trace!("Ntoskrnl size: {:#x}"self.ntoskrnl_size);    let function_va = unsafe {        if let Some(va) = get_export_by_hash(self.ntoskrnl_base_pa as _self.ntoskrnl_base_va as _, function_hash) {            va        } else {            let ssdt_function_address =                SsdtHook::find_ssdt_function_address(syscall_number as _falseself.ntoskrnl_base_pa as _self.ntoskrnl_size as _);            match ssdt_function_address {                Ok(ssdt_hook) => ssdt_hook.guest_function_va as *mut u8,                Err(_=> return Err(HypervisorError::FailedToGetExport),            }        }    };    if enable {        self.ept_hook_function(vm, function_va as _, function_hash, ept_hook_type)?;    } else {        self.ept_unhook_function(vm, function_va as _, ept_hook_type)?;    }    Ok(())}

二级地址转换 (SLAT): EPT (Intel) 与 NPT (AMD)

在深入探讨系统调用钩子 (syscall hooks) 和内存拦截技术之前,我们有必要解析其底层实现原理——特别是针对不熟悉内存虚拟化的读者。

二级地址转换 (Second-Level Address Translation, SLAT),亦称嵌套分页 (nested paging),是一种硬件虚拟化特性。该技术允许虚拟机监控程序 (Hypervisor) 定义第二层页表转换机制。CPU 通过 Hypervisor 定义的映射关系,自动完成客户机物理地址 (Guest Physical Addresses, GPAs) 到宿主机物理地址 (Host Physical Addresses, HPAs) 的转换,无需每次内存访问都进行软件干预。SLAT 在 GPA 与 HPA 之间引入了额外的地址转换层。

客户操作系统 (Guest OS) 通过自维护的页表完成客户虚拟地址 (Guest Virtual Addresses, GVAs) 到 GPAs 的转换,而 Hypervisor 则通过扩展页表 (Extended Page Tables, EPT) 或嵌套页表 (Nested Page Tables, NPT) 实现 GPAs 到 HPAs 的映射。这两个转换阶段均由硬件内存管理单元 (MMU) 在内存访问时自动完成,无需软件参与。

当前主流的 SLAT 实现方案包括:

  • Intel VT-x 架构下的扩展页表 (EPT)
  • AMD SVM 架构下的嵌套页表 (NPT)

这些技术支持客户操作系统独立管理自身页表,同时由 Hypervisor 负责第二层内存转换。

下图展示了客户操作系统内部传统 x64 分页机制如何将 48 位虚拟地址转换为物理地址的过程。此转换过程由客户操作系统自主配置,与是否启用 SLAT 无关。

基于虚拟化技术的内存监控与逆向工程(很长)

图 4: 客户操作系统执行的 x64 传统虚拟地址转换流程(来源:Guided Hacking, YouTube)

EPT 钩子技术解析 (build_identity())

Hypervisor 启动时,会通过扩展页表 (EPT) 建立 1:1 的恒等映射 (identity map)——即客户物理地址直接映射到相同的主机物理地址。这种映射机制使得客户机可以正常运行,同时 Hypervisor 能在页表级别精细控制内存访问,且不干扰客户机自身的页表管理。

负责该功能的 build_identity() 实现策略如下:

  • 前 2MB 内存使用 4KB EPT 页表进行映射
  • 剩余客户物理地址默认使用 2MB 大页 (large pages) 映射
  • 需要更高粒度控制的场景(如钩子植入)则采用小页映射

虽然理论上支持 1GB 页,但 illusion-rs 选择 2MB 页映射主要基于以下考量:

  1. 简化 EPT 管理复杂度
  2. 确保与 VMware 等不支持 1GB EPT 页的平台兼容
  3. 满足早期启动内存自省 (early boot introspection) 和系统调用钩子 (syscall hooking) 的实际需求

Code Reference (ept.rs)

/// Represents the entire Extended Page Table structure.////// EPT is a set of nested page tables similar to the standard x86-64 paging mechanism./// It consists of 4 levels: PML4, PDPT, PD, and PT.////// Reference: Intel® 64 and IA-32 Architectures Software Developer's Manual: 29.3.2 EPT Translation Mechanism#[repr(C, align(4096))]pub struct Ept {    /// Page Map Level 4 (PML4) Table.    pml4: Pml4,    /// Page Directory Pointer Table (PDPT).    pdpt: Pdpt,    /// Array of Page Directory Table (PDT).    pd: [Pd512],    /// Page Table (PT).    pt: Pt,}pub fn build_identity(&mut self) -> Result<(), HypervisorError> {    let mut mtrr = Mtrr::new();    trace!("{mtrr:#x?}");    trace!("Initializing EPTs");    let mut pa = 0u64;    self.pml4.0.entries[0].set_readable(true);    self.pml4.0.entries[0].set_writable(true);    self.pml4.0.entries[0].set_executable(true);    self.pml4.0.entries[0].set_pfn(addr_of!(self.pdpt) as u64 >> BASE_PAGE_SHIFT);    for (i, pdpte) in self.pdpt.0.entries.iter_mut().enumerate() {        pdpte.set_readable(true);        pdpte.set_writable(true);        pdpte.set_executable(true);        pdpte.set_pfn(addr_of!(self.pd[i]) as u64 >> BASE_PAGE_SHIFT);        for pde in &mut self.pd[i].0.entries {            if pa == 0 {                pde.set_readable(true);                pde.set_writable(true);                pde.set_executable(true);                pde.set_pfn(addr_of!(self.pt) as u64 >> BASE_PAGE_SHIFT);                for pte in &mut self.pt.0.entries {                    let memory_type = mtrr                        .find(pa..pa + BASE_PAGE_SIZE as u64)                        .ok_or(HypervisorError::MemoryTypeResolutionError)?;                    pte.set_readable(true);                    pte.set_writable(true);                    pte.set_executable(true);                    pte.set_memory_type(memory_type as u64);                    pte.set_pfn(pa >> BASE_PAGE_SHIFT);                    pa += BASE_PAGE_SIZE as u64;                }            } else {                let memory_type = mtrr                    .find(pa..pa + LARGE_PAGE_SIZE as u64)                    .ok_or(HypervisorError::MemoryTypeResolutionError)?;                pde.set_readable(true);                pde.set_writable(true);                pde.set_executable(true);                pde.set_memory_type(memory_type as u64);                pde.set_large(true);                pde.set_pfn(pa >> BASE_PAGE_SHIFT);                pa += LARGE_PAGE_SIZE as u64;            }        }    }    Ok(())}

该恒等映射(identity map)将在后续安装 EPT 钩子(EPT hooks)时使用。它使得虚拟机监控程序(Hypervisor)能够影子化客户机内存(shadow guest memory)、修改 EPT 权限(例如将页面设置为仅可执行),并安全地将执行流重定向到钩子逻辑——所有这些操作都无需直接修改客户机内存。

安装钩子载荷(ept_hook_function()

ept_hook_function() 是 Illusion 项目中基于 EPT(Extended Page Tables)的函数挂钩(function hooking)核心逻辑。该函数实现了对选定客户机函数的影子化(shadowing)、修改和挂钩操作,整个过程完全不触及原始内存。通过改变 EPT 权限使其指向修改后的影子页面(shadow page)而非原始页面,实现了执行流的重定向,从而在不改变客户机状态的前提下完成内存自省(introspection)和控制。

本节将详细解释该函数的功能、内部调用关系及每个步骤的必要性。文中的 步骤1 至 步骤9 对应前文架构图中的处理流程。

Code Reference (hook_manager.rs)

大页映射技术 (map_large_page_to_pt())

我们首先确保包含目标函数的 2MB 大页(large page)已在虚拟机监控程序(Hypervisor)的内部内存管理结构中完成注册。illusion-rs 采用客户机物理地址 (GPA) 与宿主机物理地址 (HPA) 的 1:1 恒等映射(identity mapping),但在进行任何内存操作或权限控制前,必须先将该大页与预分配的页表(page table)建立关联。

这些预分配页表(pre-allocated page tables)并非运行时动态分配——而是在 Hypervisor 启动时,通过用户定义的预分配堆内存(pre-allocated heap)预留固定大小的内存池。该内存池在所有逻辑处理器间共享,用于支撑影子页面(shadow pages)和页表等内部结构。堆内存采用链表式内存分配器(类似经典 free-list 策略,非 slab 或 buddy),从连续内存块(默认 64MB)执行分配。具体支持的分配数量取决于用户定义的内存大小和工作负载模式,但所有分配都严格限制(strictly bounded)。若内存池耗尽,后续分配将在使用点失败,除非显式处理否则可能触发 panic。

通过调用 map_large_page_to_pt(),我们将大页的 GPA 与已知内部结构建立链接,实现受控的页分裂(splitting)、影子化(shadowing)和权限控制(permission enforcement)。该机制也便于在需要移除或切换钩子时,追踪和恢复原始页映射(original page mappings)。

self.memory_manager.map_large_page_to_pt(guest_large_page_pa.as_u64())?;

Step 1 - 页分裂操作 (is_large_page() -> split_2mb_to_4kb())

当目标函数位于 2MB 大页(large page)时,直接修改权限将影响整个内存区域——可能导致无关代码中断并触发全范围的虚拟机退出(VM-exit)。为避免这种情况,我们首先检查目标区域是否由大页支持。若确认存在大页,则使用预分配页表(pre-allocated page table)将其分裂为 512 个独立的 4KB 条目。这种操作实现了隔离式函数挂钩(function hooking)所需的细粒度控制,确保只有目标页面会触发 VM-exit。

if vm.primary_ept.is_large_page(guest_page_pa.as_u64()) {    let pre_alloc_pt = self        .memory_manager        .get_page_table_as_mut(guest_large_page_pa.as_u64())        .ok_or(HypervisorError::PageTableNotFound)?;    vm.primary_ept.split_2mb_to_4kb(guest_large_page_pa.as_u64(), pre_alloc_pt)?;}

影子页面处理技术 (is_guest_page_processed() -> map_guest_to_shadow_page())

在安装任何绕道(detour)之前,我们首先检查目标客户机页面是否已分配并映射影子页面(shadow page)。若映射已存在,表明该页面已被处理过且无需额外操作。否则,我们从预分配内存池(pre-allocated pool)中提取影子页面,并通过 map_guest_to_shadow_page() 将其与客户机页面建立关联。该机制确保不会重复安装钩子(hooks),并防止为同一目标创建多个影子页面。这对系统正确性至关重要:当因 EPT 违规(EPT violation)触发 VM-exit 时,我们必须能可靠地检索到与故障客户机页面关联的影子页面。

if !self.memory_manager.is_guest_page_processed(guest_page_pa.as_u64()) {    self.memory_manager.map_guest_to_shadow_page(        guest_page_pa.as_u64(),        guest_function_va,        guest_function_pa.as_u64(),        ept_hook_type,        function_hash,    )?;}

Step 2 - 代码克隆 (unsafe_copy_guest_to_shadow())

当影子页面(shadow page)完成分配和映射后,我们使用 unsafe_copy_guest_to_shadow() 将客户机原始 4KB 内存页完整克隆到影子页面。该操作会创建客户机内存的逐字节精确副本(byte-for-byte replica),所有修改操作都将在该隔离副本中安全执行。通过在隔离的影子副本(而非直接操作客户机内存)中进行修改,我们有效规避了 PatchGuard 等完整性校验机制(integrity verification checks)的检测,同时保留原始代码以便未来恢复(restoration)。

let shadow_page_pa = PAddr::from(    self.memory_manager        .get_shadow_page_as_ptr(guest_page_pa.as_u64())        .ok_or(HypervisorError::ShadowPageNotFound)?,);Self::unsafe_copy_guest_to_shadow(guest_page_pa, shadow_page_pa);

步骤 3 - 内联钩子安装 (Inline Hook Installation)

当影子页面(shadow page)准备就绪后,我们首先计算目标函数相对于页面起始地址的精确偏移量。该计算确保钩子被精准植入指令边界(instruction boundary)。在确定偏移位置后,我们插入内联绕道(inline detour)——通常使用 VMCALL 操作码(opcode)——当被挂钩函数执行时将触发受控的虚拟机退出(controlled VM-exit)。整个重定向流程完全由虚拟机监控器(hypervisor)处理。

在 UEFI 环境下,由于虚拟机监控器运行在客户机地址空间之外,我们避免使用传统的基于 JMP 指令的钩子方案。虽然在技术层面可以通过向客户机内存注入钩子逻辑实现(如早期版本 illusion-rs 所探索的方案),但最终选择 EPT+VMCALL 方案主要基于以下考量:将核心逻辑完全保留在宿主机端,同时作为技术研究实践。关于客户机辅助设计(guest-assisted design)的更多背景,请参阅附录:客户机辅助挂钩模型。

let shadow_function_pa = PAddr::from(Self::calculate_function_offset_in_host_shadow_page(shadow_page_pa, guest_function_pa));InlineHook::new(shadow_function_pa.as_u64() as *mut u8, inline_hook_type).detour64();

Step 4 - 撤销执行权限 (modify_page_permissions())

为确保绕道(detour)机制生效,我们通过 EPT 撤销客户机原始页面的执行权限。这将导致任何从该页面进行的指令获取操作都会因 EPT 违规(EPT violation)而触发 VM-exit(虚拟机退出)。虚拟机监控器(hypervisor)可据此事件接管控制流,并将执行重定向至已安装绕道的影子页面(shadow page)。关键之处在于,我们保留原始页面的读写权限以维持系统稳定性,同时避免触发 PatchGuard 等内存保护机制。

vm.primary_ept.modify_page_permissions(    guest_page_pa.as_u64(),    AccessType::READ_WRITE,    pre_alloc_pt,)?;

步骤 5 - 刷新 TLB 与 EPT 缓存 (invept_all_contexts())

当从客户机原始页面移除执行权限并替换为影子钩子(shadow hook)后,CPU 内部缓存中可能仍存在陈旧转换条目(stale translations)。为确保更新后的 EPT 映射立即生效,虚拟机监控器(hypervisor)需通过 INVEPT 指令刷新虚拟化转换缓存(virtualization translation caches)。

invept_all_contexts();

该调用执行 All Contexts (全上下文)无效化操作,指示 CPU 丢弃当前 EPT 指针 (EPTP) 对应的所有 EPT 衍生转换条目。根据 Intel 架构手册 (SDM) 规范,此操作确保无论关联的 VPID 或 PCID 值如何,所有陈旧映射 (stale mappings) 都将被清除。

由于 EPT 转换条目按逻辑处理器 (logical processor) 进行缓存,无论虚拟机监控器使用共享 EPT 还是每核 EPT,都必须为每个 vCPU 执行 INVEPT 指令。若缺乏适当的同步机制,在线程迁移 (thread migration) 或指令重放 (instruction replay) 过程中可能产生竞态条件 (race conditions),导致跨核心的陈旧映射和不一致的钩子行为。

INVVPID 在此场景中无需使用。该指令用于使与 VPID 绑定的客户机虚拟地址映射失效,与基于 EPT 的物理地址转换无关。对于修改客户机物理 EPT 映射的用例,单独使用 INVEPT 即可满足需求。

此步骤完成钩子安装流程。此后客户机内核将继续正常运行,但任何尝试执行被挂钩函数的行为都将触发 EPT 违规 (EPT violation),使虚拟机监控器能够拦截执行路径——整个过程无需修改客户机内存。

步骤 6-7 - 通过 EPT 违规捕获执行流 (handle_ept_violation())

当发生 EPT 违规导致的 VM-exit(虚拟机退出)时,首要任务是确定触发故障的内存页。我们从 VMCS(虚拟机控制结构)中读取故障客户机物理地址 (GPA),并将其对齐到 4KB 和 2MB 页边界。此操作可精确定位被访问的特定内存页,并为后续在影子页追踪结构中执行查找操作做好准备。

Code Reference (ept_violation.rs)

let guest_pa = vmread(vmcs::ro::GUEST_PHYSICAL_ADDR_FULL);let guest_page_pa = PAddr::from(guest_pa).align_down_to_base_page();let guest_large_page_pa = guest_page_pa.align_down_to_large_page();

当获取到触发故障的客户机页面地址(faulting guest page address)后,我们检索先前在钩子安装过程中映射并准备好的对应影子页面(shadow page)。该页面包含我们修改后的函数副本,其中已插入 VMCALL 绕道(detour)。影子页面查找失败表明存在异常客户机行为——例如发生未关联影子映射的 EPT 执行违规(EPT execute violation)——此类情况将被视为致命错误(fatal error)。若未找到与故障页面对应的影子页面,则表明发生了与已安装钩子无关的意外 EPT 违规。在 illusion-rs 中,此条件被视为致命错误并终止 VM-exit 处理流程。对于生产级虚拟机监控器(production-grade hypervisor),此类情况应进行日志记录并采取更优雅的处理方式,以便检测客户机异常行为(guest misbehavior)、内存篡改(memory tampering)或钩子追踪中的逻辑错误。

let shadow_page_pa = PAddr::from(    hook_manager        .memory_manager        .get_shadow_page_as_ptr(guest_page_pa.as_u64())        .ok_or(HypervisorError::ShadowPageNotFound)?);

在确定应对措施前,我们通过读取 EXIT_QUALIFICATION 字段分析违规成因。该字段明确记录客户机尝试的内存访问类型(读取、写入或执行),为后续采取针对性处置措施提供依据。

let exit_qualification_value = vmread(vmcs::ro::EXIT_QUALIFICATION);let ept_violation_qualification = EptViolationExitQualification::from_exit_qualification(exit_qualification_value);

若违规表明存在尝试执行不可执行页面(即页面具有 readable(可读)和 writable(可写)权限但缺乏 executable(可执行)权限),我们将置换入影子页面(shadow page)并标记为仅执行权限(execute-only)。此操作将执行流重定向至经篡改的内存区域,该区域包含内联钩子(inline hook,如 VMCALL 指令),从而使虚拟机监控器(hypervisor)能够接管控制权。

if ept_violation_qualification.readable && ept_violation_qualification.writable && !ept_violation_qualification.executable {    vm.primary_ept.swap_page(guest_page_pa.as_u64(), shadow_page_pa.as_u64(), AccessType::EXECUTE, pre_alloc_pt)?;}

此重定向将执行流移交至我们的影子页面 (shadow page)——原始内存的字节级克隆副本,其中前几条指令已被覆写为 VMCALL。此时客户机恢复执行但未推进 RIP 寄存器,意味着 CPU 将重新执行相同指令——但此次来自影子页面。当 CPU 执行到 VMCALL 指令时,将触发另一次 VM-exit。由于我们已替换函数的原始序言 (prologue),这些指令后续必须通过 监视器陷阱标志 (Monitor Trap Flag, MTF) 单步执行机制进行恢复和重放。在基于 Windows 内核驱动程序的 Matrix 虚拟机监控器中,影子页面包含触发 VM-exit 的 INT3 钩子;监控器将客户机 RIP 设置为钩子处理程序,执行内省操作后通过蹦床 (trampoline) 返回执行。而在 illusion(基于 UEFI 的虚拟机监控器)中,我们选择 EPT + MTF 方案。这种纯宿主导向 (host-side) 的执行流重定向设计更为简洁且具有教学意义,无需客户机模式内存分配或内部控制流配置。(关于涉及客户机内存注入的替代方案,参见附录:客户机辅助挂钩模型。)

步骤 8 - 处理 VMCALL 钩子 (handle_vmcall())

VMCALL 指令作为内联钩子 (inline hook) 被插入到影子函数首条指令位置。当执行该指令时,将引发无条件 VM-exit,将控制权转移至虚拟机监控器。这使我们能够精确捕获客户机调用被挂钩函数的时刻。

代码参考 (vmcall.rs)

我们首先解析触发 VMCALL 的客户机物理页面 (guest physical page),并核验其是否属于钩子管理器 (hook manager) 预先注册的影子页面。若在影子映射基础设施 (shadow mapping infrastructure) 中找到该页面,即可确认执行流源自我们挂钩的函数。此条件检查确保在处理后续内存状态转换前,当前 VM-exit 确由合法钩子触发。至此,我们已精确掌握被调用的目标函数,并可通过虚拟机监控器的完全控制权,检查其参数、追踪执行过程,以及按需进行客户机内存或寄存器内省 (introspect)。

let exit_type = if let Some(shadow_page_pa) = hook_manager.memory_manager.get_shadow_page_as_ptr(guest_page_pa.as_u64()) {    let pre_alloc_pt = hook_manager        .memory_manager        .get_page_table_as_mut(guest_large_page_pa.as_u64())        .ok_or(HypervisorError::PageTableNotFound)?;

在虚拟机监控器(hypervisor)完成所有内省分析操作——包括检查函数参数、追踪执行流程或审查客户机内存后——我们开始恢复客户机状态。具体而言,我们会换回原始(未修改)的客户机页面,并临时恢复 READ_WRITE_EXECUTE 完整权限。此操作对于安全执行被内联 VMCALL 绕道(detour)覆盖的原始指令至关重要(通常涉及目标函数序言部分的 2-5 个字节)。

vm.primary_ept.swap_page(guest_page_pa.as_u64(), guest_page_pa.as_u64(), AccessType::READ_WRITE_EXECUTE, pre_alloc_pt)?;

在启用 MTF 前,我们需检索钩子元数据(hook metadata)并确定内联 VMCALL 置换的指令数量。若直接恢复原始页面并继续执行将导致崩溃风险——由于函数序言(prologue)从未执行——并使函数失去监控。为防止此情况,我们需要通过 MTF 对置换指令进行单步执行。在恢复客户机前,我们初始化重放计数器(replay counter)、设置 监视器陷阱标志 (Monitor Trap Flag, MTF) 并禁用客户机中断(guest interrupts),以防止单步逐指令重执行过程中发生意外中断处理。此步骤为后续章节将继续的指令重放流程奠定基础。

let instruction_count = HookManager::calculate_instruction_count(...);vm.mtf_counter = Some(instruction_count);set_monitor_trap_flag(true);update_guest_interrupt_flag(vm, false)?;

若在故障客户机页面中未找到影子页面映射(shadow mapping),则假定该 VMCALL 指令无效或执行上下文异常。为模拟 CPU 预期行为,illusion-rs 将注入 #UD(未定义指令异常),这与处理器在 VMX 非操作模式下处理 VMCALL 指令的机制保持一致。

步骤 9 - 使用监视器陷阱标志实现单步执行 (handle_monitor_trap_flag())

监视器陷阱标志 (Monitor Trap Flag, MTF) 使虚拟机监控器能够对因内联 VMCALL 而被置换的指令进行单步执行。客户机每执行一条指令都会触发 VM-exit,此时我们将递减指令重放计数器(instruction replay counter)。

Code Reference (mtf.rs)

*counter = counter.saturating_sub(1);

在虚拟机监控器(hypervisor)的监管下,客户机以单指令步进方式持续执行,直至所有被置换字节完成重放。当计数器归零时,表明函数序言(prologue)已完整恢复。此时我们通过换回影子页面(shadow page)并设置其权限为仅执行(execute-only),重新激活钩子(hook),确保该函数的下次调用将再次触发 VMCALL 指令。

vm.primary_ept.swap_page(guest_pa.align_down_to_base_page().as_u64(), shadow_page_pa.as_u64(), AccessType::EXECUTE, pre_alloc_pt)?;

最后,我们通过直接省略 set_monitor_trap_flag(true) 函数调用来禁用监视器陷阱标志(Monitor Trap Flag, MTF),并重新启用客户机中断,使客户机能够无缝恢复正常执行流程。

restore_guest_interrupt_flag(vm)?;

至此绕道路由周期(detour cycle)完整闭环。客户机继续无感知地运行,完全感知不到其控制流曾通过我们的虚拟机监控器(hypervisor)被临时重定向。

捕获读写违规行为 (handle_ept_violation())

当客户机尝试访问当前标记为仅执行(execute-only)的页面进行读写操作时,由于扩展页表(EPT)强制执行严格的访问权限控制,此时会触发 EPT 违规导致的 VM-exit。这种违规行为通常源于目标页面缺乏相应的读/写权限配置。

Code Reference (ept_violation.rs)

if ept_violation_qualification.executable && !ept_violation_qualification.readable && !ept_violation_qualification.writable {    vm.primary_ept.swap_page(guest_page_pa.as_u64(), guest_page_pa.as_u64(), AccessType::READ_WRITE_EXECUTE, pre_alloc_pt)?;    vm.mtf_counter = Some(1);    set_monitor_trap_flag(true);    update_guest_interrupt_flag(vm, false)?;}

为确保安全处理此场景,我们临时恢复原始客户机页面为完全读写执行(Read/Write/Execute)权限。这种机制可确保指令成功执行——即使其使用 RIP 相对寻址(RIP-relative addressing)或访问同页面数据——从而有效避免 VM-exit 循环、系统崩溃或钩子暴露。随后我们启用 监视器陷阱标志 (Monitor Trap Flag, MTF) 并单步执行一条指令后重新应用原始钩子,保持隐蔽性和系统稳定性。

Illusion 执行追踪:概念验证演练

本概念验证(Proof-of-Concept, PoC)演示了 Illusion 虚拟机监控器如何将早期启动阶段的 EPT 挂钩(EPT hooking)与用户态控制通道集成。从 UEFI 初始化 hypervisor 后,通过命令行工具使用被拦截的 CPUID 指令实现内核钩子的实时切换——无需内核模式驱动或直接修改客户机虚拟/物理内存。

通过超级调用控制 EPT 钩子

在测试钩子逻辑前,我们首先从 UEFI Shell 直接启动 hypervisor。这确保 hypervisor 在系统启动时加载,并与 Windows 内核保持隔离。

基于虚拟化技术的内存监控与逆向工程(很长)

图 5:从 UEFI Shell 直接引导 Illusion hypervisor

加载完成后,我们可通过用户态简易客户端发送指令。该 CLI 工具通过 hypervisor 暴露的密码保护后门进行通信。通信通道使用 CPUID 指令实现——这是种广泛使用的非特权 x86 指令,在被拦截时可可靠触发 VM-exit。由于 CPUID 是用户态可用的非特权指令,这使得我们无需任何内核组件即可实现隐蔽的超级调用(hypercalls)。

基于虚拟化技术的内存监控与逆向工程(很长)

图 6:通过 CPUID 超级调用控制内核钩子的命令行工具

客户端可实时启用/禁用特定系统调用函数(如 NtCreateFile)的钩子。这对需要外部控制钩子生命周期的内省工具尤为重要。

下图展示了实时 EPT 挂钩的实际运作。左侧显示 hypervisor 日志记录的挂钩流程:首先将 2MB 页面与预分配页表关联,然后拆分为 512 个 4KB 条目;从预分配池提取影子页面(shadow page)映射至目标客户机页面;克隆客户机原始 4KB 内存至影子页面,插入 VMCALL 内联钩子,并撤销原始页面的执行权限。该绕道路由(detour)用于在函数执行时触发 VM-exit。右侧 WinDbg 验证显示影子映射地址(0xab0c360)正确包含 VMCALL 操作码(0f01c1),而原始 NtCreateFile 在 0xfffff8005de16360 保持未修改。

这种机制在虚拟内存层面保持钩子隐形:原始 GVA 仍解析为相同 GPA,但 hypervisor 将最终映射重定向至影子页面的 HPA。从客户机常规视角(除非检查物理内存),内存看似未修改——但钩子已激活。

基于虚拟化技术的内存监控与逆向工程(很长)

图 7:展示隐蔽 EPT 挂钩执行的调试日志与 WinDbg 输出

Matrix:基于 Windows 内核驱动的双 EPT 架构 Hypervisor

Matrix 是面向运行时内省(runtime introspection)和系统调用重定向的 Windows 内核驱动型 hypervisor。该项目开发早于 illusion-rs,但采用不同实现路径:Matrix 作为 Windows 驱动安装并在内核模式运行,利用两个扩展页表(Extended Page Table, EPT)上下文——一个用于原始内存,另一个用于包含钩子逻辑的影子页面。

与 Illusion 在启动时配置单一 EPT 并使用 MTF 控制不同,Matrix 通过双 EPT 动态捕获执行流。这使我们能够配置仅执行(execute-only)钩子、在不修改客户机页面的情况下重映射内存,并实现运行时函数重定向控制。我们的实现通过 VM-exit 触发的动态 EPTP 切换在双 EPT 间切换——主 EPT 用于正常客户机执行,次 EPT 用于重定向流。部分 hypervisor 扩展此设计使用三个或更多 EPT(例如为不同执行阶段或进程上下文维护独立 EPT),也有实现选择按逻辑处理器隔离 EPT。相比之下,Matrix 采用跨所有逻辑处理器共享的最小化双 EPT 架构,侧重核心概念的简洁性与可测试性。

下图展示 Matrix 的工作流程:原始页面在主 EPT 中失去执行权限,在次 EPT 中被镜像为仅执行权限,指向影子副本中的跳板逻辑(trampoline logic)。目标函数的运行时执行触发 VM-exit,我们借此切换上下文并将控制流重定向至钩子处理器。

基于虚拟化技术的内存监控与逆向工程(很长)

图 8:Matrix Windows 内核驱动型 hypervisor 中双 EPT 架构函数挂钩控制流

下文将详细解析图中每个步骤。

初始化主次 EPT (virtualize_system())

当内核模式驱动加载时,我们通过分配并建立 1:1 恒等映射(identity-mapping)两个独立 EPT 上下文来初始化虚拟化:主 EPT 和次 EPT。两者初始均配置完全 READ_WRITE_EXECUTE 权限以镜像客户机内存。主 EPT 提供无干扰的客户机内存原始视图,次 EPT 用于应用钩子的影子页面。这种双重映射使我们无需修改原始内存即可选择性地重定向执行流,按需切换 EPT 来捕获和分析函数调用。

Code Reference (lib.rs)

primary_ept.identity_2mb(AccessType::READ_WRITE_EXECUTE)?;secondary_ept.identity_2mb(AccessType::READ_WRITE_EXECUTE)?;

步骤 1-2 - 创建影子钩子与蹦床设置 (hook_function_ptr())

在启用虚拟化前,我们通过解析目标函数并设置绕道机制(detour)来准备钩子。我们挂钩两个内核函数:从导出表解析获取的 MmIsAddressValid,以及通过系统调用号从 SSDT 解析的 NtCreateFile。对于每个目标函数,我们创建蹦床(trampoline)以保留原始函数序言(prologue),确保钩子逻辑执行后能实现干净返回。

具体实现时,我们将包含目标函数的 4KB 内存页复制到影子区域(shadow region),计算目标函数在复制页内的精确偏移位置,并插入内联 INT3 断点(breakpoint)以触发 VM-exit(虚拟机退出)。这些钩子被添加至内部钩子管理器(hook manager)并保持休眠状态,直至双 EPT 重映射(dual-EPT remapping)配置完成。虽然 illusion-rs 可采用相同方案,但实际选择使用 VMCALL 指令——部分原因是为避免断点异常(breakpoint exception),部分则是为了与 matrix-rs 已实现的方案形成技术差异化。

let mm_is_address_valid =    Hook::hook_function("MmIsAddressValid", hook::mm_is_address_valid as *const())        .ok_or(HypervisorError::HookError)?;if let HookType::Function { ref inline_hook } = mm_is_address_valid.hook_type {    hook::MM_IS_ADDRESS_VALID_ORIGINAL        .store(inline_hook.trampoline_address(), Ordering::Relaxed);}let ssdt_nt_create_file_addy = SsdtHook::find_ssdt_function_address(0x0055false)?;let nt_create_file_syscall_hook = Hook::hook_function_ptr(    ssdt_nt_create_file_addy.function_address as _,    hook::nt_create_file as *const(),).ok_or(HypervisorError::HookError)?;if let HookType::Function { ref inline_hook } = nt_create_file_syscall_hook.hook_type {    hook::NT_CREATE_FILE_ORIGINAL.store(inline_hook.trampoline_address(), Ordering::Relaxed);}let hook_manager = HookManager::new(vec![mm_is_address_valid, nt_create_file_syscall_hook]);

我们支持通过函数名(hook_function)或原始指针(hook_function_ptr)两种方式创建钩子。基于名称的方法从内核导出表解析目标函数,而基于指针的方法适用于通过 SSDT(System Service Descriptor Table)定位系统调用或未公开例程地址的场景。在底层实现中,hook_function_ptr 会克隆包含目标函数的 4KB 内存页至影子区域(shadow region),计算该函数在克隆页内的精确偏移量,并注入内联 INT3 断点(0xCC)以触发 VM-exit(虚拟机退出)。为实现安全返回原始逻辑,FunctionHook::new 会构建蹦床(trampoline)—— 该小型存根(stub)负责恢复被覆写的字节,并通过 RIP 相对间接跳转(jmp qword ptr [rip+0])返回到原始函数的剩余部分。此机制确保控制流在执行完我们的处理程序后能干净利落地恢复,且无需修改客户机内存。

Code Reference (hooks.rs)

let original_pa = PhysicalAddress::from_va(function_ptr);let page = Self::copy_page(function_ptr)?;let page_va = page.as_ptr() as *mut u64 as u64;let page_pa = PhysicalAddress::from_va(page_va);let hook_va = Self::address_in_page(page_va, function_ptr);let hook_pa = PhysicalAddress::from_va(hook_va);let inline_hook = FunctionHook::new(function_ptr, hook_va, handler)?;

步骤 3、4、5 与 6 - 双 EPT 重映射实现影子执行 (enable_hooks())

代码参考 (hooks.rs)

完成钩子准备后,我们配置双 EPT(Extended Page Table,扩展页表)映射以支持影子执行(shadow execution)。针对每个被挂钩地址,执行以下操作:

  1. 页表粒度转换:将包含目标地址的 2MB 大页拆分为 4KB 页条目,该操作在 primary(主)和 secondary(次)EPT 中同步执行
  2. 权限隔离配置
    • 在 primary EPT 中将页面标记为 READ_WRITE(读写),显式移除执行权限
    • 在 secondary EPT 中将相同页面标记为 EXECUTE(仅执行),并将其重映射至包含内联钩子(inline hook)和蹦床逻辑(trampoline logic)的影子副本
  3. 双视图架构:此配置确保:
    • 读写访问通过 primary EPT 中的原始映射进行
    • 指令获取操作(instruction fetches)在后续发生 EPT 违规(EPT violation)切换至 secondary EPT 时,触发从影子页面(包含绕道逻辑)的执行

该架构通过硬件辅助的地址空间隔离,实现原始内存的读写访问与修改后执行流的并行存在,有效规避传统内存钩子方案中存在的内存完整性校验机制。

primary_ept.split_2mb_to_4kb(original_page, AccessType::READ_WRITE_EXECUTE)?;secondary_ept.split_2mb_to_4kb(original_page, AccessType::READ_WRITE_EXECUTE)?;primary_ept.change_page_flags(original_page, AccessType::READ_WRITE)?;secondary_ept.change_page_flags(original_page, AccessType::EXECUTE)?;secondary_ept.remap_page(original_page, hooked_copy_page, AccessType::EXECUTE)?;

步骤 7 - 配置虚拟机控制结构以捕获断点退出 (setup_vmcs_control_fields())

在虚拟机控制结构 (VMCS) 初始化阶段,我们配置异常位图 (EXCEPTION_BITMAP) 以捕获 INT3 指令,确保断点异常会触发虚拟机退出 (VM-exit)。系统初始化时加载主扩展页表指针 (primary_eptp),为客户机内存提供初始的读写视图。

Code Reference (vmcs.rs)

vmwrite(vmcs::control::EXCEPTION_BITMAP1u64 << (ExceptionInterrupt::Breakpoint as u32));vmwrite(vmcs::control::EPTP_FULL, shared_data.primary_eptp);

步骤 8 - 通过动态 EPTP 切换处理 EPT 违规 (handle_ept_violation())

当客户机尝试执行在主 EPT(Extended Page Table,扩展页表)中标记为不可执行的页面时,将因 EPT 违规(EPT violation)触发虚拟机退出(VM-exit)。作为响应,我们切换至次 EPTP(secondary EPTP),该操作将相同的客户机物理地址(GPA)重新映射至仅含 EXECUTE 权限的影子页面(shadow page)——其中包含我们预先植入的绕道逻辑(detour)。这种动态页表指针切换机制允许客户机从包含钩子的函数版本继续执行。

Code Reference (ept.rs)

let guest_physical_address = vmread(vmcs::ro::GUEST_PHYSICAL_ADDR_FULL);let exit_qualification_value = vmread(vmcs::ro::EXIT_QUALIFICATION);let ept_violation_qualification = EptViolationExitQualification::from_exit_qualification(exit_qualification_value);if ept_violation_qualification.readable && ept_violation_qualification.writable && !ept_violation_qualification.executable {    let secondary_eptp = unsafe { vmx.shared_data.as_mut().secondary_eptp };    vmwrite(vmcs::control::EPTP_FULL, secondary_eptp);}

当客户机后续尝试对该页面执行读取或写入操作时(这些操作在次 EPT 中未被允许),我们检测到该违规行为并切换回主 EPTP,为数据操作恢复完整的 READ_WRITE 访问权限。

if !ept_violation_qualification.readable && !ept_violation_qualification.writable && ept_violation_qualification.executable {    let primary_eptp = unsafe { vmx.shared_data.as_mut().primary_eptp };    vmwrite(vmcs::control::EPTP_FULL, primary_eptp);}

Matrix 目前不支持同一页面内混合访问模式(如 RWX 或 RX),而 Illusion 通过 MTF(监视器陷阱标志,Monitor Trap Flag)安全重放被置换指令的方案实现了该功能。

步骤 9 - 通过断点处理程序重定向执行流 (handle_breakpoint_exception())

当客户机执行影子页面中嵌入的 INT3 指令时,断点异常将触发 VM-exit(虚拟机退出)。我们解析客户机当前 RIP(指令指针寄存器)并核验其是否匹配内部管理器注册的钩子。若匹配成功,则将 RIP 重定向至我们的钩子处理程序,从而获得完全执行控制权。在此阶段,我们可以检查参数、记录活动或内省客户机内存,最终通过预置的蹦床函数(trampoline)返回原始函数。

Code Reference (exceptions.rs)

if let Some(Some(handler)) = hook_manager.find_hook_by_address(guest_registers.rip).map(|hook| hook.handler_address()) {    guest_registers.rip = handler;    vmwrite(vmcs::guest::RIP, guest_registers.rip);}

步骤 10 - 通过蹦床函数返回原始客户机函数 (mm_is_address_valid() 与 nt_create_file())

当钩子逻辑执行完成后,我们通过蹦床函数 (trampoline) 将执行流转发回原始内核函数。钩子处理程序从原子全局变量 (atomic global) 中获取预先保存的原始函数入口点,并通过安全类型转换 (safe casting) 恢复正确的函数签名 (signature)。这种无缝交接机制确保客户机执行流如同未受干扰般继续运行,完整维持客户机执行假象 (guest illusion)。

Code Reference (hook.rs)

let fn_ptr = MM_IS_ADDRESS_VALID_ORIGINAL.load(Ordering::Relaxed);let fn_ptr = unsafe { mem::transmute::<_, MmIsAddressValidType>(fn_ptr) };fn_ptr(virtual_address as _)
let fn_ptr = NT_CREATE_FILE_ORIGINAL.load(Ordering::Relaxed);let fn_ptr = unsafe { mem::transmute::<_, NtCreateFileType>(fn_ptr) };fn_ptr(...)

Matrix 执行跟踪:概念验证演练

此截圖展示了客户机执行 MmIsAddressValid 时触发的实时 EPT 违规(EPT violation)。调试输出(左侧)显示,对原始客户机物理页 0xfffff801695ad370 的 EXECUTE 访问引发了 VM-exit(虚拟机退出),因为主 EPT 中已移除执行权限。我们通过切换至次 EPT 进行响应,将客户机物理地址重映射至位于 0x239d38370 的影子副本。

在影子页面中,我们用单字节 INT3 指令覆盖函数序言(function prologue),从而触发断点异常。这导致另一个 VM-exit,在此过程中我们定位钩子、重定向客户机 RIP 至处理程序并恢复执行。处理程序完成后,执行流转发至位于 0xffffdb0620809f90 的蹦床函数(trampoline),继续原始函数执行。该蹦床通过间接跳转 jmp qword ptr [0xffffdb0620809f9a] 实现重定向,该指令解析为 0xffffdb0620809f9a——即被覆盖指令后的地址——从而恢复执行流。

基于虚拟化技术的内存监控与逆向工程(很长)

图 9: MmIsAddressValid 的影子页面重定向与蹦床设置

调试日志确认 MmIsAddressValid 钩子处理程序已成功调用,并打印了其首个参数,证明重定向逻辑与处理程序执行符合预期。

基于虚拟化技术的内存监控与逆向工程(很长)

图 10: MmIsAddressValid 的 EPT 违规处理与钩子调用流程

与 Illusion 不同,Matrix 当前不支持用户态通信,尽管添加该功能较为简单。我们展示的是一个完整的概念验证:通过 EPTP 交换、指令捕获和内存虚拟化实现内核执行流重定向——全程无需修改客户机内存。这实现了基于 Windows 内核驱动型 hypervisor 的隐蔽内省(stealth introspection)、系统调用监控和控制流重定向。虽然尚未强化为生产级方案,Matrix 为推进基于 EPT 的规避技术、动态分析和内存保护研究奠定了基础。

钩子重定向技术:INT3、VMCALL 与 JMP

虽然 INT3 钩子提供了轻量级、低侵入性的控制流重定向方法,但每个钩子会引发两次 VM-exit:一次 EPT 违规和一次断点异常。这种权衡在 Illusion(使用 VMCALL)中同样存在,导致钩子执行期间额外 VM-exit。替代方案是使用 14 字节间接跳转指令(如 jmp qword ptr [rip+0]),该指令通过读取紧随其后的内存地址实现绝对跳转,完全避免断点并减少 VM-exit 至仅一次(仅 EPT 违规)。

Matrix 通过 jmp [rip+0] 桩代码(stub)加 8 字节目标地址支持这种 JMP 钩子。该方法避免寄存器污染(不同于 mov rax, addr; jmp rax 序列),减少副作用风险。实现方案通过内联嵌入跳转目标避免使用通用寄存器,简化重定向逻辑并保持客户机寄存器完整性。默认情况下,Matrix 使用 INT3 钩子以简化实现并减少 shellcode 体积。

然而,JMP 方案需要更大的 shellcode 来覆盖原始函数序言,增加了指令对齐和相对寻址的复杂性。其他指令如 CPUIDVMCALL 甚至未定义操作码也可用于陷入 hypervisor,为 Matrix 或 Illusion 未来实现可配置/混合钩子技术提供了方向。

Hypervisor 检测向量

尽管本文聚焦基于 EPT 的函数重定向和隐蔽内存操作,但需注意 hypervisor 辅助的钩子即使无特权提升也可从用户态检测。这些检测技术通常依赖时序差异(timing discrepancies)、故障触发行为或指令级分析——通常由内存访问或特权指令处理期间的 VM-exit 引发。

虽然超出本文范围,以下列出部分已知检测方法:

  • 对未使用代码填充区的写校验(如 0xCC 替换为 0xC3
  • 基于 RDTSC 的时序检测(用于发现 EPT 页面交换)
  • 跨 CPU 核心的线程时序差异分析
  • CPUID 执行特征分析(包括延迟测量和厂商 ID 泄露)
  • 使用 APERF 等计数器的指令执行时间 (IET) 差异检测
  • 通过非法 XSETBV/MSR/控制寄存器 (CR) 访问进行故障注入
  • 对 0x40000000 范围的合成 MSR 探测
  • WoW64 模式下的 SIDT/SGDT 描述符长度校验
  • 强制 VM 退出时的 LBR(Last Branch Record)堆栈不匹配检测
  • 通过 INVD/WBINVD 指令滥用测试缓存一致性
  • VMCALL 异常处理行为分析(如不规范的#GP 异常注入)
  • CR0/CR4影子寄存器不匹配或VMXE位暴露
  • 异常/NMI 传递路径异常检测(如意外的#PF 或#UD 行为)
  • UEFI 内存映射分析(用于发现隐藏的 hypervisor 区域)
  • CR3 寄存器冲刷攻击(破坏基于进程的内存映射跟踪机制)
  • 描述符表 (GDT/IDT) 完整性检查(检测未能正确隔离或模拟的 hypervisor)
  • 页表一致性校验(针对未完全分离主客内存环境的 hypervisor,如共享 CR3 或不当的影子分页)

详细技术分析可参考以下资源:

  • 《BattlEye 反作弊系统的 Hypervisor 检测技术》- @vmcall, @daax
  • 《反作弊系统如何检测系统仿真》- @daax, @iPower, @ajkhoury, @drew
  • 《PatchGuard:基于 Hypervisor 的内省技术 [上]》- Nick Peterson (@everdox), Aidan Khoury (@ajkhoury)
  • 《PatchGuard:基于 Hypervisor 的内省技术 [下]》- Nick Peterson (@everdox), Aidan Khoury (@ajkhoury)
  • 《通过 EFER 寄存器实现系统调用挂钩》- Nick Peterson (@everdox)
  • 《检测 Hypervisor 辅助挂钩技术》- Maurice Heumann (@momo5502)

尽管部分文献年代较早,其核心检测原理仍具参考价值。关于规避技术、隐蔽性和 hypervisor 检测的深入探讨,留待读者自行研究。

附录

客户机辅助挂钩模型

在 illusion-rs 的早期开发中,我们实现并测试了客户机辅助挂钩模型。该技术涉及在客户机内分配内存、注入辅助代码,并通过 hypervisor 重定向执行流(RIP)。虽然技术可行,但增加了复杂性和被检测风险。

传统基于 JMP 的内联挂钩未被采用,因为 hypervisor 运行在 UEFI 环境下的客户机地址空间之外。实现这类挂钩需要修改客户机内存、手动解析 API、协调执行上下文,以及管理早期内核阶段的同步——这些都会增加暴露面和脆弱性。

该模型与 Satoshi Tanda 研究的方案类似,他通过 C 语言实现的 GuestAgent 在内核初始化阶段劫持控制权,实现客户机内的系统调用挂钩。

尽管功能完整,该技术使恢复过程复杂化且需要精细的客户机状态协调。最终,illusion-rs 移除了该方案,转而采用更简洁的设计:EPT 影子页表技术结合内联 VMCALL 迂回和 MTF 单步调试恢复。该方案通过 hypervisor 控制的影子页面重定向执行流,完全避免修改客户机内存,简化控制流并实现精确重定向。

EPT 挂钩模型对比:每核 vs 共享

本文介绍的两种 hypervisor——illusion-rs 和 matrix-rs——实现了不同的 EPT 挂钩模型,各自在设计、实现复杂性和控制粒度方面做出权衡。

选择 illusion-rs 的场景:

  • 需要完全主机端的内省控制,无需依赖客户机代码或内存分配
  • 需在驱动或安全控制加载前实现早期启动阶段的内核行为监控/劫持

选择 matrix-rs 的场景:

  • 偏好动态加载的 Windows 内核驱动型 hypervisor
  • 需要共享 EPT 模型且不依赖 UEFI/固件集成

Matrix(跨逻辑处理器共享 EPT)

matrix-rs 是基于 Windows 内核驱动的 hypervisor,采用跨所有逻辑处理器共享的 EPT。该设计受 not-matthias 的 AMD hypervisor 启发,始于 2022 年末的学习项目。共享 EPT 模型简化了实现——EPT 违规可触发 EPTP 切换,且挂钩状态全局一致。

优势:

  • 更少的 EPT 上下文管理(单系统级 EPTP)
  • 简化的挂钩配置(更新全局生效)
  • 每次挂钩变更只需单次 INVEPT 指令

劣势:

  • 跨处理器竞态条件风险
  • 难以实现每核动态挂钩状态管理
  • 对单 CPU 重定向控制精度较低

两种模型在挂钩变更时都需要 EPT 缓存失效(INVEPT),由于 TLB 按逻辑处理器独立,无论采用 illusion-rs 的每核 EPT 还是 matrix-rs 的共享 EPT,都需在每个逻辑处理器上执行 INVEPT。

Illusion(每逻辑处理器 EPT+MTF)

illusion-rs 是基于 UEFI 的 hypervisor,为每个逻辑处理器维护独立的 EPT。该方案开发于 2023 年末,旨在探索通过 MTF(Monitor Trap Flag)单步调试实现指令回放的启动时内省模型,完全避免向客户机注入跳板代码。

优势:

  • 挂钩逻辑完全驻留主机端(无需客户机代码)
  • 通过 MTF 实现被覆盖指令的干净回放
  • 细粒度的每逻辑处理器重定向

劣势:

  • 挂钩更新需同步到所有 EPT 上下文
  • 每次挂钩变更需在所有逻辑处理器执行 INVEPT
  • 跨处理器保持挂钩状态一致性的复杂度
  • MTF 单步调试导致每次指令回放产生额外 VM-exit,在频繁挂钩场景可能产生性能开销

与传统挂钩模型立即恢复执行不同,MTF 方案为每个回放指令产生 VM-exit。单个挂钩影响可忽略,但在高频系统级挂钩场景可能产生显著开销。

其他权衡因素(如设计约束、集成复杂度、客户机兼容性等)超出本文范围,留待读者自行探索。尽管 illusion-rs 引入了更简洁的内存管理器和预分配页表,两个 hypervisor 仍属概念验证阶段,为底层内存内省和控制流重定向研究提供基础框架。

对于动态运行时挂钩场景,matrix-rs 的共享 EPT 模型更易集成。对于固件级内省和早期启动控制,illusion-rs 以复杂度为代价提供更精细的执行控制。

结论

本文阐述了如何利用扩展页表(Extended Page Tables,EPT)构建基于 Rust 的 hypervisor,实现隐蔽的内核内省与函数挂钩技术。我们探索了两个概念验证实现:illusion-rs(基于 UEFI 的 hypervisor,用于早期启动阶段挂钩系统调用)和 matrix-rs(基于 Windows 内核驱动的 hypervisor,采用双 EPT 上下文切换实现运行时执行流重定向)。

我们演示了如何检测 ntoskrnl.exe 中 SSDT(System Service Descriptor Table)的初始化完成状态,如何部署仅可执行(execute-only)的影子页面,以及如何通过 VMCALLCPUID 或 INT3 安全重定向执行流而不修改客户机内存。在 Illusion 方案中,我们依赖 Monitor Trap Flag (MTF)单步调试实现被替换指令的干净回放,而 Matrix 方案则利用断点异常和跳板逻辑(trampoline logic)进行控制流转发。

两种方案均通过 EPT 支持的地址重映射(EPT-backed remapping)保持客户机内存完整性,避免直接修改内核而触发 PatchGuard 机制。最终实现细粒度执行控制的系统调用挂钩,适用于植入程序(implants)、内存内省或安全研究。

本文展示的示例并非突破性创新,而是提供可复现的起点。一旦建立控制,这些技术可扩展用于隐藏线程/进程/句柄、伪装内存区域,或嵌入 shellcode/反射式 DLL 等载荷——所有操作均无需修改客户机内存。然而基于虚拟化的安全机制(Virtualization-Based Security,VBS)显著增加了自定义 hypervisor 挂钩的难度:从阻止第三方 hypervisor 加载,到破坏 EPT 重定向技术。诸如 Intel VT-rp、嵌套虚拟化屏障(nested virtualization barriers)和完整性验证等防御措施,使得在 Hyper-V 之下或并行建立控制变得困难——除非准备在启动时实施 Hyper-V 劫持(hyperjacking),或通过嵌套虚拟化在 Hyper-V 之上运行自定义 hypervisor。尽管如此,构建自有 hypervisor 能提供更强的控制力、灵活性和底层认知——这也往往是真正创新工作的起点。

所有演示技术均基于公开文档实现——无需 NDA 协议、私有 SDK 或未公开内部机制。这些技术长期应用于游戏破解领域,并逐渐被安全研究和商业产品采纳。但实用指南和开源实现仍相对稀缺,特别是针对早期启动阶段 hypervisor 的研究。

illusion-rs 和 matrix-rs 均已开源可供实验。对于希望探索更精简教学示例的读者,Satoshi Tanda 开发的 barevisor 为跨 Intel/AMD 平台的 hypervisor 开发提供了干净起点——同时支持 Windows 内核驱动和 UEFI 两种实现方式。

若需要预构建的模块化虚拟化内省(Virtual Machine Introspection)库,可关注 Petr Beneš (@wbenny)近期开源的 vmi-rs 项目。

致谢、参考文献与研究启发

文章、工具与研究资料

  • Daax (@daaximus)

    • 虚拟化七日谈
    • 通过 Intel EPT 实现 MMU 虚拟化
  • Satoshi Tanda (@tandasat)

    • 安全研究者的 Hypervisor 开发指南
    • Rust 语言 Hypervisor 开发入门
    • barevisor
    • Hello-VT-rp
    • MiniVisorPkg
    • Intel VT-rp 技术解析 - 第一部分:重映射攻击与 HLAT
    • Intel VT-rp 技术解析 - 第二部分:分页写入与客户机分页验证
  • Sina Karvandi (@Intel80x86)

    • Hypervisor 开发教程
    • 从零构建 Hypervisor
  • Matthias (@not-matthias)

    • AMD 平台 hypervisor 实现
  • Nick Peterson (@everdox) 与 Aidan Khoury (@ajkhoury)

    • PatchGuard 对 Hypervisor 内省的检测机制 [上]
    • PatchGuard 对 Hypervisor 内省的检测机制 [下]
    • 通过 EFER 寄存器实现系统调用挂钩
  • Maurice Heumann (@momo5502)

    • Hypervisor 辅助挂钩的检测技术
  • Alex Ionescu

    • SimpleVisor(极简 x64 Hypervisor)
  • Back Engineering (@_xeroxz / IDontCode)

    • 通过逆向工程开发 AMD-V Hypervisor
    • bluepill 项目
  • Guided Hacking

    • x64 虚拟地址转换详解
  • Namazso (@namazso)

    • UnKnoWnCheaTs 技术讨论
  • Petr Beneš (@wbenny)

    • vmi-rs 项目

社区研究与启发

  • The Secret Club 技术社区

    • 系统仿真检测技术 作者:@daaximus、@iPower、@ajkhoury、@drew
    • BattlEye 反作弊系统的 Hypervisor 检测技术 作者:@vmcall、@daaximus
    • EAC 完整性检查绕过技术 作者:iPower (@iPower)
  • DarthTon 的 HyperBone 项目 基于 Alex Ionescu 版本,首发于 UnknownCheats 论坛

  • Joanna Rutkowska

    • 蓝药丸(Blue Pill)技术初探

特别致谢

  • Daax
  • Satoshi Tanda (@tandasat)
  • Drew (@drew)
  • iPower (@iPower)
  • Namazso (@namazso)
  • Jess (@jessiep_)
  • Feli (@vmctx)
  • Matthias @not-matthias
  • Ryan McCrystal / @rmccrystal
  • Wcscpy (@Azvanzed)

文档与规范

  • Intel 64 与 IA-32 架构软件开发人员手册(SDM)

原文始发于微信公众号(securitainment):基于虚拟化技术的内存监控与逆向工程(很长)

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年6月5日03:20:34
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   基于虚拟化技术的内存监控与逆向工程(很长)https://cn-sec.com/archives/4134202.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息