前言
在32位的系统下,我们想要实现某些监控十分简单,只需要找到对应的API实现挂钩操作即可检测进程。但在64位系统下随着Patch Guard
的引入,导致我们如果继续使用挂钩API的方式进行监控会出现不可控的情况发生。微软也考虑到了用户程序的开发,所以开放了方便用户调用的系统回调API函数,在64位系统下的监控,使用系统回调相对于直接hook的方式往往是更值得青睐的一方。
进程监控&保护
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位结构体的定义不同,需要分开定义
BOOLEAN bypass_signcheck(PDRIVER_OBJECT pDriverObject)
{
#ifdef _WIN64
typedef struct _KLDR_DATA_TABLE_ENTRY
{
LIST_ENTRY listEntry;
ULONG64 __Undefined1;
ULONG64 __Undefined2;
ULONG64 __Undefined3;
ULONG64 NonPagedDebugInfo;
ULONG64 DllBase;
ULONG64 EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING path;
UNICODE_STRING name;
ULONG Flags;
USHORT LoadCount;
USHORT __Undefined5;
ULONG64 __Undefined6;
ULONG CheckSum;
ULONG __padding1;
ULONG TimeDateStamp;
ULONG __padding2;
} KLDR_DATA_TABLE_ENTRY, *PKLDR_DATA_TABLE_ENTRY;
#else
typedef struct _KLDR_DATA_TABLE_ENTRY
{
LIST_ENTRY listEntry;
ULONG unknown1;
ULONG unknown2;
ULONG unknown3;
ULONG unknown4;
ULONG unknown5;
ULONG unknown6;
ULONG unknown7;
UNICODE_STRING path;
UNICODE_STRING name;
ULONG Flags;
} KLDR_DATA_TABLE_ENTRY, *PKLDR_DATA_TABLE_ENTRY;
#endif
PKLDR_DATA_TABLE_ENTRY pLdrData = (PKLDR_DATA_TABLE_ENTRY)pDriverObject->DriverSection;
pLdrData->Flags = pLdrData->Flags | 0x20;
return TRUE;
}
到这里我们就已经绕过了微软的强制完整性检查,能够调用PsSetCreateProcessNotifyRoutineEx
函数,可以看到PsSetCreateProcessNotifyRoutineEx
的第一个参数指向CREATE_PROCESS_NOTIFY_ROUTINE_EX
,来执行我们需要执行的回调函数
PCREATE_PROCESS_NOTIFY_ROUTINE_EX
第一个参数是Process
,指向EPROCESS
结构,第二个参数ProcessId
就是PID,第三个参数CreateInfo
是一个指向PS_CREATE_NOTIFY_INFO
的指针,当它为NULL
时表明进程退出,不为NULL
时表明进程创建,里面存储着要创建的进程信息
msdn的定义如下
然后我们再去看一下PS_CREATE_NOTIFY_INFO
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
的结构定义回调函数
那么我们这里通过PsSetCreateProcessNotifyRoutineEx
设置回调函数,通过判断status
的返回值判断回调函数是否设置成功
NTSTATUS SetReFunction()
{
NTSTATUS status = PsSetCreateProcessNotifyRoutineEx((PCREATE_PROCESS_NOTIFY_ROUTINE_EX)CreateProcessNotifyEx, FALSE);
if (!NT_SUCCESS(status))
{
DbgPrint("回调函数设置失败, status=%X", status);
}
else
{
DbgPrint("进程监控已开启\r\n");
}
}
然后进行回调函数的实现
VOID CreateProcessNotifyEx(PEPROCESS Process, HANDLE ProcessId, PPS_CREATE_NOTIFY_INFO CreateInfo)
首先判断CreateInfo
的值,如果为NULL
则表示进程退出,如果不为NULL
才为进程的创建
那么这里通过PsGetProcessImageFileName
获取进程名之后进行判断,如果是我们想要拦截的进程就通过设置CreationStatus
的值为STATUS_UNSUCCESSFUL
来阻止进程的创建
这里我们的回调函数就已经完成,这里需要注意,在卸载驱动的时候就需要将回调函数摘除,否则新创建或者退出的进程会因为找不到回调函数而导致蓝屏
VOID DriverUnload(IN PDEVICE_OBJECT driverObject)
{
NTSTATUS status = PsSetCreateProcessNotifyRoutineEx((PCREATE_PROCESS_NOTIFY_ROUTINE_EX)CreateProcessNotifyEx, TRUE);
if (!NT_SUCCESS(status))
{
DbgPrint("回调函数删除失败\r\n status=%X", status);
}
else
{
DbgPrint("回调函数成功删除\r\n");
}
DbgPrint("驱动卸载完成\r\n");
}
实现效果
首先注册一下驱动
然后这里首先执行一下我们的exe
然后加载我们的驱动可以看到这里test.exe
已经不能够运行
那么这里我们再卸载一下驱动可以发现又可以运行成功
这里可能有点不太明显,我们将拦截的exe改成notepad.exe
看下效果
启动驱动可以看到这里启动失败
卸载驱动即可启动成功