使用JVMTI實(shí)現(xiàn)SpringBoot的jar加密,防止反編譯
1.背景
ToB項(xiàng)目私有化部署,攜帶有項(xiàng)目jar包,防止別人下載jar,反編譯出源碼
2.JVMTI解釋
JVMTI(Java Virtual Machine Tool Interface)即指 Java 虛擬機(jī)工具接口,它是一套由虛擬機(jī)直接提供的 native 接口,它處于整個(gè) JPDA(Java Platform Debugger Architecture) 體系的最底層,所有調(diào)試功能本質(zhì)上都需要通過(guò) JVMTI 來(lái)提供。
通過(guò)這些接口,開(kāi)發(fā)人員不僅調(diào)試在該虛擬機(jī)上運(yùn)行的 Java 程序,還能查看它們運(yùn)行的狀態(tài),設(shè)置回調(diào)函數(shù),控制某些環(huán)境變量,從而優(yōu)化程序性能。
3.使用JVMTI思路
在SpringBoot項(xiàng)目打包后,通過(guò)dll動(dòng)態(tài)鏈接庫(kù)(so共享對(duì)象)加密生成新的jar,在啟動(dòng)jar時(shí),讓jvm啟動(dòng)時(shí)調(diào)用jvmti提供的Agent_OnLoad方法,用c++去實(shí)現(xiàn),去解密對(duì)應(yīng)的class文件。
利用c++編譯型語(yǔ)言特性,無(wú)法反編譯出對(duì)應(yīng)的加密解密方法。
4.實(shí)現(xiàn)源碼
c++加密實(shí)現(xiàn)和jvm啟動(dòng)監(jiān)聽(tīng),需要jni.h,jvmti.h,jni_md.h三個(gè)頭文件,分別在java環(huán)境變量下include和include/linux(win32)下
// c++加密頭文件
#include <jni.h>
#ifndef _Included_com_cxp_demo_encrypt_ByteCodeEncryptor
#define _Included_com_cxp_demo_encrypt_ByteCodeEncryptor
#ifdef __cplusplus
extern "C"
{
#endif
JNIEXPORT jbyteArray JNICALL Java_com_cxp_demo_encrypt_ByteCodeEncryptor_encrypt(JNIEnv *, jclass, jbyteArray);
#ifdef __cplusplus
}
#endif
#endif
#ifndef CONST_HEADER_H_
#define CONST_HEADER_H_
const int k = 4;
const int compare_length = 18;
const char* package_prefix= "com/cxp/demo";
#endif// c++加密解密源碼
#include <iostream>
#include <jni.h>
#include <jvmti.h>
#include <jni_md.h>
#include "demo_bytecode_encryptor.h"
void encode(char *str)
{
unsigned int m = strlen(str);
for (int i = 0; i < m; i++)
{
str[i] = str[i] + k;
}
}
void decode(char *str)
{
unsigned int m = strlen(str);
for (int i = 0; i < m; i++)
{
str[i] = str[i] - k;
}
}
extern"C" JNIEXPORT jbyteArray JNICALL Java_com_cxp_demo_encrypt_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;
if (name && strncmp(name, package_prefix, compare_length) == 0 && strstr(name, "BySpringCGLIB") == NULL)
{
for (int i = 0; i < class_data_len; i++)
{
_data[i] = class_data[i];
}
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;
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;
}java加密邏輯:
// JNI調(diào)用c++代碼
public class ByteCodeEncryptor {
static {
String currentPath = ByteCodeEncryptor.class.getResource("").getPath().split("SpringBootJarEncryptor.jar!")[0];
if (currentPath.startsWith("file:")) {
currentPath = currentPath.substring(5);
}
String dllPath;
String os = System.getProperty("os.name");
if (os.toLowerCase().startsWith("win")) {
dllPath = currentPath + "SpringBootJarEncryptor.dll";
} else {
dllPath = currentPath + "SpringBootJarEncryptor.so";
}
System.load(dllPath);
}
public native static byte[] encrypt(byte[] text);
}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;
// 加密jar代碼
public class JarEncryptor {
public static void encrypt(String fileName, String dstName) {
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buf = new byte[1024];
File srcFile = new File(fileName);
File dstFile = new File(dstName);
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) {
bos.write(buf, 0, len);
}
byte[] bytes = bos.toByteArray();
String name = entry.getName();
if (name.startsWith("com/cxp/demo") && name.endsWith(".class")) {
try {
bytes = ByteCodeEncryptor.encrypt(bytes);
} catch (Exception e) {
e.printStackTrace();
}
}
JarEntry ne = new JarEntry(name);
dstJar.putNextEntry(ne);
dstJar.write(bytes);
bos.reset();
}
srcJar.close();
dstJar.close();
dstFos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
if (args == null || args.length == 0) {
System.out.println("please input parameter");
return;
}
if (args[0].endsWith(".jar")) {
JarEncryptor.encrypt(args[0], args[0].substring(0, args[0].lastIndexOf(".")) + "_encrypted.jar");
} else {
File file = new File(args[0]);
if (file.isDirectory()) {
String[] list = file.list();
if (list != null) {
for (String jarFilePath : list) {
if (jarFilePath.endsWith(".jar")) {
JarEncryptor.encrypt(args[0] + "/" + jarFilePath, args[0] + "_encrypted/" + jarFilePath);
}
}
}
} else {
System.out.println("this is not a folder or folder is empty");
}
}
}
}實(shí)現(xiàn)步驟:
1.先把c++打成dll(so)動(dòng)態(tài)鏈接庫(kù)
2.通過(guò)java調(diào)用dll文件,加密jar
3.啟動(dòng)加密后的jar,加上啟動(dòng)參數(shù)-agentpath:*.dll
gradle.build打包:
task copyJar(type: Copy) {
delete "$buildDir/libs/lib"
from configurations.runtime
into "$buildDir/libs/lib"
}
jar {
enabled = true
dependsOn copyJar
archivesBaseName = "app"
archiveVersion = ""
manifest {
attributes 'Main-Class': "主類全限定名",
'Class-Path': configurations.runtime.files.collect { "lib/$it.name" }.join(' ')
}
}Dockerfile文件:
# 加密jar RUN mkdir ./build/libs/lib_encrypted RUN java -jar ./encrypt/SpringBootJarEncryptor.jar ./build/libs/app.jar RUN java -jar ./encrypt/SpringBootJarEncryptor.jar ./build/libs/lib RUN rm -r ./build/libs/lib RUN rm ./build/libs/app.jar RUN mv ./build/libs/lib_encrypted ./build/libs/lib RUN mv ./build/libs/app_encrypted.jar ./build/libs/app.jar # --- java --- FROM java8 # 提取啟動(dòng)需要的資源 COPY --from=builder /build/libs/app.jar /application/app.jar COPY --from=builder /build/libs/lib /application/lib COPY --from=builder /encrypt /encrypt COPY --from=builder /start.sh start.sh ENTRYPOINT ["sh", "start.sh"]
啟動(dòng)命令:
java -agentpath:./SpringBootJarEncryptor.dll -jar app.jar
5.踩坑
java使用jni調(diào)用c++函數(shù),c++被調(diào)用的函數(shù)名要與java方法全限定名一致,如:com.cxp.demo.encrypt.ByteCodeEncryptor#encrypt=》Java_com_cxp_demo_encrypt_ByteCodeEncryptor_encrypt,注意java和c++數(shù)據(jù)類型的對(duì)應(yīng)關(guān)系
JVMTI只能作用Java原生啟動(dòng)類加載器,而SpringBoot有自己?jiǎn)?dòng)類加載器,讀取SpringBoot規(guī)定的原文件路徑(BOOT-INF/classes)和依賴路徑(BOOT-INF/lib),所以SpringBoot要打包成普通jar方式,bootjar是SpringBoot默認(rèn)的打包方式,改為jar打包方式無(wú)法打入依賴包,只能把依賴包打到同文件夾下,修改MANIFEST.MF文件的CLass-Path屬性,把依賴包添加進(jìn)去。可以看下SpringBoot的啟動(dòng)jar和普通jar區(qū)別。
c++在linux下打成so庫(kù),可以自行搜索下,推薦使用cmake
java引入dll動(dòng)態(tài)庫(kù),只能放在java的classpath下或絕對(duì)路徑
引包方式改為可以打出依賴包的方式,如compile,運(yùn)行時(shí)不需要的可以不用
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Java后端服務(wù)間歇性響應(yīng)慢的問(wèn)題排查與解決
之前在公司內(nèi)其它團(tuán)隊(duì)找到幫忙排查的一個(gè)后端服務(wù)連接超時(shí)問(wèn)題,問(wèn)題的表現(xiàn)是服務(wù)部署到線上后出現(xiàn)間歇性請(qǐng)求響應(yīng)非常慢(大于10s),但是后端業(yè)務(wù)分析業(yè)務(wù)日志時(shí)卻沒(méi)有發(fā)現(xiàn)慢請(qǐng)求,所以本文給大家介紹了Java后端服務(wù)間歇性響應(yīng)慢的問(wèn)題排查與解決,需要的朋友可以參考下2025-03-03
J2EE Servlet上傳文件到服務(wù)器并相應(yīng)顯示功能的實(shí)現(xiàn)代碼
這篇文章主要介紹了J2EE Servlet上傳文件到服務(wù)器,并相應(yīng)顯示,在文中上傳方式使用的是post不能使用get,具體實(shí)例代碼大家參考下本文2018-07-07
java并發(fā)編程中的SynchronousQueue實(shí)現(xiàn)原理解析
這篇文章主要介紹了java并發(fā)編程中的SynchronousQueue實(shí)現(xiàn)原理解析,SynchronousQueue是一個(gè)比較特別的隊(duì)列,此隊(duì)列源碼中充斥著大量的CAS語(yǔ)句,理解起來(lái)是有些難度的,為了方便日后回顧,本篇文章會(huì)以簡(jiǎn)潔的圖形化方式展示該隊(duì)列底層的實(shí)現(xiàn)原理,需要的朋友可以參考下2023-12-12
java實(shí)現(xiàn)后臺(tái)返回base64圖形編碼
這篇文章主要介紹了java實(shí)現(xiàn)后臺(tái)返回base64圖形編碼,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-06-06FeignClient服務(wù)器拋出異??蛻舳颂幚矸桨?/a>
這篇文章主要介紹了FeignClient服務(wù)器拋出異??蛻舳颂幚矸桨?,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-06-06
@PathParam和@QueryParam區(qū)別簡(jiǎn)析
這篇文章主要介紹了@PathParam和@QueryParam區(qū)別,分享了相關(guān)實(shí)例代碼,小編覺(jué)得還是挺不錯(cuò)的,具有一定借鑒價(jià)值,需要的朋友可以參考下2018-01-01
Java利用EasyExcel實(shí)現(xiàn)導(dǎo)出導(dǎo)入功能的示例代碼
EasyExcel是一個(gè)基于Java的、快速、簡(jiǎn)潔、解決大文件內(nèi)存溢出的Excel處理工具。本文廢話不多說(shuō),直接上手試試,用代碼試試EasyExcel是否真的那么好用2022-11-11
IntelliJ IDEA 2021.1 EAP 1 發(fā)布支持 Java 16 和 WSL 2
這篇文章主要介紹了IntelliJ IDEA 2021.1 EAP 1 發(fā)布支持 Java 16 和 WSL 2,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-02-02
Spring細(xì)數(shù)兩種代理模式之靜態(tài)代理和動(dòng)態(tài)代理概念及使用
代理是一種設(shè)計(jì)模式,提供了對(duì)目標(biāo)對(duì)象另外的訪問(wèn)方式,即通過(guò)代理對(duì)象訪問(wèn)目標(biāo)對(duì)象??梢圆恍薷哪繕?biāo)對(duì)象,對(duì)目標(biāo)對(duì)象功能進(jìn)行拓展。在我們學(xué)習(xí)Spring的時(shí)候就會(huì)發(fā)現(xiàn),AOP(面向切面編程)的底層就是代理2023-02-02

