由于 Java 属于解释型语言,在 class 文件被 JVM 加载之前,可以很容易的将其反编译,得到源码。对比网上提供的很多方法,比如使用混淆器或是自定义类加载器,都是基于Java层面的,一样可以被反编译。最后,终于找到一种更有效的解决方案:使用 JVMTI 实现 jar 包字节码加密。
JVMTI 简介
JVMTI(JVM Tool Interface)是 Java 虚拟机所提供的 native 编程接口,可以探查JVM内部状态,并控制JVM应用程序的执行。可实现的功能包括但不限于:调试、监控、线程分析、覆盖率分析工具等。
实现思路
JVMTI 能够监听class加载事件,因此我们可以使用一套加密算法,对即将发布的 jar 包进行字节码加密,然后在 JVM 加载这些类之前再解密。由于这部分代码最终会以动态库(.dll, .so 文件)的形式发布出去,不容易被破解,因此对源代码可以达到较好的保护效果。
实现步骤
打开 com_seaboat_bytecode_ByteCodeEncryptor.cpp ,编写具体的加解密算法,并设指定哪些类需要解密
#include <iostream>
#include "com_seaboat_bytecode_ByteCodeEncryptor.h"
#include "jni.h"
#include <jvmti.h>
#include <jni_md.h>
void encode(char *str)
{
unsigned int m = strlen(str);
for (int i = 0; i < m; i++)
{
//str[i] = ((str[i] - 97)*k) - ((str[i] - 97)*k) / q*q + 97;
str[i] = str[i] + 1;
}
}
void decode(char *str)
{
unsigned int m = strlen(str);
//int k2 = (q + 1) / k;
for (int i = 0; i < m; i++)
{
//str[i] = ((str[i] - 97)*k2) - ((str[i] - 97)*k2) / q*q + 97;
str[i] = str[i] - 1;
}
}
extern"C" JNIEXPORT jbyteArray JNICALL
Java_com_seaboat_bytecode_ByteCodeEncryptor_encrypt(JNIEnv * env, jclass cla, jbyteArray text)
{
char* dst = (char*)env->GetByteArrayElements(text, 0);
encode(dst);
env->SetByteArrayRegion(text, 0, strlen(dst), (jbyte *)dst);
return text;
}
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(class_data_len, new_class_data);
unsigned char* _data = *new_class_data;
//指定要解密的类,此处将会对 cn.zzp 包下面所有的类进行解密
if (name&&strncmp(name, "cn/zzp/", 6) == 0) {
for (int i = 0; i < class_data_len; i++)
{
_data[i] = class_data[i];
}
printf("%s\n","INFO: decode class... \n");
decode((char*)_data);
}
else {
for (int i = 0; i < class_data_len; i++)
{
_data[i] = class_data[i];
}
}
}
JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved)
{
jvmtiEnv *jvmti;
//Create the JVM TI environment(jvmti)
jint ret = vm->GetEnv((void **)&jvmti, JVMTI_VERSION);
if (JNI_OK != ret)
{
printf("ERROR: Unable to access JVMTI!\n");
return ret;
}
jvmtiCapabilities capabilities;
(void)memset(&capabilities, 0, sizeof(capabilities));
capabilities.can_generate_all_class_hook_events = 1;
capabilities.can_tag_objects = 1;
capabilities.can_generate_object_free_events = 1;
capabilities.can_get_source_file_name = 1;
capabilities.can_get_line_numbers = 1;
capabilities.can_generate_vm_object_alloc_events = 1;
jvmtiError error = jvmti->AddCapabilities(&capabilities);
if (JVMTI_ERROR_NONE != error)
{
printf("ERROR: Unable to AddCapabilities JVMTI!\n");
return error;
}
jvmtiEventCallbacks callbacks;
(void)memset(&callbacks, 0, sizeof(callbacks));
callbacks.ClassFileLoadHook = &ClassDecryptHook;
error = jvmti->SetEventCallbacks(&callbacks, sizeof(callbacks));
if (JVMTI_ERROR_NONE != error) {
printf("ERROR: Unable to SetEventCallbacks JVMTI!\n");
return error;
}
error = jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_CLASS_FILE_LOAD_HOOK, NULL);
if (JVMTI_ERROR_NONE != error) {
printf("ERROR: Unable to SetEventNotificationMode JVMTI!\n");
return error;
}
return JNI_OK;
}
编译生成加解密所需要的动态库
cl /EHsc -LD com_seaboat_bytecode_ByteCodeEncryptor.cpp -FeByteCodeEncryptor.dll
注:在这里我使用 Visual Studio 完成编译,期间报错:找不到 jvmti.h,进入 jdk 所在目录,将 bin/ 以及 bin/win32/ 目录下对应的文件放入 Visual Studio 安装目录中的 bin/include/ 下即可解决。
将生成的动态库文件 FeByteCodeEncryptor.dll 加入系统环境变量中,有时需要重启系统才能生效。
用 Java 对即将发布的 jar 包加密,得到加密之后的jar包 helloworld_encrypted.jar
package com.seaboat.bytecode;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
public class ByteCodeEncryptor {
// 加载库(com_seaboat_bytecode_ByteCodeEncryptor.cpp),向 JVM 注册本地方法
static{
System.loadLibrary("ByteCodeEncryptor");
}
public native static byte[] encrypt(byte[] text); //表示这个方法的具体实现在本地方法中
public static void main(String[] args){
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buf = new byte[1024];
String fileName = "D:\\jarpath\\helloworld.jar";
File srcFile = new File(fileName);
File dstFile = new File(fileName.substring(0, fileName.indexOf("."))+"_encrypted.jar");
FileOutputStream dstFos = new FileOutputStream(dstFile);
JarOutputStream dstJar = new JarOutputStream(dstFos);
JarFile srcJar = new JarFile(srcFile);
for (Enumeration<JarEntry> enumeration = srcJar.entries(); enumeration.hasMoreElements();) {
JarEntry entry = enumeration.nextElement();
InputStream is = srcJar.getInputStream(entry);
int len;
while ((len = is.read(buf, 0, buf.length)) != -1) {
baos.write(buf, 0, len);
}
byte[] bytes = baos.toByteArray();
String name = entry.getName();
if(name.startsWith("cn/zzp/")){ //对 cn.zzp 包下面的 所有 class 文件加密
try {
bytes = ByteCodeEncryptor.encrypt(bytes);
} catch (Exception e) {
e.printStackTrace();
}
}
JarEntry ne = new JarEntry(name);
dstJar.putNextEntry(ne);
dstJar.write(bytes);
baos.reset();
}
srcJar.close();
dstJar.close();
dstFos.close();
System.out.println("encrypt finished");
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行 jar 包,需要指定所依赖的动态库,以及 jar 包的入口(主方法所在的类)
java -agentlib:ByteCodeEncryptor -cp helloworld_encrypted.jar cn.zzp.HelloWorld
最终效果
加密前:
加密后:
另外,加密所需要的文件可以从这里获得
------------------------------------------------------------------------------------------------------------------------