Python 构建壳来启动加密的 SpringBoot Jar 包,增加反编译难度
Python 构建壳来启动加密的 SpringBoot Jar 包
通过将 Python 打包成可执行二进制文件来启动 SpringBoot Jar,可以避免 Jar 包容易被反编译的情况。
1.构建加密 SpringBoot jar
// 需要传入三个参数:
// 第一个:jar 包加密密钥,和后面 launcher.py 里面的 self.encryption_key 相对应;
// 第二个:未加密的 jar 包;
// 第三个:已加密的 jar 包;java -jar JarEncryptor-1.0-SNAPSHOT.jar [b9e8c40dc6ca4eb9928930344812daf4] [origin.jar] [ecrypted.jar]
JarEncryptor-1.0-SNAPSHOT.jar 源码如下。
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;public class JarEncryptor {private static final String ALGORITHM = "AES/CBC/PKCS5Padding";public static void main(String[] args) throws Exception {// # 此处是密钥,和下面构建 launcher.bin 的 Python 脚本密钥相同,可自行修改,但需要和下面保持一致String encryptionKey = args[0];String inputFile = args[1];String outputFile = args[2];// 生成密钥和 IVbyte[] key = generateKey(encryptionKey);byte[] iv = generateIv(encryptionKey);// 加密文件encryptFile(inputFile, outputFile, key, iv);System.out.println("JAR encrypted successfully");System.out.println("Python launcher command:");System.out.printf("python launcher.py %s%n", outputFile);}private static byte[] generateKey(String seed) throws NoSuchAlgorithmException {MessageDigest digest = MessageDigest.getInstance("SHA-256");byte[] hash = digest.digest(seed.getBytes());// 取前 32 字节作为 AES-256 密钥byte[] key = new byte[32];System.arraycopy(hash, 0, key, 0, 32);return key;}private static byte[] generateIv(String seed) throws NoSuchAlgorithmException {MessageDigest digest = MessageDigest.getInstance("MD5");return digest.digest(seed.getBytes());}private static void encryptFile(String inputPath, String outputPath, byte[] key, byte[] iv) throws Exception {Cipher cipher = Cipher.getInstance(ALGORITHM);SecretKeySpec keySpec = new SecretKeySpec(key, "AES");IvParameterSpec ivSpec = new IvParameterSpec(iv);cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);try (FileInputStream in = new FileInputStream(inputPath);FileOutputStream out = new FileOutputStream(outputPath)) {byte[] buffer = new byte[8192];int bytesRead;while ((bytesRead = in.read(buffer)) != -1) {byte[] encrypted = cipher.update(buffer, 0, bytesRead);out.write(encrypted);}byte[] encrypted = cipher.doFinal();out.write(encrypted);}}}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.xxx.com</groupId><artifactId>JarEncryptor</artifactId><version>1.0-SNAPSHOT</version><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target></properties><packaging>jar</packaging><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-jar-plugin</artifactId><configuration><archive><!--生成的jar中,不要包含pom.xml和pom.properties这两个文件--><addMavenDescriptor>true</addMavenDescriptor><manifest><mainClass>com.xxx.jar.encryptor.JarEncryptor</mainClass></manifest></archive></configuration></plugin></plugins></build></project>
2.构建 libhidejar.so
用来构建 linux 环境变量库,用来隐藏 java -jar 后面的真实的 jar 包路径。
# 创建 hide_jar_path.c 文件,内容如下:
vim hide_jar_path.c
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>int (*_main) (int, char * *, char * *);
static int pretend_main(int argc, char **argv, char **env)
{char tmp[200];strcpy(tmp, argv[1]);strncpy(argv[1], "", strlen(argv[1]));argv[1] = tmp;char tmp2[200];strcpy(tmp2, argv[argc-1]);strncpy(argv[argc-1], "", strlen(argv[argc-1]));argv[argc-1] = tmp2;return _main(argc, argv, env);
}int (*orig_start_main)(int (*main)(int, char **, char **),int argc,char **argv,void (*init) (void),void (*fini) (void),void (*_fini) (void),void (*stack_end));int __libc_start_main(int (*main)(int, char **, char **),int argc, char **argv,void (*init)(void),void (*fini)(void),void (*_fini)(void),void (*stack_end))
{orig_start_main = dlsym(RTLD_NEXT, "__libc_start_main");_main = main;return orig_start_main(pretend_main, argc, argv, init, fini, _fini, stack_end);
}
通过如下命令构建
# 系统没有 gcc 环境的,需要先安装 gcc 环境gcc -shared -fPIC -o libhidejar.so hide_jar_path.c -ldl
3.构建 launcher.bin
注意,构建操作应该在和实际运行环境相同的 Linux 操作系统上进行。依赖
pycryptodome
和nuitka
,可以通过 pip 进行安装。新建 launcher.py 脚本,内容如下。
# 安装依赖库
pip install pycryptodome
pip install nuitka# 新建 launcher.py 脚本,输入如下内容
vim launcher.py
#!/usr/bin/env python3
import argparse
import hashlib
import os
import signal
import subprocess
import sys
import tempfilefrom Crypto.Cipher import AES
from Crypto.Util.Padding import unpadclass JarLauncher:def __init__(self):self.process = Noneself.temp_dir = Noneself._setup_signal_handlers()# 此处是密钥,和上面构建加密 SpringBoot Jar 相同,可自行修改,但需要和上面保持一致self.encryption_key = 'b9e8c40dc6ca4eb9928930344812daf4'def _setup_signal_handlers(self):"""设置信号处理器确保资源清理"""signal.signal(signal.SIGINT, self._handle_signal)signal.signal(signal.SIGTERM, self._handle_signal)def _handle_signal(self, signum, frame):"""处理终止信号"""print(f"\nReceived signal {signum}, terminating...")self.cleanup()sys.exit(1)def _get_encryption_key(self):"""从安全位置获取加密密钥"""# 实际使用中可以从环境变量、密钥管理服务或加密配置文件中获取key_seed = self.encryption_key# 使用 SHA-256 生成固定长度的密钥return hashlib.sha256(key_seed.encode()).digest()[:32] # AES-256需要32字节def _get_iv(self):"""获取初始化向量(IV)"""# 可以与密钥一起存储或使用固定值(降低安全性但简化实现)iv_seed = self.encryption_keyreturn hashlib.md5(iv_seed.encode()).digest() # 16字节def decrypt_jar(self, encrypted_path, key, iv):"""解密 JAR 文件"""with open(encrypted_path, 'rb') as f:encrypted_data = f.read()cipher = AES.new(key, AES.MODE_CBC, iv)try:return unpad(cipher.decrypt(encrypted_data), AES.block_size)except ValueError as e:print(f"Decryption failed: {str(e)}")print("Possible causes: incorrect key/IV or corrupted file")sys.exit(1)def execute_jar(self, jar_data, java_args=None):"""执行 JAR 文件"""self.temp_dir = tempfile.mkdtemp(prefix='jar_launcher_')temp_jar_path = os.path.join(self.temp_dir, 'application.jar')try:# 写入临时文件with open(temp_jar_path, 'wb') as f:f.write(jar_data)# 构建 Java 命令cmd = ['java', '-jar']wrapper_jar_file_path = os.path.join(os.path.dirname(__file__), 'WrapperLauncher.jar')ld_reload_path = os.path.join(os.path.dirname(__file__), 'libhidejar.so')if java_args:cmd.extend(java_args.split())cmd.append(wrapper_jar_file_path)env = os.environ.copy()env["LD_PRELOAD"] = ld_reload_path# 该环境变量和 WrapperLauncher 中相对应,要改则需要一起改env["AWESOME_JAR_XXX"] = temp_jar_pathself.process = subprocess.Popen(cmd,stdout=sys.stdout,stderr=sys.stderr,stdin=sys.stdin,bufsize=1,universal_newlines=True,env=env)# 等待进程结束return self.process.wait()except Exception as e:print(f"Failed to execute JAR: {str(e)}")return 1def cleanup(self):"""清理资源"""if self.process and self.process.poll() is None:self.process.terminate()try:self.process.wait(timeout=5)except subprocess.TimeoutExpired:self.process.kill()if self.temp_dir and os.path.exists(self.temp_dir):try:import shutilshutil.rmtree(self.temp_dir)except Exception as e:print(f"Warning: Failed to clean temp directory: {str(e)}")def main():parser = argparse.ArgumentParser(description='SpringBoot JAR Launcher')parser.add_argument('--jar-file', help='Path to the encrypted JAR file', default='')parser.add_argument('--java-args', help='Additional Java VM arguments', default='')args = parser.parse_args()launcher = JarLauncher()try:# 获取密钥key = launcher._get_encryption_key()iv = launcher._get_iv()# 解密 JARprint("Decrypting JAR file...")jar_file = args.jar_fileif not jar_file:jar_file = os.path.join(os.path.dirname(__file__), 'application.jar')jar_data = launcher.decrypt_jar(jar_file, key, iv)# 执行 JARprint("Starting application...")exit_code = launcher.execute_jar(jar_data, args.java_args)sys.exit(exit_code)except Exception as e:print(f"Fatal error: {str(e)}", file=sys.stderr)launcher.cleanup()sys.exit(1)finally:launcher.cleanup()if __name__ == '__main__':main()
通过如下命令进行构建。
python3 -m nuitka --standalone --onefile --output-dir=out \--include-data-files=[your path of jar encrypted]=application.jar \--include-data-files=./WrapperLauncher-1.0.0-SNAPSHOT.jar=WrapperLauncher.jar \--include-data-files=./libhidejar.so=libhidejar.so \launcher.py
构建成功后,在 out 目录里面会有 launcher.bin 启动类。
4.启动 launcher.bin
launcher.bin
是将 Springboot Jar 包含在内的二进制文件,增加逆向的难度。
./launcher.bin [--jar-file=your external encrypted jar] [--java-args="[your extra args]"]
启动完成后,是看不到解密后的 jar 路径的。
5.WrapperLauncher
WrapperLauncher 是通过环境变量获取实际的 jar 路径,通过类加载器加载实际的 springboot jar 包,增加一层寻找真实 jar 的障碍。下面是其源码和 pom.xml 配置
import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;public class WrapperLauncher {public static void main(String[] args) throws Exception {// 该环境变量和 launcher.py 中相对应,要改则需要一起改String awesomeJarPath = System.getenv("AWESOME_JAR_XXX");URL jarUrl = new File(awesomeJarPath).toURI().toURL();URLClassLoader loader = new URLClassLoader(new URL[]{jarUrl});Class<?> mainClass = loader.loadClass("org.springframework.boot.loader.JarLauncher");Method mainMethod = mainClass.getMethod("main", String[].class);mainMethod.invoke(null, (Object) args);}}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.xxx.com</groupId><artifactId>WrapperLauncher</artifactId><version>1.0.0-SNAPSHOT</version><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target></properties><packaging>jar</packaging><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-jar-plugin</artifactId><configuration><archive><addMavenDescriptor>true</addMavenDescriptor><manifest><mainClass>com.xxx.wrapper.launcher.WrapperLauncher</mainClass></manifest></archive></configuration></plugin></plugins></build></project>