本帖最后由 赶码人 于 2022-2-1 22:30 编辑
总结:把so拖到IDA,找到数组出现在的函数,对着函数名按X或者打断点看LR寄存器,找到函数调用栈(调用关系),直到遇到bss段的地址,然后根据分析调用栈上函数的参数关系。
游戏名是坦克英雄激光战争,很老的一个国外单机游戏,长期未更新,挺适合我这种新手分析着玩,如果存在错误或者有更好的技巧,欢迎指点~~
准备工作: 解压游戏安装包,把so文件拖到IDA.
一、认识数组
首先要知道,通常游戏逆向领域所谓的找数组是啥意思,数组是最常见的用于存储敌人信息的结构,这个数组当中的每个元素都对应着一个敌人的结构体,数组里的元素可能是这个结构体的地址(大部分游戏是这样的),也可能直接就是某个结构体,说着可能有点绕,可以看看下面这个代码,
[C] 纯文本查看 复制代码 #include <stdio.h>
struct Role{
int hp; //血量
float x;
float y;
float z;
//...实际上,属性会多的很,并且可能有指向另一个结构体(比如武器)的指针,这里省略...
};
#define ROLE_MAX_COUNT 20
struct GameData1{
int roleCount; //敌人实际数量
struct Role* roles[ROLE_MAX_COUNT]; //敌人数组
//...省略
} gd1;
struct GameData2{
int roleCount; //敌人实际数量
struct Role roles[ROLE_MAX_COUNT]; //敌人数组
//...省略
} gd2;
int main(void){
gd1.roleCount = 3;
struct Role r1, r2, r3;
r1.hp = 100;
r1.x = 1;
r1.y = 2;
r1.z = 3;
r2.hp = 90;
r2.x = 10;
r2.y = 2;
r2.z = 30;
r3.hp = 95;
r3.x = 30;
r3.y = 2;
r3.z = 20;
gd1.roles[0] = &r1;
gd1.roles[1] = &r2;
gd1.roles[2] = &r3;
gd2.roleCount = 3;
gd2.roles[0].hp = 100;
gd2.roles[0].x = 1;
gd2.roles[0].y = 2;
gd2.roles[0].z = 3;
gd2.roles[1].hp = 90;
gd2.roles[1].x = 10;
gd2.roles[1].y = 2;
gd2.roles[1].z = 30;
gd2.roles[2].hp = 95;
gd2.roles[2].x = 30;
gd2.roles[2].y = 2;
gd2.roles[2].z = 20;
getchar();
return 0;
}
代码中gd1和gd2就是两种不同的敌人数组的形式,用NDK编译,推到手机运行,ce+ceserver附加,添加地址两次,分别直接写gd1和gd2:
(上图随便run-as了一个debuggable=true的apk包名,是为了能在GG看到咱的hello进程)
上图是刚才所说的“情况一”
GG视角(以下分别是情况一和情况二):
GG视角 情况一
GG视角 情况二
二、IDA找敌人数组相关函数思路(有符号so)
要找数组的基址偏移,首先找到数组出现的位置,先猜猜数组可能出现到的函数,有以下思路(个人经验,仅供参考):
1. “AI”表示人机,搜索带有AI的函数
2. 找单个敌人的行为相关函数,例如fire,damage,move等
分辨哪里用到了数组,并不难,找while循环和for循环就行了,多留意观察循环中哪个变量有变化(以及变了多少),哪个变量无变化,以及循环的终止条件;我们最后写代码的时候,也是要通过while或者for循环去遍历所有敌人,循环的条件到时候也可以“照抄”。
找到数组出现的函数之后,接下来找调用栈(函数之间的调用关系),这样就可以通过观察参数变化,找到参数的来源,就可以分析出数组的基址、偏移。
找调用栈思路:对着函数名按X,如果按X找不到,打断点看LR寄存器。
比如,GmSimInitAITANKs和GmSimAiUpdate这两个函数,(双击函数名字之后按F5)
如上图所示,我们可以猜测724就是敌人结构体的长度。
像AIMoveAndFireAITANK这种比较复杂的可以先大概看一眼,根据刚才的推测,724可能是结构体长度,这里和724相乘的a2不被循环影响,a2显然是敌人在数组中的下标,所以可以对着这个函数名按X,一定有地方有for或者while循环且在循环体当中调用了AIMoveAndFireAITANK函数。
对着函数名按一下X(光标先插到函数名的地方),果然,是GmSimAiUpdate调用了这个函数,数组在GmSimAiUpdate函数中被遍历,AIMoveAndFireAITANK的参数2是循环到的次数(可以直接理解成敌人的序号)
接下来以找单个敌人的行为相关函数再演示一次,比如move,可以发现GmSimMoveTANKs这个函数里面就已经有循环了,
总之就是,找行为动作相关函数,然后按X,找引用调用它的函数,调用它的函数内部很可能就有for或while循环
比如GmSimIsFriendlyFire,
如上图,按X,就能找到GmSimCheckProjectileCollisionWithAllTANKs这个函数,并且可以看到一个很清晰的循环。
对于本游戏,也可以通过数量下手,搜索count、get num of···,这样可以找到GmSimGetNumOfDeadAiTANKs这个函数(搜索“AI”也能看到它),
根据函数名称可以猜测这个函数作用是统计已经死亡的人机坦克数量,
返回v3,v3最初是0,v2最初是参数1,每一次循环,v2都会自增724,也就是说,while循环过程中,v2是每个人机坦克的结构体地址,而参数一就是整个数组的起始地址,每个敌人结构体可以通过偏移72这个byte类型数值来判断是否为死亡的人机。所以这里用来判断敌人是否死亡的偏移也得到了,后续可以用来过滤死掉的敌人。
接下来通过GmSimGetNumOfDeadAiTANKs这个函数进行“溯源”(因为它看起来最清晰,代码只是在很单纯的遍历,从这个函数的名字就能看出它只是负责统计坦克数量),得到数组的基址偏移。
三、IDA找敌人数组相关函数调用关系
目前已知GmSimGetNumOfDeadAiTANKs这个函数的参数就是敌人数组的起始地址,接下来找它的调用栈,通过调用栈上的函数,找到参数的来源。
能对着函数名按X,就按X,如果按X出现的列表没有“BL”指令,就给函数打断点,观察LR寄存器的值,接下来演示操作:
按X找到了两个函数,我们先进去第一个看看:
可以得知,GmSimGetNumOfDeadAiTANKs的参数一,是GmSceneCampaignCallbackSimEventTANKKilled的参数一a1+32得到的地址所指向的数值(注意这个*(_DWORD *)(a1 + 32),把a1+32转为了指针,并读取出了这个指针指向的地址的值)
再对着这个函数按X,发现并没有出现BL指令,只有一个GmSceneCampaignResume函数当中用到了该函数的指针,后续游戏会通过函数指针的形式调用这个函数,这种情况IDA不能静态分析出来,
所以接下来要回到GmSceneCampaignCallbackSimEventTANKKilled,直接如图所示,给这个函数第一条语句打上断点(点击行号左边的蓝色圆圈):
先回到GmSimGetNumOfDeadAiTANKs函数,这次进入xrefs窗口的第二个函数GmScenePreviewCallbackSimEventTANKKilled,可以看到,这两个函数参数关系是*(_DWORD *)(a1 + 4)
对着GmScenePreviewCallbackSimEventTANKKilled函数按X,发现仍然找不到BL指令调用,所以,这里也打个断点
现在按X已经遇到阻碍了,接下来要通过LR寄存器继续找调用关系,需要用到IDA目录的dbgsrv文件夹的android_server,这部分教程网络上已经有很多了,比如可以参考这篇:https://www.52pojie.cn/thread-1511844-1-2.html
在手机端启动android_server之后,IDA操作如图所示:
选中游戏进程点OK,然后:
如果出现上图这个弹窗,是在问你当前打开的so和内存中的so是否一样(就是说 解压的apk和安装的apk是否是同一个版本),选择Same就行了,
然后游戏进程就被暂停掉了,等待上面这张图的窗口消失,
刚刚已经打过俩断点了,然后在游戏里面动一动,等待触发断点,然后你会发现,在子弹即将接触到某个角色时,游戏被暂停,此时触发断点,
1
所以是GmSceneCampaignCallbackSimEventTANKKilled这个函数的断点触发了,并且是GmSimDealDamageToTANK调用了它,参数一是v17【36】,
这里先简单记一下这个函数内部和v18的参数一有关的代码:
[C] 纯文本查看 复制代码 int __fastcall GmSimDealDamageToTank(int result, int a2, int a3, int a4)
{
v4 = (_DWORD *)result;
v17 = v4 + 0x2E00;
result = v18(v17[36], a2, a3); // v18就是GmSceneCampaignCallbackSimEventTankKilled
然后继续,找这个函数是谁调用:
先点击去第一个看看:
参数一不可能是float,不多解释了,直接改类型:
记一下:
[C] 纯文本查看 复制代码 int __fastcall DealCollateralDamage(int result, float *a2, int a3, int a4)
{
v4 = (int *)result;
result = GmSimDealDamageToTank((int)v4, i, a4, (int)(float)((float)((float)a3 * v10) + (float)((float)a3 * v10)));
继续找:
记下来:
[C] 纯文本查看 复制代码 int __fastcall sub_BE464A28(int result, int a2)
{
result = DealCollateralDamage(result, (float *)(a2 + 4), *(_DWORD *)(a2 + 56), *(_DWORD *)(a2 + 40));
找:
这个时候,前两个函数进去按X找不到调用者,打断点并没有遇到他们的触发条件,然后干脆将剩下四个也打断点,最终触发了GmSimProcessInstantProjectile,所以进这个函数:
该记就记,该找就找,
[C] 纯文本查看 复制代码 int __fastcall GmSimProcessInstantProjectile(_DWORD *a1, int a2, float *a3)
{
return sub_BE464A28((int)a1, a2);
[C] 纯文本查看 复制代码 int __fastcall GmSimUpdateProjectiles(int result)
{
v1 = (_DWORD *)result;
result = GmSimProcessInstantProjectile(v1, (int)v1 + v3, (float *)v43);
剩下的都是以此类推,看图吧,
到目前为止,已经找到的调用栈:(个人习惯这样记录,仅供参考)
[C] 纯文本查看 复制代码 int __fastcall GmSimDealDamageToTank(int result, int a2, int a3, int a4)
{
v4 = (_DWORD *)result;
v17 = v4 + 0x2E00;
result = v18(v17[36], a2, a3); // v18GmSceneCampaignCallbackSimEventTankKilled
int __fastcall DealCollateralDamage(int result, float *a2, int a3, int a4)
{
v4 = (int *)result;
result = GmSimDealDamageToTank((int)v4, i, a4, (int)(float)((float)((float)a3 * v10) + (float)((float)a3 * v10)));
int __fastcall sub_BE464A28(int result, int a2)
{
result = DealCollateralDamage(result, (float *)(a2 + 4), *(_DWORD *)(a2 + 56), *(_DWORD *)(a2 + 40));
int __fastcall GmSimProcessInstantProjectile(_DWORD *a1, int a2, float *a3)
{
return sub_BE464A28((int)a1, a2);
int __fastcall GmSimUpdateProjectiles(int result)
{
v1 = (_DWORD *)result;
result = GmSimProcessInstantProjectile(v1, (int)v1 + v3, (float *)v43);
int *__fastcall GmSimUpdateSim(int *result, int a2)
{
v2 = (int)result;
GmSimUpdateProjectiles(v2);
int __fastcall GmSceneCampaignUpdate(int a1)
{
GmSimUpdateSim(*(int **)(a1 + 0x20), 30);
四、根据函数调用关系写出基址偏移
上一小节已经找到了GmSceneCampaignUpdate这个函数,继续,打断点,点LR寄存器旁边的箭头之后如图:
四
上图的蓝色框,是点击LR之后默认选中的地方,但实际上这行代码还没开始执行。可以给这两行以及GmSceneCampaignUpdate函数都打上断点,观察执行顺序。v3+32是GmSceneCampaignUpdate的参数。
[C] 纯文本查看 复制代码 v3 = gmc + 40 * *(_DWORD *)(gmc + 288);
v7 = (*(int (__fastcall **)(_DWORD))(v3 + 4))(*(_DWORD *)(v3 + 0x20)); //GmSceneCampaignUpdate
现在已经不用再跟了,注意这里出现了个“gmc”,v3和gmc有关,双击它之后:
发现这是bss段的一个数值,在游戏开发过程中,gmc可能是一个全局变量,总之,bss段的地址不会变,这里的gmc相对于libnative.so这个模块的偏移是固定的。可以拿模块加载到内存中的地址算一下,如图:
接下来回顾我们找到的调用栈,写出基址偏移,这个基址直接用gmc表示,写代码的时候再转成libnative.so+0x689cc的形式。
现在要注意的就是先关掉IDA的动态调试,再去用CE附加游戏,否则ceserver会有“没ROOT”之类的提示(我印象中是这样的,感兴趣可以根据ceserver的输出自己解决这个问题)。刚才的 gmc + 40 * *(_DWORD *)(gmc + 288),写到CE就是:【gmc】 + 0x28 * 【【 gmc】 + 0x120】
GmSceneCampaignUpdate接收到的参数,实际是:【gmc】+0x28*【【gmc】+0x120】+0x20
于是就有了基址偏移:
调用栈如图,从下往上看,这些函数都是只看参数一就够了。v17看做一个表示地址值的int变量,v17【36】相当于v17 + 36*4(十进制),也就是v17+0x90,这里v18就是GmSceneCampaignCallbackSimEventTankKilled,参数一就是:
于是:
[C] 纯文本查看 复制代码 int __fastcall GmSimGetNumOfDeadAiTanks(int a1)
{
int v1; // r2
int v2; // r1
int v3; // r3
v1 = 0;
v2 = a1; // [[[[gmc]+0x28*[[gmc]+0x120]+0x20]+0x20]+0xB890] + 0x20
v3 = 0;
while ( v1 < *(_DWORD *)(a1 + 20) )
//所以 循环终止条件这里是: [[[[[gmc]+0x28*[[gmc]+0x120]+0x20]+0x20]+0xB890]+0x20]
{
if ( !*(_BYTE *)(v2 + 72) )
// [[[[gmc]+0x28*[[gmc]+0x120]+0x20]+0x20]+0xB890]+0x724*n + 0x48 表示敌人n已死亡
++v3;
++v1;
v2 += 724;
// [[[[gmc]+0x28*[[gmc]+0x120]+0x20]+0x20]+0xB890]+0x724*n 这个就是每个敌人的结构体地址
}
*(_DWORD *)(a1 + 24) = v3;
return v3;
}
于是终于得到了敌人最大数量和每个敌人的结构体起始地址的基址偏移是:
[Asm] 纯文本查看 复制代码 [[[[ [gmc] + 0x28 * [ [gmc] + 0x120] + 0x20] + 0x20 ]+ 0xB890] + 0x20] + 0x14
[[[[ [gmc] + 0x28 * [ [gmc] + 0x120] + 0x20] + 0x20 ]+ 0xB890] + 0x20] + 0x2d4 * n
另外,该游戏的整套绘制血量教程已经做好了,可以参考这个目录:https://www.52pojie.cn/thread-1582463-1-1.html
|