Python 构建壳来启动加密的 SpringBoot Jar 包
通过将 Python 打包成可执行二进制文件来启动 SpringBoot Jar,可以避免 Jar 包容易被反编译的情况。
1.构建加密 SpringBoot jar
bash
// 需要传入三个参数:
// 第一个: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 源码如下。
java
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];
// 生成密钥和 IV
byte[] 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
<?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 包路径。
bash
# 创建 hide_jar_path.c 文件,内容如下:
vim hide_jar_path.c
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);
}
通过如下命令构建
bash
# 系统没有 gcc 环境的,需要先安装 gcc 环境
gcc -shared -fPIC -o libhidejar.so hide_jar_path.c -ldl
3.构建 launcher.bin
注意,构建操作应该在和实际运行环境相同的 Linux 操作系统上进行。依赖
pycryptodome
和nuitka
,可以通过 pip 进行安装。新建 launcher.py 脚本,内容如下。
bash
# 安装依赖库
pip install pycryptodome
pip install nuitka
# 新建 launcher.py 脚本,输入如下内容
vim launcher.py
python
#!/usr/bin/env python3
import argparse
import hashlib
import os
import signal
import subprocess
import sys
import tempfile
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
class JarLauncher:
def __init__(self):
self.process = None
self.temp_dir = None
self._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_key
return 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_path
self.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 1
def 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 shutil
shutil.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()
# 解密 JAR
print("Decrypting JAR file...")
jar_file = args.jar_file
if not jar_file:
jar_file = os.path.join(os.path.dirname(__file__), 'application.jar')
jar_data = launcher.decrypt_jar(jar_file, key, iv)
# 执行 JAR
print("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()
通过如下命令进行构建。
bash
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 包含在内的二进制文件,增加逆向的难度。
bash
./launcher.bin [--jar-file=your external encrypted jar] [--java-args="[your extra args]"]

启动完成后,是看不到解密后的 jar 路径的。
5.WrapperLauncher
WrapperLauncher 是通过环境变量获取实际的 jar 路径,通过类加载器加载实际的 springboot jar 包,增加一层寻找真实 jar 的障碍。下面是其源码和 pom.xml 配置
java
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
<?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>