吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 2332|回复: 48
收起左侧

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

  [复制链接]
yellowtail 发表于 2024-12-8 01:44

概述

游戏是(base64) aHR0cHM6Ly93d3cudGFwdGFwLmNuL2FwcC8xNTA0NTQ=
steam 上有

找到游戏安装目录,看到了 Unity
img-20241207233128.png

再去看一下引擎,看到了 mono
img-20241207233539.png

再加上找到了 Assembly-CSharp.dll 那么说明破解方向是 mono (另外一个方向是 IL2CPP)

mono 通用套路是:使用dnspy 分析代码+修改IL指令字节码

门槛较低,本文侧重分享 IL指令修改经验

Assembly-CSharp.dll

直接用 dnSpy-net-win64 打开

首先玩一会儿游戏,知道游戏中几个概念:

  1. 角色
  2. 子弹
  3. 血量
  4. 金币

我们先搜索 血量,常见关键字为: hp damage

一通搜索之后,找到了一个看起来很关键的文件 CharacterInBattleModel

img-20241208002214.png

看名字是 角色在战斗中的模型

构造方法里有: 最大血量(MaxHealthValue),当前血量(currentHealth)、护甲(armor)

因为战斗的时候,敌人和我们都有血量和护甲,所以直接改这里会对自己和敌人都生效,肯定不能改这里,我们只修改自己的角色

在构造方法上,右键,分析,看一下调用的地方
img-20241208003534.png

地方还比较多,我们怎么知道应该修改哪一个呢?
我的思路是:梭哈,把可疑的都给修改了,但是改的效果不一样,这样进游戏就知道是哪里的修改生效了;比如第一个地方把最大血量改为111,第二个改为123;进了游戏血量是多少,就知道是哪里的改动生效

我这里先修改两个带 hero的方法 InsHero()  InsHero(int) 给大家示范一下

InsHero

看一下原始代码
img-20241208004624.png

第一个参数是:角色
第二个参数是:当前血量
第三个参数是: 最大血量

我们现在改为固定值试试

修改方法

img-20241208004908.png

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

我们先试试第一个
img-20241208005100.png
修改之后,点击编译,报错了,看来不行, 那我们来尝试修改IL指令

img-20241208005204.png

有没有发现看不懂?没事,看不懂是正常的

那么接下来我们就是要去看懂了

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

大家如果不想学习的话,可以看我的结论:

  1. newobj 是调用构造方法,生成一个对象
  2. call 是调用一个方法,比如 getHealth() 等
  3. newobj 之前就是在做各种参数准备的事情,包括从哪里取,要不要做计算之类的
  4. 立即数是 ldc.i4
  5. ldc.i4 又细分为 ldc.i4.0(立即数0)、ldc.i4.8(立即数8)、ldc.i4 xx (立即数xxx)

理解了以上5点就够了,就可以开始着手修改了,其余的IL指令知识可以后面感兴趣再学

修改IL指令

按照上面的IL指令知识,我标记了一下,更清晰一点
img-20241208010258.png

因为 CharacterInBattleModel 构造方法参数如下:
第一个参数是:角色
第二个参数是:当前血量
第三个参数是: 最大血量

我们先来修改 第三个参数,也就是 IL指令编辑器里的 5、6、7 三行
原始代码,占用了三行是为了读取;
我们直接改为175,一个指令就够 ldc.i4 175, 多的指令设置为 nop

img-20241208011246.png
img-20241208011305.png

这样就成功修改了一处了

同理把第二个参数;还有其它调用的地方都给改了

最后进游戏,看效果
img-20241129214742.png

成功了 ^_^

修改最大弹药

第一个角色,默认只有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
img-20241130190823.png

资源

我们玩不同的角色,初始弹药不一样,那这个逻辑是怎么控制的

按照开发经验,这种逻辑应该是在配置文件里配置的

在分析代码的时候,就发现了,角色初始数据都是存储在 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 文件

img-20241208012846.png

我们打开看一下

版本

首先需要判断版本,先用文本编辑器直接打开,可以看到
UnityFS    5.x.x 2018.4.27f1
img-20241127001613.png

信息出来了:

  • 5.x.x 版本
  • 2018.4.27f1

解包

https://zhuanlan.zhihu.com/p/343447609

可以使用 AssetStudio
img-20241127001717.png

再通过代码找到角色加载逻辑

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
img-20241127002237.png

还看到了角色信息
img-20241127002458.png

img-20241208013520.png

可以得知, 角色的初始金币,初始效果、初始子弹、初始血量都是在这里控制的

直接修改这个文件,应该是效果最明显,最便捷的思路了;

不过因为前面的IL修改已经达到了我的目的,这里就没有深入研究了,大家有兴趣可以找工具来修改,就当是课后作业了

免费评分

参与人数 8吾爱币 +7 热心值 +8 收起 理由
yuzaizi521 + 1 + 1 谢谢@Thanks!
Carinx + 1 + 1 谢谢@Thanks!
SpiralTower + 1 + 1 我很赞同!
eggge + 1 + 1 热心回复!
mmjqzf123 + 1 + 1 谢谢@Thanks!
lsq132273 + 1 + 1 我很赞同!
HillBoom + 1 + 1 用心讨论,共获提升!
Hameel + 1 热心回复!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

YanBo 发表于 2024-12-8 10:36
但是搞这么半天感觉直接下载一个修改器更快(其实我更喜欢自己折腾),想当初修改塔科夫离线版的本地文件也都费了自己很心思
8sp8 发表于 2024-12-8 07:29
Jingrun 发表于 2024-12-8 08:00
Catcherkk 发表于 2024-12-8 08:20
思路很好,值得学习
JackTheRipper 发表于 2024-12-8 08:41
感谢分享
tnancy2kk 发表于 2024-12-8 08:53
感谢分享,转存备用
Student01 发表于 2024-12-8 09:17
感谢分享
nzy8513 发表于 2024-12-8 09:18
看起来很厉害
as19880115 发表于 2024-12-8 09:37
感谢分享
义飞ing 发表于 2024-12-8 09:42
一路飘过 感谢分享
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-12-27 04:29

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表