如何利用Rootkit实现文件删除保护

xiaohui 技术 2018年7月25日发布

导语:简单来说,Rootkit就是一个数字工具箱,恶意软件或木马软件,可以通过它来隐藏自身及指定的文件、进程和网络链接等信息。

timg.jpg

简单来说,Rootkit就是一个数字工具箱,恶意软件或木马软件,可以通过它来隐藏自身及指定的文件、进程和网络链接等信息。隐藏的过程是通过Rootkit加载到系统内核中,并通过修改内核达到隐蔽的目地,比如让系统认为恶意软件占用的空间为坏块,从而避免被检测到。

Windows内核模式驱动程序和I/O请求数据包

由于这不是一篇关于内核模式或一般驱动程序的介绍性文章,所以我会对里面提到的基本概念一带而过。首先是所谓的“I/O请求数据包”(简称IRP),发送到设备驱动程序的大部分请求都打包在I/O请求数据包(IRP)中,然后操作系统组件或驱动程序将IRP发送到驱动程序,通常IRP由在堆栈中排列的多个驱动程序进行处理。堆栈中的每个驱动程序都与一个设备对象关联。如果IRP由设备堆栈进行处理,则通常首先发送IRP至设备堆栈中的顶部设备对象。例如,如果IRP由此图中显示的设备堆栈进行处理,则会首先将IRP发送至设备堆栈顶部的筛选器设备对象(筛选器 DO)。

加图.png

IRP可以是文件请求或键盘输入内容等,IRP和驱动程序的作用是IRP被发送到已注册(与I/O管理器)处理它们的堆栈中的驱动程序。

1.jpg

这样,驱动程序沿着设备堆栈向下传递IRP,直到它到达能够处理指定请求的设备或驱动程序,一旦指定请求处理完毕,又会沿着设备堆栈向上传递IRP。请注意,某些IRP沿着设备堆栈一路向下传递至物理设备对象(PDO)。其他IRP从未到达PDO,原因是这些IRP由PDO之上的驱动程序之一完成。

文件删除保护

在本文中,我将介绍如何保护文件不被删除的高级概念,为了防止文件被删除,我选择的条件是该文件必须具有. protected扩展(不区分大小写)。我刚刚已经介绍了,驱动程序沿着设备堆栈向下传递IRP,直到它到达能够处理指定请求的设备或驱动程序。如果在执行目标IRP之前,可以将特殊驱动程序插入驱动程序堆栈中的某个位置,那么它就有能力过滤请求并在需要时中断或修改它,这个概念就是文件删除保护机制的核心思想。

为了检测文件删除中的IRP是否被中断或修改,我只需要提取文件扩展名并将其与任何不允许删除的内容进行比较。如果扩展名匹配,则驱动程序将通过完成请求并将错误发送回驱动程序堆栈来阻止IRP进行任何进一步处理。

2.jpg

具体保护过程

以下代码是“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。

1886bf05e93bfd2a2e45c77275c400e80206f0bc_1_690x387.png

fa509f3ab57edcdde236dfa570d39fa163a78db6_1_690x387.png

注意,此文是我的一篇探索性文章,一些技术还不太成熟,如果你认为有不对的地方,可以反馈给我们。

参考及来源:

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

如若转载,请注明原文地址: http://www.4hou.com/technology/12714.html
点赞 0
  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论