jar-analyzer / jar-obfuscator

Jar Obfuscator - 一个 JAR/CLASS 字节码混淆工具,支持包名/类名/方法名/字段名/参数名引用分析和重命名混淆方式,支持字符串加密/整型异或混淆/垃圾代码花指令混淆/等方式,支持方法和字段的隐藏,支持 NATIVE 层的 JVMTI 代码加密,配置简单,文档教程齐全,容易上手
MIT License
313 stars 29 forks source link

[BUG] 高版本JDK JNI 加载遇到报错 sys_paths #11

Closed pcdlrzxx closed 4 months ago

pcdlrzxx commented 4 months ago

在开启JVMTI加密字节码功能后,执行过程中出现了报错,有2种情况:

1、当enableClassNameenablePackageName任意一个关闭或都关闭时,会出现以下报错:

[13:41:42] [INFO] [PatchCommand:execute:42] patch jar: MyObfuscationExample_obf.jar
[13:41:42] [INFO] [JNIUtil:extractDllSo:120] write file: C:\Users\mac\Desktop\test\jar-obfuscator-0.0.6-beta\code-encryptor-temp\libencryptor.dll
[13:41:42] [INFO] [PatchHelper:patchJar:36] start patch jar
[13:41:42] [ERROR] [JNIUtil:deleteUrls:35] delete classloader sys_paths error: java.lang.NoSuchFieldException: sys_paths
Exception in thread "main" java.lang.UnsatisfiedLinkError: 'byte[] me.n1ar4.jar.obfuscator.jvmti.CodeEncryptor.encrypt(byte[], int, byte[])'
        at me.n1ar4.jar.obfuscator.jvmti.CodeEncryptor.encrypt(Native Method)
        at me.n1ar4.jar.obfuscator.jvmti.PatchHelper.patchJar(PatchHelper.java:86)
        at me.n1ar4.jar.obfuscator.jvmti.PatchCommand.execute(PatchCommand.java:71)
        at me.n1ar4.jar.obfuscator.core.Runner.run(Runner.java:376)
        at me.n1ar4.jar.obfuscator.Main.main(Main.java:66)

. 2、当enableClassNameenablePackageName都打开时,会出现以下报错:

[13:42:24] [INFO] [PatchCommand:execute:42] patch jar: MyObfuscationExample_obf.jar
[13:42:24] [INFO] [JNIUtil:extractDllSo:120] write file: C:\Users\mac\Desktop\test\jar-obfuscator-0.0.6-beta\code-encryptor-temp\libencryptor.dll
[13:42:24] [INFO] [PatchHelper:patchJar:36] start patch jar
[13:42:24] [ERROR] [JNIUtil:deleteUrls:35] delete classloader sys_paths error: java.lang.NoSuchFieldException: sys_paths
[13:42:24] [WARN] [PatchHelper:patchJar:124] put entry: java.lang.reflect.InaccessibleObjectException: Unable to make field private java.util.HashSet java.util.zip.ZipOutputStream.names accessible: module java.base does not "opens java.util.zip" to unnamed module @78fa769e
[13:42:24] [WARN] [PatchHelper:patchJar:124] put entry: java.lang.reflect.InaccessibleObjectException: Unable to make field private java.util.HashSet java.util.zip.ZipOutputStream.names accessible: module java.base does not "opens java.util.zip" to unnamed module @78fa769e
[13:42:24] [WARN] [PatchHelper:patchJar:124] put entry: java.lang.reflect.InaccessibleObjectException: Unable to make field private java.util.HashSet java.util.zip.ZipOutputStream.names accessible: module java.base does not "opens java.util.zip" to unnamed module @78fa769e
[13:42:24] [WARN] [PatchHelper:patchJar:124] put entry: java.lang.reflect.InaccessibleObjectException: Unable to make field private java.util.HashSet java.util.zip.ZipOutputStream.names accessible: module java.base does not "opens java.util.zip" to unnamed module @78fa769e
[13:42:24] [WARN] [PatchHelper:patchJar:124] put entry: java.lang.reflect.InaccessibleObjectException: Unable to make field private java.util.HashSet java.util.zip.ZipOutputStream.names accessible: module java.base does not "opens java.util.zip" to unnamed module @78fa769e
[13:42:24] [INFO] [PatchHelper:patchJar:133] encrypt finished
[13:42:24] [INFO] [PatchHelper:patchJar:134] output file: MyObfuscationExample_obf_encrypted.jar
[13:42:24] [INFO] [ExportCommand:execute:21] execute export command
[13:42:24] [INFO] [JNIUtil:extractDllSo:120] write file: C:\Users\mac\Desktop\test\jar-obfuscator-0.0.6-beta\code-encryptor-temp\libdecrypter.dll
[13:42:24] [INFO] [LoggingStream:println:20] ----------- ADD VM OPTIONS (WINDOWS) -----------
[13:42:24] [INFO] [LoggingStream:println:20] java -XX:+DisableAttachMechanism -agentpath:/path/to/libdecrypter.dll=PACKAGE_NAME=xxx,KEY=YOUR-KEY [other-params]

