文章总结: 本文介绍了一个基于Minifilter框架实现的目录保护软件,通过驱动层拦截文件操作并通知用户层决策。软件可动态配置保护目录,拦截创建、删除等文件操作,用户可选择允许或拒绝。文章详细说明了驱动层和应用层的实现原理,包括消息传递机制、请求处理流程和超时机制。该软件为Windows系统提供了灵活的目录保护方案,适用于需要控制特定目录访问的场景。 综合评分: 88 文章分类: 应用安全,安全工具,数据安全,安全建设,终端安全

基于Minifilter实现目录保护软件,自定义保护目录,用户可选择是否允许文件行为
0346954
看雪学苑
2025年10月20日 18:00 上海
使用Minifilter使用一个文件目录保护的驱动,用户层自定义保护目录,收到特定目录下的文件行为时,驱动会通知用户程序,并支持根据用户决策放行或者拒绝文件行为。
程序分为驱动层和应用层
整体流程:
驱动层使用Minifilter框架, 可以接收用户层传递(使用FilterSendMessage函数)的目录信息,并且设置了IRP_MJ_CREATE的前回调函数,在回调函数中判断访问的目录或者文件是否在保护目录下,如果是写文件目录或者删除文件目录的情况,根据create_disposition和create_options和mask标志来判断,并且可以获取到进程名称,那么就挂起这个请求,将请求信息插入到全局的请求链表中,并唤醒自己创建的系统线程,系统线程负责使用FltSendMessage(有超时机制,略大于60s)将请求发往用户层,用户层使用FilterGetMessage函数(使用event )来驱动发过来的获取请求,用户程序根据信息展示一个GUI界面,可以选择允许或者拒绝,存在60s的超时机制,超时则回复驱动为拒绝请求,如果允许,驱动收到请求后就完成之前的请求,驱动完成当前请求后会继续获取链表是否存在请求,如果没有请求,那么等待一个事件,precreate回调函数插入信息到链表后会触发这个event,并且在驱动Unload的函数中也会触发这个event。
用户层启动后,根据命令行参数转换DOS路径(如C:\123)为NT路径(\Device\HarddiskVolume4\123),因为在驱动中precreate的回调函数中获得的文件名是NT格式的,用户层使用FilterSendMessage将 1个或者多个路径信息传递到驱动后,会发送一个开始保护的消息,此时precreate才开始工作。
用户层退出时会触发驱动层的DisConnect函数,此时驱动会取消保护,删除目录全局链表中的信息,并且完成已经保存到请求全局链表中的请求(拒绝访问该请求)。
驱动层接收用户层的FilterSendMessage的代码如下:
NTSTATUS
DirProtectMiniMessage(
__in PVOID ConnectionCookie,
__in_bcount_opt(InputBufferSize) PVOID InputBuffer,
__in ULONG InputBufferSize,
__out_bcount_part_opt(OutputBufferSize,*ReturnOutputBufferLength) PVOID OutputBuffer,
__in ULONG OutputBufferSize,
__out PULONG ReturnOutputBufferLength
)
{
PAGED_CODE();
UNREFERENCED_PARAMETER(InputBuffer);
UNREFERENCED_PARAMETER(InputBufferSize);
UNREFERENCED_PARAMETER(ReturnOutputBufferLength);
UNREFERENCED_PARAMETER( ConnectionCookie );
UNREFERENCED_PARAMETER( OutputBufferSize );
UNREFERENCED_PARAMETER( OutputBuffer );
COMMAND_HEAD* head = (COMMAND_HEAD*)InputBuffer;
WCHAR* dir_dos = NULL;
WCHAR* dir_nt = NULL;
NTSTATUS status = STATUS_INVALID_PARAMETER;
DIR_INFO* info = NULL;
do
{
if (NULL == head)
{
break;
}
__try
{
if (ENUM_DIRINFO == head->command_type)
{
if (InputBufferSize == sizeof(COMMAND_MESSAGE_DIR))
{
COMMAND_MESSAGE_DIR* dir = (COMMAND_MESSAGE_DIR*)InputBuffer;
if (NULL == dir || L'\0' == dir->protectdir_dos[0] || L'\0' == dir->protectdir_dos[0])
{
break;
}
size_t dos_len = wcslen(dir->protectdir_dos) * sizeof(WCHAR);
size_t nt_len = wcslen(dir->protectdir_nt) * sizeof(WCHAR);
if (dos_len >= sizeof(dir->protectdir_dos) || nt_len >= sizeof(dir->protectdir_nt))
{
status = STATUS_INVALID_PARAMETER;
break;
}
dir_dos = ExAllocatePoolWithTag(NonPagedPool, dos_len, DIR_PROTECT_POOL_TAG);
if (NULL != dir_dos)
{
dir_nt = ExAllocatePoolWithTag(NonPagedPool, nt_len, DIR_PROTECT_POOL_TAG);
}
if (NULL != dir_dos && NULL != dir_nt)
{
info = ExAllocatePoolWithTag(NonPagedPool, sizeof(DIR_INFO), DIR_PROTECT_POOL_TAG);
if (NULL != info)
{
memcpy(dir_dos, dir->protectdir_dos, dos_len);
info->dir_dos.Buffer = dir_dos;
info->dir_dos.Length = info->dir_dos.MaximumLength = (USHORT)dos_len;
memcpy(dir_nt, dir->protectdir_nt, nt_len);
info->dir_nt.Buffer = dir_nt;
info->dir_nt.Length = info->dir_nt.MaximumLength = (USHORT)nt_len;
if (TRUE == ExAcquireResourceExclusiveLite(&global.dir_lock, TRUE))
{
InsertTailList(&global.head_dir, &info->list);
++global.dir_count;
ExReleaseResourceLite(&global.dir_lock);
status = STATUS_SUCCESS;
}
}
else
{
status = STATUS_INSUFFICIENT_RESOURCES;
}
}
else
{
status = STATUS_INSUFFICIENT_RESOURCES;
}
}
}
else if (ENUM_START_PROTECT == head->command_type)
{
global.need_protect = TRUE;
status = STATUS_SUCCESS;
}
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
status = STATUS_INVALID_PARAMETER;
}
} while (0);
if (!NT_SUCCESS(status))
{
if (NULL != dir_dos)
{
ExFreePool(dir_dos);
dir_dos = NULL;
}
if (NULL != dir_nt)
{
ExFreePool(dir_nt);
dir_nt = NULL;
}
if (NULL != info)
{
ExFreePool(info);
info = NULL;
}
}
return status;
}
1.先判断命令类型,如果是目录信息,那么判断传递的目录是否不超过固定的缓冲区(用异常处理框架包裹处理代码),如果路径合法,那么获取独占获取共享锁global.dir_lock,接着插入到链表global.head_dir中。
2.如果是开始保护的命令,那么就设置global.need_protect标志为true。
precreate的回调函数
FLT_PREOP_CALLBACK_STATUS
DirProtectPreCreate(
__inout PFLT_CALLBACK_DATA Data,
__in PCFLT_RELATED_OBJECTS FltObjects,
__deref_out_opt PVOID* CompletionContext
)
{
NTSTATUS status;
PFLT_FILE_NAME_INFORMATION name_info = NULL;
FLT_PREOP_CALLBACK_STATUS call_status = FLT_PREOP_SUCCESS_NO_CALLBACK;
UNREFERENCED_PARAMETER(FltObjects);
UNREFERENCED_PARAMETER(CompletionContext);
UNREFERENCED_PARAMETER(Data);
PAGED_CODE();
if (KernelMode == Data->RequestorMode)
{
return call_status;
}
if (FALSE == global.need_protect)
{
return call_status;
}
if (FALSE == FLT_IS_IRP_OPERATION(Data))
{
return call_status;
}
ULONG pid = FltGetRequestorProcessId(Data);
if (pid == global.client_pid)
{
return call_status;
}
void* nbuf = NULL;
ULONG nlen = 0;
WCHAR* nptr = NULL;
MINI_REQUEST* mini_request = NULL;
SYS_2_USER* sys_2_user = NULL;
ULONG create_disposition = (Data->Iopb->Parameters.Create.Options >> 24) & 0xFF; // high 8 bit
ULONG create_options = Data->Iopb->Parameters.Create.Options & 0x00FFFFFF; // low 24 bit
BOOLEAN is_create_operation = (create_disposition == FILE_CREATE) || (create_disposition == FILE_OPEN_IF) || (create_disposition == FILE_OVERWRITE_IF);
ACCESS_MASK mask = Data->Iopb->Parameters.Create.SecurityContext->DesiredAccess;
ASK_REASON reason = REASON_NO_REASON;
if (is_create_operation)
{
if (create_options & FILE_DIRECTORY_FILE)
{
if (mask & DELETE)
reason = REASON_DELETE_DIR;
else
reason = REASON_CREATE_DIR;
}
else
{
if (mask & DELETE)
reason = REASON_DELETE_FILE;
else
reason = REASON_CREATE_FILE;
}
}
if (REASON_NO_REASON == reason)
{
if (create_options & FILE_DELETE_ON_CLOSE)
{
if (create_options & FILE_NON_DIRECTORY_FILE)
reason = REASON_DELETE_FILE;
else
reason = REASON_DELETE_DIR;
}
}
if (REASON_NO_REASON == reason)
{
if (mask & DELETE)
{
if (create_options & FILE_DIRECTORY_FILE)
reason = REASON_DELETE_DIR;
else
reason = REASON_DELETE_FILE;
}
}
status = FltGetFileNameInformation(Data,
FLT_FILE_NAME_NORMALIZED |
FLT_FILE_NAME_QUERY_DEFAULT,
&name_info);
if (REASON_NO_REASON != reason && NT_SUCCESS(status))
{
if (TRUE == ExAcquireResourceSharedLite(&global.dir_lock, TRUE))
{
BOOLEAN found = FALSE;
__try
{
PLIST_ENTRY entry = global.head_dir.Flink;
while (entry != &global.head_dir)
{
DIR_INFO* dir_info = CONTAINING_RECORD(entry, DIR_INFO, list);
if (dir_info->dir_nt.Length <= name_info->Name.Length && TRUE == RtlPrefixUnicodeString(&dir_info->dir_nt, &name_info->Name, TRUE))
{
BOOLEAN needprotect = FALSE;
if (name_info->Name.Length == dir_info->dir_nt.Length)
{
BOOLEAN isdirectory = create_options & FILE_DIRECTORY_FILE;
if (TRUE == isdirectory)
{
needprotect = TRUE;
}
}
if (name_info->Name.Length > dir_info->dir_nt.Length && L'\\' == name_info->Name.Buffer[dir_info->dir_nt.Length / sizeof(WCHAR)])
{
needprotect = TRUE;
}
if (FALSE == needprotect)
{
entry = entry->Flink;
continue;
}
Process_GetProcessName((ULONG_PTR)pid, &nbuf, &nlen, &nptr);
if (NULL == nbuf)
{
break;
}
mini_request = ExAllocatePoolWithTag(NonPagedPool, sizeof(MINI_REQUEST), DIR_PROTECT_POOL_TAG);
if (NULL == mini_request)
{
Data->IoStatus.Status = STATUS_ACCESS_DENIED;
Data->IoStatus.Information = 0;
call_status = FLT_PREOP_COMPLETE;
break;
}
sys_2_user = &mini_request->sys_2_user;
RtlZeroMemory(mini_request, sizeof(MINI_REQUEST));
sys_2_user->file_action = ACTION_CREATE;
sys_2_user->access_mask = mask;
sys_2_user->ask_reason = reason;
if ((ULONG64)name_info->Name.Length + dir_info->dir_dos.Length - dir_info->dir_nt.Length <= sizeof(sys_2_user->filename) - sizeof(WCHAR))
{
memcpy(sys_2_user->filename, dir_info->dir_dos.Buffer, dir_info->dir_dos.Length);
if (name_info->Name.Length > dir_info->dir_nt.Length)
{
memcpy((PCHAR)sys_2_user->filename + dir_info->dir_dos.Length, (PCHAR)name_info->Name.Buffer + dir_info->dir_nt.Length, name_info->Name.Length - dir_info->dir_nt.Length);
}
}
found = TRUE;
break;
}
entry = entry->Flink;
}
}
__finally
{
ExReleaseResourceLite(&global.dir_lock);
}
if (TRUE == found && NULL != sys_2_user)
{
sys_2_user->pid = pid;
wcsncpy_s(sys_2_user->processname, ARRAYSIZE(sys_2_user->processname) - 1, nptr, wcslen(nptr));
mini_request->Data = Data;
if (TRUE == ExAcquireResourceExclusiveLite(&global.minifilter_request_lock, TRUE))
{
//insert data
InsertTailList(&global.head_minirequest, &mini_request->list);
++global.minirequest_count;
ExReleaseResourceLite(&global.minifilter_request_lock);
//wake system thread
KeSetEvent(&global.event_process_request, IO_NO_INCREMENT, FALSE);
call_status = FLT_PREOP_PENDING;
}
}
}
}
if (NULL != name_info)
{
FltReleaseFileNameInformation(name_info);
name_info = NULL;
}
if (FLT_PREOP_PENDING != call_status)
{
if (NULL != mini_request)
{
ExFreePool(mini_request);
mini_request = NULL;
}
}
if (NULL != nbuf)
{
ExFreePool(nbuf);
nbuf = NULL;
}
return call_status;
}
1.如果是来自于内核的请求那么不处理,如果请求来自于发起connect请求的进程也不处理,如果此时need_protect标志为FALSE也不处理。
2.接着使用Data->Iopb->Parameters.Create.Options分别获取高8位和低24位作为create_disposition和create_options变量,使用Data->Iopb->Parameters.Create.SecurityContext->DesiredAccess记录mask标志,根据这几个标志判断是否是写文件目录或者删除行为。
3.使用FltGetFileNameInformation获取本次要操作的文件或者目录,使用共享读锁获取global.dir_lock,判断要操作的文件对象是否在这个目录下,在这个过程要处理一些边界情况,比如保护的目录是C:\123那么需要放过C:\123.txt还有C:\123456目录还有C:\123文件,放开获取到的锁。
4.如果需要保护,那么就使用ZwQueryInformationProcess获取进程名称(如果获取失败那么放过请求,比如经过测试system进程获取不到进程名称),申请内存空间后使用独占锁获取global.minifilter_request_lock,在链表global.head_minirequest插入本次请求,放开获取到的锁。
5.设置当前函数回值为FLT_PREOP_PENDING,唤醒创建的系统线程KeSetEvent(&global.event_process_request, IO_NO_INCREMENT, FALSE);
系统线程执行的代码如下:
VOID ThreadProc(PVOID StartContext)
{
UNREFERENCED_PARAMETER(StartContext);
do
{
SYS_2_USER sys_2_user = { 0 };
BOOLEAN have_data = FALSE;
if (TRUE == global.minifilter_request_lock_initialized && TRUE == ExAcquireResourceSharedLite(&global.minifilter_request_lock, TRUE))
{
__try
{
PLIST_ENTRY entry = global.head_minirequest.Flink;
while (entry != &global.head_minirequest)
{
MINI_REQUEST* request = CONTAINING_RECORD(entry, MINI_REQUEST, list);
sys_2_user = request->sys_2_user;
have_data = TRUE;
break;
}
}
__finally
{
ExReleaseResourceLite(&global.minifilter_request_lock);
}
}
if (TRUE == have_data)
{
union
{
USER_REPLY recv;
REPLY_DATA data;
}reply;
ULONG reply_len = sizeof(FILTER_REPLY_HEADER) + sizeof(REPLY_DATA);
LARGE_INTEGER timeout;
timeout.QuadPart = -70 * 1000 * 1000 * 10; // 70s
NTSTATUS status = FltSendMessage(gFilterHandle, &gClientPort, &sys_2_user, sizeof(SYS_2_USER), &reply.recv, &reply_len, &timeout);
CompleteFirstRequest(NT_SUCCESS(status) && REPLY_ALLOW == reply.data);
}
else
{
KeWaitForSingleObject(&global.event_process_request, Executive, KernelMode, FALSE, NULL);
}
} while (FALSE == global.stop);
PsTerminateSystemThread(STATUS_SUCCESS);
}
1.在系统线程中先获取锁后取出一个请求信息,使用FltSendMessage发送给用户程序,调用FltSendMessage时需要传递reply的缓冲区,此时需要注意reply需要包含一个头结构FILTER_REPLY_HEADER,后追加存放数据的结构(本次使用一个枚举类型REPLY_DATA,数据大小为4字节),用户层调用FilterReplyMessage回复消息时也需要包含这个头结构内容(头结构内容来自于用户层调用到FilterGetMessage获取到的buff内容,此结构中包含一个Minifilter框架自带的MessageId信息),需要注意的是驱动FltSendMessage收到用户层的回复后,reply_len被修改为了真正的返回数据结构的长度(sizeof(REPLY_DATA)),缓冲区直接指向的就是返回数据,而不是头结构+回复信息,,即结构体USER_REPLY 中包含了头结构以及REPLY_DATA,发送时使用USER_REPLY的内存,而收到回复后此时USER_REPLY 内存中存放的REPLY_DATA,因此上面代码中使用union结构体。
2.收到返回信息(可能是由于用户层点击了允许或者拒绝按钮,或者用户层默认拒绝,或者是用户层主动退出或崩溃导致disconnect断开连接),只有当返回信息是允许是才放过这个请求,否则拒绝这个请求,具体实现在CompleteFirstRequest函数中。
CompleteFirstRequest函数代码如下:
BOOLEAN CompleteFirstRequest(BOOLEAN allow)
{
MINI_REQUEST* request = NULL;
if (TRUE == ExAcquireResourceExclusiveLite(&global.minifilter_request_lock, TRUE))
{
__try
{
if (0 != global.minirequest_count)
{
PLIST_ENTRY entry = RemoveHeadList(&global.head_minirequest);
--global.minirequest_count;
request = CONTAINING_RECORD(entry, MINI_REQUEST, list);
}
}
__finally
{
ExReleaseResourceLite(&global.minifilter_request_lock);
}
}
if (NULL != request)
{
if (TRUE == allow)
{
FltCompletePendedPreOperation(request->Data, FLT_PREOP_SUCCESS_NO_CALLBACK, NULL);
}
else
{
request->Data->IoStatus.Status = STATUS_ACCESS_DENIED;
request->Data->IoStatus.Information = 0;
FltCompletePendedPreOperation(request->Data, FLT_PREOP_COMPLETE, NULL);
}
ExFreePoolWithTag(request, DIR_PROTECT_POOL_TAG);
request = NULL;
return TRUE;
}
return FALSE;
}
如果找不到请求,那么返回FALSE,如果完成请求那么返回TRUE,这样设计方便后续多次调用该函数拒绝链表中的所有缓存的请求(在用户程序退出disconnect发生时)。
disconnect代码如下:
VOID DirProtectMiniDisconnect(__in_opt PVOID ConnectionCookie)
{
PAGED_CODE();
UNREFERENCED_PARAMETER( ConnectionCookie );
DbgPrint("[mini-filter] DirProtectMiniDisconnect");
// Close our handle
FltCloseClientPort( gFilterHandle, &gClientPort );
gClientPort = NULL;
global.client_pid = 0;
global.need_protect = FALSE;
//clear dir
if (TRUE == ExAcquireResourceExclusiveLite(&global.dir_lock, TRUE))
{
while (global.dir_count > 0)
{
PLIST_ENTRY entry = RemoveHeadList(&global.head_dir);
--global.dir_count;
DIR_INFO* dir_info = CONTAINING_RECORD(entry, DIR_INFO, list);
if (NULL != dir_info)
{
if (NULL != dir_info->dir_dos.Buffer)
{
ExFreePool(dir_info->dir_dos.Buffer);
dir_info->dir_dos.Buffer = NULL;
}
if (NULL != dir_info->dir_nt.Buffer)
{
ExFreePool(dir_info->dir_nt.Buffer);
dir_info->dir_nt.Buffer = NULL;
}
ExFreePool(dir_info);
dir_info = NULL;
}
}
ExReleaseResourceLite(&global.dir_lock);
}
//deny cached all request
while (CompleteFirstRequest(FALSE))
{
}
}
需要关闭客户端的port,设置need_protect标志位为FALSE,清理dir信息,调用CompleteFirstRequest拒绝缓存的请求。
用户层初始化代码完成后,就开始接收驱动拦截到的请求。
用户层代码
void Run(){
size_t len = sizeof(FILTER_MESSAGE_HEADER) + sizeof(SYS_2_USER);
FILTER_MESSAGE_HEADER* header = (FILTER_MESSAGE_HEADER*)malloc(sizeof(FILTER_MESSAGE_HEADER) + sizeof(SYS_2_USER));
if (NULL == header)
{
return;
}
USER_REPLY* reply = (USER_REPLY*)malloc(sizeof(FILTER_REPLY_HEADER) + sizeof(REPLY_DATA));
if (NULL == reply)
{
free(header);
return;
}
OVERLAPPED over = { 0 };
over.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
if (NULL == over.hEvent)
{
free(header);
free(reply);
return;
}
SYS_2_USER* sys_2_user = (SYS_2_USER*)((PCHAR)header + sizeof(FILTER_MESSAGE_HEADER));
while (1)
{
memset(header, 0, len);
HRESULT result = FilterGetMessage(g_hPort, header, (DWORD)len, &over);
if (result != HRESULT_FROM_WIN32(ERROR_IO_PENDING))
{
DebugBreak();
break;
}
WaitForSingleObject(over.hEvent, INFINITE);
ASK_REASON reason = sys_2_user->ask_reason;
wprintf(L"pid:%d processname:%s access_mask:0x%x %s filename:%s\n", sys_2_user->pid, sys_2_user->processname, sys_2_user->access_mask, ReasonToString(reason), sys_2_user->filename);
INT_PTR dlg_ret = DialogBoxParamW(NULL, MAKEINTRESOURCE(IDD_DIALOG1), NULL, DlgProc, (LPARAM)sys_2_user);
if (1 == dlg_ret)
{
reply->reply_data = REPLY_ALLOW;
}
else
{
reply->reply_data = REPLY_DENY;
}
reply->reply_header.Status = 0;
reply->reply_header.MessageId = header->MessageId;
if (S_OK != FilterReplyMessage(g_hPort, (PFILTER_REPLY_HEADER)reply, sizeof(FILTER_REPLY_HEADER) + sizeof(REPLY_DATA)))
{
break;
}
}
free(header);
free(reply);
CloseHandle(over.hEvent);
}
收到请求后弹框让用户选择拒绝允许,然后调用FilterReplyMessage返回给驱动,如下图:

