游戏反作弊与作弊技术更新速度一年一个样,本文将会介绍19与20年最常见的驱动层反作弊保护实现原理,也是目前主流反作弊系统 BE EAC所使用到的原理.
之前文章:
https://www.freebuf.com/geek/235229.html
https://www.freebuf.com/articles/system/234548.html
1. "驱动保护":
所谓驱动保护,一般有两种,一种是hypervisor 也就是腾讯TP那一套.特点是非常强.接管系统内核常见读写API去阻止读写.但是涉及知识非常复杂,不宜在本文讨论.另外一套是常用的剥离句柄回调,普通外挂要读写游戏进程就需要使用NtopenProcess打开一个进程句柄,而NtopenProcess会触发Windows的ObRegisterCallbacks回调,这个时候只要驱动层捕获这些回调并且做剥离关键权限句柄(如R/W)就能防止进程被读写.
关键code:
NTSTATUS InstallCallBacks() { NTSTATUS NtHandleCallback = STATUS_UNSUCCESSFUL; NTSTATUS NtThreadCallback = STATUS_UNSUCCESSFUL; OB_OPERATION_REGISTRATION OBOperationRegistration[2]; OB_CALLBACK_REGISTRATION OBOCallbackRegistration; REG_CONTEXT regContext; UNICODE_STRING usAltitude; memset(&OBOperationRegistration, 0, sizeof(OB_OPERATION_REGISTRATION)); memset(&OBOCallbackRegistration, 0, sizeof(OB_CALLBACK_REGISTRATION)); memset(&regContext, 0, sizeof(REG_CONTEXT)); regContext.ulIndex = 1; regContext.Version = 120; RtlInitUnicodeString(&usAltitude, L"1000"); if ((USHORT)ObGetFilterVersion() == OB_FLT_REGISTRATION_VERSION) { OBOperationRegistration[1].ObjectType = PsProcessType; OBOperationRegistration[1].Operations = OB_OPERATION_HANDLE_CREATE | OB_OPERATION_HANDLE_DUPLICATE; OBOperationRegistration[1].PreOperation = MyHandleProcessCallbacks; OBOperationRegistration[1].PostOperation = HandleAfterCreat; OBOperationRegistration[0].ObjectType = PsThreadType; OBOperationRegistration[0].Operations = OB_OPERATION_HANDLE_CREATE | OB_OPERATION_HANDLE_DUPLICATE; OBOperationRegistration[0].PreOperation = MyHandleThreadCallbacks; OBOperationRegistration[0].PostOperation = HandleAfterCreat; OBOCallbackRegistration.Version = OB_FLT_REGISTRATION_VERSION; OBOCallbackRegistration.OperationRegistrationCount = 2; OBOCallbackRegistration.RegistrationContext = &regContext; OBOCallbackRegistration.OperationRegistration = OBOperationRegistration; NtHandleCallback = ObRegisterCallbacks(&OBOCallbackRegistration, &g_CallbacksHandle); // Register The CallBack if (!NT_SUCCESS(NtHandleCallback)) { if (g_CallbacksHandle) { ObUnRegisterCallbacks(g_CallbacksHandle); g_CallbacksHandle = NULL; } //DebugPrint("[DebugMessage] Failed to install ObRegisterCallbacks: 0x%08X.\n", NtHandleCallback); return STATUS_UNSUCCESSFUL; } } return STATUS_SUCCESS; }
关键回调:
OB_PREOP_CALLBACK_STATUS MyHandleThreadCallbacks(PVOID RegistrationContext, POB_PRE_OPERATION_INFORMATION OperationInformation) { if (被保护进程PID == -1) return OB_PREOP_SUCCESS; ULONG ulProcessId = (ULONG)PsGetThreadProcessId((PETHREAD)OperationInformation->Object); ULONG myProcessId = (ULONG)PsGetThreadProcessId((PETHREAD)PsGetCurrentThread()); if (ID_ALIGN(ulProcessId) == ID_ALIGN(被保护进程PID) && myProcessId != ulProcessId) { if (OperationInformation->Operation == OB_OPERATION_HANDLE_CREATE) { if ((OperationInformation->Parameters->CreateHandleInformation.OriginalDesiredAccess & PROCESS_VM_OPERATION) == PROCESS_VM_OPERATION) { OperationInformation->Parameters->CreateHandleInformation.DesiredAccess &= ~PROCESS_VM_OPERATION; } if ((OperationInformation->Parameters->CreateHandleInformation.OriginalDesiredAccess & PROCESS_VM_READ) == PROCESS_VM_READ) { OperationInformation->Parameters->CreateHandleInformation.DesiredAccess &= ~PROCESS_VM_READ; } if ((OperationInformation->Parameters->CreateHandleInformation.OriginalDesiredAccess & PROCESS_VM_WRITE) == PROCESS_VM_WRITE) { OperationInformation->Parameters->CreateHandleInformation.DesiredAccess &= ~PROCESS_VM_WRITE; } } } return OB_PREOP_SUCCESS; }
你可以看到,回调里面剥离了R/W/OP权限,R3外挂就无法利用openprocess读写进程了
当然本文不会粘贴网上一大堆烂大街的代码,接下来才是到对抗阶段,
2. 对抗
总所周知,一个进程的父进程拥有这个进程的最高句柄,所以可以通过注入到这些进程里面然后劫持这些句柄进行操作. 参考我之前的绕过杀毒软件方法: https://www.freebuf.com/vuls/220997.html
这些句柄就成为一个讨厌的存在,所以我们就要想办法剥离这些句柄!防止外挂利用这些句柄进行读写操作!
剥离句柄分三步:
1. 遍历所有进程,得到有读写权限的句柄的进程
2.得到这些进程的Eprocess,之后eprocess得到handletable,
3.剥离
首先是结构
typedef struct _SYSTEM_HANDLE_INFORMATION { ULONG ProcessId;//进程标识符 UCHAR ObjectTypeNumber;//打开的对象的类型 UCHAR Flags;//句柄属性标志 USHORT Handle;//句柄数值,在进程打开的句柄中唯一标识某个句柄 PVOID Object;//这个就是句柄对应的EPROCESS的地址 ACCESS_MASK GrantedAccess;//句柄对象的访问权限 }SYSTEM_HANDLE_INFORMATION, * PSYSTEM_HANDLE_INFORMATION; typedef struct _SYSTEM_HANDLE_INFORMATION_EX { ULONG NumberOfHandles; SYSTEM_HANDLE_INFORMATION Information[655360]; }SYSTEM_HANDLE_INFORMATION_EX, * PSYSTEM_HANDLE_INFORMATION_EX; 之后通过NtQuerySystemInformation id为0x10的操作遍历全部进程句柄 PSYSTEM_HANDLE_INFORMATION_EX QueryHandleTable() { ULONG cbBuffer = sizeof(SYSTEM_HANDLE_INFORMATION_EX); LPVOID pBuffer = (LPVOID)ExAllocatePoolWithTag(NonPagedPool, cbBuffer, POOL_TAG); PSYSTEM_HANDLE_INFORMATION_EX HandleInfo = nullptr; if (pBuffer) { pfn_NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)0x10, pBuffer, cbBuffer, NULL); HandleInfo = (PSYSTEM_HANDLE_INFORMATION_EX)pBuffer; } return HandleInfo; } 之后简单的遍历查询出来的信息,然后锁定进程得到eprocess for (int i = 0; i < HandleInfo->NumberOfHandles; i++) { //7 是 process 属性 if (HandleInfo->Information[i].ObjectTypeNumber == 7 || HandleInfo->Information[i].ObjectTypeNumber == OB_TYPE_INDEX_PROCESS || HandleInfo->Information[i].ObjectTypeNumber == OB_TYPE_INDEX_THREAD) { if (g_FlagProcessPid == (HANDLE)-1) break; if (HandleInfo->Information[i].ProcessId == (ULONG)g_FlagProcessPid || HandleInfo->Information[i].ProcessId == 4) continue; bool bCheck = ((HandleInfo->Information[i].GrantedAccess & PROCESS_VM_READ) == PROCESS_VM_READ || (HandleInfo->Information[i].GrantedAccess & PROCESS_VM_OPERATION) == PROCESS_VM_OPERATION || (HandleInfo->Information[i].GrantedAccess & PROCESS_VM_WRITE) == PROCESS_VM_WRITE); PEPROCESS pEprocess = (PEPROCESS)HandleInfo->Information[i].Object; if (pEprocess) { HANDLE handle_pid = *(PHANDLE)((PUCHAR)pEprocess + g_OsData.UniqueProcessId); HANDLE handle_pid2 = *(PHANDLE)((PUCHAR)pEprocess + g_OsData.InheritedFromUniqueProcessId); if (bCheck && (handle_pid == g_FlagProcessPid || handle_pid2 == g_FlagProcessPid)) { pEprocess = NULL; NTSTATUS status = PsLookupProcessByProcessId((HANDLE)HandleInfo->Information[i].ProcessId, &pEprocess); //得到了eprocess } } } }
Eprocess结构中有个handletable 叫做objtable,通过windbg+符号表或者直接网上搜你就很容易看到了:
之后使用ExEnumHandleTable函数进行剥离操作,不过请注意,win7和win10的第二个参数也就是 EnumHandleProcedure是不同的!一定要分开
if (NT_SUCCESS(status)) { //DebugPrint("Full Acess Handle! pid: %d \n", HandleInfo->Information[i].ProcessId); PHANDLE_TABLE HandleTable = *(PHANDLE_TABLE*)((PUCHAR)pEprocess + g_OsData.ObjTable); ExEnumHandleTable(HandleTable, g_isWin7 ? (DWORD64*)&StripHandleCallback_win7 : (DWORD64*)&StripHandleCallback_win10, (PVOID)HandleInfo->Information[i].Handle, NULL); ObDereferenceObject(pEprocess); }
WIN7:
WIN10:
注意事项:
这个剥离 不能放在第一时间剥离(比如你设置了createprocessnotify,进程启动了马上剥离的话会导致程序无法运行).
大功告成! 现在任何R3的外挂都无法有效的对你的程序进行读写了!
但是!这没有完!
你还要面对如下的东西:
1. 有数字签名的外挂驱动的直接驱动内核读写或者注入
2. 无数字签名的外挂驱动的内核读写或者注入
3. \Device\PhysicalMemory权限滥用
4. 虚拟机
3.升级对抗
以上保护只能保护R3,对方加载个驱动对你的进程进行直接物理内存读写你就没辙了.所以我要升级对抗,这部分就不贴代码防止某些无良公司ctrl+v,说一下具体思路:
1. 有数字签名的外挂驱动的直接驱动内核读写或者注入
这个最好办,外挂的驱动就绝对会加VMP以及有没有游戏进程的字符串,本地只需要判断是否有VMP加壳,或者是否有程序进程字符串,有就上传到云端进行人工分析即可.数字签名价格昂贵,确定是外挂后拉黑即可.
2.无数字签名的外挂驱动 这个主要是一些利用存在漏洞的驱动,比如CPUZ、GPUZ、VMBOX、Speedfan等驱动,他们那些驱动对自己的调用接口限制不严格导致的被其他外挂调用,调用后直接向物理内存写外挂驱动的shellcode,然后修复重定向再hook某个API启动外挂驱动.特点是无模块、pchunter无法检测.
这个就有很大学问了,大概有几步对抗方法:
1.检测通讯
由于是手工映射的驱动,他们无法使用IO码与R3外挂本体通讯,所以必须要另找法子,常见的包括不限于:
1.新建线程,用匿名管道、共享内存通讯
检测: 遍历所有系统线程、判断线程起始地址、RIP地址是否在正常驱动模块外、判断线程的起始字节是否是 JMP EAX,如果是八九不离十就是开了线程的恶意驱动,当然这个方法存在很多误报,比如腾讯TP为了隐藏自己代码就喜欢跟外挂一样ManualMap自己到系统空间里面(不知道现在还不会这样子做)导致检测为误报.
2.劫持正常IO handle用来当外挂的io handle
扫描所有正常驱动的IO控制handle,判断是否是正常驱动模块外,如果是八九不离十就是被劫持了.另外这个也存在误报,所有不应该当场封禁而是要上传内存信息继续分析.
3.hook内核函数,用来传buff通讯
没什么好说的,检测hook
2.检测本体
某些容易被利用的驱动是有自己的poolname的,如果存在这些pool name让他们自己重启电脑.
PiDDBCacheEntry: 这个是一个微软未公开的结构,这个结构存放所有加载过驱动信息,所以可以遍历这个结构去得到时间戳然后判断是否是那些被利用驱动的时间戳如果是让他们重启电脑并且上报
检测event log: 与PiDDBCacheEntry同理.
3. \Device\PhysicalMemory权限滥用
\Device\PhysicalMemory 是允许R3程序直接访问物理内存的一个句柄
system权限可以拥有读写进程权限
administrator权限只能读不能写
user完全不能读写
对于外挂来说,使用administrator来运行外挂然后滥用这个物理内存句柄就可以做基础的透视功能.或者过掉PPL保护注入代码到system.exe得到最高读写权限也不是不可以
为了防止这种,一般通过句柄表遍历进程,发现有这个物理内存句柄的进程而且不是system.exe就直接报警并且让用户退出才继续
4. 虚拟机
你以为就无懈可击了?
不还有一个叫做hypervisor的东西,顾名思义就叫做虚拟机. 外挂攻击者完全可以让系统进入自己的hypervisor里面然后使用EPT、NTP或者msr hook劫持常规的ssdt function与kernel function,让你的反作弊返回正常,就比如之前提到的遍历句柄使用NtQuerySystemInformation,外挂完全可以hook这个ssdt function去给你返回一个假的线程列表.
所以一个合格反作弊是要能检测到是否在虚拟机里面运行:
其实特别简单.不需要检测CPU ID,只需要检测 __rdtsc 这个指令的运行时间就像.因为虚拟机里面如果不对__rdtsc进行特殊处理,他的__rdtsc时间会比真机慢.所以可以直接通过判断__rdtsc执行时间是否异常从而判断出是否在虚拟机里面.当然也会有误报,所以要多试试几次.
但是你判断了虚拟机就会代表:
微软的hyperv-V服务会出现问题,网吧无盘系统会出现问题.云电脑会出现问题.
所以 请自己掂量一下是否要进行虚拟机判断.当然还有一个更好地方法,就是检测msr hook和ept hook,这里不再深究,毕竟全世界能自己独立研发虚拟机的人用手指头都扣的出来.
总结
反作弊与作弊的对抗是一个猫捉老鼠的游戏,魔高一尺道高一丈,每年技术都在更新.比如2020年的拳头FPS与faceit就已经开始全局hook 驱动iat来防止有漏洞驱动加载.
本文也不可能介绍全部的检测与反检测技术.总有一天这篇文章会过时.但这篇文章应该能让很多想学反作弊设计反作弊系统的人有所启发.