|  | 
 
| 一个编写传奇封外挂(反外挂)系统的完成过程 - 线程监测篇 编写游戏外挂时、免不了需要用到线程知识。一般会使用到远程线程注入(远程CALL)和代码注入并启动线程来运行被注入的代码(或DLL)。通过对线程的监控可检测到非法外挂的使用情况。
 
 一、远程线程注入
 远程线程注入可注入DLL或一段独立的shellcode代码、注入DLL和注入shellcode的过程大同小异。一般过程如下:
 
 1、先使用OpenProcess函数打开进程
 
 2、 使用VirtualAllocEx函数开辟新的空间用于存储shellcode代码或要注入的dll文件路径
 
 3、使用 WriteProcessMemory函数将数据写入第2步开辟的空间
 
 4、使用CreateRemoteThread函数启动新的线程执行注入代码。
 
 注入DLL演示代码:
 
 
 注入shellcode演示代码:复制代码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;
}
 二、如何通过线程检测外挂复制代码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);
                         
                        //具体处理过程隐藏,请按照自己的项目需要自行设计
                }
        }
 
        
}
    本文主要介绍通过野线程判定法判断是否正在使用外挂的情况。 
 | 
 |