在32位的系统下,我们想要实现某些监控十分简单,只需要找到对应的API实现挂钩操作即可检测进程。但在64位系统下随着Patch Guard
的引入,导致我们如果继续使用挂钩API的方式进行监控会出现不可控的情况发生。微软也考虑到了用户程序的开发,所以开放了方便用户调用的系统回调API函数,在64位系统下的监控,使用系统回调相对于直接hook的方式往往是更值得青睐的一方
进程监控&保护
PsSetCreateProcessNotifyRoutineEx
这个函数主要是设置进程回调监控进程创建与退出
1 | NTSTATUS PsSetCreateProcessNotifyRoutineEx( |
PsSetCreateProcessNotifyRoutineEx
这个函数并不是随便就能够使用的,微软为了确保安全性要求拥有数字签名的驱动才能够使用此函数。这里微软如何检测是否有数字签名呢?这里就使用到了强制完整性检查
强制完整性检查是一种确保正在加载的二进制文件在加载前需要使用签名的策略,IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY
标志在链接时通过使用/integritycheck
链接器标志在PE头中进行设置,让正在加载的二进制文件必须签名,这个标志使windows内存管理器在加载时对二进制文件进行签名检查
那么微软就是通过加载二进制文件时是否存在标志来确认驱动的发布者身份是否为已知状态,这就是强制完整性检查
这里在内核里面,windows使用到MmVerifyCallbackFunction
这个内核函数来判断
到IDA里面继续跟MmVerifyCallbackFunction
这个函数,发现其逻辑就是通过比较[rax+68h]
是否包含了0x20来判断是否拥有正确的数字签名
这里的rax
表示DriverSection
,而DriverSection
指向的是_LDR_DATA_TABLE_ENTRY
结构,那么[rax + 0x68]
指向的就是ProcessStaticImport
那么如果我们要使用PsSetCreateProcessNotifyRoutineEx
这个函数就需要拥有数字签名,这里我们就可以将DriverObject->DriverSection->Flags
的值与0x20
按位或即可
这里我们就可以编写一个绕过强制完整性检查的函数,注意一下在32位和64位结构体的定义不同,需要分开定义
1 | BOOLEAN bypass_signcheck(PDRIVER_OBJECT pDriverObject) |
到这里我们就已经绕过了微软的强制完整性检查,能够调用PsSetCreateProcessNotifyRoutineEx
函数,可以看到PsSetCreateProcessNotifyRoutineEx
的第一个参数指向CREATE_PROCESS_NOTIFY_ROUTINE_EX
,来执行我们需要执行的回调函数,这里我们继续看PCREATE_PROCESS_NOTIFY_ROUTINE_EX
这个结构
PCREATE_PROCESS_NOTIFY_ROUTINE_EX
第一个参数是Process
,指向EPROCESS
结构,第二个参数ProcessId
就是PID,第三个参数CreateInfo
是一个指向PS_CREATE_NOTIFY_INFO
的指针,当它为NULL
时表明进程退出,不为NULL
时表明进程创建,里面存储着要创建的进程信息
1 | PCREATE_PROCESS_NOTIFY_ROUTINE_EX PcreateProcessNotifyRoutineEx; |
msdn的定义如下
然后我们再去看一下PS_CREATE_NOTIFY_INFO
PS_CREATE_NOTIFY_INFO
1 | typedef struct _PS_CREATE_NOTIFY_INFO { |
msdn定义如下
这里的话我们要注意两个值,一个是ImageFileName
即要创建的进程名,一个是CreationStatus
,我们可以看到msdn里面说驱动程序可以将此值修改为错误代码以防止创建进程,这里我们如果想阻止进程创建就可以把这个值设置为STATUS_UNSUCCESSFUL
我们去WRK里面看一下实现,这个API是64位才有的,所以在WRK里面是没有PsSetCreateProcessNotifyRoutineEx
这个函数的,但是在32位下有一个PsSetCreateProcessNotifyRoutine
,我们看一下
通过源码可以发现是操作数组,这个数组里面存放的是我们填写的回调,而操作系统会依次调用回调,那我们跟随数组查看发现是个定长数组,里面只有8项,在64位系统下,这个数组的长度变为了64项
根据PCREATE_PROCESS_NOTIFY_ROUTINE_EX
的结构定义回调函数
1 | VOID CreateProcessNotifyEx( |
那么我们这里通过PsSetCreateProcessNotifyRoutineEx
设置回调函数,通过判断status
的返回值判断回调函数是否设置成功
1 | NTSTATUS SetReFunction() |
然后进行回调函数的实现
1 | VOID CreateProcessNotifyEx(PEPROCESS Process, HANDLE ProcessId, PPS_CREATE_NOTIFY_INFO CreateInfo) |
首先判断CreateInfo
的值,如果为NULL
则表示进程退出,如果不为NULL
才为进程的创建
1 | if (CreateInfo == NULL) |
那么这里通过PsGetProcessImageFileName
获取进程名之后进行判断,如果是我们想要拦截的进程就通过设置CreationStatus
的值为STATUS_UNSUCCESSFUL
来阻止进程的创建
1 | else |
这里我们的回调函数就已经完成,这里需要注意,在卸载驱动的时候就需要将回调函数摘除,否则新创建或者退出的进程会因为找不到回调函数而导致蓝屏
1 | VOID DriverUnload(IN PDEVICE_OBJECT driverObject) |
实现效果
首先注册一下驱动
然后这里首先执行一下我们的exe
然后加载我们的驱动可以看到这里test.exe
已经不能够运行
那么这里我们再卸载一下驱动可以发现又可以运行成功
这里可能有点不太明显,我们将拦截的exe改成notepad.exe
看下效果
启动驱动可以看到这里启动失败
卸载驱动即可启动成功
线程监控&保护
PsSetCreateThreadNotifyRoutine
线程监控使用到的API相对于进程监控简单,使用到PsSetCreateThreadNotifyRoutine
,而这个值并不能像进程操作的API一样进行操作,这里我们首先先使用这个API来进行线程的监控
1 | NTSTATUS PsSetCreateThreadNotifyRoutine( |
NotifyRoutine
指向PCREATE_THREAD_NOTIFY_ROUTINE
PcreateThreadNotifyRoutine
的结构如下,第一个参数为PID,第二个参数为TID,第三个参数表示是创建线程还是删除线程,创建线程则为TRUE
,删除线程则为FALSE
1 | PCREATE_THREAD_NOTIFY_ROUTINE PcreateThreadNotifyRoutine; |
那么我们这里就可以写出CREATE_THREAD_NOTIFY_ROUTINE
函数
1 | VOID CreateThreadNotifyRoutine(HANDLE ProcessId, HANDLE ThreadId, BOOLEAN Create) |
这里如果要将函数摘除,就需要用到PsRemoveCreateThreadNotifyRoutine
函数,定义如下
1 | NTSTATUS PsRemoveCreateThreadNotifyRoutine( |
1 | NTSTATUS status = PsRemoveCreateThreadNotifyRoutine(CreateThreadNotifyRoutine); |
我们如果想让进行线程的保护该如何操作呢?我们想要阻止线程的创建,就首先要找到对应的进程,再去进行拦截,因为在64位下都是通过回调的方式实现,那么我们就可以通过找到线程回调的地址,然后直接改为ret
即可起到拦截线程创建的效果
1.通过PID找到EPROCESS
2.通过TID找到ETHREAD
3.通过EPROCESS得到进程路径
4.通过进程路径对应进程名
5.判断进程名是否相同
<1>若相同则找到线程回调函数的地址修改内容为ret
<2>若不相同则退出
那么我们该如何找到线程回调函数的地址呢?这里查阅资料后发现,3环将回调函数的地址放在了ETHREAD + 0x410
偏移的Win32StartAddress
里面
那么这里我们就能进行函数的编写,首先我们通过PsSetCreateThreadNotifyRoutine
注册一个线程回调函数
1 | NTSTATUS status = PsSetCreateThreadNotifyRoutine(CreateThreadNotify); |
然后我们再去写CreateThreadNotify
这个回调函数,首先获取EPROCESS
、ETHREAD
,然后通过EPROCESS
获取进程名
1 | status = PsLookupProcessByProcessId(ProcessId, &Process); |
然后再判断进程名是否为我们要保护的线程
1 | if (strstr(pszImageName, "notepad") != NULL) |
定位到回调函数的地址判断内存空间是否可用
1 | pWin32Address = *(UCHAR**)((UCHAR*)Thread + 0x410); |
这里的话定位到了回调函数的地址,如果我们要修改回调函数的值就要修改页保护属性,但是在64位下是不允许使用内联汇编的,这里的话就需要使用到汇编生成.obj
文件来使用
1 | ClosePageProtect(); |
修改完成之后这里我们使用ObDereferenceObject
,减少引用计数
1 | if( Process ) |
这里因为我在win10 x64上做的实验,这里在关闭保护属性的时候一直报错导致0xC3
一直修改不成功,这里就不放图了
模块监控&保护
PsSetLoadImageNotifyRoutine
和之前的函数一样都是指向一个结构,这里是LOAD_IMAGE_NOTIFY_ROUTINE
1 | NTSTATUS PsSetLoadImageNotifyRoutine( |
PLOAD_IMAGE_NOTIFY_ROUTINE
这里第一个参数是指向缓冲的Unicode字符串的指针,用于标识可执行映像文件,第二个参数表示PID,第三个参数指向IMAGE_INFO
1 | PLOAD_IMAGE_NOTIFY_ROUTINE SetLoadImageNotifyRoutine; |
IMAGE_INFO
1 | typedef struct _IMAGE_INFO { |
具体成员的作用如下
- Properties
ImageAddressingMode
始终设置为IMAGE_ADDRESSING_MODE_32BIT。- SystemModeImage
设置为一个用于新加载的内核模式组件(如驱动程序),或者对于映射到用户空间的映像设置为 0。- ImageMappedToAllPids
始终设置为0。- ExtendedInfoPresent
如果设置了ExtendedInfoPresent标志,则IMAGE_INFO结构是图像信息结构的较大扩展版本的一部分(请参阅IMAGE_INFO_EX)。在Windows Vista中添加。有关详细信息,请参阅本备注部分的“扩展版本的图像信息结构”。- MachineTypeMismatch
始终设置为 0。在Windows 8 / Windows Server 2012中添加。- ImageSignatureLevel
代码完整性标记为映像的签名级别。该值是ntddk.h中的#define SESIGNING_LEVEL *常量之一。在Windows 8.1 / Windows Server 2012 R2中添加。- ImageSignatureType
代码完整性标记为映像的签名类型。该值是在ntddk.h中定义的SE_IMAGE_SIGNATURE_TYPE枚举值。在Windows 8.1 / Windows Server 2012 R2中添加。- ImagePartialMap
如果调用的映像视图是不映射整个映像的部分视图,则该值不为零; 0如果视图映射整个图像。在Windows 10 / Windows Server 2016中添加。- Reserved
始终设置为 0。- ImageBase
设置为映像的虚拟基地址。- ImageSelector
始终设置为 0。- ImageSize
映像的虚拟大小(以字节为单位)。- ImageSectionNumber
始终设置为 0。
那么我们首先还是定义一下回调函数
1 | void SetLoadImageNotifyRoutine( |
我们的回调函数在接收到消息的时候模块已经加载完成了,那么这里我们就只能够进行模块的卸载操作,在模块的ImageInfo
结构里面提供了加载的ImageBase
,那么我们只需要找到OEP,即可计算得到DriverEntry
的地址。那么我们找到入口点函数的地址之后,就可以修改错误码为STATUS_ACCESS_DENIED
即0xC0000022
,就能够达到卸载驱动模块的效果
1 | mov eax, 0xC0000022 |
对应的硬编码为B8 22 00 00 C0 C3
那么这里我们就可以进行卸载驱动模块函数的编写,首先定义指针指向OEP
1 | PIMAGE_DOS_HEADER pDosHeader = pLoadImageBase; |
然后写入shellcode,通过MmCreateMdl
写入,我们知道在内核里面是不能够随便进行读写操作的,这里就可以通过MDL写入的方式映射到虚拟内存实现shellcode的写入
1 | ULONG CodeSize = 6; |
使用MmBuildMdlForNonPagedPool
更新MDL对物理内存的描述并映射到虚拟内存,然后写入数据,释放MDL
1 | MmBuildMdlForNonPagedPool(pMdl); |
然后我们再尝试对DLL模块进行卸载,这里的话就不能像卸载驱动模块直接在入口点返回,因为DLL的入口点函数的返回值并不能够确定DLL能否加载成功。这里windows提供了一个未文档化的函数MmUnmapViewOfSection
用来卸载进程中已经加载的模块
那么我们要想卸载模块,首先就肯定要获取所有的模块,使用到PsSetLoadImageNotifyRoutine
设置回调函数来获取模块的加载信息
windows为了避免死锁,在进行模块加载回调函数的时候不能够进行其他操作,也就是说我们想要卸载DLL模块则需要等所有模块加载完毕之后才能进行卸载操作
这里来进行函数的编写
1 | NTSTATUS NoLoadDll(HANDLE ProcessId, PVOID pImageBase) |
实现效果
这里要实现的效果就是阻止DriverTest.sys
的加载
首先启动我们的监控驱动,然后加载DriverTest.sys
可以看到拒绝访问
然后再卸载我们的监控驱动之后DriverTest.sys
加载成功
然后我们再尝试注入Test.dll
,可以看到注入失败
我们再去xp上尝试一下,首先加载驱动
当我们打开一个程序的时候都会打印出当前进程加载的dll模块
然后注入DLL,也是被拦截