对反游戏外挂技术的思考及实现

Posted 你的小甜欣.~

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了对反游戏外挂技术的思考及实现相关的知识,希望对你有一定的参考价值。

前言

现如今,有很多游戏外挂软件,它们可以修改游戏显示的数据和内部代码,来达到谋取利益的目的。在实现反外挂技术前,我先介绍一下常见的三种外挂:
1.模拟类外挂:该外挂可以说是最早的外挂了,它是往游戏发送伪造的按键消息来模拟人的手工操作。实现的思路为:

1.在Ring3层使用SendMassage、PostMassage、keybd_event、mouse_event等向消息队列中发送按键消息

2.使用回调函数KeyboardClassServiceCallback和MouseClassServiceCallback往类驱动输入摁键消息。

 2.内部Call调用外挂:使用逆向技术分析出游戏内部的汇编代码,把里面对游戏玩家有利的函数作为外挂的实现功能。实现的思路为:

1.使用逆向工具,去除壳和里面的反调试功能

2.分析游戏的使用过程、玩家自身的信息、背包信息、敌人信息等

3.逆向分析出游戏的明文发包功能

4.根据游戏的明文发包函数和报文信息分析游戏内部功能实现的函数

 3.脱机外挂:分析出游戏客户端和服务端之间的通信逻辑,使用自己发送和接受数据来跟服务端进行交互。实现的思路如下:

1.使用逆向工具,去除壳和里面的反调试功能

2.逆向分析游戏中的汇编代码,分析出加解密算法及各种资源信息

3.分析出登录封包,获取各种跟游戏相关的信息

4.整合寻路算法,实现挂机

保护call函数和基址数据

1.最近玩植物大战僵尸和连连看等游戏的时候发现,这些游戏需要通过启动程序才能正常运行,那么我突发奇想,如果在程序运行前,修改游戏中的部分代码段数据,等需要运行游戏时再通过另一个程序恢复代码段数据,那么就可以起到保护游戏的作用。

2.对于游戏外挂制作者,他们经常习惯性的使用OD工具和CE工具、ida工具等,找出游戏的基址数据(也为全局变量)和内部call,然后修改基址数据或者修改执行流程,调用内部call。那么在游戏运行前就对基址数据地址和某些call后接地址进行修改,等需要运行游戏时,再修改回基址地址和call调用的地址,就可以起到反外挂的作用。

3.接下来,介绍一下这样做的作用:

4.简单介绍一下实现的流程:

1.读取PE文件信息到内存中,遍历PE文件中的代码段,找到函数头部地址和使用的全局变量地址;

2.在代码段中,找出调用函数的call指令,把call后接地址进行修改,对于数据,找到使用全局变量的mov指令,修改地址操作数;

3.运行时,使用CreateProcess函数创建线程并挂起,然后恢复代码段中的数据并运行进程

5.我开发了一款反游戏外挂工具(该工具及其源码可在文章末尾下载)来模拟这个过程,在文件选择框选择要保护的PE文件,把需要保护的函数及数据添加到左边栏中,执行保护时,在PE文件目录下会生成已保护的PE文件,点击运行时,就能正常运行。

检测代码段

1.在游戏没有运行之前,修改、添加或删除PE文件的二进制信息可以起到修改程序流程的作用,从而起到外挂的作用。实现的技术有:在代码段的空白区域添加"洞穴代码",修改OEP的值,让程序跳转到洞穴代码;使用Inline HOOK执行外挂功能函数;修改代码段中的跳转指令和call后接地址,执行游戏内部汇编指令或者调动内部call等

2.在游戏运行时,通过外挂工具动态修改、添加或删除进程代码段数据或者动态注入shellcode同样也能起到修改程序流程的作用,那么需要校验PE文件和游戏内存数据就可以检测出游戏外挂。

3.在实际运用中,可以计算出PE文件的校验值,存放在游戏第三方程序的数据段中或者存放在游戏的服务器端,那么在游戏运行前校验PE文件值和运行时动态检验进程内存,接下来使用工具来模拟这个过程:

1.选择PE文件计算PE文件校验值和记录代码段数据;

2.选择需要校验的文件,同样也计算出校验值并比较是否变化;

3.选择正在运行的进程,比较代码段数据是否变化。

4.实现的思路为:

1.使用MD5算法计算出前后PE文件的校验值,并进行比较;

 2.先使用数组存放PE文件代码段数据,后读取进程内存中的代码段数据,然后比较数组数据是否相同

 5.在游戏运行时,把游戏运行过程中使用过的函数及其相关的寄存器信息存放在服务端,在服务端检测函数的调用流程,就可以判断游戏内部call有没有非法调用。不过我这里模拟这个过程的方式是把函数运行时的信息写到日志文件中,查看日志信息来判断游戏内部call是否被非法调用。

