本帖最后由 我还是个孩子丶 于 2024-10-15 18:25 编辑
0.前言
最近在学习Windows内核编程,想要实现一个透明加解密的驱动。故对微软提供的SwapBuffer示例程序进行了分析,在此作为笔记。为了方便观看,大部分的解释我都放到了源代码的注释当中,并且我删除了一些功能无关的代码。
1.功能
SwapBuffers过滤器在读/写或目录控制操作之前引入了一个新的缓冲区,以后所有的操作都在自己定义的缓冲区上进行;直到所有操作完成后,才会将自己缓冲区的内容复制回系统提供的缓冲区。
2.结构体声明
实例中定义两种结构体,一是和卷设备相关信息保存的结构体、二是用于前操作向后操作传递参数的结构体:
[C] 纯文本查看 复制代码 //卷设备上下文
typedef struct _VOLUME_CONTEXT {
//要显示的名称
UNICODE_STRING Name;
//该卷的扇区大小。
ULONG SectorSize;
} VOLUME_CONTEXT, *PVOLUME_CONTEXT;
//前操作向后操作传递的上下文
typedef struct _PRE_2_POST_CONTEXT {
// 卷的上下文
PVOLUME_CONTEXT VolCtx;
// 新的缓冲区指针 方便传递给后操作函数
PVOID SwappedBuffer;
} PRE_2_POST_CONTEXT, *PPRE_2_POST_CONTEXT;
3初始化
初始化主要有两动作:1.初始化一个旁氏列表用于内存分配(相当于内存池) 2.将过滤器注册并启动
[C] 纯文本查看 复制代码 NTSTATUS
DriverEntry(
_In_ PDRIVER_OBJECT DriverObject,
_In_ PUNICODE_STRING RegistryPath
)
{
NTSTATUS status;
//使用 NonPagedPoolNx 进行非分页池分配(在支持的情况下)。
ExInitializeDriverRuntime(DrvRtPoolNxOptIn);
//获取调试跟踪标志
//从注册表获取参数 这里可以忽略 他只设置了一个无关紧要的打印输出的参数
ReadDriverParameters(RegistryPath);
//初始化旁视列表,用于分配上下文结构,以便在预操作回调和后操作回调之间传递信息。
//ExInitializeNPagedLookasideList 例程初始化指定大小的非分页条目的旁视列表
//Pre2PostContextList是一个NPAGED_LOOKASIDE_LIST类型的全局变量 即一个旁氏列表结构体(分配内存用的)
ExInitializeNPagedLookasideList(&Pre2PostContextList,
NULL,
NULL,
0,
sizeof(PRE_2_POST_CONTEXT),
PRE_2_POST_TAG,
0);
//注册过滤器
status = FltRegisterFilter(DriverObject,
&FilterRegistration,
&gFilterHandle);
if (!NT_SUCCESS(status)) {
goto SwapDriverEntryExit;
}
//开启过滤器
status = FltStartFiltering(gFilterHandle);
if (!NT_SUCCESS(status)) {
FltUnregisterFilter(gFilterHandle);
goto SwapDriverEntryExit;
}
SwapDriverEntryExit:
if (!NT_SUCCESS(status)) {
//启动失败 删除旁视列表
ExDeleteNPagedLookasideList(&Pre2PostContextList);
}
return status;
}
4 读操作的处理
读操作是指将磁盘中的数据读到内存中,作为一个过滤驱动,在收到读请求的时候我们并不知道读取的真实数据是什么;因此需要再读请求中做一些读之前(前操作回调)的处理然后将请求放行,待到底层驱动将数据获取完成后再进行处理(后操作回调);并且,因为后操作是在DPC中断等级下执行的对于那些不适合在DPC等级下处理的任务,需要设置一个额外的处理函数(后操作安全回调)待到中断等级下降时在进行处理。
[C] 纯文本查看 复制代码 FLT_PREOP_CALLBACK_STATUS
SwapPreReadBuffers(
_Inout_ PFLT_CALLBACK_DATA Data,
_In_ PCFLT_RELATED_OBJECTS FltObjects,
_Flt_CompletionContext_Outptr_ PVOID *CompletionContext
)
{
PFLT_IO_PARAMETER_BLOCK iopb = Data->Iopb;//请求相关的操作
//返回值 NO_CALLBACK意味着不需要后操作处理(即初始化时默认处理失败)
FLT_PREOP_CALLBACK_STATUS retValue = FLT_PREOP_SUCCESS_NO_CALLBACK;
PVOID newBuf = NULL; //新的缓冲器(即自己申请的)
PMDL newMdl = NULL; //缓冲区对应的内存映射地址
PVOLUME_CONTEXT volCtx = NULL; //上文定义的卷上下文指针
PPRE_2_POST_CONTEXT p2pCtx; //上文定义的前后操做上下文结构
NTSTATUS status; //返回状态
ULONG readLen = iopb->Parameters.Read.Length; //读取的长度
try {
// 读取长度为0时不需要任何操作 也不需要后操作
if (readLen == 0) { leave; }
//获取当前卷设备的相关信息
status = FltGetVolumeContext(FltObjects->Filter,
FltObjects->Volume,
&volCtx);
if (!NT_SUCCESS(status)) { leave; }
// 对于非缓存操作,因为要在磁盘上直接读取,因此需要将读取长度和磁盘扇区大小对齐
if (FlagOn(IRP_NOCACHE, iopb->IrpFlags)) {
readLen = (ULONG)ROUND_TO_SIZE(readLen, volCtx->SectorSize);
}
//获取一段对齐的缓冲区,其实只有在非缓冲读时才需要这么申请
//这里为了方便,所有读操作都这么申请
newBuf = FltAllocatePoolAlignedWithTag(FltObjects->Instance,
NonPagedPool,
(SIZE_T)readLen,
BUFFER_SWAP_TAG);
if (newBuf == NULL) { leave; }
//只有普通的IRP才会需要MDL这个参数 FASTIO不需要这个参数
if (FlagOn(Data->Flags, FLTFL_CALLBACK_DATA_IRP_OPERATION)) {
//if (!FlagOn(Data->Flags, FLTFL_CALLBACK_DATA_FAST_IO_OPERATION)) { //这样会不会好一些?
// 为新分配的内存(虚拟地址)分配一个内存描述符(MDL)。
newMdl = IoAllocateMdl(newBuf,
readLen,
FALSE,
FALSE,
NULL);
if (newMdl == NULL) { leave; }
// setup the MDL for the non-paged pool we just allocated
//设置我们刚刚分配的非分页池的MDL。
//更新MDL的物理映射
MmBuildMdlForNonPagedPool(newMdl);
}
// 从旁氏列表获取内存 用于保存前操作向后操作传递的上下文
p2pCtx = ExAllocateFromNPagedLookasideList(&Pre2PostContextList);
if (p2pCtx == NULL) { leave; }
// 更新缓冲区指针和MDL地址,标记我们已经更改了某些内容。
iopb->Parameters.Read.ReadBuffer = newBuf;
iopb->Parameters.Read.MdlAddress = newMdl;
// 通知操作系统数据被修改 必须通知
FltSetCallbackDataDirty(Data);
//
// Pass state to our post-operation callback.
// 将状态传递给我们的后操作回调。
// Mdl不用传 因为MDL就是映射到缓存上的
p2pCtx->SwappedBuffer = newBuf;
p2pCtx->VolCtx = volCtx;
*CompletionContext = p2pCtx;
retValue = FLT_PREOP_SUCCESS_WITH_CALLBACK;
}
finally{
// 不需要后操作说明处理失败 进行清理工作
if (retValue != FLT_PREOP_SUCCESS_WITH_CALLBACK) {
if (newBuf != NULL) {
FltFreePoolAlignedWithTag(FltObjects->Instance,
newBuf,
BUFFER_SWAP_TAG);
}
if (newMdl != NULL) {
IoFreeMdl(newMdl);
}
if (volCtx != NULL) {
FltReleaseContext(volCtx);
}
}
}
return retValue;
}
FLT_POSTOP_CALLBACK_STATUS
SwapPostReadBuffers(
_Inout_ PFLT_CALLBACK_DATA Data,
_In_ PCFLT_RELATED_OBJECTS FltObjects,
_In_ PVOID CompletionContext,
_In_ FLT_POST_OPERATION_FLAGS Flags
)
{
PVOID origBuf; //原始缓冲区
PFLT_IO_PARAMETER_BLOCK iopb = Data->Iopb;
FLT_POSTOP_CALLBACK_STATUS retValue = FLT_POSTOP_FINISHED_PROCESSING;
PPRE_2_POST_CONTEXT p2pCtx = CompletionContext;//前操作传过来的上下文
BOOLEAN cleanupAllocatedBuffer = TRUE; //清理工作标志
// 在读写请求中不应该存在分离过滤器的操作
FLT_ASSERT(!FlagOn(Flags, FLTFL_POST_OPERATION_DRAINING));
try {
//如果操作失败或计数为零,则没有数据可供处理
if (!NT_SUCCESS(Data->IoStatus.Status) ||
(Data->IoStatus.Information == 0)) {
leave;
}
// 注意 过滤管理器再调用后操作时会恢复参数
// 所以这里指向的还是原始的缓冲区
if (iopb->Parameters.Read.MdlAddress != NULL) {
// 这应该是一个简单的MDL,而不应该是链式MDL
FLT_ASSERT(((PMDL)iopb->Parameters.Read.MdlAddress)->Next == NULL);
// 获取地址 以便我们可以将数据复制回去
origBuf = MmGetSystemAddressForMdlSafe(iopb->Parameters.Read.MdlAddress,
NormalPagePriority | MdlMappingNoExecute);
if (origBuf == NULL) {
//获取失败返回
Data->IoStatus.Status = STATUS_INSUFFICIENT_RESOURCES;
Data->IoStatus.Information = 0;
leave;
}
}
else if (FlagOn(Data->Flags, FLTFL_CALLBACK_DATA_SYSTEM_BUFFER) ||
FlagOn(Data->Flags, FLTFL_CALLBACK_DATA_FAST_IO_OPERATION)) {
//0环内存是有效的和FASTIO证明我们再当前进程上下问中
//所以这两个情况 ReadBuffer是有效的
origBuf = iopb->Parameters.Read.ReadBuffer;
}
else {
//其他情况无法再DPC中断等级处理 设置安全后操作函数
if (FltDoCompletionProcessingWhenSafe(Data,
FltObjects,
CompletionContext,
Flags,
SwapPostReadBuffersWhenSafe,
&retValue)) {
// 安全后操作还需要使用 故不能清理
cleanupAllocatedBuffer = FALSE;
}
else {
// 没法调用安全后操作 返回失败
Data->IoStatus.Status = STATUS_UNSUCCESSFUL;
Data->IoStatus.Information = 0;
}
leave;
}
// 复制操作
try {
RtlCopyMemory(origBuf,
p2pCtx->SwappedBuffer,
Data->IoStatus.Information);
} except(EXCEPTION_EXECUTE_HANDLER) {
// 复制失败处理
Data->IoStatus.Status = GetExceptionCode();
Data->IoStatus.Information = 0;
}
}
finally{
//清理工作 cleanupAllocatedBuffer==TRUE时
if (cleanupAllocatedBuffer) {
FltFreePoolAlignedWithTag(FltObjects->Instance,
p2pCtx->SwappedBuffer,
BUFFER_SWAP_TAG);
FltReleaseContext(p2pCtx->VolCtx);
ExFreeToNPagedLookasideList(&Pre2PostContextList,
p2pCtx);
}
}
return retValue;
}
//是一个用户缓冲区
//需要再低中断等级互斥访问缓冲区并复制数据
FLT_POSTOP_CALLBACK_STATUS
SwapPostReadBuffersWhenSafe(
_Inout_ PFLT_CALLBACK_DATA Data,
_In_ PCFLT_RELATED_OBJECTS FltObjects,
_In_ PVOID CompletionContext,
_In_ FLT_POST_OPERATION_FLAGS Flags
)
{
PFLT_IO_PARAMETER_BLOCK iopb = Data->Iopb;
PPRE_2_POST_CONTEXT p2pCtx = CompletionContext;
PVOID origBuf;
NTSTATUS status;
UNREFERENCED_PARAMETER(FltObjects);
UNREFERENCED_PARAMETER(Flags);
FLT_ASSERT(Data->IoStatus.Information != 0);
// 锁定用户缓冲区
status = FltLockUserBuffer(Data);
if (!NT_SUCCESS(status)) {
Data->IoStatus.Status = status;
Data->IoStatus.Information = 0;
}
else {
origBuf = MmGetSystemAddressForMdlSafe(iopb->Parameters.Read.MdlAddress,
NormalPagePriority | MdlMappingNoExecute);
if (origBuf == NULL) {
Data->IoStatus.Status = STATUS_INSUFFICIENT_RESOURCES;
Data->IoStatus.Information = 0;
}
else {
RtlCopyMemory(origBuf,
p2pCtx->SwappedBuffer,
Data->IoStatus.Information);
}
}
//清理工作
FltFreePoolAlignedWithTag(FltObjects->Instance,
p2pCtx->SwappedBuffer,
BUFFER_SWAP_TAG);
FltReleaseContext(p2pCtx->VolCtx);
ExFreeToNPagedLookasideList(&Pre2PostContextList,
p2pCtx);
return FLT_POSTOP_FINISHED_PROCESSING;
}
流程总结:对于读的处理 就是在前操作中申请了一块缓冲区用来替换原始的缓冲区,这样在将请求放行以后,底层获取的数据就会传入我们申请的缓冲区当中。然后,再后操作中就可以对我们申请的缓冲区做处理,在将处理完的数据复制到原始的缓冲区中显示给上层调用者。
其他处理:对于另外两个操作,处理方式基本相同且比读操作要简单,故不在此重复。
微软官方代码示例链接:https://github.com/microsoft/Windows-driver-samples/tree/main/filesys/miniFilter/swapBuffers
疑问:为什么不能直接在原始缓存上进行处理?我之前试过在后操作中对原始缓存直接进行修改,但不知道为什么确实没有成功。但示例中的RtlCopyMemory同样需要对原始缓冲操作,为什么他的就能成功。。。
后话:然而,光知道SwapBuffer是无法实现文件透明加密的。。因为Windows的缓存机制,还需要对文件缓存实施动态的加密和解密(所有程序在打开文件时公用一个全局的文件缓存,存放与FCB中)。目前了解到两种办法:1.对缓存进行清空并重新获取。2.双缓存机制。
缓存清空了解到两种办法:1.自己调用CcFlushCache、MmFlushIMAGESection和CcPurgeCacheSection进行清空
2.触发文件写进行清空(文件写会稳定的触发文件缓存的清空)(这种方法吾爱大佬已有给出例子的)
对于缓存处理的方法,后续将会继续学习并实现。
小白第一次写帖子,欢迎各位大佬指正。
|