百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT知识 > 正文

Java代码保护方法之四:JVMTI实现Java源码保护

liuian 2025-03-04 13:07 37 浏览

大家好,我叫小丁,一名小小程序员。

今天继续介绍Java代码保护的第四种方案:JVMTI。

采用ClassFinal和自定义类加载器这两种策略来保护Java代码时,它们面临的一个共同的主要挑战在于:加解密算法及其相关代码的高度透明性。这种透明性使得攻击者能够通过反编译手段轻松获取这些至关重要的信息,进而对系统进行破解。

为了更有效地提升代码保护的安全性,我们可以考虑将加密部分的代码用C++来编写,并将其打包成DLL/SO文件。随后,Java程序通过本地方法调用(JNI)来访问这个DLL文件中的加解密功能。

由于逆向工程分析DLL/SO文件的难度远胜于分析class文件,这种方法能够显著增强代码的保护力度,使得攻击者难以窥探到加解密算法的核心细节。



然而,这一方案亦非无懈可击。

尽管加解密算法被巧妙地封装在难以反编译的动态链接库(DLL/SO)中,使得其内部逻辑得以隐藏,但由于本地接口直接暴露,攻击者仍有可能通过伪装手段调用此接口执行加解密操作。

换言之,攻击者无需深入了解加解密的复杂算法,仅需掌握如何正确调用该接口即可。这种特性为我们提供了一个结合ClassFinal进行保护的契机。

具体而言,ClassFinal能够绑定本地机器码,在解密过程中,我们增加一项校验逻辑——验证当前机器码是否与预设值一致,且这一校验逻辑同样嵌入在本地代码中。

如此一来,若要成功破解代码,攻击者要么需要深入破解DLL的内部逻辑并重写解密流程,要么就得在被绑定的特定机器上进行破解尝试。这无疑大幅提升了破解的难度和成本。或许,上述描述听起来颇为抽象,令人一时难以全然理解。

为了更加清晰明了地阐述这一方案,接下来,我将逐步引导大家将其付诸实践。

1、生成动态共享库文件

动态共享库

windows下是dll

linux/mac是so

我用的是mac系统。

提前准备

jdk1.8

gcc/g++(C/C++)

gcc安装

C/C++一般安装gcc/g++作为编译工具。

对于windows,可通过安装MinGW-w64实现。

下载链接:
https://github.com/niXman/mingw-builds-binaries/releases

建议选文件:
x86_64-14.2.0-release-win32-seh-ucrt-rt_v12-rev0.7z

然后解压,将bin目录加到环境变量中

对于Linux/MacOS,大概率系统已自带。

安装之后,通过命令 gcc -v 检查是否能查出gcc版本即可。

定义本地接口:(JarEncrypt.java)

package com.dxc.project1;


public class JarEncrypt {
static {
System.loadLibrary("jarencrypt");
}


//加密
public native static byte[] encrypt(byte[] source);

//解密
public native static byte[] decrypt(byte[] source);
}

在项目src/main/java目录中执行

javah -jni com.dxc.project1.JarEncrypt

生成c++头文件(
com_dxc_project1_JarEncrypt.h):

/* DO NOT EDIT THIS FILE - it is machine generated */
#include "jni.h"
/* Header for class com_dxc_project1_JarEncrypt */