6.实现的思路为:

1.调式进程,循环接受调式事件;

2.在刚开始调式时,把函数头部修改为CC;

3.当发现中断时,判断发生中断的地址是否为函数头部地址,是的话,把线程上下文信息记录在日志文件中

//调式进程
DebugActiveProcess(pid):                           
//循环等待调式事件                           
while (WaitForDebugEvent(&de, INFINITE))                               
    {                               
    ......                               
    PEXCEPTION_RECORD per = &pde->u.Exception.ExceptionRecord;                               
    //触发创建调式进程事件时                               
    if (CREATE_PROCESS_DEBUG_EVENT == de.dwDebugEventCode)                               
        //修改函数头部                               
        SetUserFunc(&de);                           
    //触发异常事件时                               
    else if (EXCEPTION_DEBUG_EVENT == de.dwDebugEventCode)                               
        //判断触发异常地址                            
        if (OnExceptionDebugEvent(&de))                           
            continue;                       
    //继续接受调式事件                               
    ContinueDebugEvent(de.dwProcessId, de.dwThreadId, dwContinueStatus);                               
 
    }

HOOK检测

1.现在,有各种各样的HOOK技术,包括 InLine HOOK、IAT HOOK、API HOOK等,其中大部分需要注入dll来实现HOOK功能,那么游戏运行时,服务端或者游戏第三方程序可以检测有无dll注入,有的话,及时清除掉。实现的思路为:

1.先记录游戏运行过程中用的dll文件名;2.游戏运行时遍历进程使用的dll文件名,若遇到不名dll文件名,则进行释放;

2.使用工具模拟这个过程:选择进程,选中dll文件所在目录,点击检测dll,就可检测出进程中是否有dll注入。

3.那么,除了检测dll文件外,还可以检测 IAT表和函数内部代码的跳转指令来判断游戏中是否有IAT HOOK或者InLine HOOK(跟API HOOK差不多,范围比API HOOK小,我这里就不介绍了),实现的过程为:

1.选中游戏PE文件后,定位到导入表目录,遍历INT表中的_IMAGE_THUNK_DATA,通过里面的联合体u1,来获取游戏运行后IAT表中的函数地址;

2.选中进程,读取进程内存中IAT表信息并比较函数地址,不同则说明有 IAT HOOK;

3.对于InLine HOOK,读取进程代码段中函数区域的跳转指令操作数,若发现跳转到别的函数区域,则说明有InLine HOOK;

4.使用工具模拟这个过程:选中进程和PE文件,再点击 IAT HOOK按钮或者InLine HOOK按钮就可进行检测。

多开检测和动态调式工具检测

1.现在的打金工作室可以同时开多个号来刷金币,那么需要限制游戏多开,当动态调式进程时,在任务管理器中可以看到调式工具的主线程成了调式进程,那么调式器内存里面必然会有调式进程的内存信息,那么找到调式工具后再查找里面是否含有游戏进程的内存信息,若有则可以判断,游戏处于调式状态。

2.对于防多开,经常会用到下面的代码,那么外挂制作者修改跳转指令或者NOP掉汇编代码就可以实现多开。为了防止这种情况,我这里把CreateMutex函数头部修改为CC,使用调式器的方式不断监控异常事件,当CreateMutex函数头部触发中断异常时,获取互斥体变量名(地址为:ESP+8),并判断该变量名是否出现过,若出现过,则检测出进程出现多开。实现的关键代码如下:

//创建互斥体
    HANDLE hMutex = CreateMutex(NULL,FALSE,"XXXXXX");
    DWORD dret = GetLastError();
    if(hMutex)
    {
        if (ERROR_ALREADY_EXISTS == dret)
        {
            CloseHandle(hMutex);
            return 0;
        }
    }
   .............
    //WaitForSingleObject(hMutex,INFINITE);
  ............
    ReleaseMutex(hMutex);
 
    return 0;
}

检测多开的关键代码:

//解除HOOK 恢复原值
        WriteProcessMemory(&pde->dwProcessId, g_pCreateMutex,&g_Orignal,sizeof(BYTE),NULL);
        //获取线程上下文
        ctx.ContextFlags = CONTEXT_CONTROL;
        GetThreadContext(g_cpdi2.hThread,&ctx);
        //得到ESP+8的值
        ReadProcessMemory(g_cpdi2.hProcess, LPVOID(ctx.Esp + 0x8), &EspContent, sizeof(DWORD), NULL);
        printf("EspContent: %x\\n", EspContent);
        //取出地址并获取字符串
        Name[NameNum++] = (char*)(EspContent);
        //判断有无重复互斥名
        list<string> list1;
        for (int i = 0; i < NameNum; i++)
        {
            list1.push_back(Name[i]);
        }
        int len1 = list1.size();
        list1.unique();
        int len2 = list1.size();
        //长度不等,则有重复元素
        if (len1 != len2)
        {
            IsOpenMore = 1;
            return -1;
        }
        //让EIP为当前地址
        ctx.Eip = (DWORD)g_pCreateMutex;
        //设置线程上下文
        SetThreadContext(g_cpdi2.hThread, &ctx);
        //继续调式
        ContinueDebugEvent(pde->dwProcessId, pde->dwThreadId, DBG_CONTINUE);

