作为一只程序猿,游戏自然是标配。自从入了杀戮尖塔的坑,几年来陆陆续续玩了几百小时。然而steam上的成就至今没刷完,发牌员和各路小怪次次都在针对我。在第 n n {n^n} nn次死于通往进阶20的三层小怪之手后,我感受到了出离的愤怒。
于是我决定,用技术怼它。
杀戮尖塔是用java开发,其主程序是一个.jar文件。现在要修改铁甲战士的基础卡:打击。
打击的基础伤害是6,目标是将其修改为60。
打开游戏目录,可以看到一个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。
使用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号元素
注意上面右侧的信息,包含两个重要的内容:
打开
同样,右侧上方的Generic info中包含了一个重要信息:
其中#58指的是常量池的58号索引位置。其名称叫Attribute name index。
右侧下方是构造函数的虚拟机代码。里面的指令借助栈和变量表及常量表进行了各种操作。我们需要关心的就是哪一行为变量赋值了6。
很明显地,可以看到如下代码:
27 aload_0
28 bipush 6
30 putfield #11
关键是中间的28 bipush 6
。该行代码的含义是:
同时要明确:
bipush
。将code看做一个byte数组,28号索引即code[28],其内容为bipush
指令。code[29]就是入栈的int值6。
于是,只要我们能拿到code[29],并将其修改为60即可。
注意这里入栈的int值只占1个字节,且为带符号数,因此其值不应超过127。
byecode viewer只能进行查看,不能进行修改。
要修改class文件有两种方式:
对于方式1,看似简单,实际需要直接分析代码的十六进制值,且要考虑到文件的字节对齐和加密等情况。
对于方式2,需借助第三方库。而第三方库必须集成到java中使用。故而需要创建一个java的工程。
这里使用方式2。故而先打开Intellij IDEA,新建一个JBoss工程。
java常用于修改class文件内容的库有2个:jclasslib和javassist。
两个库的安装方式如下。本篇采用jclasslib进行修改。
打开jclasslib的官方github进行下载:
https://github.com/ingokegel/jclasslib/releases
安装后,lib目录下为所有必要的jar包,修改class时需要将该文件夹导入到工程中作为依赖。
打开:
File→Project Structure...
左侧选择Modules,右侧选择Dependencies标签,点最右侧**+**号,选择JARs or directories…,在弹出的窗口中选择jclasslib安装目录下的lib文件夹。
打开javassist的官方github进行下载:
http://www.javassist.org/
下载后是个zip压缩包,解压并复制到指定目录下。
打开:
File→Project Structure...
左侧选择Modules,右侧选择Dependencies标签,点最右侧 + 号,选择JARs or directories…,在弹出的窗口中选择javassist目录下的javassist.jar文件。
实现代码:
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("", "()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);
}
}
整体思路为:
ClassFile
,将class文件载入进来。MethodInfo
对象中。这里用到的参数为构造函数的Name和Descriptor,前面的分析已经得知这两个值。CodeAttribute
。ClassFile
被修改。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直接拖进来,替换。
这样修改就完成了。运行游戏看一下: