回调的故事
本帖最后由 Panel 于 2023-4-9 11:11 编辑# 回调的故事
### 概述:
#### 讲述回调,举例通过回调监控进程、线程、映像加载状态以及进程回调函数禁止进程的创建
### 实验环境:
#### Windows 7 x86
### 实验工具:
#### Windbg、vs2013
### 0x1:什么是回调函数?
#### .回调函数(Callback Function)是一种常见的编程技术,用于将一个函数作为参数传递给另一个函数,并在需要时由另一个函数调用。回调函数通常用于实现异步操作、事件处理、消息通知等场景,可以使程序更加灵活和可扩展。GPT这样说,严谨但是晦涩,我来举例解释一下,比如:你妈妈给你分配了一个买菜的任务,要求就是你买了菜回来且要向她报告你买菜完成才算完成任务。那么此时,买菜回来是事件A,向妈妈报告买菜完成是事件B,那么B就是A的回调事件,按照常理来说我们只要成功把菜买回来就算是我们认为的买菜成功了,但实际上要加上和妈妈汇报才算得上真正地完成了买菜任务。这就是回调函数的概念。
### 0x2:进程、线程、映像加载回调
#### .向上面举例说的一样,当我们注册了进程、线程、映像的回调函数以后,那么它们的标志性完成是回调函数也执行完才算得上一个完整的事件。
#### .注册进程回调的函数:`PsSetCreateProcessNotifyRoutine`或者`PsSetCreateProcessNotifyRoutineEx`
#### .注册线程回调的函数:`PsSetCreateThreadNotifyRoutine`
#### .注册映像的回调函数:`PsSetLoadImageNotifyRoutine`
### 0x3:函数以及重要结构体简介
#### .进程注册回调函数
```c
//注册与移除函数定义
NTSTATUS
PsSetCreateProcessNotifyRoutineEx (
_In_ PCREATE_PROCESS_NOTIFY_ROUTINE_EX NotifyRoutine,
_In_ BOOLEAN Remove
);
```
```c
//回调函数原型
VOID
(*PCREATE_PROCESS_NOTIFY_ROUTINE_EX) (
_Inout_ PEPROCESS Process,
_In_ HANDLE ProcessId,
_Inout_opt_ PPS_CREATE_NOTIFY_INFO CreateInfo
);
```
```c
//PPS_CREATE_NOTIFY_INFO结构
typedef struct _PS_CREATE_NOTIFY_INFO {
_In_ SIZE_T Size;
union {
_In_ ULONG Flags;
struct {
_In_ ULONG FileOpenNameAvailable : 1;
_In_ ULONG Reserved : 31;
};
};
_In_ HANDLE ParentProcessId;
_In_ CLIENT_ID CreatingThreadId;
_Inout_ struct _FILE_OBJECT *FileObject;
_In_ PCUNICODE_STRING ImageFileName;
_In_opt_ PCUNICODE_STRING CommandLine;
_Inout_ NTSTATUS CreationStatus;
} PS_CREATE_NOTIFY_INFO, *PPS_CREATE_NOTIFY_INFO;
```
#### .PsSetCreateProcessNotifyRoutineEx中,NotifyRoutine是指向回调函数的指针,Remove代表是否移除指定回调函数,我们注册的时候就FALSE,当我们不想使用回调函数的时候则TRUE,让系统移除回调函数,回调函数中包含了进程的EPROCESS结构、进程ID、创建信息(如果是创建进程则这个参数就是创建进程的信息,如果是销毁的话这个参数是NULL)。
#### .注册线程回调的函数
```c
//注册函数
NTSTATUS
PsSetCreateThreadNotifyRoutine(
_In_ PCREATE_THREAD_NOTIFY_ROUTINE NotifyRoutine
);
```
```c
//移除函数
NTSTATUS
PsRemoveCreateThreadNotifyRoutine (
_In_ PCREATE_THREAD_NOTIFY_ROUTINE NotifyRoutine
);
```
```c
//回调函数原型
VOID
(*PCREATE_THREAD_NOTIFY_ROUTINE)(
_In_ HANDLE ProcessId,
_In_ HANDLE ThreadId,
_In_ BOOLEAN Create
);
```
#### .NotifyRoutine是指向回调函数的指针,回调函数中包含了线程的所属进程ID、线程ID、创建状态还是销毁状态,TRUE为创建,FALSE为销毁。
#### .注册映像文件加载回调的函数
```c
//注册函数
NTSTATUS
PsSetLoadImageNotifyRoutine(
_In_ PLOAD_IMAGE_NOTIFY_ROUTINE NotifyRoutine
);
```
```c
//移除函数
NTSTATUS
PsRemoveLoadImageNotifyRoutine(
_In_ PLOAD_IMAGE_NOTIFY_ROUTINE NotifyRoutine
);
```
```c
//回调函数原型
VOID
(*PLOAD_IMAGE_NOTIFY_ROUTINE)(
_In_ PUNICODE_STRING FullImageName,
_In_ HANDLE ProcessId, // pid into which image is being mapped
_In_ PIMAGE_INFO ImageInfo
);
```
```c
//IMAGE_INFO
typedef struct _IMAGE_INFO {
union {
ULONG Properties;
struct {
ULONG ImageAddressingMode: 8;// Code addressing mode
ULONG SystemModeImage : 1;// System mode image
ULONG ImageMappedToAllPids : 1;// Image mapped into all processes
ULONG ExtendedInfoPresent: 1;// IMAGE_INFO_EX available
ULONG MachineTypeMismatch: 1;// Architecture type mismatch
ULONG ImageSignatureLevel: 4;// Signature level
ULONG ImageSignatureType : 3;// Signature type
ULONG Reserved : 13;
};
};
PVOID ImageBase;
ULONG ImageSelector;
SIZE_T ImageSize;
ULONG ImageSectionNumber;
} IMAGE_INFO, *PIMAGE_INFO;
```
#### .NotifyRoutine是指向回调函数的指针,回调函数中包含了所加载映像文件的全路径名称、加载映像文件的进程ID、映像文件信息。
### 0x3:通过PsSetCreateProcessNotifyRoutineEx时的注意点
#### .当我以下面的函数注册进程回调函数时会得到注册状态返回值为`0xC0000022`,上网查阅便发现是访问被拒绝的意思,那么我们进入内核代码查看一下是啥原因,`IDA Pro`拖入`ntkrnlpa.exe`,找到`PspSetCreateProcessNotifyRoutineEx`函数F5一下,得到以下伪代码:
```c
int __stdcall PspSetCreateProcessNotifyRoutine(int a1, char a2, char a3)
{
_KTHREAD *CurrentThread; // esi
int v4; // ebx
_DWORD *v5; // eax
void *v6; // edi
int v7; // ecx
volatile signed __int32 *v9; // eax
void *v10; // ebx
char *v11; // esi
unsigned int v12; // edi
char *i; //
if ( a2 )
{
CurrentThread = KeGetCurrentThread();
--CurrentThread->KernelApcDisable;
v4 = 0;
for ( i = (char *)&PspCreateProcessNotifyRoutine; ; i += 4 )
{
v5 = (_DWORD *)ExReferenceCallBackBlock(i);
v6 = v5;
if ( v5 )
break;
LABEL_11:
if ( (unsigned int)++v4 >= 0x40 )
{
if ( !++CurrentThread->KernelApcDisable
&& ($402C2D26A9C1C09F55BBA925DBF9FC47 *)CurrentThread->ApcState.ApcListHead.Flink != &CurrentThread->64
&& !CurrentThread->SpecialApcDisable )
{
KiCheckForKernelApcDelivery();
}
return 0xC000007A;
}
}
if ( ExGetCallBackBlockRoutine(v5) == a1 )
{
if ( v7 )
{
if ( a3 )
goto LABEL_9;
}
else if ( !a3 )
{
LABEL_9:
if ( (unsigned __int8)ExCompareExchangeCallBack(v6) )
{
v9 = &PspCreateProcessNotifyRoutineCount;
if ( a3 )
v9 = &PspCreateProcessNotifyRoutineExCount;
_InterlockedExchangeAdd(v9, 0xFFFFFFFF);
ExDereferenceCallBackBlock(v6);
if ( !++CurrentThread->KernelApcDisable
&& ($402C2D26A9C1C09F55BBA925DBF9FC47 *)CurrentThread->ApcState.ApcListHead.Flink != &CurrentThread->64
&& !CurrentThread->SpecialApcDisable )
{
KiCheckForKernelApcDelivery();
}
ExWaitForCallBacks(v6);
ExFreeCallBack(v6);
return 0;
}
}
}
ExDereferenceCallBackBlock(v6);
goto LABEL_11;
}
if ( a3 && !MmVerifyCallbackFunction(a1) )
return 0xC0000022;
v10 = (void *)ExAllocateCallBack(a1, a3 != 0);
if ( !v10 )
return 0xC000009A;
v11 = (char *)&PspCreateProcessNotifyRoutine;
v12 = 0;
while ( !(unsigned __int8)ExCompareExchangeCallBack(0) )
{
v12 += 4;
v11 += 4;
if ( v12 >= 0x100 )
{
ExFreeCallBack(v10);
return 0xC000000D;
}
}
if ( a3 )
{
_InterlockedExchangeAdd(&PspCreateProcessNotifyRoutineExCount, 1u);
if ( (PspNotifyEnableMask & 4) == 0 )
_interlockedbittestandset(&PspNotifyEnableMask, 2u);
}
else
{
_InterlockedExchangeAdd(&PspCreateProcessNotifyRoutineCount, 1u);
if ( (PspNotifyEnableMask & 2) == 0 )
_interlockedbittestandset(&PspNotifyEnableMask, 1u);
}
return 0;
}
```
#### .其中a1是我们传入的第一个参数,也就是回调函数地址,a2是我们传入的第二个参数,也就是是否移除回调函数,a3不知道是哪来的,先不管。一看代码便看到了我们的错误码所在:
```c
if ( a3 && !MmVerifyCallbackFunction(a1) )
return 0xC0000022;
```
#### .那此时我们知道只要条件不成立就不会返回权限不足这个错误,那我们有两种思路不让它返回`0xC0000022`,第一种便是直接调用`int __stdcall PspSetCreateProcessNotifyRoutine(int a1, char a2, char a3)`函数,设置a3为0,第二种就是让`!MmVerifyCallbackFunction(a1)`为0,也就是`MmVerifyCallbackFunction(a1)`返回非零,第一种有点暴力,不优雅,咱们这里选择第二种,那此时就跟进`MmVerifyCallbackFunction(a1)`中去看看:
```c
int __stdcall MmVerifyCallbackFunction(unsigned int a1)
{
char v1; // al
_KTHREAD *CurrentThread; // esi
int v4; // eax
int v5; //
if ( a1 >= MiSystemRangeStart )
{
v1 = MiSystemVaType[(int)(((a1 >> 18) & 0x3FF8) - (((unsigned int)MmSystemRangeStart >> 18) & 0x3FF8)) >> 3];
if ( v1 == 1 || v1 == 11 )
return 0;
}
CurrentThread = KeGetCurrentThread();
v5 = 0;
--CurrentThread->KernelApcDisable;
ExAcquireResourceSharedLite(&PsLoadedModuleResource, 1u);
v4 = MiLookupDataTableEntry(a1, 1); // 查找我们函数地址所在模块的LDR_DATA_TABLE_ENTRY,驱动模块的话其实也就是DriverSection的地址
if ( v4 && (*(_BYTE *)(v4 + 0x34) & 0x20) != 0 )// 判断函数是否在模块中且LDR_DATA_TABLE_ENTRY的Flag成员是否为0x20,条件满足则过了检测
v5 = 1;
ExReleaseResourceLite(&PsLoadedModuleResource);
if ( !++CurrentThread->KernelApcDisable
&& ($402C2D26A9C1C09F55BBA925DBF9FC47 *)CurrentThread->ApcState.ApcListHead.Flink != &CurrentThread->64
&& !CurrentThread->SpecialApcDisable )
{
KiCheckForKernelApcDelivery();
}
return v5;
}
```
#### .上面代码注释的地方便是使得返回值`v5`为非零值的地方,那我们继续看看要使得`v5`为1的条件:
```c
v4 = MiLookupDataTableEntry(a1, 1);
if ( v4 && (*(_BYTE *)(v4 + 0x34) & 0x20) != 0 )
```
#### .`MiLookupDataTableEntry`是查找指定地址所在模块的`LDR_DATA_TABLE_ENTRY`结构地址,那此时我们的模块便是驱动模块,它取了该结构偏移0x34位置的值去与运算上0x20,首先我们知道v4一定不为0,那剩下的就是要`(*(_BYTE *)(v4 + 0x34) & 0x20)`不为0了,也就是说这个表达式的值对应bit位没有0x20的话那这个if条件就不成立了,也就导致v5不能成为非零值了,那我们去看看`LDR_DATA_TABLE_ENTRY`结构偏移+0x34的位置是啥:
```c
//部分内存结构
typedef struct _LDR_DATA_TABLE_ENTRY
{
LIST_ENTRY InLoadOrderLinks; //0x0
LIST_ENTRY InMemoryOrderLinks; //0x8
LIST_ENTRY InInitializationOrderLinks; //0x10
VOID* DllBase; //0x18
VOID* EntryPoint; //0x1c
ULONG SizeOfImage; //0x20
UNICODE_STRING FullDllName; //0x24
UNICODE_STRING BaseDllName; //0x2c
ULONG Flags; //0x34
}LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;
```
#### .从上面我们得知`+0x34`是`PDRIVER_OBJECT-->DriverSection-->Flag`,那我们只需要把这个值`或运算0x20`就可以了。
### 0x4:实现进程、线程、映像文件加载的监控
```c
//进程回调,打印进程创建或销毁时的信息
VOID CreateProcessCallBack(
_Inout_ PEPROCESS Process,
_In_ HANDLE ProcessId,
_Inout_opt_ PPS_CREATE_NOTIFY_INFO CreateInfo
)
{
if (CreateInfo)
{
DbgPrintEx(77, 0, "[进程回调]进程创建,进程id为%d,名字为:%wZ\n", ProcessId, CreateInfo->ImageFileName);
}
else
{
DbgPrintEx(77, 0, "[进程回调]进程销毁,进程id为%d\n", ProcessId);
}
}
```
```c
//线程回调,打印线程创建或销毁时的信息
VOID CreateThreadCallBack(
_In_ HANDLE ProcessId,
_In_ HANDLE ThreadId,
_In_ BOOLEAN Create
)
{
if (Create)
{
DbgPrintEx(77, 0, "[线程回调]线程创建,进程id为%d,线程id为%d\n", ProcessId, ThreadId);
}
else
{
DbgPrintEx(77, 0, "[线程回调]线程销毁,进程id为%d,线程id为%d\n", ProcessId,ThreadId);
}
}
```
```c
//映像加载回调,打印映像问价加载时的信息
VOID LoadImageCallBack(
_In_ PUNICODE_STRING FullImageName,
_In_ HANDLE ProcessId, // pid into which image is being mapped
_In_ PIMAGE_INFO ImageInfo
)
{
if (ImageInfo)
{
DbgPrintEx(77, 0, "[映像回调]映像加载,进程id为%d,映像类型值为%d,路径为%wZ\n", ProcessId, ImageInfo->SystemModeImage, FullImageName);
}
else
{
DbgPrintEx(77, 0, "[映像回调]映像销毁,进程id为%d\n", ProcessId);
}
}
```
```c
//注册以上回调函数
NTSTATUS ntStatu = 0;
//进程回调
PLDR_DATA_TABLE_ENTRY pSection = pDriver->DriverSection;
pSection->Flags |= 0x20;
ntStatu = PsSetCreateProcessNotifyRoutineEx(CreateProcessCallBack, FALSE);
DbgPrintEx(77, 0, "进程回调注册状态值:%x\n", ntStatu);
//线程回调
ntStatu = PsSetCreateThreadNotifyRoutine(CreateThreadCallBack);
DbgPrintEx(77, 0, "线程回调注册状态值:%x\n", ntStatu);
//映像回调
ntStatu = PsSetLoadImageNotifyRoutine(LoadImageCallBack);
DbgPrintEx(77, 0, "映像回调注册状态值:%x\n", ntStatu);
```
```c
//不打算使用回调时的移除
//移除进程回调
PsSetCreateProcessNotifyRoutineEx(CreateProcessCallBack, TRUE);
//移除线程回调
PsRemoveCreateThreadNotifyRoutine(CreateThreadCallBack);
//移除映像回调
PsRemoveLoadImageNotifyRoutine(LoadImageCallBack);
```
```c
//完整代码
#include <ntifs.h>
#include <string.h>
//0x78 bytes (sizeof)
typedef struct _LDR_DATA_TABLE_ENTRY
{
LIST_ENTRY InLoadOrderLinks; //0x0
LIST_ENTRY InMemoryOrderLinks; //0x8
LIST_ENTRY InInitializationOrderLinks; //0x10
VOID* DllBase; //0x18
VOID* EntryPoint; //0x1c
ULONG SizeOfImage; //0x20
UNICODE_STRING FullDllName; //0x24
UNICODE_STRING BaseDllName; //0x2c
ULONG Flags; //0x34
}LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;
//进程回调
VOID CreateProcessCallBack(
_Inout_ PEPROCESS Process,
_In_ HANDLE ProcessId,
_Inout_opt_ PPS_CREATE_NOTIFY_INFO CreateInfo
)
{
if (CreateInfo)
{
DbgPrintEx(77, 0, "[进程回调]进程创建,进程id为%d,名字为:%wZ\n", ProcessId, CreateInfo->ImageFileName);
}
else
{
DbgPrintEx(77, 0, "[进程回调]进程销毁,进程id为%d\n", ProcessId);
}
}
//线程回调
VOID CreateThreadCallBack(
_In_ HANDLE ProcessId,
_In_ HANDLE ThreadId,
_In_ BOOLEAN Create
)
{
if (Create)
{
DbgPrintEx(77, 0, "[线程回调]线程创建,进程id为%d,线程id为%d\n", ProcessId, ThreadId);
}
else
{
DbgPrintEx(77, 0, "[线程回调]线程销毁,进程id为%d,线程id为%d\n", ProcessId,ThreadId);
}
}
//映像模块回调
VOID LoadImageCallBack(
_In_ PUNICODE_STRING FullImageName,
_In_ HANDLE ProcessId, // pid into which image is being mapped
_In_ PIMAGE_INFO ImageInfo
)
{
if (ImageInfo)
{
DbgPrintEx(77, 0, "[映像回调]映像加载,进程id为%d,映像类型值为%d,路径为%wZ\n", ProcessId, ImageInfo->SystemModeImage, FullImageName);
}
else
{
DbgPrintEx(77, 0, "[映像回调]映像销毁,进程id为%d\n", ProcessId);
}
}
VOID UnloadDriver(PDRIVER_OBJECT pDriver)
{
//移除进程回调
PsSetCreateProcessNotifyRoutineEx(CreateProcessCallBack, TRUE);
//移除线程回调
PsRemoveCreateThreadNotifyRoutine(CreateThreadCallBack);
//移除映像回调
PsRemoveLoadImageNotifyRoutine(LoadImageCallBack);
}
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING pRegPath)
{
NTSTATUS ntStatu = 0;
//进程回调
PLDR_DATA_TABLE_ENTRY pSection = pDriver->DriverSection;
pSection->Flags |= 0x20;
ntStatu = PsSetCreateProcessNotifyRoutineEx(CreateProcessCallBack, FALSE);
DbgPrintEx(77, 0, "进程回调注册状态值:%x\n", ntStatu);
//线程回调
ntStatu = PsSetCreateThreadNotifyRoutine(CreateThreadCallBack);
DbgPrintEx(77, 0, "线程回调注册状态值:%x\n", ntStatu);
//映像回调
ntStatu = PsSetLoadImageNotifyRoutine(LoadImageCallBack);
DbgPrintEx(77, 0, "映像回调注册状态值:%x\n", ntStatu);
pDriver->DriverUnload = UnloadDriver;
return STATUS_SUCCESS;
}
```
#### .效果图
### 0x5:实现禁止创建指定进程
#### .首先要知道`PsSetCreateProcessNotifyRoutineEx`函数的触发时间是在进程完全创建之前。具体来说,当系统正在创建新进程时,`PsSetCreateProcessNotifyRoutineEx`注册的回调函数会在进程对象创建完成之后、但在用户空间初始化程序执行之前被调用。在这个时刻,进程的一些基本属性已经确定,如进程ID、父进程ID、映像名称等,但是进程的虚拟地址空间还未初始化,也没有执行用户空间的初始化程序。
#### .其实我们上面说过,回调函数原型中的第三个参数`_Inout_opt_ PPS_CREATE_NOTIFY_INFO CreateInfo`中包含了创建进程的信息,其中有一个成员非常重要:`CreationStatus`,这个成员只有为`STATUS_SUCCESS`时才能让进程正常加载,所以我们只要让他为`STATUS_UNSUCCESSFUL`不就可以让他加载失败了吗?这里_Inout和P开头已经告诉了我们这个参数是个指针可以双向传递,所以我们直接`CreateInfo->CreationStatus = STATUS_UNSUCCESSFUL`就实现了,这里我举例禁止创建记事本进程为例子,在进程回调函数里过滤出只要创建的进程是记事本进程就不让它加载,核心代码如下:
```c
VOID CreateProcessCallBack(
_Inout_ PEPROCESS Process,
_In_ HANDLE ProcessId,
_Inout_opt_ PPS_CREATE_NOTIFY_INFO CreateInfo
)
{
if (CreateInfo)
{
DbgPrintEx(77, 0, "[进程回调]进程创建,进程id为%d,名字为:%wZ\n", ProcessId, CreateInfo->ImageFileName);
UNICODE_STRING uniDstName = RTL_CONSTANT_STRING(L"\\??\\C:\\Windows\\System32\\notepad.exe");
if (RtlEqualUnicodeString(&uniDstName, CreateInfo->ImageFileName, TRUE))
{
CreateInfo->CreationStatus = STATUS_UNSUCCESSFUL;
}
}
else
{
DbgPrintEx(77, 0, "[进程回调]进程销毁,进程id为%d\n", ProcessId);
}
}
```
#### .效果:
### 0x6:总结
#### .回调被很多杀软利用,看自己想的思路吧。
感谢分享,回调在相机sdk里用的很经典,一旦获取帧,就触发回调函数,回调函数里对帧做一些处理(而且是不怎么耗时的处理,一般是把帧拷贝到全局变量) 哇哇,好厉害,虽然我看的不是很懂呢 学渣表示看不懂 楼主太厉害了,俺们菜鸟,只能慢慢消化! 完全看不懂,但是最近正在需要公众号和系统做一个回调设置, 不知道怎么弄 好像很好用的 感谢分享 大佬好强,不过学渣表示看不懂。 谢谢楼主分享。明白什么叫回调 感谢楼主的分享