☰
.NET + EasyHook 虚拟文件系统:拦截 Win32 API 实现路径重定向
2026/10/9 6:44:39 网站建设 项目流程

简介:这是基于 .NET 与 EasyHook 实现的 Windows 虚拟文件系统源码,面向系统编程、API Hook 或文件监控方向的中高级 .NET 开发者,可用于理解并实现进程级文件操作拦截与虚拟路径映射。压缩包共 30 个文件、约 322KB,包含 C# 源码、DLL 运行库、Visual Studio 工程配置、可执行文件及说明文档,项目按 Hook 测试、虚拟文件系统、注入库三个模块组织,结构清晰,便于直接编译和二次调试。目前已有 59 人浏览学习。资源覆盖 EasyHook 进程注入、FindFirstFileW/FindNextFileW/CreateFileW 等 Win32 API 的拦截、文件映射与日志记录等核心实现,并附带完整工程和依赖库;日志模块可用于追踪文件操作与异常信息,便于调试和观察注入效果。阅读源码可以快速理解从注入目标进程到接管文件操作的完整链路,也能作为自定义虚拟磁盘、文件访问审计、沙盒隔离或扩展任意使用 Win32 API 的程序文件处理逻辑的参考。

1. 基于 .NET 和 EasyHook 的虚拟文件系统:拦下三个 Win32 API,改掉一个路径

拿到这套基于 .NET 和 EasyHook 的虚拟文件系统源码时,我第一反应是它解决了 Windows 开发里一个很反直觉的问题:目标程序写死了绝对路径 C:\App\Data,实际文件却要落在 D:\Storage,又改不了目标程序的配置。这套源码做的事,是让目标进程以为自己仍在读写 C:\App\Data,真实文件全部转到 D:\Storage。靠的是 EasyHook 的 API 拦截:把 FindFirstFileW、FindNextFileW、CreateFileW 这些少有人直接调的 Win32 接口拦下来,对路径做一层虚拟到实际的翻译。它适合有 .NET 基础的桌面软件工程师用:游戏 Mod 文件重定向、存档路径搬家、测试环境沙箱隔离都够。下面拆关键代码、跑通最小 demo,再讲清楚注入过程的坑。

2. EasyHook 把它拆成三端:HookTest、VirtualFile、InjectedLib 各管什么

2.1 三端工程与一条完整数据链路

VirtualFile.sln 打开之后是三个工程,HookTest、VirtualFile、InjectedLib。我一开始以为 VirtualFile 是核心库,HookTest 是测试入口,InjectedLib 是编译产物,读了入口代码才发现三者的分工比名字暗示的更讲究。

HookTest 是启动器,控制台应用,负责接收命令行参数、准备映射配置、调用 RemoteHooking.Inject 把 InjectedLib 注入到目标进程。VirtualFile 是虚拟文件系统的实现工程,核心类 VirtualFileSystem.cs 管路径映射规则的组织和查询。InjectedLib 真正住在目标进程里,里面放了 FileMapping.cs、HookMonitor.cs、NativeMethods.cs、Injected.cs 四个关键文件。这个结构的妙处在于把注入器、规则引擎、被注入代码三层分开:HookTest 只跑在注入器一侧,VirtualFile 的逻辑可以在注入器侧先做单元测试,InjectedLib 保持小巧,被注入后不依赖外部服务就能独立工作。

数据链路按顺序走一遍:HookTest 先把虚拟路径到实际路径的映射对象构造好,交给 VirtualFileSystem 管理;注入成功后 InjectedLib 的 IEntryPoint 接口通过构造函数拿到映射配置,把 HookMonitor 里的 detour 函数用 LocalHook.Create 安装到目标进程;之后目标进程每次调用 FindFirstFileW、FindNextFileW、CreateFileW,调用都会先落到托管 detour,detour 查一次 ToRealPath,命中就替换路径参数,再调用 NativeMethods 里的原始 Win32 函数完成真正操作。日志在这条链路的 detour 这一层落点,排错时只看日志就能判断是配置没生效还是 hook 根本没装上。

