概述
游戏是(base64) aHR0cHM6Ly93d3cudGFwdGFwLmNuL2FwcC8xNTA0NTQ=
steam 上有
找到游戏安装目录,看到了 Unity
再去看一下引擎,看到了 mono
再加上找到了 Assembly-CSharp.dll
那么说明破解方向是 mono
(另外一个方向是 IL2CPP
)
mono 通用套路是:使用dnspy 分析代码+修改IL指令字节码
门槛较低,本文侧重分享 IL指令修改经验
Assembly-CSharp.dll
直接用 dnSpy-net-win64
打开
首先玩一会儿游戏,知道游戏中几个概念:
- 角色
- 子弹
- 血量
- 金币
我们先搜索 血量,常见关键字为: hp damage
一通搜索之后,找到了一个看起来很关键的文件 CharacterInBattleModel
看名字是 角色在战斗中的模型
构造方法里有: 最大血量(MaxHealthValue),当前血量(currentHealth)、护甲(armor)
因为战斗的时候,敌人和我们都有血量和护甲,所以直接改这里会对自己和敌人都生效,肯定不能改这里,我们只修改自己的角色
在构造方法上,右键,分析,看一下调用的地方
地方还比较多,我们怎么知道应该修改哪一个呢?
我的思路是:梭哈,把可疑的都给修改了,但是改的效果不一样,这样进游戏就知道是哪里的修改生效了;比如第一个地方把最大血量改为111,第二个改为123;进了游戏血量是多少,就知道是哪里的改动生效
我这里先修改两个带 hero的方法 InsHero() InsHero(int) 给大家示范一下
InsHero
看一下原始代码
第一个参数是:角色
第二个参数是:当前血量
第三个参数是: 最大血量
我们现在改为固定值试试
修改方法
修改代码有两个思路, 第一个是直接修改方法,第二个是在无法修改方法1的情况下修改IL指令
我们先试试第一个
修改之后,点击编译,报错了,看来不行, 那我们来尝试修改IL指令
有没有发现看不懂?没事,看不懂是正常的
那么接下来我们就是要去看懂了
IL指令简介
因为我们只是为了修改游戏,只需要学习怎么改IL指令就行,无需去学习完整的IL指令
我的经验是 找一个在线网站,大概写一下我们想要改的效果,看一下IL指令是什么,直接对照着改就行
网站是 在线IL
代码是我写的
using System;
public class C {
public void M() {
int a = 123;
int b =25;
int c = add(a+1, b);
}
public int add(int one, int two) {
return add2(one, 167);
}
public int add2(int one, int two) {
return one+two;
}
}
可以看到,涉及了立即数、临时变量、传参、参数和立即数相加
我把我的理解贴出来
IL
// Methods
.method public hidebysig
instance void M () cil managed
{
// Method begins at RVA 0x2050
// Code size 19 (0x13)
.maxstack 3
.locals init (
[0] int32 a,
[1] int32 b,
[2] int32 c
)
IL_0000: nop
IL_0001: ldc.i4.s 123
IL_0003: stloc.0 // 取出,设置到局部变量0
IL_0004: ldc.i4.s 25
IL_0006: stloc.1
IL_0007: ldarg.0 // this 的意思
IL_0008: ldloc.0 // 把局部变量0加载到堆栈
IL_0009: ldc.i4.1 // 把int32 1 加载到堆栈
IL_000a: add
IL_000b: ldloc.1
IL_000c: call instance int32 C::'add'(int32, int32)
IL_0011: stloc.2
IL_0012: ret
} // end of method C::M
.method public hidebysig
instance int32 'add' (
int32 one,
int32 two
) cil managed
{
// Method begins at RVA 0x2070
// Code size 16 (0x10)
.maxstack 3
.locals init (
[0] int32
)
IL_0000: nop
IL_0001: ldarg.0 // this
IL_0002: ldarg.1 // 入参1加载到堆栈
IL_0003: ldc.i4.2 // 数字2 加载到堆栈
IL_0004: add // add
IL_0005: ldarg.2
IL_0006: call instance int32 C::add2(int32, int32)
IL_000b: stloc.0
IL_000c: br.s IL_000e
IL_000e: ldloc.0
IL_000f: ret
} // end of method C::'add'
{
// Method begins at RVA 0x208c
// Code size 9 (0x9)
.maxstack 2
.locals init (
[0] int32
)
IL_0000: nop
IL_0001: ldarg.1
IL_0002: ldarg.2
IL_0003: add
IL_0004: stloc.0
IL_0005: br.s IL_0007
IL_0007: ldloc.0
IL_0008: ret
} // end of method C::add2
大家如果不想学习的话,可以看我的结论:
- newobj 是调用构造方法,生成一个对象
- call 是调用一个方法,比如 getHealth() 等
- newobj 之前就是在做各种参数准备的事情,包括从哪里取,要不要做计算之类的
- 立即数是 ldc.i4
- ldc.i4 又细分为 ldc.i4.0(立即数0)、ldc.i4.8(立即数8)、ldc.i4 xx (立即数xxx)
理解了以上5点就够了,就可以开始着手修改了,其余的IL指令知识可以后面感兴趣再学
修改IL指令
按照上面的IL指令知识,我标记了一下,更清晰一点
因为 CharacterInBattleModel
构造方法参数如下:
第一个参数是:角色
第二个参数是:当前血量
第三个参数是: 最大血量
我们先来修改 第三个参数,也就是 IL指令编辑器里的 5、6、7 三行
原始代码,占用了三行是为了读取;
我们直接改为175,一个指令就够 ldc.i4 175, 多的指令设置为 nop
这样就成功修改了一处了
同理把第二个参数;还有其它调用的地方都给改了
最后进游戏,看效果
成功了 ^_^
修改最大弹药
第一个角色,默认只有3颗弹药,我们来改大一些
搜索 ammo 可以找到所有的弹药相关逻辑
找到了以下代码
public static int AmmoMax
{
get
{
if (AdventureData.CurrentSkin2 != null)
{
return AdventureData.CurrentSkin2.StartAmmo + AdventureData.Event_AmmoMaxImprove;
}
return AdventureData.CurrentSkin.StartAmmo + AdventureData.Event_AmmoMaxImprove;
}
}
我们依旧用上面的IL知识,用立即数来不变应万变,改为一个固定值
资源
我们玩不同的角色,初始弹药不一样,那这个逻辑是怎么控制的
按照开发经验,这种逻辑应该是在配置文件里配置的
在分析代码的时候,就发现了,角色初始数据都是存储在 CharacterDictionary
里的
这个文件有一个 Init
方法,看起来就是 读取配置文件、初始化数据的
public void InitDictionary()
{
if (!CharacterDictionary.isInit)
{
this.TableStr.Clear();
this.Table.Clear();
this.ParamList.Clear();
string[] array = null;
array = ReadTable.Read("Character");
for (int i = 0; i < array.Length; i++)
可以看到读取了一个字符串 Character
看一下是读的什么文件, 一通追踪,发现读取的是 tableassets
文件
我们打开看一下
版本
首先需要判断版本,先用文本编辑器直接打开,可以看到
UnityFS 5.x.x 2018.4.27f1
信息出来了:
解包
https://zhuanlan.zhihu.com/p/343447609
可以使用 AssetStudio
再通过代码找到角色加载逻辑
array = ReadTable.Read("Hero");
Utils_File.ReadStreamingAssetAllLinesAsAsset(path);
public static string[] ReadStreamingAssetAllLinesAsAsset(string assetPath)
{
return Utils_File.ReadStreamingAssetAllLinesAsAssetFromBundle(assetPath);
}
public static string[] ReadStreamingAssetAllLinesAsAssetFromBundle(string assetPath)
{
string assetPath2 = AssetbundleLoader.GetAssetPath("tableassets");
AssetBundle assetBundle = Utils_File.loadedBundleLookup.ContainsKey(assetPath2) ? Utils_File.loadedBundleLookup[assetPath2] : AssetBundle.LoadFromFile(assetPath2);
if (assetBundle == null)
{
throw new ArgumentException("Bundle " + assetPath2 + " not found");
}
Utils_File.loadedBundleLookup[assetPath2] = assetBundle;
TextAsset textAsset = assetBundle.LoadAsset<TextAsset>(assetPath);
if (textAsset == null)
{
throw new ArgumentException("Asset " + assetPath + " not found in " + assetPath2);
}
return Regex.Split(textAsset.text, "\n|\r\n");
}
可以看出来是读取 tableassets
资源文件里的 Hero
还看到了角色信息
可以得知, 角色的初始金币,初始效果、初始子弹、初始血量都是在这里控制的
直接修改这个文件,应该是效果最明显,最便捷的思路了;
不过因为前面的IL修改已经达到了我的目的,这里就没有深入研究了,大家有兴趣可以找工具来修改,就当是课后作业了