点击拒绝后,右侧的cmd会显示拒绝访问

总结:
完成了一个可以动态配置保护目录,动态启用保护,可以根据用户层决策来允许或者拒绝文件请求的Minifilter框架程序,可以拦截到创建文件、创建目录,删除文件、删除目录。
还需要修改的地方:在precreate根据标志猜测行为,可以拦截到但是显示在界面上的行为不准确,还需要进行调试修改;对于拖动文件到保护文件夹下的情况选择拒绝后(拒绝访问保护目录)还是可以拖动成功,这个还需要调整。
在虚拟机中设置测试模式,安装驱动时需要有sys文件、inf文件、cat文件,右键inf安装驱动,再在cmd下运行sc start dirprotect 或者fltmc load dirprotect启动驱动服务,使用sc stop dirprotect 或者fltmc unload dirprotect停止驱动服务。
驱动启动后,如果要监控C:\123目录,可以运行程序ProtectControl.exe C:\123 传入参数C:\123,也可以监控多个目录,启动后不要关闭程序,后续可以接收驱动发过来的请求。
代码参考原帖附件。
#
#

看雪ID:0346954
https://bbs.kanxue.com/user-home-762319.htm
*本文为看雪论坛优秀文章,由 0346954 原创,转载请注明来自看雪社区

报名中!看雪·第九届安全开发者峰会(SDC 2025)
往期推荐
无”痕”加载驱动模块之傀儡驱动 (上)
为 CobaltStrike 增加 SMTP Beacon
隐蔽通讯常见种类介绍
buuctf-re之CTF分析
物理读写/无附加读写实验


球分享

球点赞

球在看

点击阅读原文查看更多
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论