2.2 EasyHook 的 LocalHook.Create 与远程注入原理

EasyHook 为什么比微软 Detours 更适合这种场景,我的体验集中在两点:LocalHook.Create 的使用门槛低,远程注入是内置流程。

LocalHook.Create 的三个参数分别是目标函数地址、替代委托、回调上下文。目标函数地址用 LocalHook.GetProcAddress("kernel32.dll", "CreateFileW") 取;替代委托必须是托管委托,它会在目标函数被调用时接管执行;回调上下文用来在异常时定位对象,一般直接传 this。安装完还要用 ThreadACL 控制拦截线程范围,很多人第一次会在这上面懵:ThreadACL 不是过滤 API 的,是过滤线程的。SetInclusiveACL 表示只拦截指定线程,SetExclusiveACL 表示排除指定线程,目标进程的业务线程不落在 ACL 内,detour 就不触发。

远程注入的原理是在目标进程里创建远程线程,让远程线程加载托管运行时和注入库。这个过程中 EasyHook32Svc.exe 和 EasyHook64Svc.exe 两个服务进程负责托管运行时初始化,所以源码包里的 EasyHook32.dll、EasyHook64.dll、EasyHook32Svc.exe、EasyHook64Svc.exe、EasyLoad32.dll、EasyLoad64.dll 缺一不可。如果改动输出目录结构,服务进程找不到对应运行库,注入会报加载失败。我习惯把整套 EasyHook 运行库固定在输出目录里,两份 32 位和 64 位都留着,由 EasyHook 按目标进程位数自己挑选,开发期不折腾路径问题。

2.3 最小注入入口:IEntryPoint 的 Run 生命周期

InjectedLib 里的 Injected.cs 是最容易看懂的入口。EasyHook 要求注入库实现 IEntryPoint 接口,构造函数接收 RemoteHooking.IContext 和自定义参数,Run 方法在目标进程内被回调。一个最小实现的轮廓长这样:

