ntkrnlmp/svmtrace

GitHub: ntkrnlmp/svmtrace

基于 AMD SVM 硬件虚拟化技术的实验性 AMD64 逐指令执行追踪器,提供 C++ API、CLI 和离线解码器以捕获和分析指令边界的完整 CPU 状态。

Stars: 0 | Forks: 0

# SvmTrace [![Windows CI](https://static.pigsec.cn/wp-content/uploads/repos/cas/99/993938d8ce5e902ccfb9d6747725c320d855dea3235ed9a304cedf0d94c9321f.svg)](https://github.com/ntkrnlmp/svmtrace/actions/workflows/ci.yml) ![C++20](https://img.shields.io/badge/C%2B%2B-20-00599C?style=flat-square) ![Platform](https://img.shields.io/badge/platform-Windows-0078D4?style=flat-square) ![Capture](https://img.shields.io/badge/capture-AMD%20SVM-ED1C24?style=flat-square) ![Trace](https://img.shields.io/badge/trace-instruction%20boundaries-7C3AED?style=flat-square) ![Driver](https://img.shields.io/badge/driver-experimental-B45309?style=flat-square) SvmTrace 是一个实验性的 AMD64 执行追踪器,能够逐条指令地观察目标。 它使用 AMD SVM 执行门来启动会话,设置 guest 的 Trap Flag,将产生的 debug 异常作为 VM exits 进行拦截,并重构包含丰富寄存器信息的 instruction-boundary 记录。 这并非声称其他追踪器无法进行单步执行或收集寄存器。不同寻常之处在于此处实现的完整组合,特别是在 AMD SVM Windows 研究路径上。源码使这种区别变得具体化:VM-exit 处理程序处理退出码 `0x41`,验证 `DR6.BS`,记录 guest 状态,并在恢复 guest 之前再次设置 Trap Flag。 ## 目录 - [SvmTrace 的不同之处](#why-svmtrace-is-different) - [单步指令的意义](#what-one-instruction-step-means) - [快速开始](#quick-start) - [具体合成演练](#concrete-synthetic-walkthrough) - [C++ API](#c-api) - [架构](#architecture) - [捕获生命周期](#capture-lifecycle) - [Trace 格式](#trace-format) - [SvmTrace 与仅分支追踪](#svmtrace-and-branch-only-tracing) - [构建和测试](#build-and-test) - [驱动工作流](#driver-workflow) - [验证](#validation) - [项目状态](#project-status) - [出处](#provenance) ## SvmTrace 的不同之处 大多数追踪可视化从边缘开始:发生了一个分支,一个调用到达了目的地,或者执行返回。SvmTrace 则从连续 instruction boundaries 处的 CPU 状态开始。 ``` flowchart LR subgraph Branch[Branch-oriented view] B1[Source address] --> B2[Branch event] B2 --> B3[Destination address] end subgraph SVM[SvmTrace view] S1[Execute gate NPF] --> S2[Set guest TF] S2 --> S3[Execute one instruction] S3 --> S4[Intercept #DB VM exit] S4 --> S5[Capture RIP, RFLAGS, CR3, GPRs] S5 --> S6[Set TF and resume] S6 --> S3 end ``` | 问题 | 仅分支事件流 | 当前 SvmTrace 记录模型 | |---|---|---| | 控制流去了哪里? | 控制流事件前后的源和目标 | 每个捕获指令边界处的 RIP | | 指令改变了什么? | 需要从其他数据重构 | 连续的记录揭示了 GPR 和选定的 RFLAGS 变化 | | 哪个地址空间处于活动状态? | 取决于生产者 | CR3 是重构状态的一部分 | | 可以跟踪直线的代码吗? | 分支之间不需要事件 | 每次拦截到的单步 debug 退出都可以产生一条记录 | | 状态如何存储? | 特定于生产者的数据包 | 每 CPU v5 流中的完整锚点加上紧凑增量 | 实时捕获路径为每个接受的原始槽盖上以下印记: - instruction-boundary RIP; - RFLAGS 和 CR3; - RAX、RBX、RCX、RDX、RSI、RDI、RBP、RSP 以及 R8 到 R15; - 原始状态的全局序列值和 TSC; - 当 gate 后的第一个 debug 退出到达时的 session-start 标记。 v5 线上格式和 `TraceReader` 还对 16 个 YMM 寄存器和 `xstate_bv` 字段进行了建模。当前的 VM-exit 热路径故意将这些字段清零,因此来自此源代码版本的实时捕获提供了 GPR 丰富的状态,但没有实时的 YMM 值。这种区别在 `driver/svmtrace_driver.c` 中直接可见。 ## 单步指令的意义 debug 异常在处理器执行指令**之后**发生。因此,捕获的 RIP 指向下一条指令,而寄存器包含刚完成的指令所产生的状态。 ``` sequenceDiagram participant Guest as Target thread participant CPU as AMD CPU participant VMM as SvmTrace VM-exit handler participant Ring as Per-CPU raw ring VMM->>CPU: Set guest RFLAGS.TF VMM->>Guest: Resume Guest->>CPU: Execute one instruction CPU->>VMM: #DB VM exit, exit code 0x41 VMM->>VMM: Require DR6.BS and WeSetTf VMM->>Ring: Store resulting RIP, RFLAGS, CR3, and GPRs VMM->>CPU: Set guest RFLAGS.TF again ``` 这是连续的单步观察,而不是交互式调试器命令。公共客户端可以武装、查询、停止和关闭捕获,但它不暴露步进、暂停或编辑寄存器的命令。 处理程序还检查 debug 退出是否属于 SvmTrace。它同时要求 DR6 中的单步状态位和显示 SvmTrace 设置了 Trap Flag 的每 CPU 标记。内核模式的 RIP 会清除 Trap Flag,而稍后当执行返回到被跟踪的用户模式代码时,后续跟踪的执行错误可以重新启用步进。 ## 快速开始 默认构建包含 C++20 库、CLI 和合成解析器测试。它不会构建或加载驱动程序。 ``` git clone https://github.com/ntkrnlmp/svmtrace.git cd svmtrace cmake -S . -B build -A x64 cmake --build build --config Release ctest --test-dir build -C Release --output-on-failure ``` 检查一个每 CPU v5 流: ``` build\Release\svmtrace.exe inspect C:\svmtrace_trace_cpu0.bin ``` 将解码记录转储为 CSV: ``` build\Release\svmtrace.exe dump C:\svmtrace_trace_cpu0.bin build\Release\svmtrace.exe dump C:\svmtrace_trace_cpu0.bin 100 ``` 可选的最后一个参数会在输出指定数量的解码行后停止输出。CLI 会打印以下 schema: ``` cpu,sequence,kind,rip,rflags,cr3,reason ``` 解码器在读取帧时访问记录,因此调用者不需要将整个 trace 加载到内存中。 ## 具体合成演练 假设目标入口页是执行门,RFLAGS 初始为 `0x202`,并且目标以以下指令开始: ``` 0x0000000140001000 mov eax, 7 0x0000000140001005 add eax, 1 0x0000000140001008 test eax, eax ``` gate 页首先引起一个 nested page fault。SvmTrace 清除 gate 的 NX 位,启动一个 trace 会话,并设置 Trap Flag。接下来的两次拦截到的 debug 退出可以这样解释: | 完成的指令 | `#DB` 报告的 RIP | 生成的 RAX | 生成的 RFLAGS | 记录类型 | |---|---:|---:|---:|---| | 位于 `0x140001000` 的 `mov eax, 7` | `0x140001005` | `0x7` | `0x202` | `session_start` | | 位于 `0x140001005` 的 `add eax, 1` | `0x140001008` | `0x8` | `0x202` | `instruction` | 该合成序列的示例 CLI 输出: ``` cpu,sequence,kind,rip,rflags,cr3,reason 3,10,session_start,0x140001005,0x202,0x12345000,0 3,11,instruction,0x140001008,0x202,0x12345000,0 ``` 关键点在第一行可见:`RIP=0x140001005` 描述了 `mov` 之后的边界,尽管 CLI 的紧凑 CSV 视图没有打印每个 GPR,但通过 C++ 记录仍然可以获取 `RAX=7`。
相同的状态如何变成锚点和增量 ``` flowchart LR A[Session start state
RIP 0x140001005
RAX 7] -->|full 704-byte record| B[Anchor baseline] B --> C[Next state
RIP +3
RAX +1] C -->|RIP varint + GPR mask + RAX delta| D[Compact delta] D --> E[TraceReader reconstructs
RIP 0x140001008
RAX 8] ``` 编码器将每个原始状态与前一个状态进行比较。它在 16 位掩码后写入更改的 GPR,将附近的 RIP 和 GPR 更改表示为有符号变长整数,并定期再次发射完整的锚点。
## C++ API `TraceReader` 为每个重构的记录调用一次访问者。返回 `false` 可提前停止。 ``` #include #include #include #include int main() { const svmtrace::TraceReader trace("svmtrace_trace_cpu0.bin"); std::optional previous; const auto summary = trace.read([&](const svmtrace::Record& record) { if (previous && previous->gpr[0] != record.state.gpr[0]) { std::printf("seq=%" PRIu64 " rip=0x%" PRIx64 " rax: 0x%" PRIx64 " -> 0x%" PRIx64 "\n", record.sequence, record.state.rip, previous->gpr[0], record.state.gpr[0]); } previous = record.state; return true; }); std::printf("decoded %" PRIu64 " instruction records\n", summary.instructions); } ``` 公共模型是类型化的,独立于内核驱动程序: ``` svmtrace::Record .kind // instruction, session_start, or session_end .cpu // logical processor that owns this stream .sequence // reconstructed record order .tsc // present on anchors and session-end records .reason // gate id, exit reason, or session-end reason .state.rip .state.rflags .state.cr3 .state.gpr[16] .state.ymm[16][4] // represented by v5; current capture path writes zeros .state.xstate_bv // represented by v5; current capture path writes zero ``` 驱动控制是一个独立的 API: ``` #include svmtrace::DriverClient driver; driver.connect(); driver.set_target(pid); driver.arm(); const auto status = driver.status(); driver.stop(); ``` 因此,分析应用程序可以使用 `TraceReader` 而无需连接到 `\\.\svmtrace` 设备。 ## 架构 ``` flowchart LR A[Target entry page] -->|execute NPF| B[SVM VM-exit handler] B -->|clear NX and set TF| C[Target instruction] C -->|intercepted #DB| B B -->|full raw state| D[Per-CPU ring] D --> E[Per-CPU worker] E --> F[Anchor and delta encoder] F --> G[LZ4 or raw frames] G --> H[(Per-CPU v5 file)] H --> I[TraceReader] I --> J[C++ visitor] I --> K[inspect] I --> L[dump] ``` | 层 | 源码 | 职责 | |---|---|---| | SVM 捕获 | `driver/svmtrace_driver.c`, `driver/vmexit.asm` | 配置 VMCB 和 NPT,处理执行错误,拦截 debug 退出,捕获 guest 状态 | | 每 CPU 缓冲 | `driver/svmtrace_driver.c` | 将编码、压缩和文件 I/O 排除在 VM-exit 热路径之外 | | v5 编码 | `driver/svmtrace_driver.c` | 将完整的原始槽转换为锚点和增量,然后写入分帧的每 CPU 文件 | | 解码器 | `src/trace.cpp`, `src/lz4_block.cpp` | 验证帧,解码 LZ4 块,并重构类型化记录 | | CLI 和客户端 | `cli/main.cpp`, `src/client.cpp` | 检查流并发出显式的驱动程序 IOCTL | 捕获和分析这两部分共享的是文件契约,而不是运行时依赖。这使得在未加载实验性驱动程序时,离线解码仍然可用。 ## 捕获生命周期 独立的 gate 定义包含一个目标 gate。在 `arm` 期间,驱动程序读取选定进程的 PE `AddressOfEntryPoint`,将该页解析为 guest 物理地址,并标记相应的 nested page-table 条目为 NX。 ``` sequenceDiagram participant CLI as svmtrace CLI participant Driver as SVM driver participant NPT as Nested page table participant CPU as Target CPU participant Worker as Per-CPU worker CLI->>Driver: set-target PID Driver->>Driver: resolve image and PE entry point CLI->>Driver: arm Driver->>NPT: mark entry page NX Driver->>CPU: request translation flush CPU->>Driver: execute NPF at entry page Driver->>NPT: clear NX on gate page Driver->>CPU: set guest RFLAGS.TF loop while the target session remains active CPU->>Driver: intercepted single-step #DB Driver->>Driver: verify DR6.BS and WeSetTf Driver->>Worker: publish full state to per-CPU ring Driver->>CPU: set RFLAGS.TF and resume end Worker->>Worker: encode anchors and deltas Worker->>Worker: write raw or LZ4 frames ```
捕获状态转换 ``` stateDiagram-v2 [*] --> Idle Idle --> TargetSelected: set-target PID TargetSelected --> Armed: arm Armed --> Recording: entry-page execute NPF Recording --> Recording: accepted #DB step Recording --> Armed: target CR3 changes Recording --> TargetSelected: stop Armed --> TargetSelected: stop TargetSelected --> Idle: select another target Idle --> [*]: shutdown SVM ```
VM-exit 汇编代码切换到专用的 host 栈,并在 C 处理程序周围使用 VMSAVE 和 VMLOAD。C 热路径将状态复制到固定的原始槽中。稍后,一个 worker 会执行增量编码、LZ4 压缩和文件 I/O。 ## Trace 格式 每个逻辑处理器写入 `C:\svmtrace_trace_cpu{N}.bin`。`TraceReader` 识别版本 5,这是一个分帧的小端流。 ``` +-------------------------------+ | 16-byte file header | +-------------------------------+ | decoded_size : u32 | frame header | stored_size : u32 | +-------------------------------+ | raw or LZ4 block payload | +-------------------------------+ | next frame ... | +-------------------------------+ ``` ### 文件头 | 偏移量 | 大小 | 字段 | |---:|---:|---| | `0x00` | 4 | Magic bytes `FTRC` | | `0x04` | 4 | 格式版本 `5` | | `0x08` | 4 | 逻辑 CPU 标识符 | | `0x0c` | 4 | 标志,其中 bit 0 标记为分帧主体 | 解码后的帧最大为 64 KiB。当 `decoded_size == stored_size` 时,payload 是原始数据。否则,payload 是一个 LZ4 块,其解码大小必须与声明的大小完全一致。在记录可能跨越帧边界之前,worker 会启动一个新帧。 ### 记录流 ``` flowchart LR A[Session start
full state] --> D1[Delta
changed fields] D1 --> D2[Delta
changed fields] D2 --> AN[Anchor
full state] AN --> D3[Delta
changed fields] D3 --> Z[Session end
RIP, TSC, reason] ``` | 记录 | 线上类型 | 解码器结果 | |---|---:|---| | 锚点 | 头部低位中的 `01` | 一条带有完整基线的 `instruction` 记录 | | 会话开始 | 头部低位中的 `10` | 一条带有完整基线和 gate id 的 `session_start` 记录 | | 增量 | 头部低位中的 `00` | 一条重构的 `instruction` 记录 | | 会话结束 | 头部低位中的 `11` | 一条带有最终 RIP 和原因的 `session_end` 记录 | 完整的锚点携带序列、TSC、退出原因、RIP、RFLAGS、CR3、16 个 GPR、v5 YMM 区域和 `xstate_bv`。编码器在会话开始时、重置后的第一个状态时,以及距离上一个锚点已过去 64 条记录时,发射一个完整的锚点。 增量头部位宣告记录是否包含更改的 GPR、RFLAGS、YMM 寄存器或完整的 RIP。否则,RIP 和 GPR 更改使用有符号变长整数。`TraceReader` 将每个增量应用于前一个状态,并递增序列值。 ### 防御性解码 解码器在将记录公开给访问者之前会检查数据: - 文件 magic bytes、版本和分帧主体标志; - 完整的帧头和 payload; - 解码和存储的帧大小; - 精确的 LZ4 输出大小、偏移量和匹配长度; - 完整的固定记录和变长整数; - 增量掩码选择的每个字段的可用性; - 增量必须跟在已初始化的锚点之后的要求。 ## SvmTrace 与仅分支追踪 SvmTrace 不是 branch-packet 解码器。它的源码实现了一条刻意不同的观察路径。 | 属性 | SvmTrace 实现 | 仅分支模型 | |---|---|---| | 会话开始后的触发器 | Trap Flag 在一条指令后产生 debug 异常 | 控制流事件产生数据包或事件 | | 直线指令 | 通过连续的 debug 退出可见 | 它们之间没有分支事件发生 | | 此代码库中的状态 | 接受的实时槽中的 RIP、RFLAGS、CR3 和 16 个 GPR | 状态必须来自其他来源或被推断出来 | | 存储 | 完整锚点加上已更改字段的增量 | 面向控制流的编码 | | 运行时影响 | 每个拦截到的步进都会进入 VM-exit 处理程序 | 取决于分支追踪机制 | | 预期用途 | 状态转换至关重要的狭窄研究会话 | 更广泛的重构控制流 | ``` flowchart TB Q{What must the trace answer?} Q -->|Which edge executed?| B[Branch-oriented trace] Q -->|What state followed each instruction?| S[SvmTrace] S --> R[Compare consecutive GPR and flag values] S --> C[Follow straight-line execution] S --> A[Associate state with CR3 and sequence] ``` 此处不进行性能比较。该代码库不包含可以支持性能比较的硬件捕获记录或基准测试工具。 ## 构建和测试 ### 用户模式组件 要求: - Windows 10 或 Windows 11 - Visual Studio 2022 及 Desktop C++ 工具 - CMake 3.22 或更高版本 ``` cmake -S . -B build -A x64 cmake --build build --config Release ctest --test-dir build -C Release --output-on-failure ``` 默认配置构建: | 目标 | 输出 | 目的|---|---|---| | `svmtrace` | 由 CMake 选择的静态库或导入库 | Reader、LZ4 解码器和驱动程序客户端 | | `svmtrace_cli` | `svmtrace.exe` | `inspect`、`dump` 和 `driver` 命令 | | `svmtrace_tests` | 测试可执行文件 | 合成的原始和 LZ4 v5 解析器检查 | ### 实验性驱动程序 可选的驱动程序构建需要匹配的 Windows Driver Kit: ``` cmake -S . -B build-driver -A x64 ` -DSVMTRACE_BUILD_DRIVER=ON ` -DSVMTRACE_WDK_VERSION=10.0.26100.0 cmake --build build-driver --config Release ``` 该代码库不提供驱动程序二进制文件或加载程序。请在隔离的机器上使用正常的 Windows 签名或测试签名驱动程序路径。 ## 驱动工作流 在安装并启动驱动程序后,从提权的 shell 中执行: ``` svmtrace driver set-target 1234 svmtrace driver arm svmtrace driver status svmtrace driver stop svmtrace driver shutdown ``` `status` 报告: ``` bootstrap: complete|pending watchdog: clear|triggered armed: yes|no active CPUs: target PID: target image: .. NX pages: NPF exits: debug exits: ``` 状态字段直接映射到 `DriverStatus` 和驱动程序的 `SVMTRACE_STATUS_RESPONSE`。 ## 验证 签入的测试使用合成文件,这使得它们的证据精确且可重复,而不意味着 CI 执行了 AMD SVM 硬件测试。 | 检查 | 此代码库中的证据 | |---|---| | 库、CLI 和测试在 Windows 上编译 | `.github/workflows/ci.yml` 配置并构建默认的 CMake 目标 | | 原始分帧 v5 解码 | `parser_round_trip(false)` 构建并读取合成的原始帧 | | LZ4 分帧 v5 解码 | `parser_round_trip(true)` 将相同的测试装置包装在一个仅包含字面量的 LZ4 块中 | | 锚点重构 | 测试断言 CPU id、初始 RIP 和初始 RAX | | 增量重构 | 测试断言序列、RIP 增量、RAX 增量和 RFLAGS 更改 | | 会话结束解码 | 测试断言记录类型和结束原因 | | 无效 magic bytes 被拒绝 | `bad_magic_is_rejected()` 验证 `TraceError` | | 实时 SVM 捕获 | 未由当前 CI 工作流执行 | 在本地运行相同的验证: ``` ctest --test-dir build -C Release --output-on-failure ``` 本 README 中的图表和演练是对源码和合成记录语义的解释性渲染。没有将伪造的硬件屏幕截图或捕获结果作为验证展示。 ## 项目状态 SvmTrace 是一个研究原型,而不是生产级的追踪工具。默认构建执行离线 reader 和 CLI。驱动程序编译是可选的,实时捕获需要在合适的 AMD 硬件上进行独立测试。 每个 CPU 写入一个独立的流。`TraceReader` 保留每 CPU 的顺序,但不合并流或标准化 TSC 值。当前的实时热路径将 v5 YMM 和 `xstate_bv` 字段置零。Ring 的背压会造成序列间隙,而单步 VM exits 会实质性地干扰目标执行。 在解释 trace 时,这些属性很重要。成功的构建和合成的解析器测试证明了签入的用户模式组件在 v5 文件契约上达成了一致。它们并不能证明在特定处理器、固件版本或 Windows 构建版本上的实时捕获是正确的。 ## 出处 AMD SVM 驱动程序最初是私有研究工作区中一个未提交的原型,于 2026 年 7 月 21 日提取出来。源代码目录没有提交历史记录,因此无法做出更强的出处声明。特定于目标的二进制文件、trace、微码、加载程序代码和相关的未复制研究数据均未被复制。 `driver/sflz4.c` 和 `driver/sflz4.h` 改编自 Nigel Tao 的 SFLZ4 代码。该原型将该代码标识为 Apache License 2.0,但未记录确切的上游版本。此改编接受调用者拥有的哈希表,并使用 Windows 内核内存例程。原始格式文章可在 获取。`src/lz4_block.cpp` 中的 C++ 解码器是已记录的 LZ4 块格式的独立实现。 未添加项目范围的许可证。只有在确认原始驱动程序原型的作者身份和权利之后,才应选择许可证。
标签:AMD SVM, Bash脚本, C++20, 内核驱动开发, 客户端加密, 控制流追踪, 虚拟化技术