3.对于多开检测,我还想出了三种检测方法:

1.获取进程代码段信息,使用代码段信息匹配的方式来判断是否有相同的进程运行;

2.获取进程所有的窗口句柄,匹配查找有无跟窗口标题相同的窗口,有则说明多开;

3.对进程名进行匹配;

4.对于调式工具检测,还是使用代码段识别法:获取OD工具中代码段信息匹配寻找相同进程,若有,则再识别内部有无游戏进程的内存信息并且再判断调式进程是否为调式工具的子进程,两者有一成立,则说明游戏进程处于调式状态,实现的关键代码如下:

void ToolAndOpenMore::GetToolCode(BYTE* ToolCode, CString strPath)
{
    //把PE文件加载到内存中
    PVOID PeBuffer =PEFileToMemory(CStringToCharSz(strPath));
    //PE文件头
    pPEHeader = (PIMAGE_FILE_HEADER)((DWORD)PeBuffer + pDosHeader->e_lfanew + 0x4);
    //节表数量
    DWORD SectionNum = pPEHeader->NumberOfSections;
    //PE可选头
    pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pPEHeader + IMAGE_SIZEOF_FILE_HEADER);
    DWORD EP = pOptionHeader->AddressOfEntryPoint;
    //代码段节表头
    pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pOptionHeader + pPEHeader->SizeOfOptionalHeader);
    tpSectionHeader = pSectionHeader;
    int j = 0;
    //遍历OEP之后的代码段数据
    for (int i = tpSectionHeader->PointerToRawData; i < tpSectionHeader->PointerToRawData + tpSectionHeader->SizeOfRawData && j<SZLEN; i++)
    {
        if (DataHs.FoaToRva(PeBuffer, i) < EP)
            continue;
        ToolCode[j++] = *((BYTE*)((DWORD)PeBuffer + i));
    }
    return;
}

5.接下来,使用工具进行检测:多开检测只需选中进程点击“多开检测”按钮即可;调式工具检测要选中进程和调式工具,操作情况如下所示:

添加反调式法

1.在游戏的PE文件中添加反调试,不为是一种较好的反外挂方法。在PE文件中修改TLS表信息,扩大最后一个节表并在空白区域添加PIMAGE_TLS_DIRECTORY结构信息、TLS回调函数,并在TLS回调函数中添加反调试功能即可实现反调试。

 2.实现反调试的思路为:

1.把PE文件加载到内存中,使用偏移的方式定位到TLS数据目录,修改其中的 RVA字段值和Size字段值,让 RVA指向PIMAGE_TLS_DIRECTORY起始地址;

2.定位到最后的节表结构并增加SizeOfRawData的值和修改Characteristics的值,使最后的节表具有可执行特性。

3.最后把生成的PIMAGE_TLS_DIRECTORY结构数据和回调函数ShellCode复制到最后的节表中。

3.除了往PE文件添加TLS反调式外,还可以添加SEH反调试,实现的思路为:
 

1.先在代码段,寻找足够大的空闲区域,若没有找到,则新增最后节表大小;

2.确定空闲区域开始地址,修改OEP的值,在空闲区域添加触发SEH异常的Shellcode,在该Shellcode中有异常处理函数,该异常处理函数会判断游戏是否处于调式中,如果没有处于调式状态则跳回OEP中,否则中止;

4.接下来使用工具添加反调试,选中文件,点击按钮即可。

以上使用的工具和源码可在该处下载:https://github.com/chenpan01/Anti_GameAssist。希望该工具及源码对大家有所帮助,工具存在bug的话,欢迎提出,大家共同探讨,共同提升!

以上是关于对反游戏外挂技术的思考及实现的主要内容,如果未能解决你的问题,请参考以下文章

Canvas贪吃蛇大作战斗实现及思考

Unity模拟经营类游戏Demo部分代码及技术总结

从片段调用 Google Play 游戏服务

代码片段如何使用CSS来快速定义多彩光标

分布式锁的技术选型及思考

20155201 李卓雯 《网络对抗技术》实验一 逆向及Bof基础