缘起

作为一只程序猿,游戏自然是标配。自从入了杀戮尖塔的坑,几年来陆陆续续玩了几百小时。然而steam上的成就至今没刷完,发牌员和各路小怪次次都在针对我。在第 n n {n^n} nn次死于通往进阶20的三层小怪之手后,我感受到了出离的愤怒
于是我决定,用技术它。

思路

杀戮尖塔是用java开发,其主程序是一个.jar文件。现在要修改铁甲战士的基础卡:打击。
在这里插入图片描述
打击的基础伤害是6,目标是将其修改为60。

  1. 使用Java Decompiler反编译.jar文件,找到目标class文件和目标代码。
  2. 使用jclasslib的byecode viewer或插件分析目标class文件,对目标代码进行定位。
  3. 创建java工程,引入jclasslib的jar包,载入目标class文件并进行修改。
  4. 将最终生成的新class文件替换到主程序的.jar中。

分析

Java Decompiler反编译

打开游戏目录,可以看到一个desktop-1.0.jar。这就是游戏的主程序。
首先使用Java Decompiler对其进行反编译。打开jd-gui,将desktop-1.0.jar拖进来:
在这里插入图片描述
如上,进入目录:

desktop-1.0\com\megacrit\cardcrawl\cards\red\

这是卡片目录,red即红色,也就是铁甲战士的红色卡。
第一个文件叫Anger.class,也就是愤怒。我们向下找,可以到找到文件Strike_Red.class,从名称看,这就是要找的打击了。
在这里插入图片描述
选中后可以看到右侧的源码,其构造函数中有这样一行代码:

this.baseDamage = 6;

这行代码设置了打击的基础伤害是6。我们的目标是将其修改为60。
然而,Java Decompiler只能查看class文件反编译后的结果,并不能对其进行修改。
并且由于该class文件import了众多库,无法直接使用javac命令来对单个文件进行编译。于是就需要借助jclasslib。

jclasslib byecode viewer分析class文件

使用winrar对游戏目录下的desktop-1.0.jar进行解压。
在这里插入图片描述
上面已经定位了Strike_Red.class的路径。在解压desktop-1.0.jar后生成的文件夹中找到该class文件,打开jclasslib byecode viewer,将class拖进来。
在这里插入图片描述
可以看到这里是用Java虚拟机的结构来查看class文件的。此部分内容可以参考《Java虚拟机规范》。
左侧的Constatnt Pool即常量池,Methods即方法。
通过前面的分析可知,要修改的内容位于构造函数中。
于是打开Methods,其下的0号元素<init>即构造函数。
在这里插入图片描述
注意上面右侧的信息,包含两个重要的内容:

  • Name: <init>
  • Descriptor: ()V

打开<init>,其下的0号属性是Code(很重要),这就是构造函数的代码。点击Code,可以在右侧面板查看其具体的实现:
在这里插入图片描述

同样,右侧上方的Generic info中包含了一个重要信息:

  • Attribute name index: cp_info #58

其中#58指的是常量池的58号索引位置。其名称叫Attribute name index
右侧下方是构造函数的虚拟机代码。里面的指令借助栈和变量表及常量表进行了各种操作。我们需要关心的就是哪一行为变量赋值了6。
很明显地,可以看到如下代码:

27 aload_0
28 bipush 6
30 putfield #11 <com/megacrit/cardcrawl/cards/red/Strike_Red.baseDamage>

关键是中间的28 bipush 6。该行代码的含义是:

  • 该指令从code的第28号索引开始。
  • 指令为入栈一个int。
  • 入栈的int值为6。

同时要明确:

  • code是以byte为单位。
  • 每个指令占一个字节,比如这里的bipush
  • 参考下一行代码,可知该行代码占2个字节,即28和29。

将code看做一个byte数组,28号索引即code[28],其内容为bipush指令。code[29]就是入栈的int值6。
于是,只要我们能拿到code[29],并将其修改为60即可。
注意这里入栈的int值只占1个字节,且为带符号数,因此其值不应超过127。
byecode viewer只能进行查看,不能进行修改。

修改

要修改class文件有两种方式:

  1. 借助二进制文件修改工具。
  2. 创建一个java工程,使用第三方库。

对于方式1,看似简单,实际需要直接分析代码的十六进制值,且要考虑到文件的字节对齐和加密等情况。
对于方式2,需借助第三方库。而第三方库必须集成到java中使用。故而需要创建一个java的工程。

