我们基于CS1.6 3248版本开发了一个简单的修改器,通过DLL注入实现了如下功能
- 免疫伤害
- 一击毙命
- 透视
- 无限手雷
- 反烟雾弹
- 反闪光弹
整个工程分为三个部分:
- DLL注入器
- OpenGL hack
- mp.dll hack
我们主要使用了两个工具:
- Cheat Engine(以下简称CE)
- IDA Pro
- 运行CS1.6 3248版本,并创建游戏
- 运行inject.exe,进行dll的注入
- 默认开启无敌+暴击模式,F1-F3分别切换透视、反闪光、反烟雾功能
- 我们将自己的DLL注入到目的进程中,获得目的进程内存空间的访问权限。
- 获取权限后,我们要修改内存中的关键语句,使其按照我们的想法执行。这一步又分为两类
- Hook。修改原函数的内容,使其在执行前先跳转到我们注入的函数,之后再跳转回原函数。
- Overwrite。直接修改原函数的语句。
我们以生命值(HP)为例来说明我们如何定位关键数据的内存地址。
我们首先在游戏中通过各种方式反复改变自己的HP,同时利用CE监控内存变化。如果内存中的某个数据总是和我们的HP保持一致,那么我们就可以认为这个地址很大概率上储存了玩家的生命值。但在实际操作中,HP的值往往在内存中存在多个副本,还需要逐个分析筛选,找到唯一的记录HP的内存地址。
但是即便我们这里得到了一个地址,这个地址也不会是一成不变的。在重每次新运行进程的时候,进程的地址都有可能变化,因此我们需要把获得的绝对地址转化为相对于进程的偏移地址。具体来说,我们一级一级寻找它的指针,直到这个地址位于游戏主进程cstrike.exe里面。一个基本的步骤如下
- 我们获得了HP存储的地址 0x0B221D5C。
- 我们监测修改该处值的代码,并使自己受伤,然后可以看到修改该地址的语句含有[eax+00000160]的字样。
- 因此我们可以大胆断定,eax此时存的是某个结构体的指针,而hp作为结构体的成员被存储在+160的位置。因此我们读取eax的值,然后直接在内存中搜索这个值,就可以找到存放这个指针的位置。
至此,我们就成功找到了hp上一级的地址。重复上述操作直到该地址不再是绝对的内存地址,而是cstrike.exe + offset的形式时,我们就找到了hp的正确访问方式,而不用担心进程加载以及malloc的时候开辟空间的不同造成的影响。
同样的,我们也对其他容易观测并修改的值进行了定位,并依靠其获得了许多有用的数据如下
地址 | 描述 |
---|---|
[[[cstrike.exe+11069BC]+7c]+4]+160 | 玩家HP |
[[[cstrike.exe+11069BC]+7c]+4]+16C | 玩家是否可以受伤 |
[[[cstrike.exe+11069BC]+7c]+4]+1BC | 玩家护甲 |
[[[cstrike.exe+11069BC]+7c]+4]+8 | 玩家坐标 X |
[[[cstrike.exe+11069BC]+7c]+4]+C | 玩家坐标 Y |
[[[cstrike.exe+11069BC]+7c]+4]+10 | 玩家坐标 Z |
[[cstrike.exe+11069BC]+7c]+1CC | 玩家金钱 |
[[cstrike.exe+11069BC]+7c]+3C4 | 到达特定区域(包点、购枪点、人质点)会更新 |
[[[cstrike.exe+11069BC]+7c]+5EC]+CC | 玩家当前武器子弹 |
其中像X,Y,Z坐标的信息是难以通过搜索内存定位的。但从已经确定的如玩家HP,护甲等信息,我们可以猜到[cstrike.exe+11069BC]+7c]存放着玩家的相关信息,而[[cstrike.exe+11069BC]+7c]+4]里面存放的就是和玩家物体直接相关的信息。那么我们通过对[[cstrike.exe+11069BC]+7c]+4]里所有的offset进行监控,也就能定位出这样一些难以直接定位的数据。
在找到关键数据后,我们需要找到修改这些数据的函数语句。我们首先使用CE监视修改关键数据的语句,这里往往会得到很多条;然后需要逐条查看反汇编,进行筛选,最后找到最核心的语句。这样我们就拿到了语句相对于动态链接库起始位置的地址。由于动态链接库在内存中的加载地址并不固定,之后我们还需要借助Windows提供的一系列API来计算出这条语句在内存中加载的实际地址,才能进行后续的操作。
定位目标函数的一般流程如下:
- 使用CE监视内存,不断修改游戏中的某个状态,找到内存中控制这个状态的数据地址。
- 使用CE找到修改这条数据的语句的地址(实际上关心的是在dll中的偏移量)。
- 基于IDA Pro对dll的反汇编,根据上面得到的地址,找到对应的语句,阅读包含这条语句的函数的汇编代码,理解含义。
修改分为两类,一种是简单地写入新语句,覆盖原来的内容。这部分相对比较简单,只需要把机器码直接写入内存指定即可。但是限制较多,一般只是写入0x90(nop),相当于移除了某条指令。
更为通用的办法是进行hook。假设原代码的地址为A,要注入的代码地址为B,大体思路如下:
- 申请一块内存C,保存原代码A的第一条语句。
- 将原代码A的第一条语句改为跳转到注入代码B。
- 注入代码执行完毕后,最后一条指令设置为跳转到C。
- C执行完原代码A的第一条指令后,最后一条指令设置为跳转到原代码A的第二条指令。 这样,我们就在执行原代码之前,先执行了我们自己的代码。
这个功能的代码量只有不到20行,但是非常抽象,具体实现请参考我们的代码。
远线程注入,与《逆向工程核心原理》书上的例子类似,用了自下到上的编程思想,其中CreateRemoteThread是关键函数,大体思路如下:
- OpenProcess获得要注入进程的句柄。
- VirtualAllocEx在目标进程中开辟出一段内存。
- WriteProcessMemory将要注入的dll写入分配的内存。
- 获取LoadLibraryA()API的地址。
- 在目标进程中创建远线程。
在CS的设置里是可以设置OpenGL/D3D模式的,而主流都是使用OpengL模式,因此我们选择对OpenGL里面的关键函数进行hook。
OpenGL的核心函数是glBegin,在绘图前都会调用这个函数。因此我们抢在调用前对将要绘制的实体进行检测和判断就可以达到hack的目的。
首先我们去检查glBegin绘制了哪些实体。通过对不同的mode类型赋予不同的颜色,我们发现静态的地图文件并没有实时通过glBegin绘制,而像枪械、人物等实体则是调用了该函数。同时我们也获得了cs在绘制人物时所选取的mode类型。有了这些铺垫,一个简单的透视外挂就呼之欲出了
- hook进glBegin函数
- 在glBegin调用之前检测是否为人物的mode,如果是,则修改其z深度(或者直接禁用深度检测)
- 重新接回正常的glBegin函数进行绘制
至此,我们就在绘制人物的时候将其深度调低,使其能出现在障碍物前面实现透视。其他像反闪光、反烟雾等也采用了类似的思想——只要检测到绘制的是相关的物件,直接return而不去绘制就能达到类似的效果了。
mp.dll是cs的核心组件。cs的核心逻辑,如移动、伤害判定等都是在mp.dll中完成的。通过对mp.dll的overwrite和hook,我们实现了伤害免疫、一击毙命、无限手雷。
在查看血量周围的变量时,发现+16C和+170这两个值在死亡、复活后会被修改。而在修改该段代码时,发现+170的变量可以标识玩家与观察者。当该值非零时BOT将不再攻击玩家,然而玩家也会被切换为观察者视角。而在修改+16C的时候,发现该值决定了玩家是否会受到伤害(实际上是判定子弹是否会和玩家碰撞)。当玩家死后进入观察者模式后该值置0表示不再受到子弹的伤害,而每局开始该值都会被置为2表示可以受伤。发现了这点后,我们找到每局开始时修改该值的汇编代码,并对该语句进行hook。每次执行完置2的操作时,我们都把玩家的值置为0,这样就达到了伤害免疫的效果。
通过分析反汇编得到的代码,我们发现游戏中的角色在受到伤害后会调用一个函数来减少自身的生命值与护甲值,这个函数具有四个参数,第一个参数是指向攻击者的指针,第三个参数是伤害值。我们在这个函数的入口处进行了hook,根据第一个参数判断攻击者是否为玩家,如果是的话则增大第三个参数(通过[ebp+offset]),提高玩家的伤害以达成一击毙命。
在mp.dll中每一种武器在开火时都会触发一个函数,这个函数里定义了武器开火的各种操作,如减少自身子弹数量等。我们在定位手雷的这个开火函数后,将其中的dec eax(将剩余手雷数量减一)修改为nop,就达到了无限手雷的目的。实际上,通过类似的方法,我们可以实现任意武器的无限子弹,甚至可以将dec改成inc使子弹越打越多,我们这里只选取了手雷作为一个例子。
- 相比于其他项目,我们的代码量很小,还有大量的cpp代码,但我们实际接触到的汇编代码却非常多,而且更难理解。我们阅读的汇编代码都是由反汇编得出的,我们发现编译器编译得到的汇编代码常常和标准的汇编有很大的区别,比如dll内的匿名函数经常没有push ebp & mov ebp esp的典型操作,我们甚至还看到了mov ebp, ecx这种匪夷所思的语句。这些都是课上所没有讲到过,却又确实被编译器采用的。此外,课上没有涉及到的FPU操作,在我们逆向得到的汇编代码中也几乎是随处可见。
- 我们认为,汇编课的目的并不是练习汇编,用汇编语言重新实现C语言实现过的功能,而是能更好地理解汇编,并藉此理解高级程序语言,在需要的时候使用汇编完成一些特殊的任务。从这个角度,我们的作业和课程要求是完全一致的。我们在项目中接触到的汇编都是由高级语言编译生成的,在开发过程中能够切实地感受到高级语言和汇编千丝万缕的联系,体会到高级语言的特性在底层是如何实现的,是如何被计算机理解执行的。
- 对CS进行逆向远高于普通的小游戏,因为
- CS作为一个竞技类游戏,本身就一定的有反作弊措施。比如同一个变量有相当多的副本,单纯搜索常常难以定位。
- 在进行hack的时候,我们无法像对普通游戏一样直接写入内存来修改变量的值,因为CS本身是会不断根据运算更新这些值的。我们只能通过阅读汇编语句,通过改变语句的方式从根源上修改变量的值。