游戏检测的对抗与防护艺术

  • A+
所属分类:逆向工程

今天我们就来聊一聊所谓的游戏检测对抗与防护。本次用于实验的游戏的老版本的口袋西游。

从游戏外挂的利用角度来看,外挂的行为无非有如下两种:

1、修改游戏的关键数据和代码:属于篡改行为

2、call 游戏函数:属于未授权访问行为

游戏设计者针对这两种行为产生了无数的防护机制。然而道高一尺魔高一尺,逆向人员也根据游戏内的反外挂机制衍生出了相应的对抗手段。

首先我们需要先找到游戏中的一个功能 call,通过这个 call 来进行游戏检测的对抗与防护

通过 send 函数定位吃药 call

在 Windows 网络编程中,send 函数和 WSASend 函数都是 ws_32.dll 的导出函数,主要负责发送数据包。网络游戏客户端要与服务器通讯,必须调用 send 函数。

只要在 send 函数下断,然后进行堆栈回溯分析,就能准确定位关键函数的调用链。在这条链上,进行排查就能得到想要的功能 call

游戏检测的对抗与防护艺术

直接用 OD 附加游戏,在 send API 函数下断

游戏检测的对抗与防护艺术

随便吃一个药品,这里记下药品的位置是在第 22 格。此时游戏断下

游戏检测的对抗与防护艺术

打开调用堆栈,对每个 call 进行快速排查,最后可以确定吃药 call 是 491C50,右键,显示调用

游戏检测的对抗与防护艺术

在这个地方下个断点 让程序断下,这个 call 只需要分析一个参数就是 edx

edx 的值是 0x15,也就是十进制的 21。药品的位置在第 22 格,如果从 0 开始计数的话,那么这个参数大概就是药品的位置了。

游戏检测的对抗与防护艺术

接着我们用注入工具调用一下这个 call 的代码,除去 edx 需要为当前的药品位置,所有的汇编全部照抄即可。调用成功以后,准备工作完成

变量检测

单纯从技术角度来看,call 一个游戏函数来实现某项特定功能是很简单的,构造函数执行环境,传入必要参数,直接调用 call 就行了,其难点在于 call 游戏函数之后不被检测到

构造检测程序

由于口袋西游这个程序没有 call 检测,这里我们自己写了一个 dll 用于模拟 call 的检测,原理和实际的检测 call 是一样的。

首先打开吃药 call 检测,正常在游戏里吃药是不会有任何反应的

游戏检测的对抗与防护艺术

但是当我们点击吃药 call 的时候会弹出一个警告框。这样就构造了一个 call 的检测(吃药 call 参数写死了,记得把药品放到第一个格子)

处理变量检测

call 的检测原理

当我们正常在游戏中吃药是没有检测的,那是因为我们完完整整的把游戏的代码全部调用了;而我们直接点吃药 call的时候执行的代码是不完全的。

那么游戏的开发者就可以在吃药 call 的外层设置一个变量,并且给变量赋值,然后在吃药 call 内层的某一个位置对这个变量的值进行检测。如果值不对,说明代码没有被完全执行。

在检测完成之后,再修改这个变量的值到原始状态,然后进行下一次检测。整个流程的伪代码如下:

//用于检测的全局变量int status=0;function(){    //调用其他代码...........    status=1;    吃药call();     //调用其他代码...........}吃药call(){     //调用其他代码...........    if(status==1)    {        //正常执行代码    }    else    {        //检测到外挂 进行处理    }    //再对status进行赋初始值 以便进行下一次检测    status=0;     //调用其他代码...........}

查找检测变量

这个变量的值其实就相当于一个标志位,0 和 1 的可能性比较大。所以我们可以通过搜索 0 和 1 的方式来找到这个变量,如果不行再尝试未知的初始值。

游戏检测的对抗与防护艺术

首先在吃药 call 下断,正常吃药让程序断下,这个时候外层的检测变量已经被赋值了

游戏检测的对抗与防护艺术

我们在 CE 里扫描 1。然后 F9 运行程序,这个时候吃药 call 执行完毕,检测标志位也被恢复到原始状态

游戏检测的对抗与防护艺术

