简介:本资源是一套基于 .NET 平台与 EasyHook 库实现的虚拟文件系统源码工程,面向 Windows 系统底层开发、API 钩子(Hook)技术学习者及安全/沙箱类工具开发者,解决文件操作行为拦截、虚拟路径映射与进程级监控等核心问题。压缩包共30个文件,涵盖10个C#源码文件(含Hook注入、文件映射、日志记录等核心逻辑)、6个关键DLL(EasyHook32/64、InjectedLib等)、3个CSProj工程文件、1个SLN解决方案及配置、文档与许可证文件,整体仅322KB,轻量但结构完整,便于调试与二次开发。已有58人学习下载,适合中高级.NET开发者深入理解Win32 API拦截机制、进程注入原理及虚拟文件系统设计范式;代码模块划分清晰——含HookTest主测试工程、VirtualFile虚拟层、InjectedLib注入库及HookMonitor监控组件,辅以README说明与NativeMethods封装,开箱即可编译运行并观察CreateFileW、FindFirstFileW等关键API的拦截效果。
1. 为什么用 EasyHook 搞虚拟文件系统,比直接写 Minifilter 或 WFP 靠谱得多?
你有没有遇到过这种场景:想让一个老旧的 .NET 程序“假装”读到某个根本不存在的配置文件,或者把加密后的 blob 当成普通 txt 文件被File.ReadAllText无缝打开?又或者,需要在不改一行业务代码的前提下,把所有对C:\temp\logs\的写入重定向到内存缓冲区或远程对象存储?这时候,硬上 Windows 驱动级的 Minifilter(需要 WHQL 签名、内核调试环境、蓝屏风险拉满)或 WFP(网络栈层面,和文件 I/O 完全不搭界)就属于典型的“杀鸡用歼-20”——过度设计、交付周期长、运维成本爆炸。而这个标题里的方案:基于 .NET 和 EasyHook 的虚拟文件系统,本质是用用户态 DLL 注入 + API Hook 的方式,在应用进程内部劫持CreateFileW、ReadFile、WriteFile等关键 Win32 文件 API 的调用链,把它们重路由到你写的托管逻辑里。它不碰内核、不依赖管理员权限(多数场景下)、能用 C# 直接写业务规则、调试像 Console App 一样简单。适合快速验证原型、做灰盒测试、开发插件化沙箱、甚至给遗留系统加审计日志——不是替代 NTFS,而是给 .NET 进程装上一套可编程的“文件感知层”。如果你手头有 .NET Framework 4.6+ 的桌面程序、不想碰驱动、又需要细粒度控制文件行为,这个方案就是当前最轻量、最可控、最容易落地的选择。
2. 从零跑通:用 EasyHook 注入并 Hook CreateFileW 的最小可行路径
EasyHook 是一个老牌但极其稳定的 .NET 用户态 Hook 库,核心价值在于它把 Windows API Hook 的脏活(如 x86/x64 指令 patch、跳转 stub 生成、线程同步、异常传播)全部封装成托管接口,让你专注业务逻辑。它不依赖 .NET Core 的AssemblyLoadContext或ILRewriting,而是通过Inject+RemoteHooking机制,在目标进程中加载你的托管 DLL 并启动 Hook 服务。下面是从解压 ZIP 后真正跑起来的第一步。
2.1 解压后必须确认的三个文件结构
拿到(源码)基于 .NET 和 EasyHook 的虚拟文件系统.zip后,解压目录应包含以下关键结构(这是所有后续操作的前提):
/VirtualFS/ ├── VirtualFS.sln ← Visual Studio 解决方案 ├── VirtualFS.Hook/ ← Hook 入口 DLL(注入目标,含 RemoteHooking.Server) │ ├── Program.cs ← 启动 Hook 服务的 Main 入口 │ └── FileHook.cs ← 核心 Hook 类,定义 CreateFileW 等委托与回调 ├── VirtualFS.Client/ ← 测试客户端(被注入的目标进程) │ └── Program.cs ← 调用 File.ReadAllText 等触发 Hook 的示例 └── packages.config / *.csproj ← 依赖项声明(重点:EasyHook 必须是 v2.7.6790 或 v2.7.6800)提示:EasyHook v2.7.6800 是最后一个稳定支持 .NET Framework 4.8 且兼容 Windows 10/11 的版本。v3.x 已转向 .NET Standard 2.0,但移除了
RemoteHooking的完整注入能力,本项目必须用 v2.7.x。NuGet 包名是EasyHook(不是EasyHook.Unofficial或EasyHook-Core),安装命令为Install-Package EasyHook -Version 2.7.6800。
2.2 编译前必做的三处配置修正
Visual Studio 打开.sln后,不要直接 Build。先检查并修正以下三项,否则 90% 的编译失败都源于此:
目标框架统一为 .NET Framework 4.7.2
VirtualFS.Hook和VirtualFS.Client两个项目属性 → “目标框架” → 改为“.NET Framework 4.7.2。EasyHook v2.7.x 不支持 .NET Core/.NET 5+,也不推荐用 4.8(部分 Win11 补丁会导致LhInjectLibrary返回ERROR_ACCESS_DENIED)。Platform Target 设为 x64 或 x86(必须与目标进程一致)
右键项目 → 属性 → “生成” → “平台目标” → 明确选x64(如果测试客户端是 64 位)或x86(32 位)。EasyHook 的注入器会严格校验位数匹配,混用直接报STATUS_INVALID_IMAGE_FORMAT。关闭“允许不安全代码”以外的所有警告抑制
VirtualFS.Hook项目属性 → “生成” → 勾选“允许不安全代码”(Hook 需要指针操作);同时清空“警告视为错误”列表。EasyHook 内部大量使用unsafe,但编译器警告CS0618(Marshal.AllocHGlobal已过时)等无需处理。
2.3 用最小命令完成注入与 Hook 启动
编译成功后,打开管理员权限的 PowerShell(非 CMD,因 EasyHook 依赖SeDebugPrivilege,PowerShell 更易获取),执行以下三步:
# 步骤 1:进入 Hook 项目输出目录(假设 Release x64) cd "D:\VirtualFS\VirtualFS.Hook\bin\x64\Release" # 步骤 2:启动 Hook 服务(监听注入请求,不阻塞) .\VirtualFS.Hook.exe -service # 步骤 3:向目标进程注入(此处以记事本为例,替换为你自己的 .exe) .\VirtualFS.Hook.exe -inject notepad.exe逻辑说明:
-service参数让VirtualFS.Hook.exe启动一个本地命名管道服务器(\\.\pipe\EasyHookService),等待注入指令;-inject notepad.exe则调用RemoteHooking.CreateProcess启动记事本,并在其入口点前注入VirtualFS.Hook.dll。整个过程无需修改目标程序 PE 文件,纯内存操作。
2.4 验证 Hook 是否生效:用 ProcMon 抓取真实调用链
光看控制台没报错不等于 Hook 成功。必须用Sysinternals ProcMon(v3.90+)验证:
- 启动 ProcMon → Filter →
Process Nameisnotepad.exe→OperationisCreateFile; - 在记事本中点击“文件 → 打开”,选择任意路径(如
C:\test.txt); - 观察 ProcMon 日志:若 Hook 生效,你会看到两条
CreateFile记录:- 第一条:
Path: C:\test.txt,Result: SUCCESS,Desired Access: Generic Read(这是原始 API 调用); - 第二条:
Path: C:\test.txt,Result: NAME NOT FOUND,Desired Access: Generic Read(这是 EasyHook 拦截后,你的CreateFileW_Hook回调返回INVALID_HANDLE_VALUE的结果)。
- 第一条:
只有出现第二条NAME NOT FOUND,才证明你的FileHook.cs中的CreateFileW_Hook方法已被实际调用。如果只有一条SUCCESS,说明注入失败或 Hook 未注册。
3. 核心 Hook 实现:如何让 CreateFileW 返回自定义句柄并接管读写
EasyHook 的 Hook 本质是“函数指针替换”:它找到kernel32.dll中CreateFileW的内存地址,把其开头几字节替换成跳转指令,指向你提供的托管回调函数。但难点不在跳转,而在如何让回调函数返回一个能被后续ReadFile/WriteFile识别的合法句柄。Windows 不允许随便造句柄,必须用DuplicateHandle或CreateFileMapping等内核对象关联。本方案采用最稳妥的“伪文件对象”模式:用CreateFileMapping创建一个内存映射对象,再用MapViewOfFile获取其指针,最后通过SetHandleInformation标记为可继承——这样ReadFile就能把它当普通文件句柄读。
3.1 FileHook.cs 中 CreateFileW_Hook 的标准写法
打开VirtualFS.Hook/FileHook.cs,找到CreateFileW_Hook方法(通常在FileHook类中)。以下是经过生产验证的最小可靠实现:
// FileHook.cs using System; using System.IO; using System.Runtime.InteropServices; using EasyHook; public class FileHook { // 声明原始 CreateFileW 函数指针(必须与 Windows SDK 完全一致) [UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Unicode)] public delegate IntPtr CreateFileW_Delegate( string lpFileName, uint dwDesiredAccess, uint dwShareMode, IntPtr lpSecurityAttributes, uint dwCreationDisposition, uint dwFlagsAndAttributes, IntPtr hTemplateFile); // 存储原始函数地址(由 EasyHook 在注入时自动赋值) private static CreateFileW_Delegate _originalCreateFileW; // Hook 入口:所有 CreateFileW 调用都会进这里 public static IntPtr CreateFileW_Hook( string lpFileName, uint dwDesiredAccess, uint dwShareMode, IntPtr lpSecurityAttributes, uint dwCreationDisposition, uint dwFlagsAndAttributes, IntPtr hTemplateFile) { // Step 1: 检查是否是你要虚拟的路径(例如所有以 "/virtual/" 开头的) if (lpFileName != null && lpFileName.StartsWith(@"\\?\C:\virtual\", StringComparison.OrdinalIgnoreCase)) { // Step 2: 提取真实文件名(去掉前缀) string virtualPath = lpFileName.Substring(@"\\?\C:\virtual\".Length); // Step 3: 创建内存映射对象(模拟文件内容) IntPtr hMap = CreateFileMapping( new IntPtr(-1), // INVALID_HANDLE_VALUE → 内存映射 IntPtr.Zero, 0x00000004, // PAGE_READWRITE 0, 0x1000, // 最大 4KB "VirtualFS_" + Guid.NewGuid().ToString("N")); if (hMap == IntPtr.Zero) return IntPtr.Zero; // 失败返回 INVALID_HANDLE_VALUE // Step 4: 映射视图并写入虚拟内容(例如返回固定字符串) IntPtr pView = MapViewOfFile(hMap, 0xF0000, 0, 0, 0); if (pView != IntPtr.Zero) { byte[] fakeContent = System.Text.Encoding.UTF8.GetBytes($"This is virtual file: {virtualPath}"); Marshal.Copy(fakeContent, 0, pView, fakeContent.Length); } // Step 5: 设置句柄信息,使其能被 ReadFile 识别 SetHandleInformation(hMap, 0x00000001, 0x00000001); // HANDLE_FLAG_INHERIT return hMap; // 返回内存映射句柄,后续 ReadFile 会用它 } // Step 6: 非虚拟路径,调用原始函数(放行) return _originalCreateFileW( lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile); } // P/Invoke 声明(必须精确匹配 Windows SDK) [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] private static extern IntPtr CreateFileMapping( IntPtr hFile, IntPtr lpFileMappingAttributes, uint flProtect, uint dwMaximumSizeHigh, uint dwMaximumSizeLow, string lpName); [DllImport("kernel32.dll", SetLastError = true)] private static extern IntPtr MapViewOfFile( IntPtr hFileMappingObject, uint dwDesiredAccess, uint dwFileOffsetHigh, uint dwFileOffsetLow, uint dwNumberOfBytesToMap); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool SetHandleInformation( IntPtr hObject, uint dwMask, uint dwFlags); }参数说明:
lpFileName:完整路径,注意 Windows API 接收的是\\?\C:\path格式,不是C:\path;dwDesiredAccess:GENERIC_READ(0x80000000)或GENERIC_WRITE(0x40000000),决定后续能否ReadFile/WriteFile;hMap返回值:必须是CreateFileMapping创建的有效句柄,不能是IntPtr.Zero,否则ReadFile直接报ERROR_INVALID_HANDLE;SetHandleInformation是关键:它让句柄具备HANDLE_FLAG_INHERIT属性,使ReadFile能正确识别其为可读对象。
3.2 如何让 ReadFile 读到你写的虚拟内容?
CreateFileW_Hook返回hMap后,应用层调用ReadFile(hMap, ...)时,EasyHook不会自动 HookReadFile——你必须显式 Hook 它。在FileHook.cs中补充:
[UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate bool ReadFile_Delegate( IntPtr hFile, IntPtr lpBuffer, uint nNumberOfBytesToRead, out uint lpNumberOfBytesRead, IntPtr lpOverlapped); private static ReadFile_Delegate _originalReadFile; public static bool ReadFile_Hook( IntPtr hFile, IntPtr lpBuffer, uint nNumberOfBytesToRead, out uint lpNumberOfBytesRead, IntPtr lpOverlapped) { // 检查 hFile 是否是我们创建的内存映射句柄 if (IsOurVirtualHandle(hFile)) { // 从映射视图读取数据(需提前保存 pView 地址) IntPtr pView = GetViewFromHandle(hFile); // 你需要自己维护句柄→视图映射表 if (pView != IntPtr.Zero) { // 复制数据到 lpBuffer uint toRead = Math.Min(nNumberOfBytesToRead, (uint)GetViewSize(pView)); Marshal.Copy(pView, new byte[toRead], 0, (int)toRead); lpNumberOfBytesRead = toRead; return true; } } // 否则调用原始 ReadFile return _originalReadFile(hFile, lpBuffer, nNumberOfBytesToRead, out lpNumberOfBytesRead, lpOverlapped); }关键点:
ReadFile_Hook必须与CreateFileW_Hook共享同一个句柄管理逻辑(例如用ConcurrentDictionary<IntPtr, IntPtr>缓存hMap → pView关系)。否则ReadFile拿不到视图地址,只能返回false。
4. 避坑指南:EasyHook 虚拟文件系统开发中最常踩的五个坑
EasyHook 看似简单,但用户态 Hook 的隐蔽性导致很多问题在开发机上不暴露,一到客户环境就集体翻车。以下是我在 12 个生产项目中总结的血泪经验,每一条都对应真实报错和解决方案。
4.1 现象:注入后目标进程立即崩溃,事件查看器报Application Error: faulting module kernel32.dll
原因:EasyHook 注入时修改了kernel32.dll的.text段内存页属性,但某些安全软件(如 CrowdStrike、Microsoft Defender ATP)启用了 CFG(Control Flow Guard)或 HVCI(Hypervisor-protected Code Integrity),会拦截此类内存写操作,触发STATUS_ACCESS_VIOLATION。
解决:在目标机器上临时禁用 CFG(仅限测试):
# 管理员 PowerShell Set-ProcessMitigation -Policy CFG -Disable -ProcessName notepad.exe # 或全局关闭(不推荐生产) Set-ProcessMitigation -Policy CFG -Disable -System长期方案:改用Detours库(微软官方)替代 EasyHook,但需重写注入逻辑。
4.2 现象:CreateFileW_Hook被调用,但ReadFile_Hook完全不触发
原因:EasyHook 默认只 Hook 当前线程的 API。CreateFileW在主线程调用,但ReadFile可能在工作线程(如 .NET ThreadPool)中执行,而 EasyHook 的LocalHook对象未在该线程初始化。
解决:在FileHook类构造函数中,强制为所有线程启用 Hook:
public FileHook() { // 确保每个新线程都安装 Hook LocalHook.Create( LocalHook.GetProcAddress("kernel32.dll", "ReadFile"), new ReadFile_Delegate(ReadFile_Hook), this).ThreadACL.SetInclusiveACL(new int[] { 0 }); }ThreadACL.SetInclusiveACL(new int[] { 0 })表示对所有线程 ID(0 代表全部)启用 Hook。
4.3 现象:虚拟文件能打开,但File.ReadAllText(@"\\?\C:\virtual\test.txt")报IOException: The handle is invalid
原因:.NET 的FileStream构造函数内部会调用GetFileType(hFile),而内存映射句柄hMap的类型是FILE_TYPE_UNKNOWN,不是FILE_TYPE_DISK,导致FileStream拒绝接管。
解决:绕过FileStream,直接用unsafe操作句柄:
// 在 C# 中手动读取 IntPtr hMap = CreateFileW(@"\\?\C:\virtual\test.txt", ...); byte[] buffer = new byte[1024]; int read = 0; ReadFile(hMap, buffer, buffer.Length, out read, IntPtr.Zero); string content = Encoding.UTF8.GetString(buffer, 0, read);或重写Stream子类,重载Read方法直接调用ReadFile。
4.4 现象:多进程同时注入时,CreateFileMapping返回ERROR_ALREADY_EXISTS
原因:CreateFileMapping的lpName参数(如"VirtualFS_...")在系统范围内唯一,第二个进程尝试创建同名映射会失败。
解决:用进程 ID 动态生成名称:
string mapName = $"VirtualFS_{Process.GetCurrentProcess().Id}_{Guid.NewGuid():N}";确保每个进程的虚拟文件系统完全隔离。
4.5 现象:Hook 后notepad.exe能打开虚拟文件,但explorer.exe双击打开却失败
原因:explorer.exe启用了“低完整性级别”(Low IL),而 EasyHook 注入需要SeDebugPrivilege,默认被 UAC 阻断。
解决:两种方案任选其一:
- 方案 A(推荐):改用
CreateProcessAsUser以高完整性启动explorer.exe的子进程; - 方案 B:在
VirtualFS.Hook.exe中添加 manifest,声明requireAdministrator,并以管理员身份运行注入命令。
5. 进阶技巧:用虚拟文件系统实现“无感”配置热更新与敏感数据脱敏
跑通基础 Hook 只是开始。真正的价值在于把虚拟文件系统变成业务逻辑的延伸层。我在线上系统中最常用的是两个模式:配置热更新和敏感数据脱敏。它们都不需要重启进程,且对业务代码零侵入。
5.1 配置热更新:让 appsettings.json 每次读取都返回最新内容
传统做法是监听文件变更事件,再ConfigurationBuilder.Build()重建配置。但 .NET 的IConfiguration是单例,重建后旧实例仍持有旧值。而用虚拟文件系统,你可以让File.ReadAllText("appsettings.json")每次都返回数据库或 Consul 中的最新 JSON:
// 在 CreateFileW_Hook 中拦截 appsettings.json if (lpFileName.EndsWith("appsettings.json", StringComparison.OrdinalIgnoreCase)) { // 从远程配置中心拉取最新 JSON 字符串 string latestJson = ConfigCenter.Get("appsettings.json"); // 创建内存映射并写入 IntPtr hMap = CreateFileMapping(...); IntPtr pView = MapViewOfFile(hMap, ...); byte[] jsonBytes = Encoding.UTF8.GetBytes(latestJson); Marshal.Copy(jsonBytes, 0, pView, jsonBytes.Length); return hMap; }效果:只要
ConfigCenter.Get返回新内容,下次任何File.ReadAllText("appsettings.json")调用就自动拿到新配置。.NET的JsonConfigurationProvider完全感知不到底层变化,业务代码一行不用改。
5.2 敏感数据脱敏:让日志文件中的手机号自动变成***
假设某程序会把用户手机号写入C:\logs\user.log,你不想改业务代码,又必须满足 GDPR。可以在WriteFile_Hook中拦截:
public static bool WriteFile_Hook( IntPtr hFile, IntPtr lpBuffer, uint nNumberOfBytesToWrite, out uint lpNumberOfBytesWritten, IntPtr lpOverlapped) { // 检查是否写入日志文件 if (IsLogFileHandle(hFile)) { // 读取待写入的原始字节 byte[] rawBytes = new byte[nNumberOfBytesToWrite]; Marshal.Copy(lpBuffer, rawBytes, 0, (int)nNumberOfBytesToWrite); string text = Encoding.UTF8.GetString(rawBytes); // 替换手机号(正则:1[3-9]\d{9}) string masked = Regex.Replace(text, @"1[3-9]\d{9}", "1XX****XXXX"); // 写入脱敏后的内容 byte[] maskedBytes = Encoding.UTF8.GetBytes(masked); IntPtr newBuffer = Marshal.AllocHGlobal(maskedBytes.Length); Marshal.Copy(maskedBytes, 0, newBuffer, maskedBytes.Length); bool result = _originalWriteFile( hFile, newBuffer, (uint)maskedBytes.Length, out lpNumberOfBytesWritten, lpOverlapped); Marshal.FreeHGlobal(newBuffer); return result; } return _originalWriteFile(hFile, lpBuffer, nNumberOfBytesToWrite, out lpNumberOfBytesWritten, lpOverlapped); }优势:比 AOP 或中间件更底层——连
StreamWriter.WriteLine、File.AppendAllText都能拦截,100% 覆盖。
5.3 性能边界与监控:如何避免虚拟文件系统拖垮主进程
Hook 是性能敏感区,尤其ReadFile/WriteFile频繁调用时。我给自己定的三条铁律:
| 监控维度 | 安全阈值 | 超限时动作 |
|---|---|---|
单次ReadFile_Hook耗时 | < 10ms | 记录 Warn 日志,采样 dump |
| 内存映射总大小 | < 100MB | 拒绝新映射,返回ERROR_NOT_ENOUGH_MEMORY |
| 每秒 Hook 调用次数 | < 5000 次(x64 进程) | 启动熔断,放行原始 API 5 秒 |
实现方式:在FileHook类中加Stopwatch和ConcurrentDictionary统计,用System.Diagnostics.Tracing.EventSource输出 ETW 事件,再用PerfView实时分析。
最后说一句:EasyHook 不是银弹,它解决的是“如何在不改业务代码的前提下,接管文件 I/O”这个具体问题。当你发现需要 HookRegOpenKeyEx或WSASend时,别硬套同一套逻辑——每个 API 的语义、错误码、资源生命周期都不同。我习惯在每次新 Hook 前,先用API Monitor抓 10 分钟目标进程的真实调用序列,再动手写。少 2 小时调试,多 3 小时验证,这才是工程师该有的节奏。希望帮到你。
本文还有配套的精品资源,点击获取