简介:本资源是一套基于 .NET 平台与 EasyHook 库实现的轻量级虚拟文件系统源码,面向 Windows 底层开发、安全研究及逆向工程学习者,解决传统文件监控与路径虚拟化难以在托管环境中高效落地的问题。项目通过 API Hook 技术拦截 FindFirstFileW、CreateFileW 等关键 Win32 文件操作函数,支持虚拟路径映射、进程注入与操作日志记录,适用于沙箱环境构建、软件行为分析及文件访问策略定制等场景。压缩包共30个文件,含10个核心 C# 源码(如 VirtualFileSystem.cs、Injected.cs)、6个运行依赖 DLL(含 EasyHook32/64.dll 及注入库)、4个配置文件(App.config 等)、3个项目文件(.csproj)、2个可执行程序(HookTest.exe、EasyHookSvc.exe)及解决方案文件(.sln),整体仅322KB,结构紧凑、模块职责清晰。目前已有58人学习下载,提供完整可编译工程、跨架构 Hook 支持(x86/x64)、配套 README 说明与 LICENSE 授权信息,便于快速理解 Hook 原理并二次开发。
1. 这不是“挂个钩子就完事”的玩具:一个能真正在 Win32 层拦截 CreateFileW 并重映射路径的 .NET 虚拟文件系统
你有没有试过让一个老程序(比如十年前写的 Delphi 工具、某工业软件的配置加载器,甚至某个不开放源码的闭源客户端)去读取一个它根本“看不见”的路径?比如让它打开C:\Virtual\config.json,而实际读的是D:\AppData\Encrypted\cfg_20240523.bin?不是靠符号链接(symbolic link),不是靠注册表重定向,更不是改它的配置文件——是它调用CreateFileW(L"C:\\Virtual\\config.json", ...)的那一瞬间,内核还没碰磁盘,你的代码就已把参数悄悄替换成真实路径,并返回一个指向真实文件句柄的 HANDLE。这项目就是干这个的。它用 EasyHook 在用户态对kernel32.dll中的CreateFileW、FindFirstFileW、FindNextFileW等关键 API 做了无侵入式 Hook,所有逻辑跑在 .NET Framework 4.6+ 环境下,注入后不依赖服务、不修改注册表、不重启目标进程。适合做沙箱环境隔离、旧系统兼容层、敏感数据路径虚拟化、或逆向分析时的 I/O 行为观测。如果你正被“程序硬编码路径”、“无法修改第三方 exe”、“想绕过 UAC 文件虚拟化限制”卡住,这份源码不是概念验证,而是可编译、可调试、可嵌入生产级注入流程的实战组合包——它连 32/64 位双架构注入器、进程通信通道、错误日志回传都给你配齐了。
2. 从 sln 编译到进程注入:五步走通虚拟文件系统落地链
2.1 理解项目分层结构:为什么必须分 InjectedLib、VirtualFile、HookTest 三个工程?
这不是一个单体项目,而是典型的“宿主-注入体-测试端”三件套设计。
VirtualFile.sln是主解决方案,包含三个核心项目:VirtualFile:宿主工程,负责构建虚拟文件映射规则(如"C:\\Virtual\\*" → "D:\\Real\\")、管理 Hook 状态、提供日志输出接口;InjectedLib:注入体工程,编译为InjectedLib.dll,它才是真正被EasyHook注入到目标进程地址空间里的动态库。它不引用任何 UI 或高级框架,只依赖EasyHook.dll和System.Runtime.InteropServices,确保最小内存 footprint 和最大兼容性;HookTest:测试宿主,一个控制台程序,模拟你要监控的目标进程(比如你自己的业务程序)。它本身不做任何虚拟化,只调用标准 Win32 API,用来验证 Hook 是否生效。
提示:
InjectedLib.dll必须与目标进程位数严格一致(32 位进程只能注入InjectedLib32.dll,64 位同理),EasyHook 的EasyLoad32.dll/EasyLoad64.dll就是为此设计的加载器,它们不包含业务逻辑,只负责把InjectedLib.dll安全载入目标进程并触发EntryPoint。
2.2 编译前必做的三处手动配置:避开 90% 的首次构建失败
项目使用的是较老的.NET Framework项目格式(.csproj中含<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>),但 EasyHook 的 NuGet 包(packages.config中指定)默认拉取的是EasyHook 2.7.6886,该版本在 VS2022 中需手动补全两处缺失:
<!-- 在 VirtualFile.csproj 和 InjectedLib.csproj 的 <PropertyGroup> 内添加 --> <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects> <GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>第二处是 EasyHook 的原生 DLL 引用路径。InjectedLib.csproj中有如下<Reference>:
<Reference Include="EasyHook"> <HintPath>..\packages\EasyHook.2.7.6886\lib\net40\EasyHook.dll</HintPath> </Reference>但实际下载后路径是packages\EasyHook.2.7.6886\lib\net40-client\EasyHook.dll—— 注意末尾多了一个-client。必须手动修正HintPath,否则编译报错CS0006: Metadata file '...' could not be found。
第三处是App.config中的<startup>配置。原始文件里是:
<startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/> </startup>但项目目标是v4.6.1,必须改为:
<startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1"/> </startup>否则运行时抛出System.IO.FileLoadException: Could not load file or assembly 'EasyHook'—— 因为运行时加载器找不到匹配的 .NET 版本。
2.3 编译与生成物清单:看清每个文件的使命
执行Build Solution后,你会在bin\Debug\下得到以下关键产物(以 x64 架构为例):
| 文件名 | 类型 | 作用 | 是否必需 |
|---|---|---|---|
VirtualFile.exe | 宿主控制台 | 启动 Hook 监控、加载映射规则、接收注入反馈 | ✅ |
InjectedLib.dll | 注入 DLL | 运行在目标进程内存中,实现 API Hook 和路径重写 | ✅ |
EasyHook64.dll | EasyHook 核心 | 提供跨进程 Hook 机制,由EasyLoad64.dll加载 | ✅ |
EasyLoad64.dll | EasyHook 加载器 | 负责将InjectedLib.dll注入到 64 位目标进程 | ✅ |
EasyHook64Svc.exe | EasyHook 服务代理 | 在需要高权限 Hook(如系统进程)时启用,普通场景可不用 | ⚠️ 可选 |
InjectedLib.pdb | 调试符号 | 调试注入体时必备,否则断点无效 | ✅(调试时) |
注意:
VirtualFile.exe和InjectedLib.dll必须放在同一目录下,因为VirtualFile.exe会通过相对路径查找InjectedLib.dll并传给EasyHook。若移动InjectedLib.dll,需同步修改VirtualFile\Program.cs中的LocalHook.Create()调用里@"InjectedLib.dll"的路径参数。
2.4 注入目标进程:一行命令完成 Hook 启动
假设你要监控记事本notepad.exe的文件操作,且已确认它是 64 位进程(可通过任务管理器“详细信息”页看“平台”列):
# 在管理员权限的 CMD 中执行(UAC 提权必需) VirtualFile.exe -p notepad.exe -a "C:\\Virtual\\*;D:\\Real\\"参数说明:
-p notepad.exe:按进程名匹配目标(支持完整路径,如-p "C:\Windows\System32\notepad.exe");-a "C:\\Virtual\\*;D:\\Real\\":添加一条虚拟映射规则,语法为"虚拟路径模式;真实根路径",*是通配符,表示匹配所有子路径;- 其他可用参数:
-l启用日志到VirtualFile.log,-v输出详细 Hook 调试信息,-k卸载所有已注入 Hook。
执行后,VirtualFile.exe会:
- 调用
EasyHook.LocalHook.Create()创建CreateFileWHook; - 调用
EasyHook.RemoteHooking.Inject()将InjectedLib.dll注入notepad.exe; InjectedLib.dll在目标进程中初始化,替换CreateFileW的 IAT(Import Address Table)条目;- 后续
notepad.exe所有CreateFileW(L"C:\\Virtual\\test.txt", ...)调用,都会先进入InjectedLib中的CreateFileWHook函数,由你定义的VirtualFileSystem.MapPath()重写lpFileName参数。
2.5 验证 Hook 是否生效:用 Process Monitor 抓包比看日志更可靠
别只信VirtualFile.log里那句Hook installed for CreateFileW。真实世界里,Hook 失败往往静默发生。最硬核的验证方式是用微软官方工具Process Monitor (ProcMon):
- 启动 ProcMon,设置 Filter:
Process Name is notepad.exe+Operation is CreateFile; - 在
notepad.exe中尝试打开C:\Virtual\hello.txt; - 观察 ProcMon 日志:
- 若 Hook 成功,你会看到两条记录:
CreateFile操作,Path = C:\Virtual\hello.txt,Result = NAME NOT FOUND(因为物理路径不存在);- 紧接着一条
CreateFile,Path = D:\Real\hello.txt,Result = SUCCESS(这才是真实打开);
- 若 Hook 失败,则只有第一条
C:\Virtual\hello.txt记录,且Result = PATH NOT FOUND,说明请求根本没被拦截重写。
- 若 Hook 成功,你会看到两条记录:
这是唯一能 100% 确认 Hook 已进入 Win32 API 调用链路的证据。日志只是“我声称我 Hook 了”,ProcMon 才是“操作系统说你确实改了”。
3. 路径映射不是字符串替换:四个边界坑与真实世界的文件语义
3.1 坑一:C:\Virtual\*不等于C:\Virtual\**—— 通配符层级与路径规范化冲突
你以为VirtualFileSystem.MapPath("C:\\Virtual\\sub\\file.txt")会自动映射到D:\\Real\\sub\\file.txt?错。原始代码中MapPath的实现是简单前缀匹配:
// VirtualFileSystem.cs 第 42 行(简化) public static string MapPath(string virtualPath) { if (virtualPath.StartsWith(VirtualRoot, StringComparison.OrdinalIgnoreCase)) return virtualPath.Replace(VirtualRoot, RealRoot); return virtualPath; }问题来了:VirtualRoot = "C:\\Virtual\\",而virtualPath实际传入的是"C:\\Virtual\\sub\\file.txt"(注意双反斜杠)。但 Windows API 在调用CreateFileW时,常把路径规范化为"C:\\Virtual\\sub\\file.txt"(单反斜杠),导致StartsWith失败!
解决:在MapPath开头强制标准化路径:
virtualPath = virtualPath.Replace('/', '\\'); // 统一斜杠 virtualPath = System.Text.RegularExpressions.Regex.Replace(virtualPath, @"\\+", "\\"); // 合并多余反斜杠 if (virtualPath.StartsWith(VirtualRoot, StringComparison.OrdinalIgnoreCase)) return virtualPath.Substring(VirtualRoot.Length).Insert(0, RealRoot);3.2 坑二:FindFirstFileW返回的WIN32_FIND_DATAW.cFileName是相对路径,不能直接MapPath
当程序调用FindFirstFileW(L"C:\\Virtual\\*.*")时,InjectedLib的 Hook 函数会先调用真实FindFirstFileW(L"D:\\Real\\*.*"),拿到WIN32_FIND_DATAW结构体。但其中cFileName字段是"file1.txt"、"subdir"这样的相对名,不是完整路径。若你直接对cFileName调用MapPath,结果是"D:\\Real\\file1.txt"—— 这完全错误,它应该仍是"file1.txt",因为FindFirstFileW的语义是“列出目录内容”,不是“打开文件”。
解决:FindFirstFileWHook中需区分两种场景:
- 若原始
lpFileName是目录模式(含*或?),则cFileName不重写,仅在FindNextFileW返回时保持原样; - 若原始
lpFileName是具体文件(如C:\\Virtual\\test.dat),则cFileName应设为"test.dat",而非映射后的全路径。
3.3 坑三:CreateFileW的dwCreationDisposition参数决定行为,映射后必须透传
CreateFileW第 4 个参数dwCreationDisposition控制文件不存在时是否创建(CREATE_ALWAYS)、只读打开(OPEN_EXISTING)等。若你重写路径后,把OPEN_EXISTING错误地当成CREATE_NEW,会导致程序崩溃。原始代码中CreateFileWHook对此参数完全忽略。
解决:在CreateFileWHook中保留原始参数,并在调用真实CreateFileW时原样传递:
// 在 CreateFileWHook 函数内 IntPtr realHandle = CreateFileW( realPath, // 已 MapPath 后的真实路径 dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, // ← 必须透传! dwFlagsAndAttributes, hTemplateFile );3.4 坑四:长路径(>260 字符)触发\\?\前缀,StartsWith匹配失效
Windows 支持\\?\C:\Very\Long\Path\...这种 Unicode 长路径格式。此时virtualPath可能是\\?\C:\Virtual\file.txt,而VirtualRoot是C:\Virtual\,StartsWith必然失败。
解决:在MapPath中增加长路径前缀处理:
public static string MapPath(string virtualPath) { const string longPathPrefix = @"\\?\"; bool isLongPath = virtualPath.StartsWith(longPathPrefix, StringComparison.OrdinalIgnoreCase); string pathToCheck = isLongPath ? virtualPath.Substring(longPathPrefix.Length) : virtualPath; if (pathToCheck.StartsWith(VirtualRoot, StringComparison.OrdinalIgnoreCase)) { string mapped = pathToCheck.Substring(VirtualRoot.Length).Insert(0, RealRoot); return isLongPath ? longPathPrefix + mapped : mapped; } return virtualPath; }4. 注入失败排查:五类高频现象与秒级定位法
4.1 现象:VirtualFile.exe报错Failed to inject into process: Access is denied
原因:目标进程以更高完整性级别(High/Medium Integrity)运行,而你的VirtualFile.exe是 Medium(普通用户),或未以管理员身份启动。EasyHook 的Inject()需要PROCESS_VM_OPERATION权限,低权限进程无法获得。
解决:右键VirtualFile.exe→ “以管理员身份运行”;或在 CMD 中用runas /user:Administrator "VirtualFile.exe -p notepad.exe"。
4.2 现象:注入成功,但notepad.exe打开C:\Virtual\test.txt时弹窗报错The system cannot find the path specified.
原因:InjectedLib.dll中的CreateFileWHook函数未正确返回 HANDLE,或返回了INVALID_HANDLE_VALUE且未设置Marshal.GetLastWin32Error()。EasyHook 要求 Hook 函数必须严格遵循原 API 签名和错误码约定。
解决:检查CreateFileWHook最后是否调用Marshal.SetLastWin32Error(0)(成功时)或Marshal.SetLastWin32Error(3)(ERROR_PATH_NOT_FOUND);确保返回值是IntPtr类型 HANDLE,不是int。
4.3 现象:VirtualFile.log显示Hook installed,但 ProcMon 中看不到D:\Real\*的CreateFile记录
原因:InjectedLib.dll被注入,但其内部CreateFileWHook函数未被正确注册为LocalHook的回调。常见于Injected.cs中CreateFileWHook方法签名与CreateFileW原函数不一致(如少lpSecurityAttributes参数,或dwFlagsAndAttributes类型写成uint而非int)。
解决:对照kernel32.dll导出的CreateFileW签名( MSDN ),逐字核对Injected.cs中委托定义:
[UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Unicode)] public delegate IntPtr CreateFileWHookDelegate( string lpFileName, uint dwDesiredAccess, uint dwShareMode, IntPtr lpSecurityAttributes, uint dwCreationDisposition, uint dwFlagsAndAttributes, IntPtr hTemplateFile );4.4 现象:注入后notepad.exe立即崩溃,事件查看器报Application Error: faulting module InjectedLib.dll
原因:InjectedLib.dll依赖的EasyHook.dll版本与EasyLoad64.dll不匹配。例如InjectedLib.dll编译时引用EasyHook 2.7.6886,但运行时加载的是EasyHook 2.7.6800(旧版),导致EasyHook内部结构体偏移错乱。
解决:删除bin\Debug\下所有EasyHook*.dll,重新执行nuget restore VirtualFile.sln,确保packages\EasyHook.2.7.6886\lib\net40-client\下的 DLL 被复制到输出目录。
4.5 现象:32 位程序注入失败,报Could not load file or assembly 'EasyHook'
原因:EasyHook32.dll未放在VirtualFile.exe同目录,或InjectedLib32.dll编译目标平台为AnyCPU(会尝试加载 64 位EasyHook.dll)。
解决:
- 确保
EasyHook32.dll、EasyLoad32.dll、InjectedLib32.dll全部存在; - 在
InjectedLib.csproj中显式设置<PlatformTarget>x86</PlatformTarget>; - 用
corflags InjectedLib32.dll验证其32BITREQ标志为1。
5. 进阶技巧:用命名管道实现跨进程实时规则热更新
5.1 为什么需要热更新?—— 避免每次改映射都要重启目标进程
想象你在监控一个 24 小时不间断运行的工业采集程序,它每分钟读取C:\Virtual\Sensor\*.csv。某天你需要把数据源从D:\Real\切到E:\Backup\,传统做法是:停程序 → 改VirtualFile.exe启动参数 → 重启 → 等待采集恢复。而热更新允许你在VirtualFile.exe运行时,通过命令行或脚本发送新规则,InjectedLib.dll在内存中即时生效,目标进程无感知。
5.2 实现原理:命名管道(Named Pipe)作为宿主与注入体的通信信道
EasyHook 本身不提供进程间通信(IPC),但InjectedLib.dll运行在目标进程空间,可以自由调用 Win32 API。我们用CreateNamedPipeW在InjectedLib中创建服务端管道,VirtualFile.exe作为客户端连接并发送 JSON 规则:
// 在 InjectedLib.cs 的 EntryPoint 中启动管道监听线程 private static void StartRulePipeServer() { Task.Run(() => { while (true) { using (var pipe = new NamedPipeServerStream( "VirtualFileRulePipe", PipeDirection.In, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous)) { pipe.WaitForConnectionAsync().Wait(); using (var reader = new StreamReader(pipe)) { string json = reader.ReadToEnd(); var rule = JsonConvert.DeserializeObject<MappingRule>(json); VirtualFileSystem.UpdateRule(rule); // 更新内存中的映射表 } } } }); }5.3 客户端发送规则:一行 PowerShell 完成切换
# 发送新规则:把 C:\Virtual\* 映射到 E:\Backup\ $rule = @{ VirtualRoot = "C:\Virtual\"; RealRoot = "E:\Backup\" } | ConvertTo-Json $pipe = New-Object System.IO.Pipes.NamedPipeClientStream(".", "VirtualFileRulePipe", [System.IO.Pipes.PipeDirection]::Out) $pipe.Connect() $writer = New-Object System.IO.StreamWriter($pipe) $writer.Write($rule) $writer.Flush() $pipe.Close()提示:
InjectedLib.dll中的管道监听必须用Task.Run启动独立线程,不能阻塞EntryPoint,否则目标进程会卡死。同时,VirtualFileSystem.UpdateRule()需加锁(lock(_ruleLock))保证线程安全,因为CreateFileWHook是多线程并发调用的。
5.4 生产级加固:管道通信的超时与重连策略
真实环境中,VirtualFile.exe可能意外退出,导致管道断连。InjectedLib的监听线程不能因此崩溃。我们在StartRulePipeServer中加入健壮性处理:
while (true) { try { using (var pipe = new NamedPipeServerStream(...)) { await pipe.WaitForConnectionAsync().ConfigureAwait(false); // ... 处理消息 } } catch (IOException ex) when (ex.Message.Contains("The pipe has been ended")) { // 客户端主动断开,正常,继续下一轮监听 continue; } catch (Exception ex) { // 记录错误但不退出线程 LogError($"Pipe server error: {ex.Message}"); await Task.Delay(1000).ConfigureAwait(false); // 防止疯狂重连 } }从那以后我每次部署虚拟文件系统,都强制走一遍ProcMon抓包验证 +corflags检查位数 +PowerShell热更新测试三连。不是怕代码写错,而是怕 Windows 的路径解析、UAC 的完整性检查、EasyHook 的加载时机这些黑匣子,在你最不想翻车的时候,默默给你一个ERROR_ACCESS_DENIED当后悔药。希望帮到你。
本文还有配套的精品资源,点击获取