1.
背景
在
windows
平台下,应用程序通常使用
API
函数来进行文件访问,创建,打开,读写文件。从
kernel32
的
CreateFile/ReadFile/WriteFile
函数,到本地系统服务,再到
FileSystem
及其
FilterDriver
,经历了很多层次。在每个层次上,都存在着安全防护软件,病毒或者后门作监视或者过滤的机会。作为安全产品开发者,我们需要比别人走得更远,因此我们需要一个底层的
“windows
平台内核级文件访问
”
的方法来确保我们能够看到正确的干净的文件系统。
2.
用途
直接的内核级别文件访问,在信息安全领域内有广泛的用途。用于入侵者的方面,可以让他绕过杀毒软件,
IDS
等安全保护系统的监视。用于检测者的方面,可以看到一个干净的系统,以此来查杀隐藏的后门或者
rootkit
。用于监控者的方面,则可以了解最新的绕过监控的技术,可以根据来设计更新的监控方案。
3.
直接访问
FSD
的内核级别文件访问
FSD(FileSystemDriver)
层是文件
API
函数经过本地系统服务层
(native API)
最后到达的驱动层次。如果我们可以模仿操作系统,在我们自己的驱动程序里直接向
FSD
发送
IRP
,就可以绕过那些
native API
和
win32 API
了,也就可以绕过设置在这些层次上面的
API
钩子等监控措施。
3.1
文件的
Create
和
Open
文件的
Create
和
Open
可以通过发送
IRP_MJ_CREATE
给
FSD
,或者调用
IoCreateFile
函数来完成。
Create
和
Open
的区别实际上在于
IoCreateFile/IRP_MJ_CREATE
的一个参数
Disposition
的取值。使用
IoCreateFile
函数的样例代码:
HANDLE openfile(WCHAR
*
name,ACCESS_MASK access,ULONG share)
{ // return 0 for error. HANDLE hfile; IO_STATUS_BLOCK iosb; int stat; OBJECT_ATTRIBUTES oba; UNICODE_STRING nameus; /**/ /// if (KeGetCurrentIrql() > PASSIVE_LEVEL) { return 0 ;} RtlInitUnicodeString(& nameus,name); InitializeObjectAttributes( & oba, & nameus,OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE, 0 , 0 ); stat = IoCreateFile( & hfile,access, & oba, & iosb, 0 ,FILE_ATTRIBUTE_NORMAL,share,FILE_OPEN, 0 , 0 , 0 , 0 , 0 , 0 ); if ( ! NT_SUCCESS(stat)) { return 0 ;} return hfile; }
HANDLE createnewfile(WCHAR
*
name,ACCESS_MASK access,ULONG share)
{ // return 0 for error. HANDLE hfile; IO_STATUS_BLOCK iosb; int stat; OBJECT_ATTRIBUTES oba; UNICODE_STRING nameus; /**/ /// if (KeGetCurrentIrql() > PASSIVE_LEVEL) { return 0 ;} RtlInitUnicodeString(& nameus,name); InitializeObjectAttributes( & oba, & nameus,OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE, 0 , 0 ); stat = IoCreateFile( & hfile,access, & oba, & iosb, 0 , // AllocationSize this set to 0 that when file opened it was zeroed. FILE_ATTRIBUTE_NORMAL,share,FILE_OVERWRITE_IF, 0 , 0 , 0 , 0 , 0 , 0 ); if ( ! NT_SUCCESS(stat)) { return 0 ;} return hfile; }
通过发送
IRP_MJ_CREATE
给
FSD
的方法与此类似,可以参考
IFSDDK document
的
IRP_MJ_CREATE
说明。不同于上面方法的是需要自己创建一个
FILE_OBJECT
,好于上面方法的是这种方法不需要一个
HANDLE
,
HANDLE
是线程依赖的
,FileObject
则是线程无关。
3.2
文件的
Read
和
Write
我们通过给
FSD
发送
IRP_MJ_READ
来读取文件,给
FSD
发送
IRP_MJ_WRITE
来改写文件。
如果我们是通过一个
HANDLE
来执行
(
如使用
IoCreateFile
打开的文件
)
,就要先用
ObReferenceObjectByHandle
函数来获得这个
Handle
对应的
FileObject
。我们只能给
FileObject
发送
IRP
。
stat
=
ObReferenceObjectByHandle(handle,GENERIC_READ,
*
IoFileObjectType,KernelMode,(PVOID
*
)
&
fileob,
0
);
之后我们使用
IoAllocateIrp
分配一个
IRP
。根据
FileObject->DeviceObject->Flags
的值,我们判断目标文件系统使用什么样的
IO
方式。
if
(fileob
->
DeviceObject
->
Flags
&
DO_BUFFERED_IO)
{ irp -> AssociatedIrp.SystemBuffer = buffer; // buffered io }
else
if
(fileob
->
DeviceObject
->
Flags
&
DO_DIRECT_IO)
{ mdl = IoAllocateMdl(buffer,count, 0 , 0 , 0 ); MmBuildMdlForNonPagedPool(mdl); irp -> MdlAddress = mdl; // direct io }
else
{ irp -> UserBuffer = buffer; // neither i/o, use kernel buffer }
对每种不同的
IO
方式使用不同的地址传递方式。随后我们填充
IRP
内的各个参数域,就可以发送
IRP
了。以
Read
为例:
irpsp
->
FileObject
=
fileob; irpsp
->
MajorFunction
=
IRP_MJ_READ; irpsp
->
MinorFunction
=
IRP_MN_NORMAL;
//
0
irpsp
->
Parameters.Read.ByteOffset
=
offsetused; irpsp
->
Parameters.Read.Key
=
0
; irpsp
->
Parameters.Read.Length
=
count;
接着要考虑如果
IRP
不能及时完成,会异步的返回的情况,我们安装一个
CompletionRoutine
,在
CompletionRoutine
里面设置一个事件为已激活,通知我们的主线程读取或者写入操作已经完成。
IoSetCompletionRoutine(irp,IoCompletion,
&
event
,
1
,
1
,
1
); NTSTATUS IoCompletion( IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp, IN PVOID Context )
{ KeSetEvent((PRKEVENT)Context, IO_DISK_INCREMENT, 0 ); return STATUS_MORE_PROCESSING_REQUIRED; }
现在可以发送
IRP
了。如果不采取特殊的措施的话,
IRP
发送目标是
FileObject
对应的
DeviceObject
。发送后,等待
IRP
的完成并且释放资源,返回。
stat
=
IoCallDriver(fileob
->
DeviceObject,irp);
if
(stat
==
STATUS_PENDING)
{ KeWaitForSingleObject( & event , Executive,KernelMode, 0 , 0 ); stat = irp -> IoStatus.Status; }
if
(
!
NT_SUCCESS(stat))
{ IoFreeIrp(irp); if (mdl) {IoFreeMdl(mdl);} // if DO_DIRECT_IO return - 1 ; }
stat
=
irp
->
IoStatus.Information;
//
bytes read
IoFreeIrp(irp);
if
(mdl)
{IoFreeMdl(mdl);}
//
if DO_DIRECT_IO
return
stat;
3.3
文件的
Delete Delete
实际上是通过向
FSD
发送
IRP_MJ_SET_INFORMATION
的
IRP
,并把
IrpSp->Parameters.SetFile.FileInformationClass
设置为
FileDispositionInformation
,用一个
FILE_DISPOSITION_INFORMATION
结构填充
buffer
来执行的。
fdi.DeleteFile
=
TRUE; irpsp
->
MajorFunction
=
IRP_MJ_SET_INFORMATION; irpsp
->
Parameters.SetFile.Length
=
sizeof
(FILE_DISPOSITION_INFORMATION); irpsp
->
Parameters.SetFile.FileInformationClass
=
FileDispositionInformation; irpsp
->
Parameters.SetFile.DeleteHandle
=
(HANDLE)handle;
3.4
文件的
Rename
类似于
Delete
,
Rename
是向
FSD
发送
IRP_MJ_SET_INFORMATION
的
IRP
,把
IrpSp->Parameters.SetFile.FileInformationClass
设置为
FileRenameInformation
,填充
buffer
为
FILE_RENAME_INFORMATION
结构。
fri.ReplaceIfExists
=
TRUE; fri.RootDirectory
=
0
;
//
Set fri.FileName to full path name.
fri.FileNameLength
=
wcslen(filename)
*
2
; wcscpy(fri.FileName,filename);
//
If the RootDirectory member is NULL, and the file is being moved to a different directory, this member specifies the full pathname to be assigned to the file.
irpsp
->
MajorFunction
=
IRP_MJ_SET_INFORMATION; irpsp
->
Parameters.SetFile.Length
=
sizeof
(FILE_FILE_RENAME_INFORMATION); irpsp
->
Parameters.SetFile.FileInformationClass
=
FileRenameInformation;
综上,于是我们可以在驱动里面通过发送 IRP 来直接访问文件系统了,绕过了 native API 和 win32 API 层次。 4.绕过文件系统过滤驱动和钩子 有了第三部分的内容,我们目前可以直接给 FSD 发送请求操作文件。但是这还不够,因为有很多的杀毒软件或者监视工具使用 FSD Filter Driver 或者 FSD Hook 的办法来监控文件操作。在今天这篇文章里我讲一些原理性的东西,提供绕过 FSD Filter Driver / FSD Hook 的思路。 4.1对付文件系统过滤驱动 文件系统过滤驱动 Attach 在正常的文件系统之上,监视和过滤我们的文件访问。文件系统驱动栈就是由这一连串的 Attach 起来的过滤驱动组成。我们可以用 IoGetRelatedDeviceObject 这个函数来获得一个 FileObject 对应的最底层的那个功能驱动对象 (FDO) 。但是这样虽然绕过了那些过滤驱动,却同时也绕过了正常的 FSD 如 Ntfs/Fastfat ,因为正常的 FSD 也是作为一个过滤驱动存在的。磁盘文件对象的对应的最底层的 FDO 是 Ftdisk.sys ,它已经因为过于底层而不能处理我们投递的 IRP 请求。 其实正常的 FSD 信息存储在一个 Vpb 结构中,我们可以使用 IoGetBaseFileSystemDeviceObject 这个未公开的内核函数来得到它。它就是我们发送 IRP 的目标了。 4.2对付替换 DispatchRoutine 的 FSD Hook 这是一种常用的 FSD Hook 方式。我们需要得到原本的 DispatchRoutine ,向原本的 DispatchRoutine 发送我们的 IRP 。这里提供一个思路:我们可以读取原本 FSD 驱动的 .INIT 段或者 .TEXT 段,查找其 DriverEntry 函数,在它的 DriverEntry 函数中肯定设置了自己的 DriverObject 的各个 DispatchRoutine 。在这个函数中我们就能找到我们想要的 DispatchRoutine 的地址。只需要使用特征码搜索的方法就可以搜索到这个值。 4.3对付 Inline Hook DispatchRoutine 函数本身的 FSD Hook 这种 Hook 方法比较狠毒,但不是非常常见于安全产品中,一般应用在木马和 rootkit 上,比如我自己写的 rootkit 。它没有更改 DriverObject 里面的 DispatchRoutine 的函数指针,而是向函数开头写入汇编指令的 JMP 来跳转函数。对付它的基本思路就是读取存在磁盘上的 FSD 的文件,加载到内存一份干净的备份,察看我们要调用的 DispatchRoutine 开头的几个字节和这个干净备份是否一致。如果不一致,尤其是存在 JMP,RET,INT3 一类的汇编指令的时候,很可能就是存在了 Inline Hook 。(但要充分考虑重定位的情况。)如果存在 Inline Hook ,我们就把干净的函数开头拷贝过来覆盖掉被感染的函数头。然后在发送 IRP ,就不会被 Inline Hook 监视或篡改了。