EasyHook实现.NET用户态虚拟文件系统

发布时间:2026/10/7 13:01:03
EasyHook实现.NET用户态虚拟文件系统 简介本资源是一套基于 .NET 平台与 EasyHook 库实现的虚拟文件系统源码工程面向 Windows 系统底层开发、API 钩子Hook技术学习者及安全/沙箱类工具开发者解决文件操作行为拦截、虚拟路径映射与进程级监控等核心问题。压缩包共30个文件涵盖10个C#源码文件含Hook注入、文件映射、日志记录等核心逻辑、6个关键DLLEasyHook32/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而是通过InjectRemoteHooking机制在目标进程中加载你的托管 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.2VirtualFS.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 位或x8632 位。EasyHook 的注入器会严格校验位数匹配混用直接报STATUS_INVALID_IMAGE_FORMAT。关闭“允许不安全代码”以外的所有警告抑制VirtualFS.Hook项目属性 → “生成” → 勾选“允许不安全代码”Hook 需要指针操作同时清空“警告视为错误”列表。EasyHook 内部大量使用unsafe但编译器警告CS0618Marshal.AllocHGlobal已过时等无需处理。2.3 用最小命令完成注入与 Hook 启动编译成功后打开管理员权限的 PowerShell非 CMD因 EasyHook 依赖SeDebugPrivilegePowerShell 更易获取执行以下三步# 步骤 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 ProcMonv3.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:\pathdwDesiredAccessGENERIC_READ0x80000000或GENERIC_WRITE0x40000000决定后续能否ReadFile/WriteFilehMap返回值必须是CreateFileMapping创建的有效句柄不能是IntPtr.Zero否则ReadFile直接报ERROR_INVALID_HANDLESetHandleInformation是关键它让句柄具备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共享同一个句柄管理逻辑例如用ConcurrentDictionaryIntPtr, 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启用了 CFGControl Flow Guard或 HVCIHypervisor-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类构造函数中强制为所有线程启用 Hookpublic 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 })表示对所有线程 ID0 代表全部启用 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 小时验证这才是工程师该有的节奏。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询