0x01 几种反反编译的方法

Java代码是可以反编译的,但是很多时候一些Java开发者并不想让其他人知道自己的代码是怎么写的,就会对Java代码进行加密或混淆等操作,一般来说有三个思路:

  1. 将class文件加密,这个是最安全的,但也费事儿,因为要重写classloader来解密class文件;
  2. 使用花指令,使得class文件不能反编译(利用反编译工具漏洞);安全性一般,还是有花指令破解器;
  3. 代码混淆,提高代码阅读成本;简单易操作,一般采用这种或者与其它方式结合;

当然,这几种方法都是可以被破解的,只是不同方法的破解成本不一样而已。

0x02 准备jar包

新建一个ClassEncode项目,再新建两个类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.mi1k7ea;

public class Test {
public static final String a = "This is Test Class ..";
public static void main(String[] args) {
System.out.println(a);
new Test2();
}
}



package com.mi1k7ea;

public class Test2 {
public Test2() {
System.out.println("This is Test2 Class ..");
}
}

接着将该项目打包成jar包。

IDEA打包jar

在项目上鼠标右键 –> Open Module Settings:

Artifacts –> + –> JAR –> From modules with dependencies:

接着是设置Create JAR from Modules:

Main Class是这个项目(脚本)的主方法,就是要运行的类,选Test类。

关于JAR files from libraries的两个选项:

  • 选中第一个的话,打完包后是一个Jar包;

  • 选中第二个的话,打完包后是一个Jar包,外带你项目所用的Jar包;

接下来是MF文件的存放目录。注意,不能使用默认目录,必须自定义目录,不然会运行错误,也不能使用src/main/resources这个目录。这里设置的是项目根目录下的src目录下来新建resource目录。

设置完之后,此时应该也必须有META-INF此文件,不然会运行错误。若JAR files from libraries选择src/main/resources目录,是没有META-INF文件的。Output directory是jar包的保存目录:

点击Build:

然后在out目录中看到生成了Jar包:

这时的运行jar包就能执行了:

ok,我们就拿这个包作为Demo进行反编译操作。

手动打包jar

当然,也可以手动打包jar文件。

目录结构如下:

  • com

    • mi1k7ea

      ​ Test.class

      ​ Test2.class

  • META-INF

    ​ MANIFEST.MF

其中MANIFEST.MF中的Main-Class要指定主要的执行类:

1
2
Manifest-Version: 1.0
Main-Class: com.mi1k7ea.Test

注意:最后必须要回车空行出来。

最后在当前目录运行一下命令打包成jar并执行即可:

1
2
jar cvfm test.jar META-INF\MANIFEST.MF .
java -jar test.jar

0x03 加密Jar包和class文件

利用JVMTI实现反反编译

JVMTI(JVM Tool Interface)是Java虚拟机所提供的native编程接口,可以探查JVM内部状态,并控制JVM应用程序的执行。可实现的功能包括但不限于:调试、监控、线程分析、覆盖率分析工具等。

JVMTI能够监听class加载事件,因此我们可以使用一套加密算法,对即将发布的Jar包进行字节码加密,然后在JVM加载这些类之前再解密。由于这部分代码最终会以动态库(.dll、.so文件)的形式发布出去,不容易被破解,因此对源代码可以达到较好的保护效果。

这里用到一个工具:https://github.com/AloneMonkey/JarEncrypt

解压之后得到如下的目录结构及文件:

  • JarEncrypt/encrypt:加密库
    • encrypt.cpp
    • Makefile
    • Encrypt.java(Java加密执行文件)
  • JarEncrypt/decrypt:解密库
    • decrypt.cpp
    • Makefile

打开Encrypt.java文件,修改需要进行加密的类为以”com.mi1k7ea”开头的包下的所有类:

然后打开decrypt子目录下的decrypt.cpp文件,修改需要进行解密的类为以”com.mi1k7ea”开头的包下的所有类:

接着,进入encrypt目录,执行make,编译生成libencrypt.so:

注意,在Linux可能会报找不到jni.h和jni_md.h文件的错误,这里需要通过locate jni.hlocate jni_md.h命令来找到这两个文件所在的路径,然后在Makefile中第一个INCLUDEDIR项中添加进去即可:

libdecrypt.so的编译同上:

接着,通过javac Encrypt.java命令将该java文件转换为class文件:

通过如下命令加密jar包:

1
java -Djava.library.path=. -cp . Encrypt -src ClassEncode.jar

此时用反编译工具是没办法成功反编译的:

此时运行肯定是会失败的:

使用刚刚的编译生成的解密库来执行就ok了:

1
2
export LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:/root/jvmti/decrypt
java -agentlib:decrypt -jar ClassEncode_encrypt.jar

至此,已经通过加密Jar包和class文件实现了反反编译。在发布时候,需要把ClassEncode_encrypt.jar和libdecrypt.so发布出去,执行时候引入libdecrypt即可。

通过Java-Agent绕过反反编译

这部分在JavaAgent中会具体说明。

MainAgent.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.dumpclass;

import java.lang.instrument.Instrumentation;

public class MainAgent {
public static void main(String[] args) {

}

public static void premain(String args, Instrumentation instrumentation) {
instrumentation.addTransformer(new DumpClassTransformer());
}
}

DumpClassTransformer.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package com.dumpclass;

import java.io.File;
import java.io.FileOutputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class DumpClassTransformer implements ClassFileTransformer {
private static final String DUMP_PACKAGE = System.getProperty("dump_package");
private static final String OUT_FOLDER = System.getProperty("dump_out_folder");

@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if (className == null || className.isEmpty()) {
return classfileBuffer;
}

if (classfileBuffer == null) {
return classfileBuffer;
}

String tmpClassName = className.replace("/", ".");
if (tmpClassName.startsWith(DUMP_PACKAGE)) {
try {
writeClass(className, classfileBuffer);
} catch (Exception e) {
e.printStackTrace();
}
}

return classfileBuffer;
}

private boolean writeClass(String className, byte[] classfileBuffer) {
File file = null;
FileOutputStream fileOutputStream = null;

try {
String folder = OUT_FOLDER;
if (!folder.endsWith(File.separator)) {
folder = folder + File.separator;
}

String classPath = className.substring(0, className.lastIndexOf("/"));
className = className.substring(className.lastIndexOf("/") + 1, className.length());

String path = OUT_FOLDER + File.separator + classPath;
file = new File(path);
if (!file.exists()) {
file.mkdirs();
}
file = new File(path + File.separator + className + ".class");

fileOutputStream = new FileOutputStream(file);
fileOutputStream.write(classfileBuffer);

fileOutputStream.flush();
} catch (Exception e) {
e.printStackTrace();
return false;
} finally {
try {
if (fileOutputStream != null) {
fileOutputStream.close();
fileOutputStream = null;
}
} catch (Exception e) {
e.printStackTrace();
}
}

return true;
}
}

MANIFEST.MF:

1
2
3
4
Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: com.dumpclass.MainAgent

打包成ClassEncode_encrypt.jar。

通过以下命令,指定JavaAgent的jar包,然后在目标jar包主执行类方法执行之前先执行DumpClass.jar中的premain()方法,从而从内存将加密的目标jar类的字节码Dump下来:

1
java -Ddump_package=com.mi1k7ea -Ddump_out_folder=/tmp -agentlib:decrypt -javaagent:DumpClass.jar -jar ClassEncode_encrypt.jar

下载下来,此时就能从成功反编译

获取到加密class文件的内容了:

0x04 Java代码混淆

参考:https://www.cnblogs.com/nevermorewang/p/8041548.html

这种反反编译方法的没啥破解方法,就是代码比较难读而已,花点时间精力就可以搞定。

0x05 使用花指令

这部分还未研究,有待补充。。。

0x06 参考

jar包加密保护解决方案