public class Injected : IEntryPoint { private HookMonitor _monitor; public Injected( RemoteHooking.IContext context, string channelName, FileMapping mapping) { // 构造函数在目标进程内执行,此时托管运行时已就绪 _monitor = new HookMonitor(mapping); } public void Run( RemoteHooking.IContext context, string channelName) { // 安装所有需要拦截的 API _monitor.Install(); // 通知注入器进程:目标进程初始化完成 RemoteHooking.WakeUpProcess(); // 保持注入库不卸载,否则 hook 会随模块卸载一起失效 while (true) { Thread.Sleep(1000); } } }

构造函数和 Run 方法都运行在目标进程里,但都是托管代码,所以你可以用 .NET 的类库做复杂逻辑,这就是 EasyHook 比纯 C++ hook 工程舒服的地方。参数里的 FileMapping 是注入时从 HookTest 通过 IPC 通道传过来的,里面是虚拟路径到实际路径的映射配置。RemoteHooking.WakeUpProcess 的作用是告诉注入器,目标进程已经成功加载注入库;最后的死循环是为了让注入库的模块一直驻留,Run 方法一旦返回,注入库可能被卸载,hook 全部失效。我把这个循环里的 Thread.Sleep 时间调到 500ms 甚至更短都不会有性能问题,因为线程本身是阻塞挂起的。

如果注入后目标进程抛出托管异常,EasyHook 会通过 IPC 通道把异常信息回传给注入器。你会在 HookTest 的控制台里看到类似 "The injected library contains an exception" 之类的提示,后面跟着调用栈。排错时先看这段信息,能省掉很多盲猜。

3. 虚拟文件系统核心逻辑:VirtualFileSystem.cs 与 FileMapping.cs 的路径换算

3.1 FileMapping:虚拟路径到实际路径的映射规则

FileMapping.cs 定义了映射关系的数据结构,它不复杂,但所有路径重定向都从这里开始。我把关键实现摘出来看一眼:

public class FileMapping { public string VirtualPath { get; set; } public string RealPath { get; set; } public string TryMap(string inputPath) { if (string.IsNullOrEmpty(inputPath)) { return inputPath; } // 统一分隔符,Windows 上反斜杠和正斜杠都能被 API 接受 string normalizedInput = inputPath.Replace('/', '\\'); string normalizedVirtual = NormalizePath(VirtualPath); if (normalizedInput.StartsWith( normalizedVirtual, StringComparison.OrdinalIgnoreCase)) { string relative = normalizedInput.Substring(normalizedVirtual.Length); return Path.Combine(RealPath, relative.TrimStart('\\')); } return inputPath; } private static string NormalizePath(string path) { return path.Replace('/', '\\').TrimEnd('\\') + '\\'; } }

TryMap 的逻辑很直白:输入路径以虚拟路径开头,就截掉前缀,把剩余相对路径拼到实际路径后面。这里有两个容易翻车的细节。一是把正斜杠统一成反斜杠,很多程序会用 C:/App/Data 这种 Unix 风格传路径,Win32 API 认正斜杠,但字符串比较时两种写法的字符不一致,比较前不 normalize 就会漏匹配。二是用 TrimEnd('\') 再补一个反斜杠,避免 VirtualPath 是 C:\App 而输入是 C:\AppData 时被错误命中,那种情况下截出来的是 Data,拼出来变成 C:\App 下的 Data,指向完全错误的地方。

3.2 VirtualFileSystem:统一入口与多规则匹配

VirtualFileSystem.cs 在 VirtualFile 工程里,它不直接做 P/Invoke,而是把多组 FileMapping 组织成一套可查询的规则表。项目里把它独立成类,有个容易被忽略的好处:HookTest 启动时可以先加载配置、验证规则,再决定注入到哪个进程;规则可以动态增删,不用改 InjectedLib 的代码。

public class VirtualFileSystem { private readonly List<FileMapping> _mappings; public VirtualFileSystem() { _mappings = new List<FileMapping>(); } public void AddMapping(FileMapping mapping) { _mappings.Add(mapping); } public string ToRealPath(string virtualPath) { // 按添加顺序匹配,先命中先返回 foreach (FileMapping mapping in _mappings) { string realPath = mapping.TryMap(virtualPath); if (!string.Equals( realPath, virtualPath, StringComparison.OrdinalIgnoreCase)) { return realPath; } } return virtualPath; } }

这套统一入口的设计思路是把路径翻译和 API 拦截解耦。InjectedLib 里的 detour 函数只需要调用 ToRealPath,不需要关心规则怎么组织;要加规则,只需在 HookTest 启动端 AddMapping。匹配顺序在写多规则时要留意,它按列表顺序先命中先返回,所以两条规则如果有路径前缀重叠,比如 C:\App 和 C:\App\Sub,一定要把更精确的那条放在前面,否则短的会先吞掉长路径。

3.3 CreateFileW 拦截:参数改写与转调原始 API

CreateFileW 是这套虚拟文件系统里价值最高的拦截点,几乎所有文件读写、创建、打开操作最终都会经过它。HookMonitor.cs 里需要声明一个与 CreateFileW 签名完全一致的委托,然后用 LocalHook.Create 把委托安装到 kernel32.dll 的 CreateFileW 导出函数上。签名对照是这一步最容易出错的地方:

参数Win32 类型托管类型是否改写
lpFileNameLPCWSTRstring改写
dwDesiredAccessDWORDuint原样
dwShareModeDWORDuint原样
lpSecurityAttributesLPSECURITY_ATTRIBUTESIntPtr原样
dwCreationDispositionDWORDuint原样
dwFlagsAndAttributesDWORDuint原样
hTemplateFileHANDLEIntPtr原样

detour 函数的写法就是把路径参数替换后,其余全部透传:

[UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Unicode)] private delegate IntPtr CreateFileDelegate( string lpFileName, uint dwDesiredAccess, uint dwShareMode, IntPtr lpSecurityAttributes, uint dwCreationDisposition, uint dwFlagsAndAttributes, IntPtr hTemplateFile); private IntPtr CreateFileDetour( string lpFileName, uint dwDesiredAccess, uint dwShareMode, IntPtr lpSecurityAttributes, uint dwCreationDisposition, uint dwFlagsAndAttributes, IntPtr hTemplateFile) { // detour 里第一件事就是查映射 string mappedPath = _vfs.ToRealPath(lpFileName); if (!string.Equals(mappedPath, lpFileName, StringComparison.OrdinalIgnoreCase)) { Log($"[CreateFileW] {lpFileName} -> {mappedPath}"); } // 这里调用的是 NativeMethods 里的原始函数,不是 detour 自身 return NativeMethods.CreateFileW( mappedPath, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile); }

这个 detour 的核心行为是「参数改写,行为保留」:只改 lpFileName,其他六个参数原样传给原始 API。这样做成功率最高,因为访问模式、共享模式、创建方式都和最终落在哪个文件路径上相关,路径变了但文件系统语义不能乱。日志打到独立文件或 OutputDebugString,开发和排错靠它。调用原生函数时一定要用 NativeMethods 里的 DllImport 声明,不要用 GetProcAddress 取地址后转成委托再调用,那样容易重新回到 detour 入口造成递归,后面避坑章节会专门讲。

3.4 FindFirstFileW / FindNextFileW 枚举重定向的处理思路

文件查找 API 的重定向比 CreateFileW 麻烦,因为 FindFirstFileW 返回的是句柄,FindNextFileW 要用这个句柄继续枚举,句柄里的目录上下文在系统内部维护。做目录枚举时,常见做法是调用真正的 FindFirstFileW 去枚举实际目录,然后把返回的 WIN32_FIND_DATAW 透传给调用方:

private IntPtr FindFirstFileDetour( string lpFileName, out WIN32_FIND_DATAW lpFindFileData) { string mappedPath = _vfs.ToRealPath(lpFileName); IntPtr handle = NativeMethods.FindFirstFileW(mappedPath, out lpFindFileData); if (handle != new IntPtr(-1)) { Log($"[FindFirstFileW] {lpFileName} 已映射为 {mappedPath}"); } return handle; }

处理这类 API 的原则是尽量不自己拼 WIN32_FIND_DATAW 结构体,除非做复杂过滤。原始 FindFirstFileW 返回的数据结构已经填好,真实路径上的文件属性、文件大小、时间戳都是真实的,直接交给调用方即可。有些实现会在这里做“只显示虚拟目录下某些文件”的过滤,那就要在 detour 里自己构造 WIN32_FIND_DATAW 并维护一份虚拟句柄表,复杂度和出错率明显上升。如果只是做路径重定向、不搞过滤,直接透传是最稳的。FindNextFileW 的 detour 同理,拿到句柄就继续调原始函数,不要尝试修改句柄本身。

4. 从源码到跑通:MSBuild 构建、RemoteHooking.Inject 注入与 vfs.log 验证

4.1 输出目录里的 EasyHook 运行库布置

源码包里的 EasyHook 运行库很齐全:EasyHook32.dll、EasyHook64.dll、EasyHook32Svc.exe、EasyHook64Svc.exe、EasyLoad32.dll、EasyLoad64.dll、EasyHook.dll,以及 EasyHook.xml 注释文档。EasyHook.dll 是托管程序集,工程引用它就行;EasyHook32.dll 和 EasyHook64.dll 是非托管运行库,取决于目标进程位数。最容易踩的坑是:目标进程是 64 位,输出目录里却只有 32 位运行库,注入时 EasyHook 找不到对应 DLL 会抛异常。我习惯在构建脚本里把整套 EasyHook 文件全部复制到输出目录,两份都在,由 EasyHook 自行选择。

InjectedLib.dll 是注入库本体,必须和 EasyHook 运行库放同一个目录,因为远程注入时 EasyHook 需要在目标进程里加载这个托管 DLL。如果路径不对,RemoteHooking.Inject 会报找不到模块或者 BadImageFormatException,后面全白搭。

4.2 用 MSBuild 编译 VirtualFile.sln 的两种方式

有 Visual Studio 时双击 VirtualFile.sln 就能出工程图,但命令行构建更适合排查。先按解决方案配置编译一次:

msbuild VirtualFile.sln /p:Configuration=Release /p:Platform="Any CPU" /v:m

这条命令把 HookTest、VirtualFile、InjectedLib 三个项目一起编出来。需要留意的是 InjectedLib 如果要注入 64 位进程,最好单独编一版 x64:

msbuild InjectedLib/HeyoVirtualFile.csproj /p:Configuration=Release /p:Platform=x64 /p:OutputPath=bin\x64\ /v:m

为什么要单独编 x64?因为 .NET 的 AnyCPU 程序集在 32 位系统或 32 位注入器环境下会以 32 位进程运行,被 EasyHook 注入到 64 位目标进程时,CLR 加载的位数和宿主进程对不上,注入会失败。保险做法是 InjectedLib 同时保留 AnyCPU 和 x64 两份输出。HookTest 本身用 AnyCPU 无所谓,因为注入器进程位数不影响它发起 RemoteHooking.Inject。如果你的机器没有把 msbuild 加进 PATH,打开 Visual Studio 的 Developer Command Prompt 再执行,或者用完整路径调用。

4.3 HookTest 启动注入:RemoteHooking.Inject 参数说明

HookTest 里的 Program.cs 是入口,核心是把 FileMapping 配置好之后传给注入库。EasyHook 自带的 IPC 通道会把参数对象传到目标进程的 IEntryPoint 构造函数里,所以参数必须是可序列化的。标准调用形态是:

static void Main(string[] args) { int targetPid = int.Parse(args[0]); var mapping = new FileMapping { VirtualPath = @"C:\VirtualApp\Data", RealPath = @"D:\RealStorage\Data" }; string injectionLibrary = Path.Combine( AppDomain.CurrentDomain.BaseDirectory, "InjectedLib.dll"); RemoteHooking.Inject( targetPid, injectionLibrary, // 32 位注入库 injectionLibrary, // 64 位注入库 out int hostPid, // 注入器申请的 IPC host 进程 PID mapping); // 传给 IEntryPoint 构造函数的参数 }

RemoteHooking.Inject 的第三个参数是 64 位版本库路径,32 位和 64 位环境都用同一个路径传也不会出错,EasyHook 会按目标进程自选。参数 mapping 在跨进程传递时要求可序列化,否则会在远程线程里抛序列化异常。第一次跑 demo,建议把目标进程锁定到一个自己写的测试程序。如果非要选现成的,不要用 notepad,因为 notepad 几乎不会访问你映射的 C:\VirtualApp;用 vs code 这类会大量读取配置和插件的编辑器,把映射指向它的配置目录,才更容易看到日志。

4.4 日志验证:看 VFS 是否真的在改路径

HookMonitor 里那个 Log 函数,我建议用 File.AppendAllText 写到一个固定路径,比如 C:\vfs.log,避开调试器依赖。日志样例最好长这样:

[CreateFileW] C:\VirtualApp\Data\save.dat -> D:\RealStorage\Data\save.dat [FindFirstFileW] C:\VirtualApp\Data\*.dat -> D:\RealStorage\Data\*.dat

看到这两行,说明 detour 生效、映射规则命中。快速验证手段是注入一个 cmd.exe,然后在被注入的 cmd 里执行 dir C:\VirtualApp\Data;如果 hook 生效,列出来的是 D:\RealStorage\Data 下的文件,vfs.log 里也会出现 FindFirstFileW 记录。如果只有注入成功信息、没有路径日志,那就要检查 detour 的路径比较是否命中,或者目标进程根本没走 Win32 API 枚举,而是用了 NtQueryDirectoryFile 这类底层调用。

注意:日志文件路径千万不要落在虚拟映射范围内。日志本身也是写文件操作,一旦落进映射区,会再次进入 detour 改写,形成“写日志→映射→再写日志→再映射”的递归,几秒钟就能把磁盘撑爆。

5. 常见问题与避坑:注入失败、递归回调、乱码路径的五次踩坑记录

这个坑位我基本都亲身踩过一遍,按“现象 → 原因 → 解决”记录,每条都能直接对着排查。

5.1 注入成功但 Hook 没触发:先查 32 位还是 64 位

现象:RemoteHooking.Inject 没有抛异常,目标进程也正常,但日志一条都没有,文件操作完全没被重定向。

原因:最常见的是位数不匹配。EasyHook 把运行库拆成 32 位和 64 位两套,注入器是 32 位、目标进程是 64 位时,远程线程加载的是 32 位注入库,LocalHook.Create 根本 hook 不到 kernel32 的 64 位导出表。还有一种情况是 InjectedLib 被编译成 x86,被 EasyHook 加载后 CLR 只能跑 32 位,问题表现一模一样。

解决:先确认目标进程位数,用任务管理器或 PowerShell 的 Get-Process 看 Architecture;再确认 InjectedLib.dll 的 PE 位数,用 dumpbin /headers 或者 corflags 查;两头都对齐到同一个位数。最省事的开发期做法是 RemoteHooking.Inject 时把 32 位和 64 位两份库路径都传上,EasyHook 会根据目标进程自动挑,避免了手工匹配的尴尬。

5.2 注入后目标进程崩溃:签名不一致与委托被回收

现象:注入完成后目标进程立刻崩,或者运行几秒后报 Access Violation,EasyHook 回传的日志里能看到线程上下文被破坏的记录。

原因:两种可能最多。一是 detour 委托的签名和原始 API 不一致,比如 Win32 的 HANDLE 在 64 位下是 8 字节指针,你声明成 int 就会错位,参数从栈上取错位置,调用栈直接被改写。二是 detour 委托作为局部变量传给 LocalHook.Create,方法返回后没有任何引用,被垃圾回收,等 hook 触发时调用已回收的委托地址就崩。

解决:委托声明为类级别字段,用 UnmanagedFunctionPointer 显式标注 CallingConvention.StdCall 和 CharSet.Unicode;HANDLE 一律用 IntPtr 承接;写完把类型和 Win32 API 文档逐字段对照一遍,别想当然。这也是为什么 NativeMethods.cs 要单独放一个文件,所有 P/Invoke 声明集中管理,每次加 API 都从文档复制签名,不手敲。

5.3 Hook 回调死循环:日志疯狂刷同一路径

现象:目标进程卡死,vfs.log 在几秒内涨到几百 MB,日志里同一路径反复出现。

原因:detour 里调用原始 API 时又走回了被 hook 的地址。具体场景是有人为了“保持和原函数一致”,在 detour 里通过 GetProcAddress 拿到函数地址再手动调用;但这个地址已经是被 EasyHook 修改过的入口,等于又调回自己。递归调用在栈被打爆前先把日志刷爆了。

解决:原始函数调用用 DllImport 的 P/Invoke 声明。NativeMethods.cs 里 CreateFileW 的 DllImport 指向 kernel32 导出函数,CLR 会直接进系统调用,不经过 EasyHook 的跳转劫持。另外用 ThreadACL 限定 detour 只在目标业务线程生效,也能规避一部分死循环场景。日志写入路径要排除在映射范围外,因为日志文件被当虚拟路径处理就会再次进入 detour,形成“写日志→映射→写日志→映射”的闭环。

5.4 中文路径乱码与映射不命中:CharSet.Unicode 不能省

现象:目标程序读写 D:\数据\存档.txt 时,detour 收到的字符串变成乱码,日志里显示 ??????,映射永远不会命中。

原因:Win32 的 W 系列 API 使用 UTF-16 编码,EasyHook 的托管委托必须用 CharSet.Unicode;如果用了 Ansi 或默认值,字符串编解码和 kernel32 的期望不一致,中文路径必然乱码。这个问题在纯英文系统上很难发现,一上中文环境就暴露。

解决:委托和 DllImport 的 CharSet 全部显式写成 CharSet.Unicode。日志写入用 File.AppendAllText 时也建议显式传 UTF8Encoding,避免日志文件本身编码混乱。如果目标是旧程序,本身用 ANSI 代码页操作路径,那还要考虑一层编码转换,但绝大多数现代 Windows 程序都走 W 系列 API,把 Unicode 钉死是正解。

5.5 高权限进程注入失败:OpenProcess 被拒

现象:目标进程是以管理员启动的 64 位程序,RemoteHooking.Inject 抛异常,内部错误是 OpenProcess 拒绝访问。

原因:目标进程和注入器进程的权限级别不对齐。EasyHook 注入内部要先 OpenProcess 拿进程句柄,没有 SeDebugPrivilege 的情况下,跨会话访问更高权限进程会被拒绝。

解决:以管理员身份运行 HookTest。如果想省事,可以在 Main 里用 Windows API 临时启用 SeDebugPrivilege:

IntPtr tokenHandle; if (OpenProcessToken( Process.GetCurrentProcess().Handle, 0x0008 | 0x0020, // TOKEN_QUERY | TOKEN_ADJUST_PRIVILEGES out tokenHandle)) { // 用 AdjustTokenPrivileges 追加 SeDebugPrivilege }

另一种省事做法是把注入器注册成 Windows 服务。不过大多数开发期场景用管理员权限跑注入器、管理员权限跑目标进程就够了;如果目标进程以 SYSTEM 权限运行,那管理员权限也未必够用,这条边界要认清。

6. 验证与扩展:用 Process Monitor 和 JSON 配置把它改成自己的重定向工具

6.1 用 Process Monitor 确认 Hook 真的生效

日志证明了 detour 被调到,但要证明路径真改到了 D 盘,我习惯用 Process Monitor 复查。打开 PM,加一条过滤:路径以 D:\RealStorage 开头。目标进程执行目录枚举、创建文件之后,如果 PM 里出现 D:\RealStorage 下的 CreateFile 操作,说明 CreateFileW detour 的转调成功。同时检查 C:\VirtualApp 下有没有实际 IO,没有就说明虚拟路径只是“名义上的”,没有产生真实文件系统活动。我一般会再对比 vfs.log 和 PM 的时间线,两边时间戳对应得上,链路就没有断点。

6.2 扩展成自己的重定向工具

验证通过后,扩展方向很明确。第一是把 FileMapping 从代码搬到 JSON 配置文件,启动时读入,这样换路径、加规则不用重编译注入库:

string json = File.ReadAllText("mappings.json"); var mappings = JsonConvert.DeserializeObject<List<FileMapping>>(json);

第二是补 API,最值得加的是 CreateDirectoryW、DeleteFileW、MoveFileW,它们和 CreateFileW 一样是字符串路径参数,照 detour 的模板复制一份、改签名即可。第三是按进程名选择注入对象,在 HookTest 里循环扫 Process.GetProcessesByName,命中目标进程就注入,让工具变成对某个程序永远生效的后台服务。源码包里 VirtualFileSystem.cs 的规则表结构已经替你省掉了最麻烦的组织逻辑,剩下的只是往表里填规则。

我每次给 hook 加 API,都会先逻辑推演一遍路径改写后的语义,再从 Process Monitor 拉一遍实际打开行为对照日志。上次漏掉 FindNextFileW 的句柄上下文细节,结果目标程序能列文件却拿不到完整列表,查了一下午才发现是句柄没透传。从那以后我再碰枚举 API 一定把句柄生命周期画出来,而且每条 detour 都强制走一遍“查位数、查签名、查递归、查编码”四查。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询