|
一个编写传奇封外挂(反外挂)系统的完成过程 - 线程监测篇
编写游戏外挂时、免不了需要用到线程知识。一般会使用到远程线程注入(远程CALL)和代码注入并启动线程来运行被注入的代码(或DLL)。通过对线程的监控可检测到非法外挂的使用情况。
一、远程线程注入
远程线程注入可注入DLL或一段独立的shellcode代码、注入DLL和注入shellcode的过程大同小异。一般过程如下:
1、先使用OpenProcess函数打开进程
2、 使用VirtualAllocEx函数开辟新的空间用于存储shellcode代码或要注入的dll文件路径
3、使用 WriteProcessMemory函数将数据写入第2步开辟的空间
4、使用CreateRemoteThread函数启动新的线程执行注入代码。
注入DLL演示代码:
- BOOL remoteCall_dll(DWORD dwPid,char* szDllPath)
- {
- //打开进程
- HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, false, dwPid);
- if (!hProcess)
- {
- return FALSE;
- }
-
- //开辟空间
- LPVOID lpDllPathAddress = VirtualAllocEx(hProcess, NULL, strlen(szDllPath) + 1, MEM_COMMIT, PAGE_READWRITE);
- if (!lpDllPathAddress)
- {
- CloseHandle(hProcess);
- return FALSE;
- }
-
- //写入要注入的DLL地址
- if (!WriteProcessMemory(hProcess, lpDllPathAddress, szDllPath, strlen(szDllPath) + 1, NULL))
- {
- VirtualFreeEx(hProcess, lpDllPathAddress, 0, MEM_RELEASE);
- CloseHandle(hProcess);
- return FALSE;
- }
-
- //获取LoadLibraryA函数地址,一般来说不同进程的函数地址是不同的
- //但是同一个操作系统下系统DLL一般加载地址相同,所以可简单地认为本进程的 LoadLibraryA 与目标进程的 LoadLibraryA 地址是相同的
- HMODULE hModlKernel32 = GetModuleHandleA("kernel32.dll");
- if (!hModlKernel32)
- {
- VirtualFreeEx(hProcess, lpDllPathAddress, 0, MEM_RELEASE);
- CloseHandle(hProcess);
- return FALSE;
- }
- LPTHREAD_START_ROUTINE fnLoadLibrary = (LPTHREAD_START_ROUTINE)GetProcAddress(hModlKernel32, "LoadLibraryA");
-
- //启动远程线程运行 LoadLibraryA
- HANDLE hRemoteThread = CreateRemoteThread(hProcess, NULL, 0, fnLoadLibrary, lpDllPathAddress, 0, NULL);
- if (!hRemoteThread)
- {
- VirtualFreeEx(hProcess, lpDllPathAddress, 0, MEM_RELEASE);
- CloseHandle(hProcess);
- return FALSE;
- }
-
- WaitForSingleObject(hRemoteThread, INFINITE);
- VirtualFreeEx(hProcess, lpDllPathAddress, 0, MEM_RELEASE);
- CloseHandle(hRemoteThread);
- CloseHandle(hProcess);
- return TRUE;
- }
复制代码 注入shellcode演示代码:
- BOOL remoteCall_shellcode(DWORD dwPid, char* szShellcode, DWORD dwCodeLength, LPVOID lpParameter)
- {
- //打开进程
- HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, false, dwPid);
- if (!hProcess)
- {
- return FALSE;
- }
-
- //开辟空间
- LPVOID lpCodeAddress = VirtualAllocEx(hProcess, NULL, dwCodeLength, MEM_COMMIT, PAGE_READWRITE);
- if (!lpCodeAddress)
- {
- CloseHandle(hProcess);
- return FALSE;
- }
-
- //写入要注入的 shellcode 代码
- if (!WriteProcessMemory(hProcess, lpCodeAddress, szShellcode, dwCodeLength, NULL))
- {
- VirtualFreeEx(hProcess, lpCodeAddress, 0, MEM_RELEASE);
- CloseHandle(hProcess);
- return FALSE;
- }
-
- //启动远程线程运行 shellcode
- HANDLE hRemoteThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)lpCodeAddress, lpParameter, 0, NULL);
- if (!hRemoteThread)
- {
- VirtualFreeEx(hProcess, lpCodeAddress, 0, MEM_RELEASE);
- CloseHandle(hProcess);
- return FALSE;
- }
-
- WaitForSingleObject(hRemoteThread, INFINITE);
- VirtualFreeEx(hProcess, lpCodeAddress, 0, MEM_RELEASE);
- CloseHandle(hRemoteThread);
- CloseHandle(hProcess);
- return TRUE;
- }
复制代码 二、如何通过线程检测外挂
我们已经知道外挂会向我们的游戏进程注入代码启动线程,那么我们就可以通过对线程的检测来判断玩家是否使用外挂。比如知名的简X挂,就是注入一段代码在user32.dll的节空闲空间,然后启动线程来执行的。
需要注意:通过线程检测游戏外挂有一些特殊情况需要处理,比如说输入法有时候也会启动线程,安全软件也可能会启动新线程,win10,win11的内存压缩功能也会启动新线程。会给我们通过线程来判断是否外挂的方案造成干扰。
应对办法: 一) 尽量采集这些合法的线程特征,加以排除。二) 对确定是外挂的线程特征加入黑名单。具体实施方案请大家自行思考处理。做成服务器端收集这些线程数据,再人为判断加入特诊库的方式,类似杀毒软件的特征库。
但是本文介绍另一种简单的懒得维护特征库的方法,配合内存模块监测法一起使用。我们不求一种方案能完全监测出所有的外挂,只要能监测出一类特征就行。将我们整个系列介绍的所有方法汇集在一起合理地使用、基本就可以杜绝所有的外挂形式了。这里介绍的方法我把它叫做‘’野线程监测法”,所谓野线程,就是我们可以预料的所有合法空间(野空间)以外的线程。其中合法空间包括PE文件正常代码节空间,和自己所申请的空间。而第三方外挂进程通过 VirtualAllocEx 函数申请的空间自然就在合法空间之外。我们可以简单地判断:只要是线程的启动地址(甚至定时监测线程的eip地址),只要在野空间中,就判定为是非法外挂。
这里判断线程可以使用线程枚举或通过peb获取线程列表、还有一种是被加载进内的DllMain函数也会接收到线程的启动和结束事件 DLL_THREAD_ATTACH/DLL_THREAD_DETACH。枚举线程会用到这些函数 CreateToolhelp32Snapshot、Thread32First、Thread32Next 以及使用微软未公开函数 ZWQueryInformationThread来获取线程的入口地址。
- typedef enum _THREADINFOCLASS {
- ThreadBasicInformation = 0,
- ThreadTimes = 1,
- ThreadPriority = 2,
- ThreadBasePriority = 3,
- ThreadAffinityMask = 4,
- ThreadImpersonationToken = 5,
- ThreadDescriptorTableEntry = 6,
- ThreadEnableAlignmentFaultFixup = 7,
- ThreadEventPair_Reusable = 8,
- ThreadQuerySetWin32StartAddress = 9,
- ThreadZeroTlsCell = 10,
- ThreadPerformanceCount = 11,
- ThreadAmILastThread = 12,
- ThreadIdealProcessor = 13,
- ThreadPriorityBoost = 14,
- ThreadSetTlsArrayAddress = 15, // Obsolete
- ThreadIsIoPending = 16,
- ThreadHideFromDebugger = 17,
- ThreadBreakOnTermination = 18,
- ThreadSwitchLegacyState = 19,
- ThreadIsTerminated = 20,
- ThreadLastSystemCall = 21,
- ThreadIoPriority = 22,
- ThreadCycleTime = 23,
- ThreadPagePriority = 24,
- ThreadActualBasePriority = 25,
- ThreadTebInformation = 26,
- ThreadCSwitchMon = 27, // Obsolete
- ThreadCSwitchPmu = 28,
- ThreadWow64Context = 29,
- ThreadGroupInformation = 30,
- ThreadUmsInformation = 31, // UMS
- ThreadCounterProfiling = 32,
- ThreadIdealProcessorEx = 33,
- ThreadCpuAccountingInformation = 34,
- ThreadSuspendCount = 35,
- ThreadActualGroupAffinity = 41,
- ThreadDynamicCodePolicyInfo = 42,
- MaxThreadInfoClass = 45,
- } THREADINFOCLASS;
-
- typedef DWORD(WINAPI* type_ZWQueryInformationThread)(
- _In_ HANDLE ThreadHandle,
- _In_ THREADINFOCLASS ThreadInformationClass,
- _In_ PVOID ThreadInformation,
- _In_ ULONG ThreadInformationLength,
- _Out_opt_ PULONG ReturnLength
- );
-
- HMODULE hMod_Ntdll = LoadLibraryA("ntdll.dll");
-
- type_ZWQueryInformationThread ZWQueryInformationThread = (type_ZWQueryInformationThread )GetProcAddress(hMod_Ntdll ,"ZWQueryInformationThread");
-
复制代码 下面是本项目实际开发中用到的函数部分参考
-
- void game_thread::detectthread(bool bForInit)
- {
- int i;
- //枚举线程,检查线程函数地址是否在模块空间内
- THREADENTRY32 te32 = { 0 };
- te32.dwSize = sizeof(THREADENTRY32);
- HANDLE hThreadSnap = g_pApis->CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
- if (hThreadSnap == INVALID_HANDLE_VALUE)
- {
- return;
- }
- DWORD dwCurPID = g_pApis->GetCurrentProcessId();
- if (g_pApis->Thread32First(hThreadSnap, &te32))
- {
- do
- {
- if (te32.th32OwnerProcessID == dwCurPID)
- {
- cslock lock(this->m_threadlist_pcs);
- GAME_THREAD_ITEM_INFORMATION* pinfo = this->findinfo(te32.th32ThreadID);
- if (pinfo == NULL || pinfo->iswhite == FALSE)
- {
- HANDLE hThread = g_pApis->OpenThread(THREAD_ALL_ACCESS, FALSE, te32.th32ThreadID);
- //获取线程地址 dwStartAddress = 0 的是主线程 ,在 bForInit = true 时已加入白名单
- DWORD dwStartAddress = 0;
- if (g_pApis->ZWQueryInformationThread(hThread, THREADINFOCLASS::ThreadQuerySetWin32StartAddress, (PVOID)&dwStartAddress, sizeof(dwStartAddress), NULL) == 0L
- && dwStartAddress != 0
- )
- {
- //具体处理过程隐藏,请按照自己的项目需要自行设计
- }
- }
- if (pinfo != NULL)
- {
- pinfo->detect_count++;
- }
- }
- } while (g_pApis->Thread32Next(hThreadSnap, &te32));
- }
- g_pApis->CloseHandle(hThreadSnap);
-
- //计数累加
- if (!bForInit)
- {
- this->m_detect_called_count++;
- }
-
- {
- cslock lock(this->m_threadlist_pcs);
-
- for (i = 0; i < this->m_thread_list.Get_Count(); i++)
- {
- GAME_THREAD_ITEM_INFORMATION* pinfo = this->m_thread_list.Get_ItemAt(i);
-
- //具体处理过程隐藏,请按照自己的项目需要自行设计
- }
- }
-
-
- }
复制代码 总结 本文主要介绍通过野线程判定法判断是否正在使用外挂的情况。
|
|