导语:简单来说,Rootkit就是一个数字工具箱,恶意软件或木马软件,可以通过它来隐藏自身及指定的文件、进程和网络链接等信息。
简单来说,Rootkit就是一个数字工具箱,恶意软件或木马软件,可以通过它来隐藏自身及指定的文件、进程和网络链接等信息。隐藏的过程是通过Rootkit加载到系统内核中,并通过修改内核达到隐蔽的目地,比如让系统认为恶意软件占用的空间为坏块,从而避免被检测到。
Windows内核模式驱动程序和I/O请求数据包
由于这不是一篇关于内核模式或一般驱动程序的介绍性文章,所以我会对里面提到的基本概念一带而过。首先是所谓的“I/O请求数据包”(简称IRP),发送到设备驱动程序的大部分请求都打包在I/O请求数据包(IRP)中,然后操作系统组件或驱动程序将IRP发送到驱动程序,通常IRP由在堆栈中排列的多个驱动程序进行处理。堆栈中的每个驱动程序都与一个设备对象关联。如果IRP由设备堆栈进行处理,则通常首先发送IRP至设备堆栈中的顶部设备对象。例如,如果IRP由此图中显示的设备堆栈进行处理,则会首先将IRP发送至设备堆栈顶部的筛选器设备对象(筛选器 DO)。
IRP可以是文件请求或键盘输入内容等,IRP和驱动程序的作用是IRP被发送到已注册(与I/O管理器)处理它们的堆栈中的驱动程序。
这样,驱动程序沿着设备堆栈向下传递IRP,直到它到达能够处理指定请求的设备或驱动程序,一旦指定请求处理完毕,又会沿着设备堆栈向上传递IRP。请注意,某些IRP沿着设备堆栈一路向下传递至物理设备对象(PDO)。其他IRP从未到达PDO,原因是这些IRP由PDO之上的驱动程序之一完成。
文件删除保护
在本文中,我将介绍如何保护文件不被删除的高级概念,为了防止文件被删除,我选择的条件是该文件必须具有. protected扩展(不区分大小写)。我刚刚已经介绍了,驱动程序沿着设备堆栈向下传递IRP,直到它到达能够处理指定请求的设备或驱动程序。如果在执行目标IRP之前,可以将特殊驱动程序插入驱动程序堆栈中的某个位置,那么它就有能力过滤请求并在需要时中断或修改它,这个概念就是文件删除保护机制的核心思想。
为了检测文件删除中的IRP是否被中断或修改,我只需要提取文件扩展名并将其与任何不允许删除的内容进行比较。如果扩展名匹配,则驱动程序将通过完成请求并将错误发送回驱动程序堆栈来阻止IRP进行任何进一步处理。
具体保护过程
以下代码是“minifilter”驱动程序的代码样本,该段代码负责处理文件系统请求。
// The callbacks array defines what IRPs we want to process. CONST FLT_OPERATION_REGISTRATION Callbacks[] = { { IRP_MJ_CREATE, 0, PreAntiDelete, NULL }, // DELETE_ON_CLOSE creation flag. { IRP_MJ_SET_INFORMATION, 0, PreAntiDelete, NULL }, // FileInformationClass == FileDispositionInformation(Ex). { IRP_MJ_OPERATION_END } }; CONST FLT_REGISTRATION FilterRegistration = { sizeof(FLT_REGISTRATION), // Size FLT_REGISTRATION_VERSION, // Version 0, // Flags NULL, // ContextRegistration Callbacks, // OperationRegistration Unload, // FilterUnloadCallback NULL, // InstanceSetupCallback NULL, // InstanceQueryTeardownCallback NULL, // InstanceTeardownStartCallback NULL, // InstanceTeardownCompleteCallback NULL, // GenerateFileNameCallback NULL, // NormalizeNameComponentCallback NULL // NormalizeContextCleanupCallback }; PFLT_FILTER Filter; static UNICODE_STRING ProtectedExtention = RTL_CONSTANT_STRING(L"PROTECTED"); NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) { // We can use this to load some configuration settings. UNREFERENCED_PARAMETER(RegistryPath); DBG_PRINT("DriverEntry called.\n"); // Register the minifilter with the filter manager. NTSTATUS status = FltRegisterFilter(DriverObject, &FilterRegistration, &Filter); if (!NT_SUCCESS(status)) { DBG_PRINT("Failed to register filter: <0x%08x>.\n", status); return status; } // Start filtering I/O. status = FltStartFiltering(Filter); if (!NT_SUCCESS(status)) { DBG_PRINT("Failed to start filter: <0x%08x>.\n", status); // If we fail, we need to unregister the minifilter. FltUnregisterFilter(Filter); } return status; }
首先,应由驱动程序处理的IRP是IRP_MJ_CREATE 1和IRP_MJ_SET_INFORMATION 1,它们分别是在创建文件(或目录)和设置元数据时发出的请求。这两个IRP都能够删除文件,至于具体原因我在稍后会详细介绍。 Callbacks数组定义了要处理的相应IRP以及预操作和操作后回调函数。预操作定义了当IRP进入堆栈时所调用的函数,而后操作是在IRP完成后重新启动时调用的函数。请注意,由于此操作中后操作为NULL,因此拦截文件删除的操作只在预操作中进行处理。
DriverEntry是驱动程序的主要函数,通常会用这个函数来填充dispatch例程的指针,这就象注册回调函数一样。有的设备要创建设备的对象,或者还要创建一个设备名字,以及其他的初始化操作。它的原型如下:
NTSTATUS DriverEntry( IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath ){ }
使用FltRegisterFilter执行过滤器管理器的注册,一旦注册成功,就要开始过滤IRP,它必须使用过滤器句柄调用FltStartFiltering函数。还要请注意,如前所述,我已将扩展名定义为.PROTECTED。
定义卸载函数也是一种很好的做法,这样,如果驱动程序被请求停止,则定义的卸载函数就可以执行必要的清理。定义卸载函数在目前的设计中只是个补充,并不是主要方向。
/* * This is the driver unload routine used by the filter manager. * When the driver is requested to unload, it will call this function * and perform the necessary cleanups. */ NTSTATUS Unload(_In_ FLT_FILTER_UNLOAD_FLAGS Flags) { UNREFERENCED_PARAMETER(Flags); DBG_PRINT("Unload called.\n"); // Unregister the minifilter. FltUnregisterFilter(Filter); return STATUS_SUCCESS; }
此段代码中的最后一个函数是PreAntiDelete预操作回调,它负责处理IRP_MJ_CREATE和IRP_MJ_SET_INFORMATION IRP。 IRP_MJ_CREATE包括请求打开“文件句柄或文件对象或设备对象”的函数,例如ZwCreateFile。 IRP_MJ_SET_INFORMATION包括请求设置“关于文件或文件句柄的元数据”的函数,例如ZwSetInformationFile。
/* * This routine is called every time I/O is requested for: * - file creates (IRP_MJ_CREATE) such as ZwCreateFile and * - file metadata sets on files or file handles * (IRP_MJ_SET_INFORMATION) such as ZwSetInformation. * * This is a pre-operation callback routine which means that the * IRP passes through this function on the way down the driver stack * to the respective device or driver to be handled. */ FLT_PREOP_CALLBACK_STATUS PreAntiDelete(_Inout_ PFLT_CALLBACK_DATA Data, _In_ PCFLT_RELATED_OBJECTS FltObjects, _Flt_CompletionContext_Outptr_ PVOID *CompletionContext) { UNREFERENCED_PARAMETER(CompletionContext); /* * This pre-operation callback code should be running at * IRQL <= APC_LEVEL as stated in the docs: * https://docs.microsoft.com/en-us/windows-hardware/drivers/ifs/writing-preoperation-callback-routines * and both ZwCreateFile and ZwSetInformaitonFile are also run at * IRQL == PASSIVE_LEVEL: * - ZwCreateFile: https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/content/ntifs/nf-ntifs-ntcreatefile#requirements * - ZwSetInformationFile: https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/content/ntifs/nf-ntifs-ntsetinformationfile#requirements */ PAGED_CODE(); /* * By default, we don't want to call the post-operation routine * because there's no need to further process it and also * because there is none. */ FLT_PREOP_CALLBACK_STATUS ret = FLT_PREOP_SUCCESS_NO_CALLBACK; // We don't care about directories. BOOLEAN IsDirectory; NTSTATUS status = FltIsDirectory(FltObjects->FileObject, FltObjects->Instance, &IsDirectory); if (NT_SUCCESS(status)) { if (IsDirectory == TRUE) { return ret; } } /* * We don't want anything that doesn't have the DELETE_ON_CLOSE * flag. */ if (Data->Iopb->MajorFunction == IRP_MJ_CREATE) { if (!FlagOn(Data->Iopb->Parameters.Create.Options, FILE_DELETE_ON_CLOSE)) { return ret; } } /* * We don't want anything that doesn't have either * FileDispositionInformation or FileDispositionInformationEx or * file renames (which can just simply rename the extension). */ if (Data->Iopb->MajorFunction == IRP_MJ_SET_INFORMATION) { switch (Data->Iopb->Parameters.SetFileInformation.FileInformationClass) { case FileRenameInformation: case FileRenameInformationEx: case FileDispositionInformation: case FileDispositionInformationEx: case FileRenameInformationBypassAccessCheck: case FileRenameInformationExBypassAccessCheck: case FileShortNameInformation: break; default: return ret; } } /* * Here we can check if we want to allow a specific process to fall * through the checks, e.g. our own application. * Since this is a PASSIVE_LEVEL operation, we can assume(?) that * the thread context is the thread that requested the I/O. We can * check the current thread and compare the EPROCESS of the * authenticated application like so: * * if (IoThreadToProcess(Data->Thread) == UserProcess) { * return FLT_PREOP_SUCCESS_NO_CALLBACK; * } * * Of course, we would need to find and save the EPROCESS of the * application somewhere first. Something like a communication port * could work. */ PFLT_FILE_NAME_INFORMATION FileNameInfo = NULL; // Make sure the file object exists. if (FltObjects->FileObject != NULL) { // Get the file name information with the normalized name. status = FltGetFileNameInformation(Data, FLT_FILE_NAME_NORMALIZED | FLT_FILE_NAME_QUERY_DEFAULT, &FileNameInfo); if (NT_SUCCESS(status)) { // Now we want to parse the file name information to get the extension. FltParseFileNameInformation(FileNameInfo); // Compare the file extension (case-insensitive) and check if it is protected. if (RtlCompareUnicodeString(&FileNameInfo->Extension, &ProtectedExtention, TRUE) == 0) { DBG_PRINT("Protecting file deletion/rename!"); // Strings match, deny access! Data->IoStatus.Status = STATUS_ACCESS_DENIED; Data->IoStatus.Information = 0; // Complete the I/O request and send it back up. ret = FLT_PREOP_COMPLETE; } // Clean up file name information. FltReleaseFileNameInformation(FileNameInfo); } } return ret; }
对于IRP_MJ_CREATE,我会检查FILE_DELETE_ON_CLOSE创建选项,该选项的作用为“当文件的最后一个句柄传递给NtClose时,文件就会被删除”。如果设置了此选项,则必须在DesiredAccess参数中设置DELETE标志。如果FILE_DELETE_ON_CLOSE创建选项不存在,我们就不用关心这一步了。此时,因此IRP_MJ_CREATE将被传递到堆栈中,以FLT_PREOP_SUCCESS_NO_CALLBACK返回值代表进一步的处理结果。请注意,NO_CALLBACK意味着当IRP完成并返回堆栈时不应该调用后操作例程,因为没有后操作,所以这个函数应该返回堆栈。
对于IRP_MJ_SET_INFORMATION,应检查FileInformationClass参数。 FileDispositionInformation的作用是“通常,将FILE_DISPOSITION_INFORMATION的DeleteFile选项设置为TRUE,以便在调用NtClose时删除文件,以释放文件对象的最后一个打开句柄,调用者必须打开在DesiredAccess参数中设置了DELETE标志的文件”。为了防止文件被简单的重命名,从而使受保护的扩展不再存在,还必须检查FileRenameInformation和FileShortNameInformation值。
如果驱动程序收到选择进行文件删除的IRP请求,则必须使用FltGetFileNameInformation和FltParseFileNameInformation函数解析文件名信息以提取扩展名。然后,在删除扩展请求的文件和受保护扩展之间进行简单的字符串比较,以确定是否应该允许删除操作。如果文件未被授权删除,那在此种情况下,操作的状态就会被驱动程序设置为STATUS_ACCESS_DENIED,并且显示预操作函数已经完成IRP。
注意,此文是我的一篇探索性文章,一些技术还不太成熟,如果你认为有不对的地方,可以反馈给我们。
参考及来源:
https://0x00sec.org/t/kernel-mode-rootkits-file-deletion-protection/7616
https://docs.microsoft.com/en-us/windows-hardware/drivers/kernel/irp-mj-create
https://msdn.microsoft.com/library/windows/hardware/ff566424