内核级驱动对抗Hook ZwSetInformationFile反删除技术

来源:互联网 发布:淘宝开店考试 编辑:程序博客网 时间:2024/05/18 03:14

网络安全中的文件删除与保护一直都是大家谈论的焦点问题,针对如何保护磁盘上属于自己的专有文件,已经存在一些现成技术,比如信息隐藏技术。笔者详细的谈论过有关基于有序规则的信息隐藏与加密技术,利用一些常见图片、视频等作为载体将文件嵌入其中,达到掩人耳目的目的。这种方式如果在图像的鲁棒性、文件规则的健壮性、加密算法的有效性上能够很好的满足,是可以很好的实现文件的保护的。下面论述的是文件在没有隐藏的情况下,如何彻底的清除文件的相关技术问题,并设计一种内核级驱动实现文件的永久删除技术。
  我看到Aosemp写了一篇程序保护之文件保护技术,阅读了一番。文中采用Hook ZwSetInformationFile技术实现文件的保护技术,这实际上就是Ring3级下修改SSDT表中的NativeAPI ZwSetInformationFile技术。其实早在2008年第1期上,夜枫的文章《内核方法实现进程保护》就已经介绍过这种技术,只不过是实现进程保护,Hook了不同的API函数而已,同样也是修改SSDT表的操作。当然,这种技术是初学者必经之路,具有一定的启发性。我看到这篇文章,突然想起了冰刃(IceSword)软件,试了试冰刃能否对抗Hook ZwSetInformationFile,果真不如所料,在冰刃面前,Hook ZwSetInformationFile技术毫无作用。
难道冰刃又对这个Native API进行了Inline Hook,正如对NtOpenProcess和NtTerminateProcess一样吗?我采用WinDbg测试了一番,下面是运行冰刃前后NtSerInformation的相关代码。

//运行前
lkd> u NtSetInformationFile
nt!NtSetInformationFile:
8057ae38 6a64  push  64h
8057ae3a 6808a44d80  push  offset nt!FsRtlLegalAnsiCharacterArray+0xf58 (804da408)
8057ae3f e8ac0ffcff  call  nt!wctomb+0x45 (8053bdf0)
8057ae44 33f6  xo  esi,esi
8057ae46 33ff  xor  edi,edi
//运行冰刃后
lkd> u NtSetInformationFile
nt!NtSetInformationFile:
8057ae38 6a64  push  64h
8057ae3a 6808a44d80  push  offset nt!FsRtlLegalAnsiCharacterArray+0xf58 (804da408)
8057ae3f e8ac0ffcff  call  nt!wctomb+0x45 (8053bdf0)
8057ae44 33f6  xor  esi,esi
8057ae46 33ff  xor  edi,edi
从上面的两段代码可以看出,并不像Inline Hook NtOpenProcess那样对NtSetInformationFile进行了JMP操作,从而转向自己定义的函数体。那么冰刃是如何实现玩转文件删除操作的呢?实际上就是采用了内核级驱动技术。下面我们就开始正式论述内核级驱动技术实现文件删除。

原理分析
本文的目的是需要强制性删除任何指定的文件,那么首先必须考虑文件链的情况,即当文件正在被其它的进程使用,存在一个或多个链接时,如何清除这些链接。对操作系统原理有一定了解的读者应该知道,每当用户请求文件读、写等I/O操作时,系统会借助I/O管理器创建一个文件对象FILE_OBJECT(注:此对象不专属于某个指定的进程或线程),然后借助Fast I/O向NT Cache管理器发送一个IRP请求此文件,NT Cache管理器则会在高速系统缓存中寻找此文件,若找到,则直接返回此文件,不然,则会产生缺页中断,借助虚拟内存管理器向FSD发送IRP请求,要求FSD通过磁盘驱动找到指定文件,然后依次返回。一旦发生此种情况,系统会借助虚拟内存管理器初始化FILE_OBJECT中的结构体SECTION_OBJECT_POINTERS,使文件缓存在Cache中。显然,如果文件正被使用,此结构体必然已经初始化了,因为其保存了与文件流相关的文件映射和缓存相关信息。
SECTION_OBJECT_POINTERS结构类似于文件控制块FCB,不论有多少个用户请求打开同一个文件,SECTION_OBJECT_POINTERS结构体只会初始化一次,也就是说所有的FILE_OBJECT都会指向同一个存放SECTION_OBJECT_POINTERS的内存地址。下面我们分析下此结构体。

