对于内核层实现监控进程的创建或者退出,你可能第一时间会想到 HOOK 内核函数 ZwOpenProcess、ZwTerminateProcess 等。确定,在内核层中的 HOOK 已经给人留下太多深刻的印象了,有 SSDT HOOK、Inline HOOK、IRP HOOK、过滤驱动等等。
但是,Windows 其实给我们提供现成的内核函数接口,方便我们在内核下监控用户层上进程的创建和退出的情况。即 PsSetCreateProcessNotifyRoutineEx 内核函数,可以设置一个回调函数,来监控进程的创建和退出,同时还能控制是否允许创建进程。
现在,本文就使用 PsSetCreateProcessNotifyRoutineEx 实现监控进程的创建的实现过程和原理进行整理,形成文档,分享给大家。
1.PsSetCreateProcessNotifyRoutineEx 函数
设置进程回调监控进程创建与退出,而且还能控制是否允许进程创建
函数声明
NTSTATUS PsSetCreateProcessNotifyRoutineEx(
_In_ PCREATE_PROCESS_NOTIFY_ROUTINE_EX NotifyRoutine,
_In_ BOOLEAN Remove
);
参数
- NotifyRoutine [in]
指向PCREATE_PROCESS_NOTIFY_ROUTINE_EX例程以注册或删除的指针。 创建新进程时,操作系统将调用此例程。 - Remove[in]
一个布尔值,指定PsSetCreateProcessNotifyRoutineEx是否会从回调例程列表中添加或删除指定的例程。 如果此参数为TRUE,则从回调例程列表中删除指定的例程。 如果此参数为FALSE,则将指定的例程添加到回调例程列表中。 如果删除为TRUE,系统还会等待所有正在运行的回调例程运行完成。
返回值
- 成功,则返回 STATUS_SUCCESS;否则,返回其它失败错误码 NTSTATUS。
2.PCREATE_PROCESS_NOTIFY_ROUTINE_EX 回调函数
函数声明
PCREATE_PROCESS_NOTIFY_ROUTINE_EX SetCreateProcessNotifyRoutineEx;
void SetCreateProcessNotifyRoutineEx(
_In_ HANDLE ParentId,
_In_ HANDLE ProcessId,
_Inout_opt_ PPS_CREATE_NOTIFY_INFO CreateInfo
)
{ ... }
参数
- ParentId [in]
父进程的进程ID。 - ProcessId [in]
进程的进程ID。 - CreateInfo [in,out,optional]
指向PS_CREATE_NOTIFY_INFO结构的指针,其中包含有关新进程的信息。为 NULL 时,表示进程退出;不为 NULL 时,表示进程创建。
返回值
- 无返回值。
3.PS_CREATE_NOTIFY_INFO 结构体
typedef struct _PS_CREATE_NOTIFY_INFO {
SIZE_T Size;
union {
ULONG Flags;
struct {
ULONG FileOpenNameAvailable :1;
ULONG IsSubsystemProcess :1;
ULONG Reserved :30;
};
};
HANDLE ParentProcessId;
CLIENT_ID CreatingThreadId;
struct _FILE_OBJECT *FileObject;
PCUNICODE_STRING ImageFileName;
PCUNICODE_STRING CommandLine;
NTSTATUS CreationStatus;
} PS_CREATE_NOTIFY_INFO, *PPS_CREATE_NOTIFY_INFO;
成员
- Size
该结构的大小(以字节为单位)。 - Flags
保留。 请改用FileOpenNameAvailable成员。 -
FileOpenNameAvailable
一个布尔值,指定ImageFileName成员是否包含用于打开进程可执行文件的确切文件名。 -
IsSubsystemProcess
指示进程子系统类型的布尔值是Win32以外的子系统。 -
Reserved
保留供系统使用。 -
ParentProcessId
新进程的父进程的进程ID。 请注意,父进程不一定与创建新进程的进程相同。 新进程可以继承父进程的某些属性,如句柄或共享内存。 (进程创建者的进程ID由CreatingThreadId-> UniqueProcess给出。) -
CreatingThreadId
创建新进程的进程和线程的进程ID和线程ID。 CreatingThreadId-> UniqueProcess包含进程ID,而CreatingThreadId-> UniqueThread包含线程ID。 -
FileObject
指向进程可执行文件的文件对象的指针。如果IsSubsystemProcess为TRUE,则此值可能为NULL。 -
ImageFileName
指向保存可执行文件的文件名的UNICODE_STRING字符串的指针。 如果FileOpenNameAvailable成员为TRUE,则该字符串指定用于打开可执行文件的确切文件名。 如果FileOpenNameAvailable为FALSE,则操作系统可能仅提供部分名称。如果IsSubsystemProcess为TRUE,则此值可能为NULL。 -
CommandLine
指向UNICODE_STRING字符串的指针,该字符串保存用于执行该过程的命令。 如果命令不可用,CommandLine为NULL。如果IsSubsystemProcess为TRUE,则此值可能为NULL。 -
CreationStatus
用于进程创建操作返回的NTSTATUS值。 驱动程序可以将此值更改为错误代码,以防止创建进程。
4.实现原理
破解 PsSetCreateProcessNotifyRoutineEx 函数的使用限制
第一种方法
在讲解怎么使用 PsSetCreateProcessNotifyRoutineEx 函数来注册回调之前,先来讲解下 Windows 对这个函数做的限制:驱动程序必须有数字签名才能使用此函数。经逆向研究,内核通过 MmVerifyCallbackFunction 验证此回调是否合法, 但此函数只是简单的验证了一下 DriverObject->DriverSection->Flags 的值是不是为 0x20:
nt!MmVerifyCallbackFunction+0x75:
fffff800`01a66865 f6406820 test byte ptr [rax+68h],20h
fffff800`01a66869 0f45fd cmovne edi,ebp
所以破解方法非常简单,只要把 DriverObject->DriverSection->Flags 的值按位或 0x20 即可。其中,DriverSection 是指向 LDR_DATA_TABLE_ENTRY 结构的值,要注意该结构在 32 位和 64 位系统下的定义。
// 注意32位与64位的对齐大小
#ifndef _WIN64
#pragma pack(1)
#endif
typedef struct _LDR_DATA_TABLE_ENTRY
{
LIST_ENTRY InLoadOrderLinks;
LIST_ENTRY InMemoryOrderLinks;
LIST_ENTRY InInitializationOrderLinks;
PVOID DllBase;
PVOID EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
ULONG Flags;
USHORT LoadCount;
USHORT TlsIndex;
union
{
LIST_ENTRY HashLinks;
struct
{
PVOID SectionPointer;
ULONG CheckSum;
};
};
union
{
ULONG TimeDateStamp;
PVOID LoadedImports;
};
PVOID EntryPointActivationContext;
PVOID PatchInformation;
LIST_ENTRY ForwarderLinks;
LIST_ENTRY ServiceTagLinks;
LIST_ENTRY StaticLinks;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;
#ifndef _WIN64
#pragma pack()
#endif
第二种方法
使用此函数, 一定要设置 IMAGE_OPTIONAL_HEADER 中的 DllCharacterisitics 字段设置为:IMAGE_DLLCHARACTERISITICS_FORCE_INTEGRITY 属性,该属性是一个驱动强制签名属性。
使用 VS2013 开发环境设置方式是:
-
右击项目,选择属性
-
选中配置属性中的链接器,点击命令行
-
在其它选项中输入: /INTEGRITYCHECK 表示设置; /INTEGRITYCHECK:NO 表示不设置
这样,设置之后,驱动程序必须要进行驱动签名才可正常运行!
创建回调并监控进程创建
我们根据上面的函数介绍,大概知道实现的流程了吧。对于设置回调函数,直接调用 PsSetCreateProcessNotifyRoutineEx 函数来设置就好。传入设置的回调函数名称以及删除标志参数设置为 FALSE,表示创建回调函数。这样,就可以成功设置进程监控的回调函数了。
那么,我们的回调函数也并不复杂,它的函数声明为:
void SetCreateProcessNotifyRoutineEx(
_In_ HANDLE ParentId,
_In_ HANDLE ProcessId,
_Inout_opt_ PPS_CREATE_NOTIFY_INFO CreateInfo
);
回调函数的名称可以任意,但是返回值类型以及函数参数类型必须是固定的,不能变更。回调函数的第一个参数 ParentId 表示父进程ID,第二个参数 ProcessId 表示进程ID,第三个参数 CreateInfo 为 NULL 时,表示进程退出;不为 NULL 时,表示进程创建。那么,创建进程的信息就存储在 PS_CREATE_NOTIFY_INFO 结构体中。
我们可以从 PS_CREATE_NOTIFY_INFO 中获取进程名称、路径、命令行、PID等进程信息。同时,可以通过设置成员 CreationStatus 的值来控制进程是否创建。当 CreationStatus 的值为 STATUS_SUCCESS 表示创建进程,否则,不创建进程。例如不创建进程的时候,CreationStatus 可以为 STATUS_UNSUCCESSFUL 错误码。
当我们要删除回调设置的时候,只需要调用 PsSetCreateProcessNotifyRoutineEx 函数,传入回调函数名称以及删除标志参数设置为 TRUE。这样,就可以成功删除设置的回调函数了。
5.编码实现
编程方式绕过签名检查
// 编程方式绕过签名检查
BOOLEAN BypassCheckSign(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;
}
设置回调
// 设置回调函数
NTSTATUS SetProcessNotifyRoutine()
{
NTSTATUS status = PsSetCreateProcessNotifyRoutineEx((PCREATE_PROCESS_NOTIFY_ROUTINE_EX)ProcessNotifyExRoutine, FALSE);
if (!NT_SUCCESS(status))
{
ShowError("PsSetCreateProcessNotifyRoutineEx", status);
}
return status;
}
回调函数
// 回调函数
VOID ProcessNotifyExRoutine(PEPROCESS pEProcess, HANDLE hProcessId, PPS_CREATE_NOTIFY_INFO CreateInfo)
{
// CreateInfo 为 NULL 时,表示进程退出;不为 NULL 时,表示进程创建
if (NULL == CreateInfo)
{
return;
}
// 获取进程名称
PCHAR pszImageFileName = PsGetProcessImageFileName(pEProcess);
// 显示创建进程信息
DbgPrint("[%s][%d][%wZ]\n", pszImageFileName, hProcessId, CreateInfo->ImageFileName);
// 禁止指定进程(520.exe)创建
if (0 == _stricmp(pszImageFileName, "520.exe"))
{
// 禁止创建
CreateInfo->CreationStatus = STATUS_UNSUCCESSFUL;
DbgPrint("[禁止创建]\n");
}
}
删除回调
// 删除回调函数
NTSTATUS RemoveProcessNotifyRoutine()
{
NTSTATUS status = PsSetCreateProcessNotifyRoutineEx((PCREATE_PROCESS_NOTIFY_ROUTINE_EX)ProcessNotifyExRoutine, TRUE);
if (!NT_SUCCESS(status))
{
ShowError("PsSetCreateProcessNotifyRoutineEx", status);
}
return status;
}
程序测试
在Win10 64位上
在Win7 64位上
总结
这个程序实现起来并不复杂,关键是对 PsSetCreateProcessNotifyRoutineEx 函数要理解透彻,理解清楚回调函数中,PS_CREATE_NOTIFY_INFO 结构体的所有成员含义。这样,我们就可以获取进程信息,以及控制进程的创建。
注意,破解 PsSetCreateProcessNotifyRoutineEx 函数的使用限制有两种方式,一种是通过编程来解决;一种是通过 VS 开发环境和数字签名来解决。
参考
参考自《Windows黑客编程技术详解》一书