某steam-unity卡牌游戏破解-通过修改IL指令

查看 133|回复 11
作者:yellowtail   
概述
游戏是(base64) aHR0cHM6Ly93d3cudGFwdGFwLmNuL2FwcC8xNTA0NTQ=
steam 上有
找到游戏安装目录,看到了 Unity


img-20241207233128.png (29.43 KB, 下载次数: 2)
下载附件
2024-12-7 23:32 上传

再去看一下引擎,看到了 mono


img-20241207233539.png (15.74 KB, 下载次数: 2)
下载附件
2024-12-7 23:35 上传

再加上找到了 Assembly-CSharp.dll 那么说明破解方向是 mono (另外一个方向是 IL2CPP)
mono 通用套路是:使用dnspy 分析代码+修改IL指令字节码
门槛较低,本文侧重分享 IL指令修改经验
Assembly-CSharp.dll
直接用 dnSpy-net-win64 打开
首先玩一会儿游戏,知道游戏中几个概念:
[ol]
  • 角色
  • 子弹
  • 血量
  • 金币
    [/ol]
    我们先搜索 血量,常见关键字为: hp damage
    一通搜索之后,找到了一个看起来很关键的文件 CharacterInBattleModel


    img-20241208002214.png (35.15 KB, 下载次数: 2)
    下载附件
    2024-12-8 00:29 上传

    看名字是 角色在战斗中的模型
    构造方法里有: 最大血量(MaxHealthValue),当前血量(currentHealth)、护甲(armor)
    因为战斗的时候,敌人和我们都有血量和护甲,所以直接改这里会对自己和敌人都生效,肯定不能改这里,我们只修改自己的角色
    在构造方法上,右键,分析,看一下调用的地方


    img-20241208003534.png (53.95 KB, 下载次数: 0)
    下载附件
    2024-12-8 00:36 上传

    地方还比较多,我们怎么知道应该修改哪一个呢?
    我的思路是:梭哈,把可疑的都给修改了,但是改的效果不一样,这样进游戏就知道是哪里的修改生效了;比如第一个地方把最大血量改为111,第二个改为123;进了游戏血量是多少,就知道是哪里的改动生效
    我这里先修改两个带 hero的方法 InsHero()  InsHero(int) 给大家示范一下
    InsHero
    看一下原始代码


    img-20241208004624.png (36.01 KB, 下载次数: 0)
    下载附件
    2024-12-8 00:47 上传

    第一个参数是:角色
    第二个参数是:当前血量
    第三个参数是: 最大血量
    我们现在改为固定值试试
    修改方法


    img-20241208004908.png (30.2 KB, 下载次数: 0)
    下载附件
    2024-12-8 00:49 上传

    修改代码有两个思路, 第一个是直接修改方法,第二个是在无法修改方法1的情况下修改IL指令
    我们先试试第一个


    img-20241208005100.png (71.96 KB, 下载次数: 0)
    下载附件
    2024-12-8 00:51 上传

    修改之后,点击编译,报错了,看来不行, 那我们来尝试修改IL指令


    img-20241208005204.png (52.67 KB, 下载次数: 0)
    下载附件
    2024-12-8 00:52 上传

    有没有发现看不懂?没事,看不懂是正常的
    那么接下来我们就是要去看懂了
    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
    大家如果不想学习的话,可以看我的结论:
    [ol]
  • newobj 是调用构造方法,生成一个对象
  • call 是调用一个方法,比如 getHealth() 等
  • newobj 之前就是在做各种参数准备的事情,包括从哪里取,要不要做计算之类的
  • 立即数是 ldc.i4
  • ldc.i4 又细分为 ldc.i4.0(立即数0)、ldc.i4.8(立即数8)、ldc.i4 xx (立即数xxx)
    [/ol]
    理解了以上5点就够了,就可以开始着手修改了,其余的IL指令知识可以后面感兴趣再学
    修改IL指令
    按照上面的IL指令知识,我标记了一下,更清晰一点


    img-20241208010258.png (110.02 KB, 下载次数: 0)
    下载附件
    2024-12-8 01:05 上传

    因为 CharacterInBattleModel 构造方法参数如下:
    第一个参数是:角色
    第二个参数是:当前血量
    第三个参数是: 最大血量
    我们先来修改 第三个参数,也就是 IL指令编辑器里的 5、6、7 三行
    原始代码,占用了三行是为了读取;
    我们直接改为175,一个指令就够 ldc.i4 175, 多的指令设置为 nop


    img-20241208011246.png (21.2 KB, 下载次数: 0)
    下载附件
    2024-12-8 01:13 上传



    img-20241208011305.png (23.59 KB, 下载次数: 0)
    下载附件
    2024-12-8 01:13 上传

    这样就成功修改了一处了
    同理把第二个参数;还有其它调用的地方都给改了
    最后进游戏,看效果


    img-20241129214742.png (113.51 KB, 下载次数: 0)
    下载附件
    2024-12-8 01:16 上传

    成功了 ^_^
    修改最大弹药
    第一个角色,默认只有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知识,用立即数来不变应万变,改为一个固定值


    img-20241130182447.png (10.89 KB, 下载次数: 0)
    下载附件
    2024-12-8 01:22 上传



    img-20241130190823.png (1.05 MB, 下载次数: 0)
    下载附件
    2024-12-8 01:22 上传

    资源
    我们玩不同的角色,初始弹药不一样,那这个逻辑是怎么控制的
    按照开发经验,这种逻辑应该是在配置文件里配置的
    在分析代码的时候,就发现了,角色初始数据都是存储在 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
    可以看到读取了一个字符串 Character
    看一下是读的什么文件, 一通追踪,发现读取的是 tableassets 文件


    img-20241208012846.png (30.94 KB, 下载次数: 0)
    下载附件
    2024-12-8 01:28 上传

    我们打开看一下
    版本
    首先需要判断版本,先用文本编辑器直接打开,可以看到
    UnityFS    5.x.x 2018.4.27f1


    img-20241127001613.png (22.53 KB, 下载次数: 0)
    下载附件
    2024-12-8 01:31 上传

    信息出来了:
  • 5.x.x 版本
  • 2018.4.27f1

    解包
    https://zhuanlan.zhihu.com/p/343447609
    可以使用 AssetStudio


    img-20241127001717.png (95.44 KB, 下载次数: 0)
    下载附件
    2024-12-8 01:32 上传

    再通过代码找到角色加载逻辑
    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(assetPath);
            if (textAsset == null)
            {
                throw new ArgumentException("Asset " + assetPath + " not found in " + assetPath2);
            }
            return Regex.Split(textAsset.text, "\n|\r\n");
        }
    可以看出来是读取 tableassets 资源文件里的 Hero


    img-20241127002237.png (68.5 KB, 下载次数: 0)
    下载附件
    2024-12-8 01:33 上传

    还看到了角色信息


    img-20241127002458.png (122.8 KB, 下载次数: 0)
    下载附件
    2024-12-8 01:34 上传



    img-20241208013520.png (151.35 KB, 下载次数: 0)
    下载附件
    2024-12-8 01:37 上传

    可以得知, 角色的初始金币,初始效果、初始子弹、初始血量都是在这里控制的
    直接修改这个文件,应该是效果最明显,最便捷的思路了;
    不过因为前面的IL修改已经达到了我的目的,这里就没有深入研究了,大家有兴趣可以找工具来修改,就当是课后作业了

    下载次数, 下载附件

  • YanBo   

    但是搞这么半天感觉直接下载一个修改器更快(其实我更喜欢自己折腾),想当初修改塔科夫离线版的本地文件也都费了自己很心思
    FCGkitty   

    请教一下,哪里有dnspy修改游戏的教程?是那种怎么找数据的教学,类似你这种的,而不是直接告诉你搜索指定关键字的那种。。
    8sp8   

    谢谢分享
    Jingrun   

    感谢分享
    Catcherkk   

    思路很好,值得学习
    JackTheRipper   

    感谢分享
    tnancy2kk   

    感谢分享,转存备用
    Student01   

    感谢分享
    nzy8513   

    看起来很厉害
    您需要登录后才可以回帖 登录 | 立即注册

    返回顶部