逆向 Counter-Strike 实现跳楼也不死

前言

游戏安全一直是敏感的灰色话题,很多技术不会公开,从事游戏攻防的也很低调。这造成一个问题,对游戏安全感兴趣的人会无从下手,比如我,就不知道面对复杂的游戏如何分析。经过一些日子的思考学习,总算有了点思路,就以CS为例,介绍如何分析游戏、辅助工具以及相应的安全防护。

选择CS有几个原因:首先它是我第一次碰电脑就接触的东西,感情深厚;其次CS逻辑清晰易懂,没有复杂的反调试、加密等防护;最后就是基本所有人都玩过,写例子大家也容易理解。打算写一个小系列,从简单的不死、子弹不减到联网状态的数据包传输、找到各个功能实现call、人物画面渲染等,最后了解整个游戏结构,并写出防护软件来保护CS。

今天写第一回,血量、子弹分析。涉及到的工具有CE、OD、IDA、PCHunter32、notepad++、CS1.6中文版、VS2005。最后实现自己不掉血、不减少子弹。假设读者有一定逆向基础,了解windows编程。并没有分析过CS,也不太了解游戏分析流程,毕竟求大神飘过无视。工具使用只会略作介绍。

子弹不减

打开CS,选fy_iceworld雪地,方便捡枪的图。先用手枪,刚开始20发子弹。打开CE,附加到CS,扫描4字节的精确十进制数20,这里我默认子弹数用四字节。首次扫描,找出了内存中所有值为20的4字节数据的地址,非常多,需要排除。打一枪,再扫描19,剩余197项;再来几枪,扫描子弹剩余数,这次都跟着变化了,如下图所示。所以挨着查看分析。

t01c1f734e3c32095d4.png

先想想,实际上我们需要找到保存子弹数目的原始地址,这个地址的值是运算得来的。其它地方,比如用于显示图上“16|40”数字的地址,它的值应该是直接复制而来的。所以我们改原始地址的值,其它地方应该会相应的改变。这是一种假设,是我们设计程序的时候采用的方法。不妨试试挨着修改。

t0152daafd7532e73ef.png

非常幸运,改地址列表中的第一个地址的值,就验证了我们的猜测。至于为什么能想到这个,我想这就是学习的目的吧。

找到了需要的原始地址,接下来该确定什么代码改写了它。内存写断点可以实现。右键CE中我们找到的地址,选“找出什么改写了这个地址”,将CS载入CE内置的调试器并设好了内存写断点。然后我们打一枪,就能找到减少子弹的代码了。如下图:

t01a86febbdae17a0d9.png

改变子弹数目的代码如下:

 

很明显dec eax,子弹减了一个。我最开始做到这一步的时候,非常高兴,马上用OD附加到CS,然后分析前后代码,纠结减少子弹和换弹夹的问题。而且上面代码的ESI明显是个对象或者结构体,试图确定每一个成员的作用。但是后来我发现那样有点事倍功半,CE的调试器很好的利用了CS的符号表,告诉我们一个非常明确的信息:现在是在mp模块的weapon_glock18部分,手枪的名字就是glock18咯。

为了验证是不是每个武器都有一部分代码,我们换一把枪,用同样的方法(子弹数扫描并查找代码),找到了AK47的子弹处理部分:

跟上面手枪的处理非常相似。这个时候其实我们可以不必用OD动态调试了,完全可以用分析mp.dll,找到其中关于武器的代码:

自动分析后看到如下函数:

CAirtank::TankThink(void)   同时还有更意外的发现,mp.dll就是CS的核心,不仅武器部分,甚至BOT的AI也是在这里实现的。我们可以看到很多有价值的函数,下面选取一部分:

可以看到,只要把mp.dll分析清楚,CS基本就搞清楚了。了解这么多之后,可以回到最初的目标:子弹不减。其实只要简单的把dec eax改成 inc eax就行了。子弹就不会减少了。

这时候我们打枪子弹会增加,直到127,然后减少,变成0之后又增加。这也说明子弹数目是一个字节来保存的。具体的变化可以自己试一下,非常有意思。

我们的第一个目标已经实现了:子弹不减。但是还有很多问题,难道我们每次换把枪都要用CE来这么一次?然后修改?这么搞都超时断开了。我们需要把武器和人物结合起来,这样就可以定点修改了。总不能敌人的子弹也不减少吧?

血量分析

经过找子弹的简单练习,应该更坚信了能实现不死的效果。可能会觉得按照上述步骤,找血、扫描、修改然后找到原始地址就行了,但是实际上血量的查找和修改要复杂不少。

还是用CE扫描,和找子弹类似,先扫描值为100的地址,然后自己跳楼减少血量,再次扫描。为什么跳楼呢?这应该是最方便的可控减血方式。这次用cs_assault图。和上述找子弹步骤类似,扫描几次之后确定了几个可能是血量保存地址的地方:

t0197d8e07f42bee8c5.png

这次依然利用上一节方法,修改并观察其他的是否跟着改变。发现这几个只有一个可以修改,但是修改之后只是显示满血,一跳楼就又变成理论血量了,该死还是死。只是看起来好看一些。

t0111238f438bd0243b.png

t019ae22b6f051ec14e.png

虽然显示变成了100,但是实际血量正如其它地方显示的:24。跳楼之后果然成功挂掉。之前的猜测不管用了。只好换一个思路。

肯 定有一个地址是与众不同的,它第一时间保存变化后的血量。其它的地址类似上面找子弹的路人甲乙丙丁地址,都是直接赋值而来。那么哪个是第一时间接收到变化 后的血量的呢?我们不妨看一下改变这些地址的代码,如果是路人甲,那么它应该被改写非常多的,即使血量不变化,也会有代码执行,毕竟得看初始地址眼色行 事,不积极一点就跟不上节奏了。所以我们就看血量不变化的情况下是这些地址有没有被写入。

0011984C疯狂的被改写,明显是路人甲:

t01ce1e0170364e57c6.png

0012BE74虽然不那么疯狂,但也是明显路人乙的节奏:

t01800f6febacaeb154.png

接下来我们看到了主角:0096C5BC,明显的稳如泰山,血量不变化的情况下不被改写。

t01e7fa724e52cfcb20.png

我们试着跳楼:

t01d48954222443c9e0.png

看到只改写了一次,可以确定这就是我们要找的地址和代码。

仔细看截图会发现跳楼前后地址变了,确实是。写文章的过程中得自己调试着来,中间不小心卡死了,重来了一次,局部变量地址发生了变化,但是思路是连续的。

目前为止,我们可以认为找到了改变血量的地址:

为了方便使用,之后的调试我们使用OD。附加到CS并在61DB003处下断。跳楼减血顺利断下,可以看到EAX的值正是将要变成的血量:24。

t01a25a355e5d0c4b10.png

现在我们的人物就是查找EAX从何而来。这里有个小技巧,我们在61DB003之前下断点,如果只有跳楼减血的时候断下,那说明这个地址的代码和61DB003是承接起来的一个流程,如果一直在断下或者跳楼也没断下,那这个地址和我们要确定的EAX来源关系应该不大。

往上翻看代码,大致了解一下流程,建议倒着看,因为我就是倒着分析的,所以思考方式也是从结果往前推导。每段注释都是针对注释下面的语句:

//     这里又是一个将[EDI+160]的值赋值到st(0)的语句,同时EDI = [ESI + 4],和我们下面看到的一样。如果当时我们纠结ESI的来源,不往前看,就有可能忽略这里。下断看看这个时候[EDI+160]是不是已经变成了将要变成的血值。下断之后发现一直断下,而且[EDI+160]的血值是之前的,而不是之后的。因为游戏刚开始的时候这里一直断下,根本走不开,而[EDI+160]的值是100.000。那么我们有必要看看下面那个call6220AE0。直觉告诉我们这里有可能是计算血量的地方。倒着看到这里,该看下一段代码了,也就是6220AE0 call的内部。

//    这个jmp直接跳到了本段代码最后,所以正常的减血赋值操作应该不会跳转,所以说下面这条指令是会执行的。下个断点跳楼就足以证明。你可能已经猜到了,在这个地方 [EDI+160]已经是将要变成的血量了,所以还得往前寻找。

//    下面两个判断分支加上几个全局call的调用,暂时先不要陷入call的追踪,还是先看看之前是不是已经有结果了。这是经验之谈,盲目的追踪一个call只会浪费时间和让人越来越烦。在这里就可以试着下断,看看赋值减血的流程是跟着哪个步骤来的。经过在AFB1和AFBD下断,我发现AFB1会断下,顺着走当然会跳过AFBD的赋值0操作。同时我发现在061DAFB1这个地址处,[EDI+160]的值已经是将要变成的血值了,也就是说下面叨咕半天的又是没用的,还得往前推。

t0155adaefe96a2f855.png

//    一个未知全局call,有可能改变 [EDI+160]

//    FLD 指令将 [EDI + 160]装入 st(0),如果多一个心眼儿看看[EDI+160]的浮点数,会发现是24.000,这其实告诉我们下面那个CALL和最终的血量没啥关系,如果提前考虑这个问题,就不必费心查看那个CALL内部了。EDI = [ESI + 4],这个时候问题已经变成追踪ESI了。

//    调用了一个函数,可能返回将要变成的血量

//    EAX 是将要变成的血量

我们看看全局call是什么功能。内部实现如下:

FSTCW dest 将FPU的控制字保存到dest

//    执行 FSTCW 语句之后 [EBP-2]的值变成027F

//    这里做一个运算,把FPU的控制字和C做或运算,然后放到[EBP-4]这个WORD中。

FLDCW src 从src装入FPU的控制字 FPU CW <-src (mem16)

//    将异或之后的值放入控制寄存器,整个过程相当于自己或。FPU = FPU | 0xC,也就是将第12为置位。这里貌似是改变进位设置,我也没有深究。

//    这里是将浮点栈的值放入 [EBP-C] 并出栈。可以看到浮点是是 QWORD 级别,8个字节

//    控制字符串恢复

//    这个赋值可以看到这个函数相当于把浮点栈顶的值放到EAX中。仔细看上面那段代码,这个call执行之前的指令是FLD DWORD PTR DS:[EDI+160],也就是说这个函数的作用只是把 [EDI+160]的值放入EAX

看到这里应该明白那个跳转的意思了,CMP EAX,DWORD PTR DS:[ESI+5B4],比较 [EDI+160]和 [ESI+5B4]的值,不相等就进入下面的处理流程,进行同步。在061DAF9C    D987 60010000   FLD DWORD PTR DS:[EDI+160] 这里下断,查看[ESI+5B4]和[EDI+5B4]的值。如下:

[EDI+160]

t01e8153505fc330bb8.png

[ESI+5B4]

t014c9a3266ba899feb.png

高下立判。[EDI+160]才是真正的血量存储池。而我们找了半天的地方其实只是一个转换。现在才有资本去找真正改变血量的地方。既然[EDI+160]是改变血量的地方,那么就在该地址下内存写入断点。

因为之前调试的时间问题,游戏可能会断开,这个时候重新开图。初始化的时候断了下来,完全的意外收获,我们找到了player初始化的地方:

t019b960654a54c538c.png

此时EAX为03C31BFC。不妨先剧透一下,这个值是让我头疼了一天一夜的值。它是“你”的对象地址。“你”指的是玩家。New一个对象的时候最想分配到的就是03C31BFC,被“你”占用了。这段初始化代码可以告诉我们很多信息,比如初始血量,42C80000显然是100.000的浮点形式。而160这个偏移也让我们浮想联翩。没错,之前那个EDI也是player对象的首地址。因为本节的目的是不掉血,所以暂时跳过这段精彩的发现。