typedef struct _SECTION_OBJECT_POINTERS{
PVOID DataSectionObject; 
PVOID SharedCacheMap;
PVOID ImageSectionObject;
}SECTION_OBJECT_POINTERS;

参数DataSectionObject:指向一个内部数据结构的指针,此结构表示由文件流创建的数据区对象,这个指针由虚拟内存管理器操作使用,这个域的值会在指定文件被Caching之后初始化,即为有效值。
参数SharedCacheMap:Cache映射用于反应特定数据流的映射轨迹。
参数ImageSectionObject:一个私有数据结构的内存地址。文件刚被创建时,或者文件不再被任一用户使用时,这些值必须初始化为NULL;文件创建后,FSD并不会立即初始化Caching,除非此文件发生I/O 读、写等操作。而在SECTION_OBJECT_POINTERS 结构体的参数说明中已经分析过,文件必须发生Caching操作,此结构体的值才会初始化为有效值。
通过上面的大量分析,我们应该知道这个事实,SECTION_OBJECT_POINTERS结构体与文件是否存在多个链接是直接相关的。只要将它的成员变量置为NULL,即可断掉所有的链接。具体分析读者可以参考Rajeev Nagar的巨著《Windows NT File System Internals》中的The NT Cache Manager等相关章节。
好了,分析了如何断掉指定文件的链接后,现在开始分析如何向FSD发送文件删除IRP了。对文件的所有操作都发生在ZwSetInformationFile上,这个API中就存有一个文件信息级别FileInformationClass成员,自定义的设备驱动可以定义它的级别,这里只需要定义为FileDispositionInformation,并设置它的成员变量DeleteFile为TRUE即可。当然,本文并不是通过修改ZwSetInformationFile实现的,否则又属于Ring3级操作了。先向FSD发送一个IRP,即IRP_MJ_SET_INFORMATION,在FSD层修改文件的属性信息,发送删除命令,再交给底层驱动执行即可。

逆向思维分析
网络安全中我们还是采用逆向思维来分析,接下来需要做些什么,如何利用前面的原理分析。首先我们需要向FSD发送IRP_MJ_SET_INFORMATION信号,这是已经确定的。向底层驱动发送IRP的函数有四个,这里我们采用带参数较少的IoAllocateIrp函数,其第一个参数为I/O堆栈的大小,在不知道底层存在多少层驱动的情况下,一般直接设置为文件对应的设备堆栈大小。这里就引出了文件设备对象概念。那么如何得到文件设备对象呢?可能有一定驱动开发经验的读者会想到函数IoGetDeviceObjectPointer。此函数调用后,可以直接获得文件对象、文件设备对象两个属性值,这是最好不过的了。但是此函数却需要一个设备名参数,而我们已知的只有文件名。思考了很久,也很难找到如何将文件名转换为设备名。这条路就断了。
那是不是会存在一个文件名向文件对象转化的函数呢?然后又根据文件对象转换到文件设备对象呢?答案是肯定的,在DDK中存在这样一个函数ObReferenceObjectByHandle,此函数可以将文件句柄转化为文件对象,再通过函数IoGetRelatedDeviceObject得到此文件对应的设备对象。这两个函数的介绍这里就不展开了,读者可以自行查阅DDK文档。
这里主要分析下IRP的分发问题。现假设某过滤驱动的分层结构如下:1、FFLDO;2、FSD;3、PDO。当调用IoAllocateIrp后,新产生的IRP的StackCount只能是≧2,因为FFLDO下还存在两个驱动,但此时的堆栈单元却是StackCount+1。即newIrp的StackCount=2,CurrentLocation=3。那么何时修改堆栈单元的值呢?其实当我们调用IoCallDriver函数向下传递IRP时,IoCallDriver函数内部已经调用了类似 IoSetNextIrpStackLocation()的操作来调整CurrentLocation的索引了,故我们在调用IoCallDriver函数时必须先初始化FSD的驱动。获取下层驱动的I/O堆栈单元的函数为IoGetNextIrpStackLocation,此时我们可以通过I/O堆栈单元IO_STACK_LOCATION结构体来设置删除信号,包括向底层发送IRP_MJ_SET_INFORMATION信号和针对此信号的相关设置。

