ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

流加密透明加解密文件过滤驱动的实现与随机读挑战

流加密透明加解密文件过滤驱动的实现与随机读挑战 简介面向Windows内核驱动开发与信息安全学习者的透明加解密过滤驱动示例聚焦文件系统底层的实时流加密。驱动通过拦截文件读写请求在用户无感知的情况下自动加密落盘数据、解密读取数据适合数据防泄漏、文档保护等应用场景也为研究IRP分发、过滤驱动框架提供了可运行模型。相比分组加密流加密逐字节处理数据更贴合动态长度文件与实时读写场景。压缩包共1个文件以C源代码为主大小仅42KB代码集中在单个文件中便于逐行梳理驱动加载、设备绑定、读写回调及加密逻辑的实现。已有332人学习下载。通过分析这段源码可掌握在驱动层挂接文件系统、管理密钥与缓冲区的思路理解流加密与文件过滤结合的工程细节并积累内核调试与异常处理的实战经验适合作为入门级参考。1. 只支持流加密的透明加解密过滤驱动到底卡在哪透明加解密文件过滤驱动往往被误以为难在加密算法上。一旦把 IRP 拦截、缓冲管理和上下文管理的代码写出来你会发现如果采用了流加密RC4、ChaCha 这类从文件头连续生成密钥流的模式真正的难点变成了如何应对应用程序的随机读。流加密在文件过滤驱动里的优势是密文长度不变写 IRP 不用改文件大小代价是无法像 AES-XTS 那样 O(1) 定位任意扇区。这个“闲暇时写的”项目选择了这种取舍那么围绕它积累的工程经验就更值得拆开说。接下来会从一个可编译的 Minifilter 骨架讲起到在 READ/WRITE 回调里加解密再到流加密的边界和验证适合写过一点内核驱动、此刻想快速搭出一套透明加解密验证环境的工程师。2. 文件过滤驱动骨架Minifilter 注册、上下文与 zip 工程布局2.1 拦截哪些 IRP 才对透明加解密有意义透明加解密“透明”的核心是应用程序无感知但驱动要在文件内容落到磁盘前改写在数据返回应用前还原。所以最基本的是IRP_MJ_WRITE和IRP_MJ_READ。只挂这两张表明显不够还要考虑句柄关闭、文件大小改变和内存映射。下表是我在实际项目里会注册的回调。回调作用是否必须IRP_MJ_CREATE打开文件流时初始化加密上下文必须IRP_MJ_WRITE写路径上对明文做加密必须IRP_MJ_READ读路径上把密文解密必须IRP_MJ_CLEANUP文件句柄关闭时释放流上下文必须IRP_MJ_SET_INFORMATION设置 EOF、文件大小等属性时同步流状态建议IRP_MJ_ACQUIRE_FOR_SECTION_SYNCHRONIZATION处理内存映射文件时避免双重加解密看支持范围如果要做视频播放器那种按 offset 随机读的透明加解密还得多处理 FastIO 和IRP_MJ_ACQUIRE_FOR_SECTION_SYNCHRONIZATION。但只支持流加密的话很多 FastIO 路径可以直接返回STATUS_NOT_SUPPORTED这反而是简化。2.2 文件上下文和流上下文流加密状态放在哪流加密要求加密状态比如密钥流位置随文件的打开实例走。常见做法是使用FltAllocateContext在IRP_MJ_CREATE里分配在IRP_MJ_CLEANUP里释放。注意 Create 回调即使上下文分配失败也不能拒绝打开文件否则会影响系统正常的文件操作。NTSTATUS OnCreate(PFLT_CALLBACK_DATA Data, PCFLT_RELATED_OBJECTS FltObjects) { PFS_CTX ctx NULL; NTSTATUS status FltAllocateContext( FltObjects-Filter, FLT_STREAM_CONTEXT, sizeof(FS_CTX), NonPagedPool, ctx); if (!NT_SUCCESS(status)) { return STATUS_SUCCESS; // 不阻止打开 } ctx-streamPos 0; ctx-fileSize 0; InitializeStreamCipher(ctx-key); status FltSetStreamContext( FltObjects-Instance, FltObjects-FileObject, FLT_SET_CONTEXT_KEEP_IF_EXISTS, ctx, NULL); if (status STATUS_FLT_CONTEXT_ALREADY_DEFINED) { FltReleaseContext(ctx); } return STATUS_SUCCESS; }FLT_STREAM_CONTEXT表示上下文绑定到文件流同一个文件被第二个进程打开时会有独立的流上下文。FLT_SET_CONTEXT_KEEP_IF_EXISTS防止重复插入返回STATUS_FLT_CONTEXT_ALREADY_DEFINED时直接释放新分配的内存即可。对于只支持流加密的驱动streamPos初始化为 0 意味着加密位置永远从文件头开始推进。2.3 zip 工程拿到手怎么编译这个项目以 zip 包分发解压后通常是.inf、.sys源码、.vcxproj工程。要编译需要本机装好对应版本的 WDK 和 SDK。常规构建命令如下msbuild filter.vcxproj /t:Build /p:ConfigurationRelease /p:Platformx64 /m/t:Build指定构建目标/p:ConfigurationRelease选择 Release 而不是 Debug因为内核驱动 Debug 构建默认会启用大量断言影响后面验证性能/p:Platformx64对应目标机器架构/m是并行编译。构建产物在x64/Release下包括.sys、.inf和.cat文件。如果 zip 包解压出来的.sys已经带签名那就没有必要重新编译直接进入加载步骤。2.4 加载驱动sc 命令与 fltmc驱动加载的常见做法是先用sc创建一个内核服务再启动它sc create filter type kernel binPath C:\drivers\filter.sys sc start filterbinPath不能带空格路径里不要用%SystemRoot%之类的环境变量。加载成功后用fltmc查看当前加载的最小过滤器fltmc如果sc start返回ERROR_FILE_NOT_FOUND或ERROR_BAD_IMG先检查路径是否正确再确认签名状态。测试环境下可以开启测试签名这部分在第 5 章单独讲。3. 透明加解密的核心在 READ/WRITE 回调里维持流状态3.1 写路径偏移必须连续因为加密状态从文件头开始向前推进所以写 IRP 的偏移必须等于当前已加密长度。如果应用跳到一个更靠后的偏移写入驱动无法直接把偏移置过去必须先花费代价把中间的密钥流“空跑”完。一个严格实现直接拒绝这种随机写NTSTATUS FilterWrite( PFLT_CALLBACK_DATA Data, PCFLT_RELATED_OBJECTS FltObjects, PVOID *Dummy) { PFS_CTX ctx GetStreamContext(FltObjects-FileObject); PFLT_IO_PARAMETER_BUFFER p Data-Iopb-Parameters.Write; LARGE_INTEGER off p-ByteOffset; if (off.QuadPart ! ctx-streamPos) { return STATUS_INVALID_DEVICE_REQUEST; } // 流加密原文和密文等长原地加密即可 EncryptBuffer(ctx, p-MdlAddress, p-WriteBuffer, p-Length); ctx-streamPos p-Length; return FltWriteFile( FltObjects-Instance, FltObjects-FileObject, p-Write.ByteOffset, p-Write.Length, p-Write.WriteBuffer, p-Write.MdlAddress, NULL, 0, NULL, 0); }p-Length是本次写入的字节数。如果应用写的是 4KB而当前流位置已经走到 4KB 之外那就先把这段明文和当前密钥流异或而不是从头生成新密钥流。所以顺序校验是保证状态一致的前提。另一个办法是遇到随机写时把中间空白区域补 0 后继续推进密钥流但会导致磁盘文件出现空洞得不偿失。3.2 读路径先跳过密钥流再解密读路径同样需要检查偏移。如果请求的是当前流位置直接解密如果请求偏移大于streamPos需要执行一个“跳过”操作把流状态推进到目标偏移但丢弃生成的密钥流。这个跳过成本会随跳越距离线性增长。NTSTATUS SkipStream(PFS_CTX ctx, ULONGLONG target) { ULONGLONG skipBytes target - ctx-streamPos; UCHAR dummy[4096]; while (skipBytes 0) { ULONG chunk (ULONG)(skipBytes sizeof(dummy) ? sizeof(dummy) : skipBytes); GenerateKeyStream(ctx, dummy, chunk); skipBytes - chunk; } ctx-streamPos target; return STATUS_SUCCESS; }这里GenerateKeyStream只推进流密码内部计数器或状态不修改文件缓冲区dummy里放什么不会影响之后的密文只要长度一致。如果应用请求偏移小于streamPos只能从文件头重新初始化密钥流再一路推进相当于把已经读过或写过的部分再算一遍。3.3 非顺序读的代价一个表格假设密钥流生成速度按 2GB/s 估算文件 256MB 时从 64MB 位置读到 128MB 位置大概只要几十毫秒但随机跳到文件尾部就能感觉到明显卡顿。下面的表格给出典型量级。文件大小seek 距离额外跳过耗时2GB/s 估算16MB8MB4ms512MB500MB250ms2GB2GB1s这还没算磁盘 I/O。这里的耗时只是把密钥流“空跑”的时间。如果流密码实现里每块还做哈希校验或者密码扩展成本更高。3.4 用通信端口下发密钥和路径透明加解密需要用户态程序设置要加密的进程或路径常见做法是FltCreateCommunicationPort建立私有端口用户态用FilterConnectCommunicationPort连接。内核侧创建端口的典型代码如下FltCreateCommunicationPort( gFilterHandle, gServerPort, gPortCookie, NULL, gSecurityDescriptor, gClientPort, ClientConnectCallback, ClientDisconnectCallback, 0);第三个参数gPortCookie防止端口被伪造gSecurityDescriptor控制用户态访问权限连接回调里可以校验进程 PID 或校验传入的密钥结构体。如果整个卷都加密会出现系统页面文件读写和崩溃转储都不可访问的问题所以通常只对特定目录和特定进程启用。zip 包里附带的用户态工具一般就是个命令行程序参数是路径和密钥内部把结构体序列化后发送给驱动。4. 流加密与文件过滤驱动的边界随机读、预读和 zip 调试陷阱4.1 为什么商用透明加解密驱动更偏爱 XTS商用文件加密驱动通常选用 AES-XTS因为 XTS 按 16 字节分块用数据所在偏移驱动 Tweak 生成子密钥加密任意扇区不依赖其他扇区。这样随机读只需要计算目标扇区的 Tweak复杂度是 O(1)。流加密则不同它把整个文件当作一条连续数据流密钥流从偏移 0 开始延展无法只凭目标偏移直接定位。也有一个变通用 CTR 模式让密钥流按偏移生成每个偏移处使用唯一计数器加密这种模式也能随机访问。但 CTR 本质上已经是可随机寻址的块加密不是单纯的“流推进”式实现。标题里说“只支持流加密”意味着没有 Counter 定位或者实现上根本没在偏移和密钥流之间建立可映射关系。因此这里把讨论范围限定为“连续流推进”的实现。4.2 三个缓解手段预读、全量缓存、顺序打开既然随机读是理论禁区实际工程里仍能靠上层约束压住。第一个是强制应用带FILE_FLAG_SEQUENTIAL_SCAN打开让缓存管理器按顺序预读避免显式 seek第二个是小于设定阈值的文件在打开时直接全量解密到内存缓存之后的随机读全部命中缓存第三个是在驱动内做密钥流的惰性推进每次 read/write 后记录当前位置当请求位置离当前位置不超过一个窗口例如 256KB时顺便把这段密钥流算好备用。如果请求位置小于当前流位置只能从文件头重新生成密钥流。这是只支持流加密最致命的特征回退重算的成本不可控。所以标题里“闲暇时写的”这个定位很准确它适合特定业务而不是通用加密文件系统。4.3 用一段 Python 脚本模拟跳过成本下面的脚本在用户态模拟“每次随机读都重新从头生成密钥流”的最坏情况。它先把文件顺序读取一遍再以随机顺序读取同一批 4KB 块对比耗时。import os import time import secrets import random path sample.bin size 512 * 1024 * 1024 with open(path, wb) as f: f.write(secrets.token_bytes(size)) def read_pattern(pattern): with open(path, rb) as f: t time.perf_counter() for pos in pattern: f.seek(pos) f.read(4096) return time.perf_counter() - t block_count size // 4096 seq [4096 * i for i in range(block_count)] rand seq[:] random.shuffle(rand) print(sequential:, read_pattern(seq)) print(random:, read_pattern(rand))这个脚本模拟的只是 seek 开销还没有真正模拟“从文件头推进密钥流”。但足够说明文件越大随机访问越慢的趋势。测试之前先确认磁盘剩余空间超过 512MB。这个结果也可以作为驱动性能调优的基线参考。4.4 zip 解压后别忘去掉 Mark of the Web很多从网上下载的 zip 包解压后会带上 Mark-of-the-WebWindows 会把解压出的文件都标记为来自互联网。对驱动项目来说这会导致编译出来的.sys在加载时被 SmartScreen 拦截也会让.vcxproj打开时提示“此项目已损坏”。去掉标记不用重新解压一行 PowerShell 就能处理Get-ChildItem -Path . -Recurse -File | Unblock-FileUnblock-File移除 Zone.Identifier 扩展属性不修改文件内容。如果这道工序不做后面所有签名和加载步骤都可能被 Windows 拒在门外而且报错信息并不直观。5. 验证调试用自签名证书和 WinDbg 检查流加密的顺位5.1 测试签名加载驱动内核驱动必须签名。临时验证最简单的方法是开启测试签名模式然后用自签名证书给.sys签名bcdedit /set testsigning on重启后生成证书并签名makecert -r -pe -ss My -n CNMyTestCert MyTestCert.cer signtool sign /v /s My /n MyTestCert /t http://timestamp.digicert.com filter.sys/s My指定从当前用户的证书存储选择证书/n指定证书名称/t提供时间戳服务器地址。签名完用fltmc load filter加载。如果目标机器承载重要数据建议在虚拟机上做这一步。5.2 用 PowerShell 验证流加密的顺序约束驱动加载后最简单的验证方式是让用户态连续两次读取不同偏移观察驱动是否返回错误。下面脚本直接打开文件后顺序读一次再跳到一个远处的偏移读测试驱动会不会拦截$fs [System.IO.File]::Open(C:\work\secret.txt, [System.IO.FileMode]::Open) $buf New-Object byte[] 4096 $fs.Seek(0, [System.IO.SeekOrigin]::Begin) | Out-Null $fs.Read($buf, 0, 4096) | Out-Null $fs.Seek(8192, [System.IO.SeekOrigin]::Begin) | Out-Null try { $fs.Read($buf, 0, 4096) | Out-Null random read ok } catch { expected error: $_ }如果第二次读取抛出STATUS_INVALID_DEVICE_REQUEST或STATUS_ACCESS_DENIED说明驱动正确阻止了随机读如果想允许这种访问就需要按 4.2 的方案补全量缓存或 Counter 定位。这个脚本很适合写进自动化验证用例里。5.3 WinDbg 里监控流推进位置最后给一个定位性能问题的技巧在流上下文中增加一个全局计数器g_maxStreamPos每次SkipStream或顺序推进后更新。在内核调试器里执行dt g_maxStreamPos如果这个数值在随机读测试中反复下降或抖动说明驱动每次 seek 都重新从文件头推进密钥流性能瓶颈就锁定在这里。只要这个计数器不是一次性增长到文件末尾就该回到 4.2 的缓存方案去优化。本文还有配套的精品资源点击获取
返回列表