#ifndef _Included_com_dxc_project1_JarEncrypt
#define _Included_com_dxc_project1_JarEncrypt
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_dxc_project1_JarEncrypt
* Method: encrypt
* Signature: ([B)[B
*/
JNIEXPORT jbyteArray JNICALL Java_com_dxc_project1_JarEncrypt_encrypt
(JNIEnv *, jclass, jbyteArray);

/*
* Class: com_dxc_project1_JarEncrypt
* Method: decrypt
* Signature: ([B)[B
*/
JNIEXPORT jbyteArray JNICALL Java_com_dxc_project1_JarEncrypt_decrypt
(JNIEnv *, jclass, jbyteArray);

#ifdef __cplusplus
}
#endif
#endif

写c++的加解密代码JarEncrypt.cpp

#include
#include
#include "jvmti.h"
#include "jni.h"
#include "jni_md.h"
#include "
com_dxc_project1_JarEncrypt.h"


/*
* Class:
com_dxc_project1_JarEncrypt

* Method: encrypt
* Signature: ([B)[B
*/
JNIEXPORT jbyteArray JNICALL
Java_com_dxc_project1_JarEncrypt_encrypt

(JNIEnv * _env, jclass _class, jbyteArray _buf){
jsize len =_env->GetArrayLength(_buf);
unsigned char* dst = (unsigned char*)_env->GetByteArrayElements(_buf, 0);

//先用异或做简单加密,可以替换
for (int i = 0; i < len; ++i)
{
dst[i] = dst[i] ^ 0x10;
}

_env->SetByteArrayRegion(_buf, 0, len, (jbyte *)dst);
return _buf;
}

/*
* Class:
com_dxc_project1_JarEncrypt

* Method: decrypt
* Signature: ([B)[B
*/
JNIEXPORT jbyteArray JNICALL
Java_com_dxc_project1_JarEncrypt_decrypt

(JNIEnv * _env, jclass _class, jbyteArray _buf){
jsize len =_env->GetArrayLength(_buf);

unsigned char* dst = (unsigned char*)_env->GetByteArrayElements(_buf, 0);

//先用异或做简单解密,可以替换

for (int i = 0; i < len; ++i)
{
dst[i] = dst[i] ^ 0x10;
}

_env->SetByteArrayRegion(_buf, 0, len, (jbyte *)dst);
return _buf;
}


将"jvmti.h" "jni.h" "jni_md.h" "
com_dxc_project1_JarEncrypt.h" 拷贝到cpp和头文件一个目录下,执行:

g++ -shared -fPIC JarEncrypt.cpp -I${JAVA_HOME}/include -I${JAVA_HOME}/include/darwin -o jarencrypt.so

生成dll文件 jarencrypt.so



2、怎么解密

可以先参考这篇文章初步了解下Java Agent相关知识

https://blog.csdn.net/hzzdecsdn/article/details/138623098

通过so中的Agent_OnLoad函数,在加载jar包时绑定钩子,当每个类加载时,会调用钩子函数,钩子函数的逻辑是:判断字节码是否是正常的class文件,不是则调用解密函数;是的话不做任何操作。

这样就通过c++提供的钩子函数解决了class文件的解密。如果不是springboot项目,上面所说的生成加密和解密这两个接口其实都不需要。

加密你可以写个maven插件,在打包后通过插件遍历jar内的class文件进行加密;解密是通过钩子函数,可以封装在dll内部,不需要接口。所以java代码没有需要调用本地代码的契机。

但是对于springboot项目,在类加载之前,需要有个校验逻辑,所以在未触发钩子进行解密之前,校验出文件格式不对,就会报错,后面会讲述这一内容。

void JNICALL

MyClassFileLoadHook(

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* my_data = *new_class_data;


// Java 类文件的魔数是 0xCAFEBABE

unsigned char magic_number[] = {0xCA, 0xFE, 0xBA, 0xBE};


if(name && memcmp(class_data, magic_number, 4)!=0){

//解密,替换成其它算法

for (int i = 0; i < class_data_len; ++i)

{

my_data[i] = class_data[i] ^ 0x10;

}

}else{

for (int i = 0; i < class_data_len; ++i)

{

my_data[i] = class_data[i];

}

}

}

//agent是在启动时加载的

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 = &MyClassFileLoadHook;

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;

}

Agent_OnLoad在加载jar包时调用,只有一次

MyClassFileLoadHook是钩子函数,每个类加载的时候都会触发这个回调钩子。

这块代码和上面的JarEncrypt.cpp写到一起,生成jarencrypt.so。

所以整个代码保护流程是这样的:

1.先生成so文件,如果是windows系统,则需要dll文件,只需要生成一次。

2.对jar包里的需要的class文件进行加密, 生成加密后的jar包 xxx-encrypt.jar;

3.java -agentpath:jarencrypt.so -jar xxx- encrypt.jar 启动服务器



3、加密程序

加密程序核心的逻辑就是(以jar包为例):

遍历class文件,对于需要加密的class,加密后替换掉原来的calss文件。用java代码可以这么写:

public static void main(String[] args) throws Exception {
try {

//获取生成的JAR文件
File jarFile = new File("xxx.jar");
String sourcePath = jarFile.getAbsolutePath();
if (!jarFile.exists()) {
return;
}

File dstFile = new File(jarFile.getParent(), "xxx-encrypt.jar");
if (dstFile.exists()){
dstFile.delete();
}
FileOutputStream dstFos = new FileOutputStream(dstFile);

JarOutputStream dstJar = new JarOutputStream(dstFos);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buf = new byte[1024];
JarFile srcJar = new JarFile(jarFile);
for (Enumerationenumeration = 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.endsWith(".class") && name.startsWith("BOOT-INF/classes")){
for (int i=0; ibytes[i] = (byte) (bytes[i] ^ 0x07);
}
}

JarEntry ne = new JarEntry(name);
int length = bytes.length;
ne.setMethod(ZipEntry.STORED);
ne.setSize(new Long(length).longValue());
ne.setCompressedSize(new Long(length).longValue());

CRC32 crc32 = new CRC32();
crc32.update(bytes);
ne.setCrc(crc32.getValue());
dstJar.putNextEntry(ne);
dstJar.write(bytes);
baos.reset();
}
srcJar.close();

dstJar.close();
dstFos.close();

jarFile.delete();
Files.move(Paths.get(dstFile.getAbsolutePath()), Paths.get(sourcePath), StandardCopyOption.REPLACE_EXISTING);

} catch (Exception e) {
throw new Exception("Error encrypting JAR file", e);
}
}

实际项目中我建议将加密程序做成maven插件,这样每次生成加密jar包时不需要手动运行这么一段代码。


4、springboot项目

如果是springboot项目,上面的方案会存在问题,具体原因大家可以参考这篇文章:

https://zhuanlan.zhihu.com/p/545268749

简单地说,原因是:在类加载之前,需要调用ClassReader构造函数,这个构造函数里会校验class文件格式,这个时候claas文件还没解密,校验会失败,导致启动报错。

解决办法:

修改spring->ClassReader源码,这个构造函数进行解密。修改后的源码:

ClassReader也要生成本地代码,和Agent_OnLoad等代码一起打成动态库。

为了防止ClassReader源码被反编译,这个class文件也进行加密。后面会附完整加密源码。


5、完整的代码

maven插件加密源码:


maven:


可以将ProGuard和JVMTI结合在一起,进一步增大破解的成本。


6、这四种保护方案存在的共同问题

前四篇文章所探讨的代码保护方案均存在一个共同的局限性:尽管jar包本身难以通过反编译手段直接查看源码,但一旦这些jar包被加载到内存中,其包含的未受保护的字节码便可通过特定工具在运行时进行反编译。

例如阿里巴巴开发的Arthas工具,其内置的jad命令能够轻松地将内存中的字节码反编译回可读的Java源码。

这一功能详见Arthas官方文档:
https://arthas.aliyun.com/doc/jad.html。

为了从根本上解决这一问题,我们可以考虑引入AOT(Ahead-Of-Time)编译技术。

与传统JVM在运行时将字节码即时编译(JIT)为机器码不同,AOT技术在编译阶段便直接将源代码或字节码转换为机器码级别的可执行文件。

这样一来,攻击者试图通过机器码反编译源码的难度将大幅提升,因为机器码与源码之间的对应关系已经变得非常复杂且难以追溯。


END

通过采用AOT技术,我们可以为Java代码提供更为强大的保护屏障,有效抵御来自反编译和逆向工程的威胁。

由于篇幅问题,下一章我会给大家实操一下AOT(Ahead-Of-Time),试验一下它是否能达到这种效果。

如果大家有疑问,欢迎随时交流。

相关推荐

驱动网卡(怎么从新驱动网卡)
驱动网卡(怎么从新驱动网卡)

网卡一般是指为电脑主机提供有线无线网络功能的适配器。而网卡驱动指的就是电脑连接识别这些网卡型号的桥梁。网卡只有打上了网卡驱动才能正常使用。并不是说所有的网卡一插到电脑上面就能进行数据传输了,他都需要里面芯片组的驱动文件才能支持他进行数据传输...

2026-01-30 00:37 liuian

win10更新助手装系统(微软win10更新助手)

1、点击首页“系统升级”的按钮,给出弹框,告诉用户需要上传IMEI码才能使用升级服务。同时给出同意和取消按钮。华为手机助手2、点击同意,则进入到“系统升级”功能华为手机助手华为手机助手3、在检测界面,...

windows11专业版密钥最新(windows11专业版激活码永久)

 Windows11专业版的正版密钥,我们是对windows的激活所必备的工具。该密钥我们可以通过微软商城或者通过计算机的硬件供应商去购买获得。获得了windows11专业版的正版密钥后,我...