//Parameters for IRP_MJ_SET_INFORMATION 
struct {
ULONG Length;
FILE_INFORMATION_CLASS POINTER_ALIGNMENT FileInformationClass;
PFILE_OBJECT FileObject;
union {
struct {
BOOLEAN ReplaceIfExists;
BOOLEAN AdvanceOnly;
};
ULONG ClusterCount;
HANDLE DeleteHandle;
};
} SetFile;
这里需要设置文件信息的级别为FileDispositionInformation;同时将文件对象结构体中的成员变量SECTION_OBJECT_POINTER的相应值全部置为NULL,断掉所有的链接即可。

代码分析
下面通过代码,详细的分析实现的各个细节,并给出一定的注释与分析。

#include <ntddk.h>
#define NT_DEVICE_NAME  L"\\Device\\DeleteFile"
#define NT_DOSDEVICE_NAME  L"\\DosDevices\\DeleteFile"
//函数说明:获取指定文件的句柄
HANDLE NTAPI GetFileHandle( IN PWCHAR szFileName ){
NTSTATUS  ntstatus = STATUS_SUCCESS;
UNICODE_STRING  UnicFileName;
OBJECT_ATTRIBUTES ObjectAttri;
HANDLE  hFile;
IO_STATUS_BLOCK IoStatusBk;
RtlInitUnicodeString(&UnicFileName ,szFileName);
InitializeObjectAttributes(&ObjectAttri, & UnicFileName,OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE ,NULL ,NULL);
ntstatus = IoCreateFile( &hFile,FILE_READ_ATTRIBUTES,& ObjectAttri,&IoStatus,0,FILE_ATTRIBUTE_NORMAL,FILE_SHARE_DELETE,FILE_OPEN,0,NULL,0,CreateFileTypeNone,NULL,IO_NO_PARAMETER_CHECKING );
if(!NT_SUCCESS(ntstatus)){
ZwClose(hFile);
KdPrint(("IoCreateFile Error"));
return NULL;
}
return hFile;
}
//函数说明:清除文件相关属性,将文件的属性改为常规属性,防止文件设置了只读、系统、隐藏、压缩、加密等属性值,注意这里必须设置,否则文件一旦被设置为隐藏等相关属性时,文件是无法强制性删除的
NTSTATUS NTAPI ClearFileAttri( IN HANDLE hFile ){
NTSTATUS     ntstatus = STATUS_SUCCESS;
PFILE_OBJECT   pFileObject;
PDEVICE_OBJECT  pDeviceObject;
PIRP  newIrp;
KEVENT  kEvent;
FILE_BASIC_INFORMATION FileBaseInfor;
IO_STATUS_BLOCK    IoStatus;
PIO_STACK_LOCATION   pIrpSt;
//将设备对象的句柄转化为与设备相关的文件对象句柄
ntstatus = ObReferenceObjectByHandle( hFile,DELETE,*IoFileObjectType,KernelMode,(PVOID*)&pFileObject,NULL );
if(!NT_SUCCESS(ntstatus)){
ObReferenceObject( pFileObject );
KdPrint(( "StripFileAttributes ObReferenceObjectByHandle Error" ));
return ntstatus;
}
//已知文件对象句柄,映射到其对应的文件设备对象句柄
pDeviceObject = IoGetRelatedDeviceObject(pFileObject);
newIrp = IoAllocateIrp( pDeviceObject->StackSize,TRUE );
if (newIrp == NULL){
ObReferenceObject( pFileObject );//解除文件引用链接
KdPrint(( "StripFileAttributes IoAllocateIrp Error" ));
return STATUS_UNSUCCESSFUL;
}
//采用IRP同步事件行为,设置同步事件
KeInitializeEvent( &kEvent ,SynchronizationEvent ,FALSE );
RtlZeroMemory(&FileBaseInfor,sizeof( FILE_BASIC_INFORMATION));
FileBaseInfor.FileAttributes = FILE_ATTRIBUTE_NORMAL;
pIrp->AssociatedIrp.SystemBuffer = &FileBaseInfor;
//设置文件信息级别,调整文件基本信息,使文件回归到正常信息状态。这里可以参考API函数GetFileAttributes的相关文件属性信息和Native API中的ZwSetInformationFile中的FILE_INFORMATION_CLASS
pIrp->UserEvent = &kEvent;
pIrp->UserIosb = &IoStatus;
pIrp->Tail.Overlay.Thread = PsGetCurrentThread();
pIrp->Tail.Overlay.OriginalFileObject = pFileObject;
pIrp->RequestorMode = KernelMode;
//获得下一个分层驱动的I/O堆栈,并设置此I/O堆栈相关信息,如I/O堆栈的主IRP
pIrpSt = IoGetNextIrpStackLocation(pIrp);
pIrpSt->MajorFunction  = IRP_MJ_SET_INFORMATION;
pIrpSt->DeviceObject  = pDeviceObject;
pIrpSt->FileObject  = pFileObject;
//修改由IRP_MJ_SET_INFORMATION引发的结构体SetFile相关成员的设置
pIrpSt->Parameters.SetFile.Length = sizeof( FILE_BASIC_INFORMATION );
pIrpSt->Parameters.SetFile.FileObject = pFileObject;
pIrpSt->Parameters.SetFile.FileInformationClass = FileBasicInformation;
//注册一个IRP完成例程,当底层驱动完成此IRP的请求后,会自动调用此IRP例程,其实注册的完成例程就是当前堆栈中的CompletionRoutine子域,当IRP在某层驱动上调用IoCompleteRequest后,会一层层出栈,当遇到CompletionRoutine非空,则立刻会调用此例程,使上层驱动重新获得IRP的控制
IoSetCompletionRoutine( pIrp ,IoSetFileCompletion ,NULL ,TRUE ,TRUE ,TRUE );
//调用底层驱动,传送此IRP
ntstatus = IoCallDriver( pDeviceObject ,pIrp );
if(ntstatus == STATUS_PENDING){
//等待同步对象
ntstatus = KeWaitForSingleObject( &kFsEvent ,Executive ,
KernelMode ,TRUE ,NULL );
KdPrint(("STATUS_PENDING\n"));
}
ObDereferenceObject( pFileObject );
return STATUS_SUCCESS;
}
IRP的处理核心是每次向下传送IRP之前,记住提前初始化下层驱动的I/O堆栈。通俗来讲就是将需要交给下层驱动做的事情交待清楚。好了,已经将指定文件的相关属性值进行了修改,完全剥开了文件的特殊属性,还原成了一个正常的文件。现在就是需要将文件的所有链接进行清零及发送文件清除命令的时候了。显然,仍然需要创建一个新的IRP,并向底层驱动发送此IRP,交给底层驱动来完成这些操作了。
//函数说明:清除指定文件所对应的文件链,创建新的IRP,设置文件删除指令,向下层驱动发送IRP指令,等待下层驱动返回IRP完成指令
NTSTATUS NTAPI CompelDeletFile(IN HANDLE hFile){
NTSTATUS  ntstatus = STATUS_SUCCESS;
PFILE_OBJECT  pFileObject;
PDEVICE_OBJECT  pDeviceObject;
PIRP   pIrp;
KEVENT  kEvent;
//设置文件删除
FILE_DISPOSITION_INFORMATION FileInformation;
IO_STATUS_BLOCK   IoStatus;
PIO_STACK_LOCATION  pIrpSt;
PSECTION_OBJECT_POINTERS  pSectionObjectPointer;
ntstatus=ObReferenceObjectByHandle( hFile,DELETE,*IoFileObjectType,KernelMode,(PVOID*)&pFileObject,NULL );
if (!NT_SUCCESS(ntstatus)){
ObReferenceObject( pFileObject );
KdPrint(( "DeleteFileBySendIrp ObReferenceObjectByHandle Error" ));
return ntstatus;
}
pDeviceObject = IoGetRelatedDeviceObject(pFileObject);
pIrp = IoAllocateIrp(pDeviceObject->StackSize ,TRUE);
if (pIrp == NULL){
ObReferenceObject(pFileObject);
KdPrint(("DeleteFileBySendIrp IoAllocateIrp Error"));
return STATUS_UNSUCCESSFUL;
}
KeInitializeEvent(&kFsEvent ,SynchronizationEvent ,FALSE);
FileInformation.DeleteFile = TRUE; //置文件删除为有效状态
//向底层驱动传输删除指令信息
pIrp->AssociatedIrp.SystemBuffer = &FileInformation;
pIrp->UserEvent = &kFsEvent;
pIrp->UserIosb = &IoStatus;
pIrp->Tail.Overlay.Thread = PsGetCurrentThread();
pIrp->Tail.Overlay.OriginalFileObject = pFileObject;
pIrp->RequestorMode = KernelMode;
pIrpSt = IoGetNextIrpStackLocation( pIrp );
//设置I/O堆栈的相关属性信息,包括设置文件信息级别中的文件删除指令FileDispositionInformation、需要处理的文件对象和其对应的设备对象
pIrpSt->MajorFunction = IRP_MJ_SET_INFORMATION;
pIrpSt->DeviceObject = pDeviceObject;
pIrpSt->FileObject = pFileObject;
pIrpSt->Parameters.SetFile.Length = sizeof(FILE_DISPOSITION_INFORMATION);
pIrpSt->Parameters.SetFile.FileObject = pFileObject;
pIrpSt->Parameters.SetFile.FileInformationClass = FileDispositionInformation;
IoSetCompletionRoutine( pIrp,IoSetFileCompletion,NULL,TRUE,TRUE,TRUE );
//对文件对象中的SectionObjectPointers进行设置,清除所有与文件相关的链接。即清除虚拟内存管理器中设定的数据区结构、文件映射区域
pSectionObjectPointer = pFileObject->SectionObjectPointer;
pSectionObjectPointer->DataSectionObject = 0;
pSectionObjectPointer->ImageSectionObject = 0;
IoCallDriver( pDeviceObject ,pIrp );
if(ntstatus == STATUS_PENDING){
//等待同步对象
ntstatus = KeWaitForSingleObject( &kFsEvent ,Executive ,
KernelMode ,TRUE ,NULL );
KdPrint(("STATUS_PENDING\n"));
}
ObDereferenceObject( pFileObject );
KdPrint(( "OK" ));
return STATUS_SUCCESS;
}
//函数功能:驱动程序入口,设置指定的文件,并创建一个驱动设备,调用前面的两个功能函数,并卸载驱动等收尾工作
NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObj,IN PUNICODE_STRING RegistryPath){
NTSTATUS ntstatus = STATUS_SUCCESS;
HANDLE hFile = NULL;
UNICODE_STRING ntDeviceName;
UNICODE_STRING ntDosDeviceName;
PDEVICE_OBJECT pDevice;
WCHAR szFileName[] = L"\\??\\G:\\DriverDevelop\\HookDelete\\test.txt";
RtlInitUnicodeString(&ntDeviceName, NT_DEVICE_NAME);
ntstatus=IoCreateDevice(pDriverObj,0,&ntDeviceName,FILE_DEVICE_UNKNOWN,0,FALSE,&pDevice);
RtlInitUnicodeString(&ntDosDeviceName, NT_DOSDEVICE_NAME);
ntstatus = IoCreateSymbolicLink(&ntDosDeviceName, &ntDeviceName );
pDriverObj->MajorFunction[IRP_MJ_CREATE] = DispatchDevControl;
pDriverObj->MajorFunction[IRP_MJ_CLOSE] =  DispatchDevControl;
pDriverObj->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchDevControl;
pDriverObj->DriverUnload = DriverOnUnload;
hFile = GetFileHandle(szFileName);
ntstatus = ClearFileAttri(hFile);
if (!NT_SUCCESS(ntstatus)){
ZwClose(hFile);
KdPrint(( "ForceDeleteFiles StripFileAttributes Error" ));
return ns;
}
ntstatus = CompelDeletFile(hFile);
if(!NT_SUCCESS(ntstatus)){
ZwClose( hFile );
KdPrint(("ForceDeleteFiles DeleteFileBySendIrp Error"));
return ntstatus;
}
ZwClose(hFile);
return ntstatus;
}