这个时候我们搜索 0。

游戏检测的对抗与防护艺术

一直重复这个过程,最后剩下十个左右的地址就可以停止搜索了。

游戏检测的对抗与防护艺术

利用二分法进行测试,将这些变量逐个修改为 1,看看哪一次调用 call 不会被检测到。这样可以定位到唯一的一个检测标志位。

然后我们在这个标志位下一个硬件写入断点,吃药让程序断下

游戏检测的对抗与防护艺术

第一次断下的位置,[68EB2374] 这个地址会被赋值为 1,F9 运行程序

游戏检测的对抗与防护艺术

第二次断下的位置,这个地方会在原来的值的基础上减一。接着判断地址的值是否为 0,不为零则执行检测代码

变量检测处理

这里有两种处理方法,第一种直接修改游戏代码,不执行检测 call。

游戏检测的对抗与防护艺术

第二种就是在调用 call 的时候,将检测变量赋值为正常调用 call 时候的数值。

堆栈检测

变量检测有一个很明显的缺点,就是需要一个实时去修改一个全局变量,这种方法很容易用CE搜索到。那么能不能通过其他方式去传递检测变量而又不被CE扫描到呢?答案是可以。这个就是堆栈检测。

堆栈检测原理 基于本地

堆栈检测的基本原理就是检测堆栈中的返回地址。如果游戏中一个正常的 call 被执行的话,那么堆栈中的所有的返回地址一定是本模块的;

正常调用吃药 call 堆栈地址如图:

游戏检测的对抗与防护艺术

所有的返回地址均为游戏主模块

相反,如果是通过自己写的 dll 注入到游戏内部来调用的话,那么堆栈中返回地址就全部是自己的 dll 模块的地址。

游戏检测的对抗与防护艺术

基于这个特点,程序只要在功能 call 内部去读特定的堆栈返回地址,检测返回地址是否属于本模块,就可以检测出当前的功能 call 是否被非法调用。

堆栈检测原理 基于服务器

另外一种情况比较少见,程序会通过一个参数将当前堆栈的一部分数据发送到服务器,服务器接收到这些数据以后,再对参数内的堆栈地址进行检测。

这种方式不太实用,首先发送这部分堆栈数据很容易被调用者发现,其次会增大服务器压力,所以这种检测方式相对比较少见。

处理一层堆栈检测

这种检测的处理方法也有两种,第一种单步跟找到这个堆栈检测 call,处理掉。但是这种检测 call 可能处在代码的任何一个位置,找起来不是那么容易。

第二种就是修改我们自己的程序代码。首先在进入 call 之后将返回地址修改为游戏模块内正确返回地址,过掉检测 call。然后在离开 call 之前将返回地址修改为我们自己的返回地址,保证游戏正常执行。其实就是写两个 HOOK 修改堆栈地址。

伪代码如下:

HOOK函数头(){    //保存原程序的返回地址    mov g_retaddr,[esp];    //修改返回地址为游戏内的返回地址    mov [esp],0x12345678}HOOK函数尾(){    //修改返回地址为原程序返回地址    mov [esp],g_retaddr;}

处理多层堆栈检测

如果堆栈检测只检测一层返回地址的话,那么就可以用上面的方式过掉检测。但是游戏往往会检测堆栈内的多个返回地址。

多层堆栈检测处理也很简单。处理一层堆栈检测要用 HOOK 的方式是因为在调用游戏内部的功能 call 的时候,我们没有办法去修改这个 call 内部的代码。

但如果是多层堆栈检测,就可以在调用这个功能 call 之前,就布局好当前的堆栈返回地址。伪代码如下:

function(){    sub esp,0x28;    //修改第一层返回地址    mov [esp],0x12345678;    //修改第二层返回地址    mov [esp+0x14],0x66666666;    //调用功能call    call xxxxxxx;    //还原堆栈    add esp,0x28;}

利用上面这种方式,不管游戏内部检测多少层堆栈返回地址,我们都可以通过直接伪造堆栈的方式达到过掉检测的目的。

CRC 检测

什么是 CRC 检测

上述的两种方式都是对 call 游戏函数的未授权访问行为进行对抗防护,而 CRC 检测是为了防止修改游戏的关键代码的篡改行为进行防护的手段。

事实上,修改游戏代码是很容易被检测到的,只要给代码段加一个校验功能,提前计算好整个代码段的 CRC 值,然后单独开一个线程,循环计算 CRC 并且比对之前预先保存的值,如果值不相等,说明代码段被修改。

构造 CRC 检测

游戏检测的对抗与防护艺术

同样,我们自己先构造一个 CRC 检测程序,并且开启检测

游戏检测的对抗与防护艺术

然后到吃药 call 的位置,在这个地方下一个断点,软件断点会将当前汇编前两个字节修改为 0xCC,也就等于是修改了代码段

游戏检测的对抗与防护艺术

然后弹出提示框,代表当前代码修改被检测到了

处理 CRC 检测

由于 CRC 检测需要实时的去访问整个代码段,然后计算代码段的 CRC 值;一般的程序是不需要对其他代码段进行访问的。

基于这个特点我们就可以在这个地方下一个内存访问断点,直接就能直接找到校验的代码

游戏检测的对抗与防护艺术

在吃药 call 下一个内存访问断点,游戏断下

游戏检测的对抗与防护艺术

简单分析一下上面的代码,eax 就是吃药 call 的地址,选中的部分代码对当前的吃药 call 地址进行一系列的计算,然后将计算结果存储到 [ebp-8] 的位置,然后比较 [ebp-8] 的值是否等于 25977C99,这个 25977C99 应该就是提前计算好的代码段的 CRC 的值。接着根据比较的结果对返回值进行赋值。

这部分就是游戏的 CRC 检测代码了,找到了检测代码,处理就简单的多。可以直接在函数头部让 eax=0,直接返回;也可以将 jnz 跳转直接 nop 掉。只要让检测代码不执行就 OK。

数据检测

在外挂行为检测方面,修改数据的行为最难定位,因为不仅外挂会修改数据,游戏本身也可能会修改数据。这个点既给游戏开发人员增加了难度,也给逆向人员增加了难度。

数据检测原理

数据检测的原理与处理方式和 CRC 检测基本一致。都是通过一系列的校验判断数据是否被修改,处理方式也可以通过访问关系定位到检测代码。

唯一的区别在于,程序的代码段是不需要去访问其他代码段的,所以只要在代码段下访问断点直接就能找到检测代码。

而数据段是会被游戏代码访问的,通过内存断点定位到访问代码以后,就多了一个步骤,需要从这些访问代码中找到哪个是数据检测代码。

一般来说检测代码都不在游戏主模块,会放在其他的dll模块。

构造检测程序

这里构造了一个对最大血量的数据检测程序,人物血量基址如下:

游戏检测的对抗与防护艺术

开启数据检测

游戏检测的对抗与防护艺术

在 CE 中修改人物血量,此时数据检测复现成功

数据检测处理

我们在血量的地址右键,查看是谁访问了这个代码。

游戏检测的对抗与防护艺术

这里有两条代码在实时访问血量地址。单从模块的角度很快就能看出第二条代码不是在游戏的主模块,如果从模块看不出来的话就只能挨个分析了。

处理方式和代码检测相同,定位到检测代码以后,对数据或者代码进行修改即可。这里不再重复。

总结

最后对以上几种检测的对抗与防护进行简单总结:

1、call 游戏函数的未授权访问行为检测——变量检测:利用 CE 搜索出检测标志位进行修改

2、call 游戏函数的未授权访问行为检测——堆栈检测:利用 HOOK 和内联汇编的方式伪造堆栈

3、修改游戏的关键代码的篡改行为检测——CRC 检测:利用访问关系下内存断点定位到检测代码进行处理

4、修改游戏的关键数据的篡改行为检测——数据检测:利用访问关系下内存断点定位到检测代码以后,排查处理

最后,附上我的 Github 地址,里面有游戏下载链接和检测代码,需要请自取(点击阅读原文直达):

https://github.com/TonyChen56/GameReverseNote

游戏检测的对抗与防护艺术

本文始发于微信公众号(信安之路):游戏检测的对抗与防护艺术

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: