传奇广告查询第一站 同步gm1.com

分享一个编写传奇封外挂(反外挂)系统的完成过程 - 线程监测篇[原创]
原创 于2026-01-05 18:07:00发布
3 阅读
0
0

分享一个编写传奇封外挂(反外挂)系统的完成过程[原创]

正文

编写游戏外挂时、免不了需要用到线程知识。一般会使用到远程线程注入(远程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); //具体处理过程隐藏,请按照自己的项目需要自行设计 } } }

总结

本文主要介绍通过野线程判定法判断是否正在使用外挂的情况。如果你也有这方面兴趣爱好交流欢迎加扣58085250与我直接沟通交流指正。

管理员
0
0
0
分享
上一篇: 如何编写AutoHotKey的脚本
下一篇: 【Python深入浅出①】Python:从诞生到闪耀,代码世界的传奇进化
评论
历史记录
回顶部
浏览时间 游戏名称 游戏IP 开区网址
注册1GM论坛账号
  • 上传头像
注册

已有账号,

微信扫码登录
重置密码
重置密码

注册

绑定关联手机号
关联手机号