此时如果再去执行生成的xxx_obf_encrypted.jar,则会产生下面的信息:

java -agentpath:./code-encryptor-temp/libdecrypter.dll=PACKAGE_NAME=com.example,KEY=4ra1n4ra1n4ra1n1 -jar MyObfuscationExample_obf_encrypted.jar
[JVMTI-LOG] INIT JVMTI ENVIRONMENT
[JVMTI-LOG] INIT JVMTI CAPABILITIES
[JVMTI-LOG] ADD JVMTI CAPABILITIES
[JVMTI-LOG] INIT JVMTI CALLBACKS
[JVMTI-LOG] SET JVMTI CLASS FILE LOAD HOOK
[JVMTI-LOG] SET EVENT NOTIFICATION MODE
[JVMTI-LOG] INIT JVMTI SUCCESS
gHotSpotVMStructs RVA: 0x0000000000b2ff80
Function Addr: 0x000007feebd6ff80
[JVMTI-LOG] HACK JVM FINISH
MyObfuscationExample_obf_encrypted.jar中没有主清单属性                                                                                                                                   
[JVMTI-LOG] UNLOAD AGENT

. 我这边的环境是win7_x64,jdk_17.0.9

4ra1n commented 4 months ago

OK 高版本 JDK 的 BUG 我回去测试下

尝试用 JAVA8 执行下看看,是否有问题,我主要测试 JAVA 8

4ra1n commented 4 months ago

排查了下,报错原因和打开关闭配置无关,是 DLL 导出的问题,第一次嵌入的 DLL 好像没成功到处,然后 JNI 加载失败,UnsatisfiedLinkError,第二次跑已经导出了,可以直接加载

第二个报错,可以尝试用 JDK8 解决

4ra1n commented 4 months ago

因为 字节码加密 功能是基于 JNI 实现,无法保证通用性,我是基于 JDK 8 编译测试的

所以目前我修改了 字节码加密 功能必须 Java 8 才能用

https://github.com/jar-analyzer/jar-obfuscator/commit/90dd2afc3d7d54be5074d206b5070bde82d48b34

pcdlrzxx commented 4 months ago

测试完了,来反馈一下:

1、关于高版本JDK,我在这里找到了类似的问题,大概意思就是JDK 12以后,禁止了以反射方式访问java.lang.ClassLoader,不知道是不是这个原因,话说大佬后续会支持高版本JDK吗?

2、JDK 8下测试的结果是jar包都能正常运行,但是部分情况下混淆结果貌似不太对,3种情况:

a、当enableClassName: true & enablePackageName: false或者enableClassName: false & enablePackageName: false时,混淆结果正常,用JD-GUI打开看class文件都是// INTERNAL ERROR //

b、当enableClassName: false & enablePackageName: true时,用JD-GUI打开看class文件虽然是// INTERNAL ERROR //,但包名却没有正确的混淆,还是显示的原包名

c、当enableClassName: true & enablePackageName: true时,用JD-GUI打开jar包虽然包名和类名正确混淆,但class文件却不是// INTERNAL ERROR //,而是混淆加密后的代码形式

目前就发现这些,还有一个小问题就是,用了JNI本地方式加密后,执行jar包时会额外输出一些其他的日志信息,这个倒是影响不大

pcdlrzxx commented 4 months ago

对了,补充问一下,用了JNI的方式加密后,是不是其他的类似enableClassName、enablePackageName、enableEncryptString等都可以关闭掉(毕竟代码都被保护到本地了),还是说即使是JNI方式,也是有办法反编译出源代码呢?

4ra1n commented 4 months ago

(1) 高版本的反射网上有办法可以绕过,之后我有计划看一下.

(2) a 混淆结果正常,但是反编译 // INTERNAL ERROR // 我觉得反而说明混淆的好,无法反编译哈哈 (2) b 这个情况可以处理下,但我觉得意义不大,因为很少有需求是 只混淆包名 不混淆类名 这样 (2) c 这个 INTERNAL ERROR 我不太明白为啥,可能是 JD-GUI 内部问题,我这没啥可处理的感觉

(3) JNI 输出信息 这个我是为了调试测试 可以去掉

(4) JNI 的加密,只要不泄露解密加密 dll 库,足够防止 99% 的人 你可以把 dll 当成钥匙,JAR 是宝箱,只有宝箱打不开,必须有钥匙;但是你的钥匙丢失了,别人拿到你的 dll 钥匙后,就会比较容易打开。由于我还设置了密码,所以会更难打开。不过如果别人有访问你系统的权限,可以拿到本地命令行启动的字符串,获得里面的 KEY 信息。

总之 JNI 的方式,尽了最大可能保护你的代码,如果愿意结合其他的混淆方式,效果是更好的

pcdlrzxx commented 4 months ago

c、当enableClassName: true & enablePackageName: true时,用JD-GUI打开jar包虽然包名和类名正确混淆,但class文件却不是// INTERNAL ERROR //,而是混淆加密后的代码形式

(2) c 这个 INTERNAL ERROR 我不太明白为啥,可能是 JD-GUI 内部问题,我这没啥可处理的感觉

关于这一点,貌似不是JD-GUI的问题,我今天又用其他的反编译工具测试了下,也是同样的结果。感觉像是在这种情况下,JNI 加密方式没有起作用,代码并没有被保护到本地,大佬有时间可以再测试一下 .

(4) JNI 的加密,只要不泄露解密加密 dll 库,足够防止 99% 的人 你可以把 dll 当成钥匙,JAR 是宝箱,只有宝箱打不开,必须有钥匙;但是你的钥匙丢失了,别人拿到你的 dll 钥匙后,就会比较容易打开。由于我还设置了密码,所以会更难打开。不过如果别人有访问你系统的权限,可以拿到本地命令行启动的字符串,获得里面的 KEY 信息。

如果需要把应用部署到客户的电脑上,让他们自己管理,这种情况下就得把dll也给到他们,这样是不是就没法很好的保护代码了,像这种情况,有没有可能再对这个dll进行保护啥的(虽然我觉得也没多大意义。。) 其实我很好奇,C/C++写的的程序,大多数也是调用dll,为什么他们的就很难反编译呢?

4ra1n commented 4 months ago
  1. 我可能没有理解,你的意思是 enableClassName: true & enablePackageName: trueenableSuperObfuscate: true 生成的 _encrypted.jar 文件其实是没有加密,可以被反编译观察到的吗

  2. DLL 的混淆有更成熟稳定强大的技术,比如 OLLVM 和 VMP 等

pcdlrzxx commented 4 months ago
  1. 我可能没有理解,你的意思是 enableClassName: true & enablePackageName: trueenableSuperObfuscate: true 生成的 _encrypted.jar 文件其实是没有加密,可以被反编译观察到的吗

是的,可以被反编译看到代码

2. DLL 的混淆有更成熟稳定强大的技术,比如 OLLVM 和 VMP 等

好吧,看来JAVA在这块还是任重而道远啊

4ra1n commented 4 months ago

是否没有正确配置

superObfuscatePackage: me.n1ar4

字节码加密功能需要重新配置包名,而不是选择 obfuscatePackage

pcdlrzxx commented 4 months ago

这个superObfuscatePackage配置项有什么规则吗,我现在是将这项与obfuscatePackage配置成了一样的(比如都配置成了 com.example)。而且我测试的结果是,只有enableClassName: true & enablePackageName: true这一种情况下,JNI 加密会失效,只要这两项不同时为 true,其他的所有配置项都一样的情况下,JNI 加密都是有效的

4ra1n commented 4 months ago

superObfuscatePackage 是一个单独的配置,仅在 enableSuperObfuscate true 时生效,与其他配置无关

所以我不太清除原因,你可以在此测试看看

pcdlrzxx commented 4 months ago

@4ra1n 大佬,我找到 JNI 字节码加密有时失效的原因了。这两天看了一下你的代码,发现在字节码加密前,会拿_obf.jar里面的包名跟配置文件中的superObfuscatePackage值进行比较,只有相同时才会执行字节码加密。但是如果开启了enablePackageName,那_obf.jar里的包名就已经是混淆后的,所以就无法匹配,导致没有执行加密。具体代码是PatchHelper类的这一段:

                if (name.toLowerCase().endsWith(ClassFile)) {
                    if (name.startsWith("BOOT-INF/classes/")) {
                        tempClassName = name.split("BOOT-INF/classes/")[1];
                        if (tempClassName.startsWith(packageName)) {
                            try {
                                bytes = CodeEncryptor.encrypt(bytes, bytes.length, key);
                            } catch (Exception e) {
                                logger.error("encrypt error: {}", e.toString());
                                return;
                            }
                        }
                    } else {
                        // encrypt target class
                        if (name.startsWith(packageName)) {
                            try {
                                bytes = CodeEncryptor.encrypt(bytes, bytes.length, key);
                            } catch (Exception e) {
                                logger.error("encrypt error: {}", e.toString());
                                return;
                            }
                        }
                    }
                }

话说,这里需要比较这两个值吗,我感觉其实比较配置文件中的superObfuscatePackageobfuscatePackage这两个值就行了,这样也不存在已经混淆了的情况

pcdlrzxx commented 4 months ago

b、当enableClassName: false & enablePackageName: true时,用JD-GUI打开看class文件虽然是// INTERNAL ERROR //,但包名却没有正确的混淆,还是显示的原包名

(2) b 这个情况可以处理下,但我觉得意义不大,因为很少有需求是 只混淆包名 不混淆类名 这样

然后关于这点,我顺便改了下代码,自测也通过了,如果需要的话,我可以拉一个PR合并一下

4ra1n commented 4 months ago

好的,欢迎,我看下问题的原因(另外我测试是我的 JNI Util 有问题,简单改一下是支持高版本JDK的)

pcdlrzxx commented 4 months ago

哦,那0.0.7已经支持高版本JDK了吗,还是说要在之后的版本呢

pcdlrzxx commented 4 months ago

@4ra1n 大佬,我找到 JNI 字节码加密有时失效的原因了。这两天看了一下你的代码,发现在字节码加密前,会拿_obf.jar里面的包名跟配置文件中的superObfuscatePackage值进行比较,只有相同时才会执行字节码加密。但是如果开启了enablePackageName,那_obf.jar里的包名就已经是混淆后的,所以就无法匹配,导致没有执行加密。具体代码是PatchHelper类的这一段:

                if (name.toLowerCase().endsWith(ClassFile)) {
                    if (name.startsWith("BOOT-INF/classes/")) {
                        tempClassName = name.split("BOOT-INF/classes/")[1];
                        if (tempClassName.startsWith(packageName)) {
                            try {
                                bytes = CodeEncryptor.encrypt(bytes, bytes.length, key);
                            } catch (Exception e) {
                                logger.error("encrypt error: {}", e.toString());
                                return;
                            }
                        }
                    } else {
                        // encrypt target class
                        if (name.startsWith(packageName)) {
                            try {
                                bytes = CodeEncryptor.encrypt(bytes, bytes.length, key);
                            } catch (Exception e) {
                                logger.error("encrypt error: {}", e.toString());
                                return;
                            }
                        }
                    }
                }

话说,这里需要比较这两个值吗,我感觉其实比较配置文件中的superObfuscatePackageobfuscatePackage这两个值就行了,这样也不存在已经混淆了的情况

我想了下,上面画线的说法不对,这样会将所有class文件都进行加密。emm,这个问题还是交给大佬你来处理吧,哈哈

4ra1n commented 4 months ago

OKOK 原来如此 我大概知道原因了

4ra1n commented 4 months ago

我解决了高版本不可用的问题,测试通过的,但还是建议使用 JAVA 8 来做字节码加密

对于这个问题,我决定如果开启包名混淆将不允许字节码加密

https://github.com/jar-analyzer/jar-obfuscator/commit/3ef2d0d8a02ba6b9b043aa2842955d83b874a96f

因为:每一个子包名都变成随机的了

例如 com.a.b 和 com.a.b.c 会变成两个不同的包,无法再通过一个参数 PACKAGE 指定 native 层的参数

完全加密感觉不可取,某些类如果被加密会出问题

这个问题是高版本 JNI 报错的问题,现在已经解决,我关闭了,有其他问题可以再提 issue

pcdlrzxx commented 4 months ago

OK,辛苦大佬了,等新版本出来我再测试下高版本 JDK 下的加密功能。

我刚拉了个PR,就是简单处理下enableClassName: false & enablePackageName: true时的混淆问题