这里使用方式2。故而先打开Intellij IDEA,新建一个JBoss工程。

依赖库安装

java常用于修改class文件内容的库有2个:jclasslib和javassist。

  • jclasslib:可通过虚拟机汇编方式查看class文件内容,且提供的jar能够对class文件进行修改。
  • javassist:常用于动态编程。

两个库的安装方式如下。本篇采用jclasslib进行修改。

安装jclasslib

下载并安装

打开jclasslib的官方github进行下载:

https://github.com/ingokegel/jclasslib/releases

这里选择exe安装版本。下载后安装。
在这里插入图片描述

安装后,lib目录下为所有必要的jar包,修改class时需要将该文件夹导入到工程中作为依赖。

导入依赖到工程中

打开:
File→Project Structure...
左侧选择Modules,右侧选择Dependencies标签,点最右侧**+**号,选择JARs or directories…,在弹出的窗口中选择jclasslib安装目录下的lib文件夹。
在这里插入图片描述

安装javassist

下载并安装

打开javassist的官方github进行下载:

http://www.javassist.org/

下载后是个zip压缩包,解压并复制到指定目录下。

导入依赖到工程中

打开:
File→Project Structure...
左侧选择Modules,右侧选择Dependencies标签,点最右侧 + 号,选择JARs or directories…,在弹出的窗口中选择javassist目录下的javassist.jar文件。

修改class

准备工作

  1. 在桌面上创建一个Test文件夹,并将Strike_Red.class复制到这里。
  2. 在Test文件夹下创建一个Change文件夹,用于存放修改后生成的Strike_Red.class文件。

编程进行修改和保存

实现代码:

import org.gjt.jclasslib.io.ClassFileWriter;
import org.gjt.jclasslib.structures.AttributeInfo;
import org.gjt.jclasslib.structures.ClassFile;
import org.gjt.jclasslib.structures.MethodInfo;
import org.gjt.jclasslib.structures.attributes.CodeAttribute;
import java.io.DataInput;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;

public class Main {
    public static void main(String[] args) throws Exception {
        String filePath = "C:\\Users\\Administrator\\Desktop\\Test\\Strike_Red.class";
        FileInputStream fis = new FileInputStream(filePath);
        DataInput di = new DataInputStream(fis);

        ClassFile cf = new ClassFile();
        cf.read(di); // 载入类文件数据

        // 获取构造函数
        MethodInfo mi = cf.getMethod("<init>", "()V");
        // 方法的所有属性
        AttributeInfo[] attributes = mi.getAttributes();
        // 0号属性为Code
        CodeAttribute codeAttribute = (CodeAttribute)attributes[0];

        byte[] code = codeAttribute.getCode();  // 获取Code的二进制内容
        code[29] = 60;  // 修改29号索引内容
        codeAttribute.setCode(code);    // 重设方法的Code内容

        fis.close();
        // 保存修改后的ClassFile
        File f = new File("C:\\Users\\Administrator\\Desktop\\Test\\Change\\Strike_Red.class");
        ClassFileWriter.writeToFile(f, cf);
    }
}

整体思路为:

  1. 创建一个ClassFile,将class文件载入进来。
  2. 获取构造函数,存放到一个MethodInfo对象中。这里用到的参数为构造函数的NameDescriptor,前面的分析已经得知这两个值。
  3. 获取构造函数所有的属性。0号属性即为Code,前面已经分析过。将获取到的0号属性转换为CodeAttribute
  4. 获取Code的二进制内容,是一个byte数组。要修改的内容位于29号索引,将其修改为60。这里若修改为>127的值IDEA会提示错误。
  5. 将修改后的Code设置回构造函数的属性中。于是构造函数内容被修改,也就是当前ClassFile被修改。
  6. 将修改后的ClassFile保存在Test/Change下。

运行后,即会在Test/Change下生成Strike_Red.class文件。
将这个生成的文件拖入到Byecode viewer中,查看构造函数的Code,可以看到baseDamage已经修改为60:
在这里插入图片描述

替换

现在需要将Strike_Red.class替换回desktop-1.0.jar
首先将Java Decompiler和Byecode viewer都关闭,以防止对正在打开的文件进行修改。
然后用winrar打开desktop-1.0.jar(是打开不是解压),找到Strike_Red.class所在的路径:
com\megacrit\cardcrawl\cards\red
将上一步生成的修改后的Strike_Red.class直接拖进来,替换。
这样修改就完成了。运行游戏看一下:
在这里插入图片描述

It’s Hammer Time!

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