【Note:本节除了3.1以外的其他监控方式,均可以通过抹掉notifymask的方式绕过。以进程创建监控为例,在 ntoskrnl.exe 的分析文件中找到进程创建函数,在下面找到这个PspNotifyEnableMask,抹掉即可】

在 sub_14034C000 初始化里,0x14034CB8C 这里会走 ObRegisterCallbacks,前面填的 OB_OPERATION_REGISTRATION 也能对上:
·ObjectType = PsProcessType
·Operations = 1
·PreOperation = sub_1400529A4
·PostOperation = nullsub_1
·Altitude = L"389613"
·RegistrationHandle = qword_140344998
Operations = 1 这个地方要注意,它对应的是 OB_OPERATION_HANDLE_CREATE。也就是说这块看的是“创建进程句柄”这件事,DuplicateHandle 那条路不在这里。ObjectType 又是 PsProcessType,所以这里盯的是进程对象句柄。
对应注册代码大概是这样:


继续跟进 sub_1400529A4。这个函数就是 Ob 的 PreOperation 回调,第二个参数 a2 可以按 POB_PRE_OPERATION_INFORMATION 看。它后面读的几个偏移基本能对上:
·a2 + 0x00 = Operation
·a2 + 0x04 = Flags
·a2 + 0x08 = Object
·a2 + 0x10 = ObjectType
·a2 + 0x20 = Parameters
一上来先卡几层基础条件:当前 IRQL 要是 0,a2 和 a2->Object 要能过 sub_1400C16D3,Info->Flags & 1 要等于 0,并且 ObjectType 必须等于 PsProcessType。sub_1400C16D3 里面能解出 MmIsAddressValid,所以这里就是先防一下空指针和无效地址。
然后它拿 a2->Object 调 PsGetProcessId,这个得到的是“被打开的进程 PID”。接着它调 PsGetCurrentProcessId,拿到的是“正在打开句柄的进程 PID”。
这里先提前说一下,qword_140344968 后面 【3.2】会讲,它就是进程回调里记下来的 cs2 PID。所以 Ob 这里筛得很直白:
TargetPid = PsGetProcessId(a2->Object)
CurrentPid = PsGetCurrentProcessId()
TargetPid != qword_140344968 -> return
CurrentPid == qword_140344968 -> return
CurrentPid == qword_140344980 -> return
Operation != 1 -> return
翻成人话就是:只有外部进程打开 cs2 进程句柄,它才继续。cs2 自己打开自己会被过滤掉,打开的目标不是 cs2 也会被过滤掉。qword_140344980 看起来像前面记下来的一个排除 PID,这里碰到也直接返回。
过了 PID 这层以后,它开始看权限。它读的是 a2->Parameters + 4,也就是 OriginalDesiredAccess。它关心的是这些位:
·0x2 PROCESS_CREATE_THREAD
·0x8 PROCESS_VM_OPERATION
·0x10 PROCESS_VM_READ
·0x20 PROCESS_VM_WRITE
·0x800 PROCESS_SUSPEND_RESUME
正常软件偶尔打开一下进程句柄不一定有事,但是要是创建线程、读写内存、改内存权限,那就很像后面要注入或者读游戏内存了。
权限过了以后,它会用 CurrentPid 调 sub_1400C6A64。里面是 PsLookupProcessByProcessId、ObOpenObjectByPointer、ZwQueryInformationProcess(ProcessImageFileName = 27),最后拿到当前打开 cs2 句柄的那个进程的完整路径 UNICODE_STRING。
也就是说,如果 external_cheat.exe 在打开 cs2,这里拿到的就是 external_cheat.exe 的完整路径。
再往下,它和线程回调那边一样,会给这个打开者进程算一个主模块 PE header MD5。流程还是先走内存版 sub_14004D3CB:
·IoGetCurrentProcess
·PsGetProcessSectionBaseAddress
·从 section base 读 PE header
·清掉 TimeDateStamp 和 ImageBase
·对处理后的 PE header 算 MD5
如果内存里算失败,它还会走 sub_14004DED2,用刚才 sub_1400C6A64 查出来的完整路径打开磁盘上的 exe,读前 0x1000 字节,再用同样方式算一次 MD5。
两个函数算出来的 16 字节 MD5 都放到同一个小结构里,后面真正上报的时候传的是 info + 0x14。
最后才走 sub_14004E7EB,事件类型这里解出来是 3:
sub_14004E7EB(
CurrentPid,
openerImagePath,
3,
info + 0x14,
0,
0
);
对应的上报路径在这里:


通俗点讲,这个 Ob 回调盯的就是谁在 OpenProcess cs2。
比如 external_cheat.exe 起来以后想打开 cs2:
OpenProcess(
PROCESS_VM_WRITE | PROCESS_VM_OPERATION | PROCESS_CREATE_THREAD,
...,
cs2_pid
);
这时候 Ob 回调能看到:目标进程是 cs2,当前进程是 external_cheat.exe,申请的权限里还带了 VM_WRITE / VM_OPERATION / CREATE_THREAD 这种敏感权限。然后它继续把 external_cheat.exe 的完整路径拿出来,再给 external_cheat.exe 的主模块 PE header 算 MD5,最后作为 type = 3 的事件丢给 sub_14004E7EB。
Ob 这里看的是“谁拿了 cs2 的进程句柄,以及拿句柄的时候要了什么权限”。如果发现某个外部进程拿 cs2 高权限句柄,这个外部进程的路径和 PE 头 MD5 就会直接被上报给客户端,事件类型是3(maybe是某美那边用来筛选用的)。
在 0x14034C977(sub_14034C000里)驱动通过 off_140104608 + 0x3EF98FDBAFBD88A7 得到 PsSetCreateProcessNotifyRoutine函数,然后注册进程回调 sub_1400506E5。


我们跟进这个sub_1400506E5 进程回调里。发现这个函数挺大的,会用到:
·PsLookupProcessByProcessId
·ObOpenObjectByPointer
·ZwQueryInformationProcess
·PsGetProcessImageFileName
·KeStackAttachProcess
·RtlInitUnicodeString
·wcsstr / _stricmp
这些函数
通过这些api他可以知道:
·这个创建的进程叫什么
·路径是什么
·命令行是什么
·父进程是谁
·是不是它关心的目标
·当前上下文是谁
通过主播的不懈努力,成功解出来了他关心或者说匹配的目标是cs2.exe。。。
在 sub_1400506E5 里,流程是这样的:


然后它运行时拼了一个“cs2.exe”字符串,对应的是0x140050E89 - 0x140050F68 那段


接着把这次回调创建的Process它比较,如果比较结果是 0,就说明这次创建的进程是 cs2.exe:


总结一下:


然后命中了就会走我上面说的写入全局变量逻辑,表示cs2启动了,也就是要开始打游戏需要保护cs2不被注入cheat了
目标进程退出的时候,它还会把这些全局变量清掉。这个逻辑也挺正常,不然 cs2 退出以后,后面 PID 复用或者别的进程创建线程,就容易把旧状态带进去。
这个全局变量后面【3.3】的线程回调 sub_1400523D4 会用到。现在这个进程回调把“我要盯cs2”记下来,线程回调再看“是不是有人往这个目标进程里创建新线程”
举个简单的例子:


[招生]科锐逆向工程师培训(2026年7月3日实地,远程教学同时开班, 第56期)!
最后于 16小时前
被Rw0terX编辑
,原因: