Y4Sec Team 2023-09-09 15:27 浙江
详细讲解如何使用JNI加密字节码,通过JVMTI解密字节码以保护代码
这不是新思路,但网上的文章不够深入和详细,因此有了这篇文章
上周偶然看到一篇文章
https://juejin.cn/post/6844903487784894477
以及 Github 仓库代码
https://github.com/sea-boat/ByteCodeEncrypt
感觉是一个很有趣的项目,对于 Jar 包以及 Class 的保护通常是使用 ProGuard 等工具,对 Class 文件本身的混淆。而该文章提到了一种更巧妙的办法,总体来说是以下的思路:
用 C 编写加密算法,调用 JNI 加密指定类名的 Class 文件并保存
启动 JVM 时利用 JVMTI 在加载 Class 文件时解密
参考原作者文章:利用JDK中JVM的某些类似钩子机制和事件监听机制,监听加载 Class 事件,使用本地方式完成 Class 的解密。C/C++ 被编译后想要反编译就很麻烦了,另外还能加壳
可以看到加密后的 Class 文件不是合法字节码文件(开头魔数做了特殊处理,让这个文件看起来是 Class 文件)
原文章和原项目有一些小问题:
原文章固定了包名,用户想加密自己的包名需要重新编译 DLL
原文章加密和解密 DLL 是同一个,这样只用 JNI 调用下加密即可解决
原文章的代码仅是 Demo 级别,无法直接上手测试和使用
原文章没有加入具体的加密算法,仅是简单的运算,需要加强
原文章的代码存在一些 BUG 和优化空间
补充:原文章没有提到这种加密如何绕过
这个思路很有意思,于是我打算深入研究下,在原作者文章基础上做一些详细的补充,并且写一些代码,尝试做一个可以直接使用的工具
0x01 JVMTI
这里我们先看一下 JVMTI 的功能,学新技术最好的办法是看官方文档
https://docs.oracle.com/javase/8/docs/platform/jvmti/jvmti.html
官方文档较长,参考原作者的介绍:
JVMTI即JVM Tool Interface,提供了本地编程接口,主要是提供了调试和分析等接口。JVMTI非常强大,通过它能做很多事,比如可以监听某事件、线程分析等等。一般使用Agent方式来使用,就是通过-agentlib和-agentpath指定Agent的本地库,然后Java启动时就会加载该动态库。这个时刻可以看成是JVM启动的时刻,而并非是Java层程序启动时刻,所以此时不涉及与Java相关的类和对象什么的。
- 必须使用 allocate 函数为修改后的类文件数据缓冲区分配空间 如果修改类文件必须修改 new_class_data 指向新 buffer
capabilities.can_generate_all_class_hook_events = 1;0x02 JVMTI 代码
有了上一章的内容,现在我们编写一个 agentlib dll 库将会很简单
在代码仓库的 start.c 文件中,开头 50 行可能看起来复杂,其实功能很简单。拿到 options 数据,根据 = 号分割,替换包名中的 . 为 / 符号。不得不说,C 语言写这样简单的一个逻辑都得几十行,Go/Java 可能只用几行
第一步初始化 JVMTI
jint ret = (*vm)->GetEnv(vm, (void **) &jvmti, JVMTI_VERSION);LOG("INIT JVMTI CAPABILITIES");jvmtiCapabilities capabilities;(void) memset(&capabilities, 0, sizeof(capabilities));capabilities.can_generate_all_class_hook_events = 1;LOG("ADD JVMTI CAPABILITIES");jvmtiError error = (*jvmti)->AddCapabilities(jvmti, &capabilities);if (JVMTI_ERROR_NONE != error) {printf("ERROR: Unable to AddCapabilities JVMTI!\n");return error;}
LOG("INIT JVMTI CALLBACKS");jvmtiEventCallbacks callbacks;(void) memset(&callbacks, 0, sizeof(callbacks));LOG("SET JVMTI CLASS FILE LOAD HOOK");callbacks.ClassFileLoadHook = &ClassDecryptHook;error = (*jvmti)->SetEventCallbacks(jvmti, &callbacks, sizeof(callbacks));if (JVMTI_ERROR_NONE != error) {printf("ERROR: Unable to SetEventCallbacks JVMTI!\n");return error;}
error = (*jvmti)->SetEventNotificationMode(jvmti, JVMTI_ENABLE, JVMTI_EVENT_CLASS_FILE_LOAD_HOOK, NULL);if (JVMTI_ERROR_NONE != error) {printf("ERROR: Unable to SetEventNotificationMode JVMTI!\n");return error;}LOG("INIT JVMTI SUCCESS");
如果包名匹配到传入的参数,才会进行解密处理,否则正常执行
void JNICALL ClassDecryptHook(jvmtiEnv *jvmti_env,JNIEnv *jni_env,jclass class_being_redefined,jobject loader,const char *name,jobject protection_domain,jint class_data_len,const unsigned char *class_data,jint *new_class_data_len,unsigned char **new_class_data) {*new_class_data_len = class_data_len;(*jvmti_env)->Allocate(jvmti_env, class_data_len, new_class_data);unsigned char *_data = *new_class_data;if (name && strncmp(name, PACKAGE_NAME, strlen(PACKAGE_NAME)) == 0) {for (int i = 0; i < class_data_len; i++) {_data[i] = class_data[i];}// ...decrypt((unsigned char *) _data, class_data_len);} else {for (int i = 0; i < class_data_len; i++) {_data[i] = class_data[i];}}}
0x03 加密解密代码以上已经有了 JVMTI 解密部分的核心代码,还差具体的加密解密代码使用 DES/AES 是一种办法,但是使用 C 实现起来比较复杂,也可以考虑使用 OpenSSL 来做,笔者这里抛砖引玉,具体加密解密可以自行发挥
我选择的加密解密算法是:XXTEA 算法 结合 位运算加密选择 XXTEA 算法由于其 C 实现代码比较简单,且有一定的强度
void tea_encrypt(uint32_t *v, const uint32_t *k) {uint32_t v0 = v[0], v1 = v[1], sum = 0, i;uint32_t delta = 0x9e3779b9;uint32_t k0 = k[0], k1 = k[1], k2 = k[2], k3 = k[3];for (i = 0; i < 32; i++) {sum += delta;v0 += ((v1 << 4) + k0) ^ (v1 + sum) ^ ((v1 >> 5) + k1);v1 += ((v0 << 4) + k2) ^ (v0 + sum) ^ ((v0 >> 5) + k3);}v[0] = v0;v[1] = v1;}void tea_decrypt(uint32_t *v, const uint32_t *k) {uint32_t v0 = v[0], v1 = v[1], sum = 0xC6EF3720, i;uint32_t delta = 0x9e3779b9;uint32_t k0 = k[0], k1 = k[1], k2 = k[2], k3 = k[3];for (i = 0; i < 32; i++) {v1 -= ((v0 << 4) + k2) ^ (v0 + sum) ^ ((v0 >> 5) + k3);v0 -= ((v1 << 4) + k0) ^ (v1 + sum) ^ ((v1 >> 5) + k1);sum -= delta;}v[0] = v0;v[1] = v1;}
具体的加密还需要特殊处理:需要想办法把 char* 转为 int32 类型,加密完需要把 int32 类型转回 char* 再写入原 chars
具体的 convert 和 revert 函数参考代码仓库,主要是一些位运算这里暂固定密钥 Y4Sec-Team-4ra1n(这个可以由 options 参数传入)
void internal(unsigned char *chars, int start) {unsigned char first[4];for (int i = start; i < start + 4; i++) {first[i - start] = chars[i];}unsigned char second[4];for (int i = start + 4; i < start + 8; i++) {second[i - start - 4] = chars[i];}uint32_t v[2] = {convert(first), convert(second)};// key: Y4Sec-Team-4ra1n// 59345365 632D5465 616D2D34 7261316Euint32_t const k[4] = {(unsigned int) 0x65533459, (unsigned int) 0x65542d63,(unsigned int) 0X342d6d61, (unsigned int) 0x6e316172,};tea_encrypt(v, k);unsigned char first_arr[4];unsigned char second_arr[4];revert(v[0], first_arr);revert(v[1], second_arr);for (int i = start; i < start + 4; i++) {chars[i] = first_arr[i - start];}for (int i = start + 4; i < start + 8; i++) {chars[i] = second_arr[i - start - 4];}}
// ClassFile {// u4 magic; (ignore)// u2 minor_version; (ignore)// u2 major_version; (ignore)// u2 constant_pool_count; (ignore)// cp_info constant_pool[constant_pool_count-1];// ...// }
JNIEXPORT jbyteArray JNICALL Java_org_y4sec_encryptor_core_CodeEncryptor_encrypt(JNIEnv *env, jclass cls, jbyteArray text, jint length) {jbyte *data = (*env)->GetByteArrayElements(env, text, NULL);unsigned char *chars = (unsigned char *) malloc(length);memcpy(chars, data, length);// 1. asm encryptencrypt(chars, length);LOG("ASM ENCRYPT FINISH");// 2. tea encryptif (length < 34) {LOG("ERROR: BYTE CODE TOO SHORT");return text;}// {[10:14],[14:18]}internal(chars, 10);LOG("TEA ENCRYPT #1");// {[18:22],[22:26]}internal(chars, 18);LOG("TEA ENCRYPT #2");// {[26:30],[30:34]}internal(chars, 26);LOG("TEA ENCRYPT #3");(*env)->SetByteArrayRegion(env, text, 0, length, (jbyte *) chars);return text;}
// 1. asm encryptencrypt(chars, length);LOG("ASM ENCRYPT FINISH");
- 位运算主要包含多次抑或,加减,非操作
link_start:if rbx >= rcx goto endcmp rbx, rcxjge magical = str[rdi+rbx]mov al, byte ptr [rdi+rbx]al = al - 2sub al, 002hal = al ^ 11hxor al, 011hal = ~alnot alal = al + 1add al, 001hal = al ^ 22xor al, 022hstr[rdi+rbx] = almov byte ptr [rdi+rbx], alebx ++inc rbxloopjmp link_start
magic:magicmov al, 0CAhmov byte ptr [rdi+000h], almov al, 0FEhmov byte ptr [rdi+001h], almov al, 0BAhmov byte ptr [rdi+002h], almov al, 0BEhmov byte ptr [rdi+003h], al
; signaturemov rsi, rcxsub rsi, 001hmov al, byte ptr [rdi+rsi]mov ah, byte ptr [rdi+004h]mov byte ptr [rdi+004h], almov byte ptr [rdi+rsi], ah; resetxor ah, ahxor al, alxor rsi, rsi
0x04 工程化加密代码如上,解密代码只要你过来即可,可以参考代码仓库,这里不再提及了。接下来我们看 Java 层的代码,逻辑很简单,读取输入 Jar 包,其中匹配到我们期望 PACKAGE NAME 的类调用 JNI 加密方法进行加密,然后把结果写入新的 Jar 包即可
// ...while (enumeration.hasMoreElements()) {JarEntry entry = enumeration.nextElement();InputStream is = srcJar.getInputStream(entry);int len;while ((len = is.read(buf, 0, buf.length)) != -1) {bao.write(buf, 0, len);}byte[] bytes = bao.toByteArray();String name = entry.getName();if (name.startsWith(packageName)) {if (name.toLowerCase().endsWith(ClassFile)) {try {bytes = CodeEncryptor.encrypt(bytes, bytes.length);} catch (Exception e) {logger.error("encrypt error: {}", e.toString());return;}}}// ...}
java -jar code-encryptor-plus.jar patch --jar your-jar.jar --package com.your.pack导出解密 DLL 文件:(默认导出到code-encryptor-plus-temp目录)
java -jar code-encryptor-plus.jar export使用解密DLL启动Jar包:(使用-agentlib参数)
由于 agentlib 的特性,要求必须是绝对路径的 DLL 且结尾去掉 DLL 才可
java -agentlib:D:\abs-path\decrypter=PACKAGE_NAME=com.your.pack --jar your-jar.jar另外支持了简易的GUI版本,选择需要加密的Jar文件即可一键加密
简单实践一下,加密我自己写的 Fake MySQL Server
java -jar .\code-encryptor-plus-0.0.1-cli.jar patch --jar .\fake-mysql-gui-0.0.3.jar --package me.n1ar4如果直接使用 java -jar 启动加密后的 jar 包,报错
导出解密 dll 文件
java -jar .\code-encryptor-plus-0.0.1-cli.jar export使用 agentlib 加载解密 dll 文件启动 jar 包,成功启动
java -agentlib:C:\JavaCode\code-encryptor-plus\target\code-encryptor-plus-temp\decrypter=PACKAGE_NAME=me.n1ar4 -jar .\fake-mysql-gui-0.0.3_encrypted.jar
如何破解这种加密呢
对于位加密来说,通过 x64dbg 手动调试看汇编是一种办法
通过 IDA F5 能看到更友好的代码
位加密可以很简单的破解
而 XXTEA 加密会稍微复杂一些
可以动态的方式拿到密钥,结合上面的算法进行解密
逆向来做是走了弯路,另有路子解决这种办法
使用 sa-jdi.jar 的 HSDB 即可
java -cp "C:\Program Files\Java\jdk1.8.0_131\lib\sa-jdi.jar" sun.jvm.hotspot.HSDB使用以下方式即可拿到 Class
sa-jdi.jar 默认会把 class 保存到 C:\User 里
这种加密方式是比较有意思的,我在原作者的基础上,做了进一步的拓展,详细讲解了 JVMTI 部分的代码。其中加密算法部分也是抛砖引玉,使用经典的 XXTEA 算法和位加密。其中还有进一步拓展的地方:比如汇编加密解密算法部分加入花指令,让逆向人员头疼;比如 DLL 可以使用 OLLVM/VMP 混淆
完整的项目代码在https://github.com/Y4Sec-Team/code-encryptor-plus
