因为字节码玩的炉火纯青,在工作休闲之余,破解了一大波 Java 系软件。最终的目标是无痛破解,这里的无痛,指的是不需要破坏原始 Jar 包或者 War 包,就可以达到破解目的
下面列举了一些折腾过的软件
- 分析 GC 日志的桌面端软件 censum
- 分析 GC 日志和线程的
gceasy
和fastthread
- Intellij 上 Mybatis 插件(低版本),高版本使用了代码混淆,导致阅读比较困难,没有去折腾
- ELK 铂金版
- 供应商的jar包对指定 Mac 地址授权,切换服务器或者切换到Docker环境以后,就没办法使用
工欲善其事必先利其器
下面是常用的一些工具
- 字节码反编译查看工具 jdgui,luyten
- 字节码浏览工具 jclasslib
- asm(后面会专门介绍)
- vim、hex editor
软件破解
学习了字节码的知识,可以开始做一些有意思的事情,比如软件破解。笔者成功破解过很多 Java 系商业软件,不过仅仅是供自己学习,没有用做商业用途。通过学习软件破解方案可以让我们更好的思考如何保护自己的软件产品。
Java 系软件破解有下面这几种常见的方式。
1、解包、直接修改 class 文件
适用于非常简单,改动一些常量就可以完成的情况。
2、解包、通过 asm 工具修改 class 文件
适用于逻辑较为复杂的情况。
3、通过 -javaagent
启动参数,动态修改(无痛破解)
前面两种都属于破坏了原始的 class 文件,不属于无痛破解,如果要破解的软件升级了,需要重新修改打包,非常麻烦。采用 javaagent 字节码改写的方式,只用在命令行启动参数里面加入一个参数即可。如果后续软件升级了,只要软件加密和证书校验部分的逻辑没有修改,都不用修改 agent 的代码就可以完成破解,使用起来更加方便。
接下来讲解的待破解的软件是我自己用 swing 写的一个小 demo,这个 jar 包可以在 github 网站(github.com/arthur-zhan… 下载。
使用 java -jar 运行这个待破解的 crack-demo.jar 文件,会弹出证书已过期的提示框,如下图 x-x 所示。
接下来介绍两种不同的破解方式。
破解方式一:直接修改 class 文件
使用 JD-GUI 打开这个 jar 包,查看反编译的代码,核心的校验逻辑在 StartupChecks 类中,如下所示。
public class StartupChecks {
private static int getDayOfMonth() {
return 7;
}
private static int getMonthOfYear() {
return 0;
}
private static int getYear() {
return 2019;
}
public static boolean canLoad() {
validateLicensing();
GregorianCalendar currentDate = new GregorianCalendar();
GregorianCalendar expiryDate = getExiryDate();
if (currentDate.after(expiryDate)) {
return false;
}
return true;
}
private static void validateLicensing() {}
public static GregorianCalendar getExiryDate() {
return new GregorianCalendar(getYear(), getMonthOfYear(), getDayOfMonth());
}
}
可以看到,这里判断 license 是否过期的方法比较简单,是拿当前时间与过期时间做对比,如果当前时间大于过期时间,就返回 license 已过期。破解这个软件最简单的思路是把过期的年份 2019
修改为 2029
或更久远的年份。
jar 包本质上就是一个 zip 压缩包,用 unzip 命令将 jar 包解压到一个临时文件夹 tmp 中,对应的目录结构如下所示。
.
├── META-INF
│ ├── MANIFEST.MF
│ └── maven
│ └── LicenseCheckSwing
│ └── LicenseCheckSwing
│ ├── pom.properties
│ └── pom.xml
└── me
└── ya
└── swing
├── AppMain.class
└── StartupChecks.class
使用 16 进制文件编辑器打开 StartupChecks.class 文件,搜索 2019
对应的十六进制表示(07E3),如下图 x-x 所示。
将 07E3 修改为 2029(07ED),然后保存文件。接下来使用 zip
命令将文件重新打包,如下所示。
zip -r ./crack-demo-v1.jar . *
使用 java 命令重新执行这个 jar 包,如下所示。
java -jar crack-demo-v1.jar
这时出现了 license 合法的提示框,如下图 x-x 所示。
这种手动修改 class 文件的方式比较麻烦,也只适用于比较简单的场景。接下来会介绍如何无痛破解,不需要手动修改 class 文件重新打包。
破解方式二:javaagent 无痛破解
有了 javaagent、ASM 这些储备知识,理解无痛破解这个主题就比较容易了。核心思路是通过 javaagent 和 ASM 来进行字节码改写,在类加载之前修改字节码逻辑。这个小节继续以 crack-demo.jar 文件为例进行讲解。
根据前面反编译的结果,license 是否合法取决于 canLoad 方法的返回值,返回 true 表示 license 合法,返回 false 表示 license 非法。那么只要在 canLoad 方法开始处插入 "return true;"
语句,让 canLoad 返回 true 即可,注入后的代码如下所示。
public static boolean canLoad() {
// 在这里强行插入 return true;
return true;
// 下面的语句不会执行到
validateLicensing();
GregorianCalendar currentDate = new GregorianCalendar();
GregorianCalendar expiryDate = getExiryDate();
if (currentDate.after(expiryDate)) {
return false;
}
return true;
}
"return true;"
语句对应的字节码语句如下所示。
ICONST_1
IRETURN
下面来看具体的代码,首先实现一个自定义的 MethodVisitor,在方法开始处插入 “return true;” 逻辑,代码如下所示。
public static class MyMethodVisitor extends AdviceAdapter {
@Override
protected void onMethodEnter() {
// 强行插入 return true;
mv.visitInsn(ICONST_1);
mv.visitInsn(IRETURN);
}
}
接下来实现一个自定义的 ClassVisitor,只处理 canLoad 方法,代码如下所示。
public static class MyClassVisitor extends ClassVisitor {
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
// 只注入 canLoad 方法
if (name.equals("canLoad")) {
return new MyMethodVisitor(mv, access, name, desc);
}
return mv;
}
}
随后实现一个自定义的 ClassFileTransformer,在 transform 方法中进行字节码改写,代码如下所示。
public static class MyClassFileTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classBytes) throws IllegalClassFormatException {
// 只注入 StartupChecks 类
if (className.equals("me/ya/swing/StartupChecks")) {
ClassReader cr = new ClassReader(classBytes);
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);
ClassVisitor cv = new MyClassVisitor(cw);
cr.accept(cv, ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG);
return cw.toByteArray();
}
return classBytes;
}
}
执行 mvn clean package
编译生成 my-crack-agent.jar
,执行 java -javaagent:/path/to/my-crack-agent.jar -jar crack-demo.jar
,发现已经成功地绕过了过期检查,弹出了 license 合法的提示框。
接下来我们来对比一下 canLoad
方法 ASM 改写前后的字节码,改写前的字节码如下所示。
public static boolean canLoad();
Code:
stack=2, locals=2, args_size=0
0: invokestatic #2 // Method validateLicensing:()V
3: new #3 // class java/util/GregorianCalendar
6: dup
7: invokespecial #4 // Method java/util/GregorianCalendar."<init>":()V
...
改写后的字节码如下所示。
public static boolean canLoad();
Code:
stack=1, locals=2, args_size=0
0: iconst_1
1: ireturn
2: nop
3: nop
4: nop
...
可以看到经过改写以后 canLoad
在字节码开始处插入了 "return true;"
,旧指令被替换为了无用的 nop 指令。