接着运行,让游戏跑起来。然后我们跳楼,这时就会找到修改 对象+160处偏移的代码,也就是掉血的代码。因为下内存写断点会非常卡,要跳楼也是需要时间的。趁这点时间写几点无关的体会和经验:关注重点,不要随便进入一个call就分析;多注意关键代码的前后逻辑,多下断尝试,注意观察关键内存地址和寄存器的值;调CS的时候鼠标可能会跳出来,这个时候摁两下win键就OK了;mp.dll每次加载基址可能会变,所以需要记下偏移,最开始我就吃了这个大亏。

好了,跳楼成功,断下在这里:

t012992d38ce3073202.png

可以看到经过一个简单的运算,算出一个减数,[ESP+54],被减数为原血值。所以最最简单的方法就是把减数[ESP+54]变成0。这样理论上血值就不会减少了,一直是100。改一下试试:

t01af191d57704dd215.png

从这个简单运算中我们也可以对player对象更进一步的了解。比如偏移+1D8的值用于计算减少的血值。通过这个值我们还能找到血量变化的运算,进而找到跳楼、射击、炸弹等动作,这都是后话。

现在改了之后所有人都是不死之身了,玩儿起来就是血腥,狂喷血,谁也死不了。发一张血腥的游戏截图,少儿不宜啊:

t01b85d506675023019.png

分析几个参数和dll注入实现最初的功能

在我们修改代码的那里下断,每回掉血都会断下来,我们分析一下这几个参数。

t01caec5d206cf9aaa3.png

EAX和EBX造型差不多,而EAX我们非常熟悉,根据之前的剧透,这正是“我”的地址。既然这是一个掉血的过程,那么一个掉血,必然有一个砍刀的,不妨猜测EBX存储的是攻击者的对象首地址,EAX存储被攻击者的首地址。为什么这么猜测呢?因为我故意不动让BOT来砍我。换我射击BOT,断下来又是另一番情景:

t012963764f75fd1b27.png

EAX 和 EBX 值换了,再仔细分析其它几个寄存器的值,包括它们作为地址存储的值,分析之后得出以下结论:

EAX = 被攻击者首地址

EBX =  攻击者首地址

ECX =  EBX – 80

EDX =  [ESI]

EBP =  ESI

EDI =  Kill or SelfKill

EDI是怎么知道的呢?我发现跳楼和其他的情况到这里的时候EDI值不同,自杀是0x0002,他杀是0x1002。

EBX在数据窗口跟随就可以看到“我”这个对象,还能找到坐标等有价值的值。

现在我们改变一下HOOK方式,不修改代码,而是跳到一个地方,根据EAX的值进行相应的变化。之前提到过,初始化时把03C31BFC地址分配给自己,所以根据EAX的值选择血值不变还是照常减少,这样又可杀敌,又可保命。

我选择远程写入方式,在CS进程分配地址并写入hook代码。根据mp.dll的加载地址和减血偏移,采用inline hook。代码如下:

编译运行之后csrike.exe进程就被patch了,这个时候只有自己是死不了的。我们的目标实现了。

问题及后续

有了这个代码,建立游戏就可以在局域网内狂虐小伙伴们了。但是如果你加入一个游戏,那就行不通了。也就是说我们之前找的都是服务器的代码段。客户端根本不会执行那些代码。现在以客户端的身份查找血值变化。只找到这么一个疑似代码段:

直接赋值操作。Eax存储的是变化后的血量。

当前需要解决的问题有:人物和武器如何对应、服务器和客户端的功能,限于篇幅,这些内容下次再写。另外,不要忘记前言提到的几大目标。我们的目的是通过逆向CS,学习游戏攻防思路技巧,并不是真的要去做CS或者其他外挂。勿忘初心。

[via@bobao]