游戏逆向基础-通过ce及dbg进行的基础功能call查找教学
本文系统介绍了游戏逆向中查找功能 call 的基本方法,围绕“经过发包函数”和“不经过发包函数”两大类场景,分别讲解了堆栈回溯、虚函数表查找、特征对象访问断点、写入断点回溯等实用技巧,并通过背包使用物品、NPC 服务、自动寻路三个实例进行演示。
在游戏逆向中,我们主要进行的查找基本分为两种:数据和call。但实际上,数据查找本质上也是为后续找 call 提供便利,因为很多 call 的参数都是某个数据对象。因此,对于游戏逆向来说,找 call 自然是非常重要的。今天就来分享一下找 call 的基本方法。
首先,我们先了解一些前置知识: 网络游戏依赖服务器来实现玩家之间以及玩家与服务器之间的通信。因此,我们在游戏中的许多操作都需要向服务器发送数据包。只要我们能够准确定位到正确的发包函数,就可以顺着调用关系进一步找到对应动作的函数,从而实现对该动作的调用。 当然,并不是所有动作都会发包。例如打开包裹这类操作,只需客户端自身知道即可,不需要其他玩家或服务器知晓,因此也就无需发包。 在API 中,三大常见的发包函数分别是 send、sendto 和 WSASend。如果我们是游戏开发者,在不考虑破解风险和外挂风险的前提下,通常会毫不犹豫地选择其中之一来使用,因为没有人愿意做徒劳无功的事情。但正如前面所说,网络游戏必然需要发包,因此只要在这三个函数下断点,再触发相应的功能调用,基本上就可以获取到大部分功能的 call。 所以其实在简单的网络游戏中我们找的call就两种,需要经过发包的功能和不需要经过发包只在本地的功能。 那么如何判断一个功能是否经过发包函数,由两种方法: 1、在send函数下断点,然后调用功能,观察是否断下。 2、在电脑管家中对目标进程进行网络禁用,再调用功能,如果可以调用,表示不经过发包 那么开始分享对于一些简单的游戏中的call的找法
经过发包函数
对于经过发包函数的功能call我们有两种查找方法:堆栈回溯和虚函数查找,我们就基于xx游戏来进行简单call的查找演示
堆栈回溯
堆栈回溯是指在程序调试或逆向分析过程中,通过读取当前线程的栈内存,沿着函数调用链逐层向上还原“是谁调用了谁”的过程。 核心原理 程序每次执行call 指令时,CPU 会自动把返回地址(即 call 的下一条指令地址)压入栈中。函数执行完毕后,ret 指令再从栈里弹出这个地址,跳回去继续执行。也就是说——栈里天然保存着一条完整的调用链。每一层函数的返回地址依次排列在栈上,只要顺着这些地址往上翻,就能还原出从当前断点一路到程序入口的调用路径。 讲过了原理就可以开始追了 我们以某个简单游戏的背包使用物品call为例 分析某个功能call,首先我们要从开发的角度先进行分析,想想如果是我们自己开发,会用到什么参数: 我们要使用背包中的物品,那么我们应该会需要背包对象,物品对象和要使用物品的id或者要使用物品在背包中的地方,也就是背包索引。应该没了,那么我们就可以开始追溯了。 打开游戏,然后x32/x64dbg对游戏进行附加 首先既然我们要从send发包函数往前追溯,那么我们需要先定位到send函数, 使用bp send在send函数处下一个断点,然后随便做一个功能让它断下 这样就能定位到send函数了
在该函数头部下断点,然后调用功能,就能开始往前追溯了
堆栈回溯还有两种常用的方法:
1、断点断下之后,点开调用堆栈窗口
在主线程中,这些用户模块就是调用的堆栈,我们需要一个一个的追,一般是从外往里追,因为越往外,就更有可能是和其他功能共用的call。
2、在send函数断点处通过断下后Ctrl+F9,F8来进行回溯
执行到函数结束(执行到返回) ctrl+F9 执行一步F8
我们这里为了更清楚,使用第二种,因为第一种有的情况下会错误,导致无法追溯。
那么我们就一层一层往上找,从send开始找,先往前追一层
这一层就是调用send的地方,整个游戏中直接调用send函数的函数只有两个,因此基本上大部分call也都会经过这两个函数,所以我们可以后面追call时直接从这个函数开始往前追,可以更简便一点,
继续往前追,断点然后Ctrl+F9,F8
我们已经又往前追了一层了,那么我们如何验证这个是不是我们要找的call呢?
首先我们要知道,我们要找的是某个功能调用时执行的call,那么对我们来说我们需要找到一个特定的,只会执行这个功能的call,这样我们在后面调用这个call时,才不会调用成其他的功能,所以,我们只需要给需要验证的call下个断点,然后调用一下其他的功能,或者做一些其他的操作,看该call是否只有目标功能能使它断下即可,
那么,对该call进行验证,发现不正确,那么再往前追
往前追几层,就能遇到第一个只能由背包使用物品断下的call
那么开始分析一下它,如何分析呢,首先看的是外平栈还是内平栈,根据平栈的大小可以判断参数数量,之后还需要判断,还有很多call时需要使用ecx,会将ecx作为参数存储数据传进去,所以还需要判断是否需要用到ecx
根据代码add esp,0x8,该call应该是外平栈,有两个参数
Push 0x5
Push EDX 最需要看的是edx
跟进call,能发现是需要ecx进行传递的,所以需要使用ecx,那还需要分析ecx
断下看一下寄存器
Edx是一个栈区,再重新断一下,使用同一个物品,发现edx是不一样的,所以就会对该数值所代表的是什么不太明确。
我们逆向分析数据和call,关键的就是找最容易分析的地方进行逆向分析,所以我们可以再追一层,看看下一层好不好分析
那么再分析这一层,
好像也是一个外平栈,有两个参数,分别是ecx和edx
我们前面分析过了使用物品call可能有的参数,首先是背包对象,这里的参数明显不是,那么是不是ecx传了背包对象呢,看一下这个call内部是否用到了ecx
发现并没有用到ecx,那么就不再分析ecx,那么这里的参数有可能是物品id或者物品所在背包位置索引,这里我更倾向于索引,那么验证一下,
对于索引的验证,我们有很简单的方法,在背包查一下,可以发现我们使用的物品在背包中的索引的确就是39(十六进制为27)。
接下来可以换索引为24的物品使用,再断一下,
可以发现,ecx的索引变成了24的十六进制,而edx是12不变,那么我们基本就分析完毕了
ecx应该是索引,edx是2不变
最后使用call测试工具测试一下
执行成功,进去游戏,查看物品是否使用,
发现的确使用成功了,那么这里其实就可以了。
我们上面分析的参数可以发现这里只用了一个,其实再往上追其他的使用物品call,有的就会有物品对象等其他的参数,只不过我们找简单的就行了,所以不继续往上找。
虚函数表查找
虚函数表查找,是指在逆向分析中定位某个虚函数(Virtual Function)的实际实现地址,以及在调用链中确认是哪个虚函数被调用了的过程。
在游戏逆向找功能 call 的场景里,虚函数是一个绕不开的坎——因为游戏引擎大量使用虚函数实现多态,功能入口往往藏在一张虚表(VTable)的某个槽位里,而不是一个固定的 call 地址。
核心原理
C++ 中,如果一个类含有虚函数,编译器会为该类生成一张虚函数表(VTable),表中按顺序存放该类所有虚函数的地址。每个对象实例的内存布局开头(通常是偏移 0)会有一个虚表指针(vptr),指向所属类的虚表。一般对于某个对象实现的call,都在该对象的虚函数表中存在
一个类的0偏移位置存放的是虚函数表的地址,虚函数表中存放的是虚函数,那么以背包类为例,和背包有关的类一般都会存放在背包虚函数表中,那么查看在实行某个背包功能时有什么访问了虚函数表中的哪一个指令及函数,根据这个就能找到了
我这里进行虚函数表的分析是先追完了背包对象的第一个物品的基址,如何查找可以看:
x32游戏逆向-列表类数据定位:从背包、仓库到周围 NPC
作者:切尔曼提察
https://xz.aliyun.com/news/92844
这篇文章可以教如何进行基础数据的查找
那么开始追溯
首先使用ce查看背包物品基址
浏览内存
这个就是物品对象,在这个地址下有很多和物品相关的数据,类似物品id,数量等等,对象的虚函数表是对象的0偏移位置,查看一下
这些都是虚函数,只看这个是无法找到的,既然函数式在虚函数表,那么说明,调用该函数时都要访问虚函数表地址,那么查看一下有什么访问了该地址
在这里我们就需要做一些操作了,既然我们调用某个功能是会调用某一个虚函数,那么同样的,这些对应功能的虚函数只有在我们调用了这些功能后才会被使用,也就是说,现在访问物品对象的执行都不是使用物品call中的指令,那么我们使用一下物品,再看一下访问情况
发现有好几个新的,那么不要急,再使用一下,看看哪些是每次使用加一的 ,那么那些就是和使用物品call有关的指令,再次使用该物品
被选中的这些都是会跟随使用物品而递增的
00A5B9BA - 8B 45 00 - mov eax,[ebp+00]
00A5D046 - 8B 45 00 - mov eax,[ebp+00]
0074C063 - 8B 16 - mov edx,[esi]
0074C09B - 8B 16 - mov edx,[esi]
0074C0B3 - 8B 16 - mov edx,[esi]
0074C190 - 8B 16 - mov edx,[esi]
0052BC0F - 8B 16 - mov edx,[esi]
009B5D96 - 8B 11 - mov edx,[ecx]
005576C2 - 8B 17 - mov edx,[edi]
00557CCF - 8B 07 - mov eax,[edi]
根据地址范围可以大体分为三部分,每一部分基本都是在一起的,一部分只需要查一个即可 使用x32附加,然后从上往下查一下
那么接下来我们就可以分析一下这一层的这一部分代码了,call edx是call虚函数,那么就可以找一下参数和ecx ebp的来源,或者觉得这一层麻烦的话还可以往前再追一层再分析
首先是call的地址edx=eax+60
能发现,eax是虚表地址,那么edx就是虚函数表+60,
然后分析ecx,断在call点,看一下ecx,还记得我们之前分析的call可能有的参数吗,根据ecx的值可以环疑ecx是物品对象,那么验证一下,怎么验证呢,我们之前说过,找这个call的前置条件要先找一下这个背包的对象,那么我们看一下这个地址+某个偏移是否是我们找到的东西,例如我们找到的背包地址+70存放的是物品数量,那么我们赋值一下ecx,然后在内存页面转到ecx存的地址,查看70的位置,发现的确是物品数量,和目前的物品数量一致,那么说明ecx存的是物品
那么这一层就分析结束了,call的地址追到了,ecx是物品对象,,其实根据这个已经可以写call测试了,但是相比来说还是比较麻烦的,因为我们还需要传物品对象,那么可以再往上追一层,看看是否简单一点
这一层代码其实也是可以跳过的,因为当你分析过堆栈回溯并做过注释后,能发现这一层再往上一层其实就是刚刚的背包使用call函数了
不经过发包函数
讲完了经过发包函数的,我们再了解一下不经过发包函数的call的查找,不经过发包函数的call的查找方式其实算是多元化,要根据你查找的东西来进行改变,那么我们这里以一个简单游戏的NPC服务和自动寻路call为例来丰富一下不经过发包函数call的查找方式:
NPC服务:
该情况为玩家附近的NPC分为两种,一种是只有一种功能(互动),右键直接实现功能,并且这种功能call是经过发包函数的。还有一种是右键后会出现一个窗口,窗口中又存在很多功能,而这个是不经过发包函数的,所以我们要通过分析来追这个call
依旧先从开发角度猜想分析一下可能有的参数,因为该功能是基于窗口的,所以应该会有窗口对象,窗口中应该会有很多功能,所以应该会有功能索引。
先使用堆栈回溯,再send下断点,然后右键窗口中的功能,发现无法断下,判断该call不经过发包函数
我们这个功能是基于右键产生的窗口的,那么如果要调用窗口中的这个功能的话,一定会对该窗口进行访问,那么我们找到该窗口的对象,然后对对象下访问断点,右键点击窗口服务,断下之后的地方就是调用访问该对象的函数,然后再往前追即可
找窗口对象,我们可以找标题这些,文字什么的,但是这些都会很麻烦,因为这个我们无法主动改变,而且一个游戏中和标题一样的有可能很多,
对于窗口我们可以使用坐标,窗口坐标一般是越往右下越大,所以我们搜索未初始值,然后慢慢改变即可,并且坐标的值也不会很大,所以这也是一个判断依据,找合适的,不断改变坐标,然后慢慢搜索符合条件的值,细节再之前的追背包对象的链接文章中也可以学习
最后能找到很多,但是左边不会特别大,因为屏幕的分辨率有限,所以我们找较小的值
然后把窗口拉到最左边,只有一个为0的,
00E8E210 - 8B 91 9C000000 - mov edx,[ecx+0000009C]
00E93EDC - 8B 9E 9C000000 - mov ebx,[esi+0000009C]
00E8C770 - 8B 91 9C000000 - mov edx,[ecx+0000009C]
61634F70这个就是窗口对象
然后找什么访问了它,看当点击了窗口中的功能时,会触发哪些指令,
00AA555F - 8B 11 - mov edx,[ecx]
00E978A4 - 8B 06 - mov eax,[esi]
00E978C3 - 8B 06 - mov eax,[esi]
00E978CE - 8B 06 - mov eax,[esi] esi = 61634F70
00E97A89 - 8B 16 - mov edx,[esi]
00AA555F - 8B 11 - mov edx,[ecx]
00E978A4 - 8B 06 - mov eax,[esi]
00E978C3 - 8B 06 - mov eax,[esi]
00E978CE - 8B 06 - mov eax,[esi]
00E97A89 - 8B 16 - mov edx,[esi]
x32附加,跳转到指令,然后断下,往前追,断点不能断太久,否则会导致游戏崩溃,所以我们可以连续Ctrl+F9 F8然后再每一层的返回地址上面的call处下一个注释,,然后再分别在每一层分析,之前我们从里往外追,这次我从外往里追,从第七层开始追,一般最里面和最外面的call都有可能会被多数功能访问,是有可能不对的,先看看第七层是不是
该call只有我们调用目标功能时会断,但分析起来不好分析
有的参数是哪怕是同一个NPC,同一个服务依旧会变化的,需要往前找很多,所以我们可以再往里找一层看看
断下,多尝试几种功能,可以发现,只能由右键点击NPC服务来导致断下,那么可以开始分析了
Add esp,0xC
有三个参数,外平栈,这三个参数在进行同一个NPC服务调用时不进行改变,
Edx应该是服务索引,两个ecx应该是对象,虽然不变,但也是不太好找的,所以不如再追一层看看是不是会简单一点
到这一层就只有两个参数了,那么就可以分析一下了,通过每一次对不同的服务或者相同的服务进行下断,观察可以发现,esi应该就是索引,而eax是会变化的,根据我们之前的分析,这个应该就是窗口对象,那么往前追一下,看看好不好追,不好追就换
00AA98B1 | 8B4424 08 | MOV EAX,DWORD PTR SS:[ESP+0x8] |
就是上一层传下来的第一个参数,往上层追一下看看
00AE7579 | 51 | PUSH ECX | 61637F40
00AE7572 | 8B8E C8020000 | MOV ECX,DWORD PTR DS:[ESI+0x2C8] |
这里就能搜到esi 的来源
这里的esi其实就能够直接在ce中查找到了,
[模块基址+15561F0]+0x2C8
只是进行call测试的话,可以写这种
push 61637F40
push 0x0
CALL 0x00AA5590
add esp,0x8
这里最后要加一个add esp,0x8是因为这个是用来平栈的,如果不平栈,会导致游戏崩溃
自动寻路call:
该游戏的自动寻路也是不经过发包的,那么我们应该从哪一个角度追呢,根据刚刚那个例子我们已经可以发现,不经过发包函数call的查找其实就是要找一个特征,能保证该功能调用时能调用的指令被我们找到。那么我们可以想想寻路有什么特征,
我们可以用坐标。首先我们来思考一下寻路call可能需要什么参数,
首先可能有地图ID,有的游戏是有多个地图的,那么如何判断要寻路到哪个地图,看的就是地图id,还有目的位置,和起始位置,这些位置其实就是坐标,通过一个参数,将坐标xyz传给call
也就是当我们要自动寻路时,我们需要有一个参数来存放目的位置也就是坐标,或许还需要我们的目前坐标,那么我们就需要寻找一下坐标在游戏中存放的位置,一个人物的坐标根据猜想有可能存放在人物对象下,
那么我们可以在玩家对象下找一下,坐标根据我们找npc对象时可以知道,是浮点数,那么我们看一下有没有看着像xyz的,作为当前坐标
三个连起来的单浮点并且比较像的就这三个了,而且当玩家动的时候,它也会动,也就是任务对象+3C的位置
查看一个npc可以发现一样的在3C的位置
寻路时不能用封包回溯来追的,那么我们应该怎么追呢,可以从参数入手,我们知道当运行自动寻路时是有一个参数是目标坐标的,那么我们可以从这个入手,我们选中不同的已知地址来调用自动寻路,然后通过这个目标坐标的区间来定位到存放目的坐标的地址,然后下一个写入断点,然后断下时,在断下的指令处往前追溯即可,
我们这里是事先通过代码得到了每一个NPC的位置,那么开始查找
先选中一个NPC,该NPC的x坐标为55,那么使用ce开始查找,点击该NPC,在自动寻路的过程中进行搜索,
寻路结束后,再换一个NPC寻路,再搜索,就这样一直下去即可
01953FFC
那么这里其实就是目前的存放目的坐标的参数对象了
然后再01953FFC此处下写入断点,寻路,此处会被写入因此断下,往前追溯,我记录了往前七层,然后开始从外往里找
外第五层就是,分析一下这个call
跟进去发现有10个参数,这些参数在同一个地图中不变,之前我们分析的,寻路call有可能存在的参数有目的坐标,起始坐标,地图id也有可能,那么可以看一下这些参数所指向的地方的数值
我们目前知道第五个参数就是目的坐标存放的地方,而第六个参数只有43,而且在目前该地图的尝试中不变,但是我将目的地址改成其他城市地图,就会改变,所以猜测这个是地图ID,那么只有第三个参数不明确了,往前追一下
00A77B8E | 8B8D 74020000 | MOV ECX,DWORD PTR SS:[EBP+0x274] |
[EBP+0x274]
00A77A9F | 8BE8 | MOV EBP,EAX |
[EAX+0x274]
00A77A89 | E8 D273E5FF | CALL xajh.8CEE60 |
004AE400 | A1 D8825201 | MOV EAX,DWORD PTR DS:[0x15282D8] | playername
004AE409 | 8B40 24 | MOV EAX,DWORD PTR DS:[EAX+0x24] |
004AE410 | 8B80 8C000000 | MOV EAX,DWORD PTR DS:[EAX+0x8C] |
发现到最后是人物对象,再结合我们开始猜测的应该有起始地址,所以这里应该就是把人物对象传过去,然后获取起始地址
[[[[0x15282D8]+0x24]+0x8C]+0x274]
pushfd
pushad
push 0x0
push 0x1
mov ecx,[0x15282D8]
mov ecx,[ecx+0x24]
mov ecx,[ecx+0x8c]
mov ecx,[ecx+0x274]
push ecx
push 0x0
push 0x001AE9C4
push 0x00000043
push 0x0
push 0x0
push 0x0
push 0x0
CALL 0x007386D0
popad
popfd
这样自动寻路call就找到了,这两个例子其实只是在追的方式是一样的,重要的是找特征的角度,从不同的方面去找特征,一般的方法都是,通过找到该特征对象,通过对该对象下访问或写入断点或者其他什么操作来找到经过该功能的指令,然后往前追
需要注意的是寻路有好几种,就是我们现在找的是点击寻路,还有NPC寻路这些,他们都是使用的同一个call但是调用地点不同,例如刚刚的call断点,如果你使用npc寻路,就不会断下,那么或者找下一个call一般是共用的,或者就在下写入断点时,使用npc寻路断下往前追
简单功能Call的查找方法就说完了,其实最重要的是相关的思维逻辑,而不是这几个call的查找方式。希望能帮到大家