性能测试
代码已经全部完成,现在就来测试一下。笔者采用Windows DDK 2600进行编译,当然需要用到SOURCE和MAKEFILE文件,我不太喜欢采用集成开发环境,所以也就没有配置,看个人的喜爱了。至于SOURCE和MakeFile文件如何写,这里就不作介绍了,读者可参考黑防2008年第1期的相关文章或相关驱动开发书籍等。
首先,采用Hook ZwSetInformation技术,简单的修改SSDT表实现文件保护。通过DriverMonitor加载由Hook ZwSetInformation技术编写的驱动,运行后,当删除受保护的文件test.txt时,弹出对话框。
下面编译本文介绍的内核级驱动技术编写的代码,将生成的.sys文件通过DriverMonitor加载并运行后,发现文件自动清除。读者可以自行将代码重新编写一遍,很好的理解其中各条语句,特别是如何向分层驱动发送IRP,处理IRP的各个过程,自行测试,看看执行后的效果。笔者还测试过如果不将文件属性改为常规属性时的特点,即不调用本文中介绍的函数ClearFileAttri。测试过程中,将待删除的文件设置为可读、隐藏、压缩文件,然后重新编译修改后的驱动,加载此驱动,发现指定的文件并没有删除。目前,笔者理解为FileDispositionInformation操作只对常规文件有效,可能还有其他的理解,希望大家共同交流,共同进步。综上,我们需要创建两个IRP,第一个IRP用于修改文件的属性,第二个IRP则用于发送文件删除指令和清除文件链结构。

小结
本文介绍了一种内核级驱动,向FSD发送特定的IRP达到强制性删除文件特效。前面的代码中没有写函数DispatchDevControl,此函数只是需要简单的调用IoCompleteRequest来完成IRP即可。在编写驱动和测试的时候,建议读者在虚拟机下玩转设备开发,以防蓝屏,其实开发驱动发生蓝屏现象是常有的情况。本文的理解需要读者具有一定的驱动开发知识,对驱动开发感兴趣的读者可以参考《Windows驱动开发技术详解》和《Linux驱动设备开发》等相关书籍。本文只是删除文件的一种技术,可能还存在其他更优秀的技术,希望大家共同交流。

原创粉丝点击