手机删过的软件恢复(手机删除过的软件怎么恢复)
手机删过的软件恢复(手机删除过的软件怎么恢复)

操作步骤:1、首先,我们需要先打开手机。然后在许多图标中找到带有[文件管理]文本的图标,然后单击“文件管理”进入页面。2、进入页面后,我们将在顶部看到一行文本:手机,最新信息,文档,视频,图片,音乐,收藏,最后是我们正在寻找的[更多],单击...

2026-01-29 23:55 liuian

一键ghost手动备份系统步骤(一键ghost 备份)

  步骤1、首先把装有一键GHOST装系统的U盘插在电脑上,然后打开电脑马上按F2或DEL键入BIOS界面,然后就选择BOOT打USDHDD模式选择好,然后按F10键保存,电脑就会马上重启。  步骤...

怎么创建局域网(怎么创建局域网打游戏)

  1、购买路由器一台。进入路由器把dhcp功能打开  2、购买一台交换机。从路由器lan端口拉出一条网线查到交换机的任意一个端口上。  3、两台以上电脑。从交换机任意端口拉出网线插到电脑上(电脑设置...

精灵驱动器官方下载(精灵驱动手机版下载)

是的。驱动精灵是一款集驱动管理和硬件检测于一体的、专业级的驱动管理和维护工具。驱动精灵为用户提供驱动备份、恢复、安装、删除、在线更新等实用功能。1、全新驱动精灵2012引擎,大幅提升硬件和驱动辨识能力...

一键还原系统步骤(一键还原系统有哪些)

1、首先需要下载安装一下Windows一键还原程序,在安装程序窗口中,点击“下一步”,弹出“用户许可协议”窗口,选择“我同意该许可协议的条款”,并点击“下一步”。  2、在弹出的“准备安装”窗口中,可...

电脑加速器哪个好(电脑加速器哪款好)

我认为pp加速器最好用,飞速土豆太懒,急速酷六根本不工作。pp加速器什么网页都加速,太任劳任怨了!以上是个人观点,具体性能请自己试。ps:我家电脑性能很好。迅游加速盒子是可以加速电脑的。因为有过之...

任何u盘都可以做启动盘吗(u盘必须做成启动盘才能装系统吗)

是的,需要注意,U盘的大小要在4G以上,最好是8G以上,因为启动盘里面需要装系统,内存小的话,不能用来安装系统。内存卡或者U盘或者移动硬盘都可以用来做启动盘安装系统。普通的U盘就可以,不过最好U盘...

u盘怎么恢复文件(u盘文件恢复的方法)

开360安全卫士,点击上面的“功能大全”。点击文件恢复然后点击“数据”下的“文件恢复”功能。选择驱动接着选择需要恢复的驱动,选择接入的U盘。点击开始扫描选好就点击中间的“开始扫描”,开始扫描U盘数据。...

系统虚拟内存太低怎么办(系统虚拟内存占用过高什么原因)

1.检查系统虚拟内存使用情况,如果发现有大量的空闲内存,可以尝试释放一些不必要的进程,以释放内存空间。2.如果系统虚拟内存使用率较高,可以尝试增加系统虚拟内存的大小,以便更多的应用程序可以使用更多...

剪贴板权限设置方法(剪贴板访问权限)
剪贴板权限设置方法(剪贴板访问权限)

1、首先打开iphone手机,触碰并按住单词或图像直到显示选择选项。2、其次,然后选取“拷贝”或“剪贴板”。3、勾选需要的“权限”,最后选择开启,即可完成苹果剪贴板权限设置。仅参考1.打开苹果手机设置按钮,点击【通用】。2.点击【键盘】,再...

2026-01-29 21:37 liuian

平板系统重装大师(平板重装win系统)

如果你的平板开不了机,但可以连接上电脑,那就能好办,楼主下载安装个平板刷机王到你的个人电脑上,然后连接你的平板,平板刷机王会自动识别你的平板,平板刷机王上有你平板的我刷机包,楼主点击下载一个,下载完成...

联想官网售后服务网点(联想官网售后服务热线)

联想3c服务中心是联想旗下的官方售后,是基于互联网O2O模式开发的全新服务平台。可以为终端用户提供多品牌手机、电脑以及其他3C类产品的维修、保养和保险服务。根据客户需求层次,联想服务针对个人及家庭客户...