旅行的青蛙Unity游戏逆向修改--Android篇

前言

其实这篇文章也没有太多的技术含量,主要是试一试Android上面Unity3D游戏C#脚本的分析与修改,话不多说先准备好旅行青蛙的apk就开始吧。

C#脚本

首先将Android apk安装包解压,查看assets/bin/Data/Managed文件夹下面的dll文件,其中Assembly-CSharp.dll便是游戏中的C#脚本,使用Reflector或者dnSpy进行反编译都可以,如何出现反编译出错的情况,那可能就是脚本被加密了,一般在尝试在libmono.so中的mono_image_open_from_data_with_name中获取解密后的dll脚本,而当前分析的游戏C#脚本并没有加密。在dnSpy中的反编译结果如下:

image

左侧就可以看到对应的类,点击类就可以看到反编译出来的代码,下面先跟游戏里面的一些特征来分析反编译出来的脚本。

汉化

首先使用adb install tabikaeru.apk命令安装apk文件,然后打开游戏可以看到如下界面:

首先来看看怎么修改显示的文字以达到汉化的效果,使用dnSpy点击编辑->搜索程序集搜索右边选择数字/字符串然后搜索名前:

点击CallTutorial可以找到刚刚在屏幕上面显示的文字:

鼠标点击右键选择编辑IL指令,修改刚刚看到的文字为汉字:

然后点击应用即可看到修改后的效果:

点击文件->全部保存替换assets/bin/Data/Managed文件夹下面对应的文件即可,然后将文件打包成apk使用jarsinger、signapk.jar或Android助手重签名安装到手机就能看到修改后的效果了。

修改三叶草数

要修改三叶草数可以从购买的时候入手,比如提示三叶草不足的时候:

直接搜索找到目标代码:

根据找到的如下代码来分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
if (SuperGameMaster.CloverPointStock() >= itemDataFormat.price)
{
if (SuperGameMaster.FindItemStock(shopDataFormat.itemId) < 99)
{
base.GetComponent<FlickCheaker>().stopFlick(true);
ConfilmPanel confilm = this.ConfilmUI.GetComponent<ConfilmPanel>();
if (itemDataFormat.type == Item.Type.LunchBox)
{
confilm.OpenPanel_YesNo(string.Concat(new object[]
{
itemDataFormat.name,
"\nを買いますか?\n(所持数\u3000",
SuperGameMaster.FindItemStock(shopDataFormat.itemId),
")"
}));
}
else
{
confilm.OpenPanel_YesNo(itemDataFormat.name + "\nを買いますか?");
}
confilm.ResetOnClick_Yes();
confilm.SetOnClick_Yes(delegate
{
confilm.ClosePanel();
});
confilm.SetOnClick_Yes(delegate
{
this.GetComponent<FlickCheaker>().stopFlick(false);
});
confilm.SetOnClick_Yes(delegate
{
this.BuyItem();
});
confilm.ResetOnClick_No();
confilm.SetOnClick_No(delegate
{
confilm.ClosePanel();
});
confilm.SetOnClick_No(delegate
{
this.GetComponent<FlickCheaker>().stopFlick(false);
});
}
else
{
base.GetComponent<FlickCheaker>().stopFlick(true);
ConfilmPanel confilm = this.ConfilmUI.GetComponent<ConfilmPanel>();
confilm.OpenPanel("もちものがいっぱいです");
confilm.ResetOnClick_Screen();
confilm.SetOnClick_Screen(delegate
{
confilm.ClosePanel();
});
confilm.SetOnClick_Screen(delegate
{
this.GetComponent<FlickCheaker>().stopFlick(false);
});
}

当三叶草的存储小于当前商品的价格的时候就会弹出这个提示,点击SuperGameMaster.CloverPointStock()得到如下代码:

1
2
3
4
public static int CloverPointStock()
{
return SuperGameMaster.saveData.CloverPoint;
}

这里返回的是一个从saveData.CloverPoint中获取的整数,可以尝试将其修改成返回一个特定的数字每次查询三叶草的数目都是这么多,右键点击编辑IL指令,然后点击重置并右键删除第一条指令,因为只是返回一个数字的话两条指令就够了:

然后修改第二条指令为一个数字,具体的指令代表的类型可以查看IL指令说明:

点击确定后就能看到修改代码的效果:

1
2
3
4
public static int CloverPointStock()
{
return 9999;
}

重新签名安装之后每次购买商品之后三叶草的数目也不会减少,一直是9999。

修改抽奖券

修改抽奖券也是同样的套路,搜索找到目标代码,其实刚刚搜索的时候就已经看到了。

1
2
3
4
5
6
7
8
9
10
11
if (SuperGameMaster.TicketStock() < 5)
{
ConfilmPanel confilm = this.ConfilmUI.GetComponent<ConfilmPanel>();
confilm.OpenPanel("ふくびき券が足りません");
confilm.ResetOnClick_Screen();
confilm.SetOnClick_Screen(delegate
{
confilm.ClosePanel();
});
return;
}

点击SuperGameMaster.TicketStock()也是差不多的代码同样修改返回数字即可:

1
2
3
4
public static int TicketStock()
{
return SuperGameMaster.saveData.ticket;
}
1
2
3
4
public static int TicketStock()
{
return 9999;
}

修改抽奖概率

每次抽奖都是抽到白球就不是很开心了,怎么能够提供抽奖的概率呢?抽到其它颜色的球的时候会提示:

搜索白玉找到如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static readonly Dictionary<Rank, string> PrizeBallName = new Dictionary<Rank, string>
{
{
Rank.White,
"白玉"
},
{
Rank.Blue,
"青玉"
},
{
Rank.Green,
"緑玉"
},
{
Rank.Red,
"赤玉"
},
{
Rank.Gold,
"黄玉"
}
};

右键点击PrizeBallName选择分析找到在那里被使用的:

双击PrizeBallName得到如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public void PushRollButton()
{
if (SuperGameMaster.TicketStock() < 5)
{
ConfilmPanel confilm = this.ConfilmUI.GetComponent<ConfilmPanel>();
confilm.OpenPanel("ふくびき券が足りません");
confilm.ResetOnClick_Screen();
confilm.SetOnClick_Screen(delegate
{
confilm.ClosePanel();
});
return;
}
SuperGameMaster.GetTicket(-5);
SuperGameMaster.set_FlagAdd(Flag.Type.ROLL_NUM, 1);
base.GetComponentInParent<UIMaster>().freezeObject(true);
base.GetComponentInParent<UIMaster>().blockUI(true, new Color(0f, 0f, 0f, 0.3f));
this.LotteryCheck();
this.ResultButton.GetComponent<RollResultButton>().CngImage((int)this.result);
this.ResultButton.GetComponent<RollResultButton>().CngResultText(Define.PrizeBallName[this.result] + "がでました");
this.LotteryWheelPanel.GetComponent<LotteryWheelPanel>().OpenPanel(this.result);
SuperGameMaster.SetTmpRaffleResult((int)this.result);
SuperGameMaster.SaveData();
SuperGameMaster.audioMgr.PlaySE(Define.SEDict["SE_Raffle"]);
this.BackFunc();
}

从代码来看就是刚刚抽奖结果提示的地方,来分析下这个结果是怎么生成的,可以看到Define.PrizeBallName[this.result]这个result决定了抽到的是什么颜色的球,那么看看这个result是在哪里生成的。在当前文件中看到了两处赋值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void LotteryCheck()
{
int num = Random.Range(0, Define.PrizeBalls[Rank.RankMax]);
this.result = Rank.White;
int i = 0;
int num2 = 0;
while (i < 5)
{
num2 += Define.PrizeBalls[(Rank)i];
if (num < num2)
{
this.result = (Rank)i;
break;
}
i++;
}
}

ublic void SetTmpResult()
{
this.result = (Rank)SuperGameMaster.GetTmpRaffleResult();
this.BackFunc();
}

下面是在UIMaster_raffle.UI_Start中调用的,判断之前的奖品有没有领取,LotteryCheck这个刚好是在显示结果之前调用的,猜测就是这里控制的不同颜色的球的出现概率,分析下这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void LotteryCheck()
{
int num = Random.Range(0, Define.PrizeBalls[Rank.RankMax]); //Rank.RankMax是5,Define.PrizeBalls[Rank.RankMax]是100,从0-100随机生成一个数
this.result = Rank.White; //默认都是白色的球
int i = 0;
int num2 = 0;
while (i < 5)
{
num2 += Define.PrizeBalls[(Rank)i]; //根据PrizeBalls的值小于60是白色,大于等于60并且少于87是蓝色,大于等于87小于96是绿色,大于等于96小于99是红色,大于等于99是金色。
if (num < num2)
{
this.result = (Rank)i;
break;
}
i++;
}
}

从上面代码分析PrizeBalls中就定义了不同颜色的球摇出概率:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public static readonly Dictionary<Rank, int> PrizeBalls = new Dictionary<Rank, int>
{
{
Rank.White,
60
},
{
Rank.Blue,
27
},
{
Rank.Green,
9
},
{
Rank.Red,
3
},
{
Rank.Gold,
1
},
{
Rank.RankMax,
100
}
};

所以直接修改这里的数字就控制摇出的概率了,比如不出白球,其它球的概率都一样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public static readonly Dictionary<Rank, int> PrizeBalls = new Dictionary<Rank, int>
{
{
Rank.White,
0 //不存在白球
},
{
Rank.Blue,
25
},
{
Rank.Green,
25
},
{
Rank.Red,
25
},
{
Rank.Gold,
25
},
{
Rank.RankMax,
100
}
};

修改农场四叶草数

农场里面大部分都是三叶草、四叶草的概率是很小的,那么怎么修改这个呢?在界面上面好像不太好找关联,先试试搜索三叶草的英语clover,点击checkCloverCreate看起来就是控制生成的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
public void checkCloverCreate()
{
this.cloverList = SuperGameMaster.GetCloverList();
bool flag = false;
if (this.cloverList.Count == 0)
{
flag = true;
//这句话翻译的意思就是:有了四叶草的初期化标志。四叶草生成
//也就是flag为true的时候会生成四叶草
Debug.Log("[CloverFarm] クローバーの初期化フラグが立ちました。四葉を生成します");
}
if (this.cloverList.Count < this.cloverMax)
{
Debug.Log(string.Concat(new object[]
{
"[CloverFarm] クローバーの数を調整します:",
this.cloverList.Count,
" > ",
this.cloverMax
}));
}
while (this.cloverList.Count < this.cloverMax)
{
CloverDataFormat cloverDataFormat = new CloverDataFormat();
cloverDataFormat.lastHarvest = new DateTime(1970, 1, 1);
cloverDataFormat.timeSpanSec = -this.cloverList.Count - 1;
cloverDataFormat.newFlag = true;
this.cloverList.Add(cloverDataFormat);
}
if (this.cloverList.Count > this.cloverMax)
{
Debug.Log(string.Concat(new object[]
{
"[CloverFarm] クローバーの数を調整します:",
this.cloverList.Count,
" > ",
this.cloverMax
}));
this.cloverList.RemoveRange(this.cloverMax - 1, this.cloverList.Count - this.cloverMax);
}
List<GameObject> list = new List<GameObject>();
for (int i = 0; i < this.cloverList.Count; i++)
{
if (!this.cloverList[i].newFlag && this.cloverList[i].timeSpanSec <= 0)
{
list.Add(this.LoadCloverObject(i, this.cloverList[i]));
}
}
int num = 0;
for (int j = 0; j < this.cloverList.Count; j++)
{
if (this.cloverList[j].newFlag)
{
//这里根据flag调用不同的函数,
if (!flag)
{
//生成三叶草
list.Add(this.NewCloverObject(j, this.cloverList[j], list));
}
else
{
//生成四叶草,不同的是第四个参数为true
list.Add(this.NewCloverObject(j, this.cloverList[j], list, true));
flag = false;
}
this.cloverList[j].x = list[list.Count - 1].transform.localPosition.x;
this.cloverList[j].y = list[list.Count - 1].transform.localPosition.y;
Clover component = list[list.Count - 1].GetComponent<Clover>();
this.cloverList[j].element = component.element;
this.cloverList[j].spriteNum = component.spriteNum;
this.cloverList[j].point = component.point;
this.cloverList[j].newFlag = false;
num++;
}
}
foreach (GameObject gameObject in list)
{
int num2 = this.cloverOrderInLayer;
foreach (GameObject gameObject2 in list)
{
if (gameObject.transform.position.y < gameObject2.transform.position.y)
{
num2++;
}
}
gameObject.GetComponent<SpriteRenderer>().sortingOrder = num2;
}
Debug.Log(string.Concat(new object[]
{
"[CloverFarm] クローバー生成完了:",
list.Count,
"\u3000/ (新規:",
num,
")"
}));
}

首先看this.NewCloverObject(j, this.cloverList[j], list)这个内部调用就是四个参数的,只不过第四个参数是false:

1
2
3
4
public GameObject NewCloverObject(int index, CloverDataFormat cloverData, List<GameObject> cloversObj)
{
return this.NewCloverObject(index, cloverData, cloversObj, false);
}

也就是第四个参数为true就是四叶草,点进去看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
public GameObject NewCloverObject(int index, CloverDataFormat cloverData, List<GameObject> cloversObj, bool fourLeafFlag)
{
Vector2 size = base.GetComponent<BoxCollider2D>().size;
PolygonCollider2D component = base.GetComponent<PolygonCollider2D>();
Vector2 vector;
vector..ctor(base.GetComponent<BoxCollider2D>().offset.x - size.x / 2f, base.GetComponent<BoxCollider2D>().offset.y - size.y / 2f);
int num = 0;
bool flag;
Vector3 vector2;
do
{
flag = false;
vector2 = new Vector2(vector.x + Random.Range(0f, size.x), vector.y + Random.Range(0f, size.y));
if (!component.OverlapPoint(vector2 + base.transform.position))
{
flag = true;
}
else
{
for (int i = 0; i < cloversObj.Count; i++)
{
Vector2 size2 = cloversObj[i].GetComponent<BoxCollider2D>().size;
if (Mathf.Abs(vector2.x - cloversObj[i].transform.localPosition.x) < size2.x / 2f && Mathf.Abs(vector2.y - cloversObj[i].transform.localPosition.y) < size2.y / 4f)
{
flag = true;
}
}
num++;
if (num >= 100)
{
break;
}
}
}
while (flag);
GameObject gameObject = Object.Instantiate<GameObject>(this.basePrefab, Vector3.zero, Quaternion.identity);
CloverDataFormat cloverDataFormat = new CloverDataFormat();
cloverDataFormat.point = 1;
cloverDataFormat.element = 0;
//如果第四个参数是false,这里就是四叶草生成的概率,是fourLeaf_percent是1那这个概率就是1/100
if (Random.Range(0f, 10000f) < this.fourLeaf_percent * 100f)
{
cloverDataFormat.element = 1;
}
//如果传了这个参数为true直接生成四叶草
if (fourLeafFlag)
{
cloverDataFormat.element = 1;
}
int element = cloverDataFormat.element;
if (element != 0)
{
if (element == 1)
{
cloverDataFormat.spriteNum = Random.Range(0, this.fourCloverSprite.Length);
gameObject.GetComponent<SpriteRenderer>().sprite = this.fourCloverSprite[cloverDataFormat.spriteNum];
}
}
else
{
cloverDataFormat.spriteNum = Random.Range(0, this.cloverSprite.Length);
gameObject.GetComponent<SpriteRenderer>().sprite = this.cloverSprite[cloverDataFormat.spriteNum];
}
gameObject.GetComponent<Clover>().SetCloverData(index, cloverDataFormat);
gameObject.transform.parent = base.transform;
gameObject.transform.localScale = Vector3.one;
gameObject.transform.localPosition = vector2;
return gameObject;
}

那么这里既可以修改四叶草生成的概率也可以修改fourLeafFlag这个为true,第一种方式和上面一样,看看第二种。右键fourLeafFlag编辑IL指令:

这里首先取fourLeafFlag判断然后跳转,所以修改为true即可。修改ldarg.s fourLeafFlagldc.i4 1,修改后的代码如下:

1
2
3
4
if (true)
{
cloverDataFormat.element = 1;
}

总结

今天修改的部分就到这里了,其它的大家可以自己去尝试,总结下分析Unity3D游戏过程,当然这篇文章讲的是最简单的情况,还有使用保护将dll脚本加密的,就需要hook函数或者从内存查找dump,还有通过C#脚本调用lua脚本来实现的,逻辑在lua脚本里面,lua脚本又加密了的情况等等。那么没保护的情况的话,一般可以从界面显示搜索来分析,然后根据一些特定单词去搜索查找关键代码部分。这篇主要是Android上面使用mono模式的情况,如果是使用IL2CPP的话比如iOS上面,C#脚本都转成了cpp文件c代码的形式的话分析起来就会麻烦很多,后面会有在iOS上面修改代码以及在非越狱平台也能修改运行的文章。

AloneMonkey wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!