Python 构建壳来启动加密的 SpringBoot Jar 包,增加反编译难度

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 操作系统上进行。依赖 pycryptodomenuitka,可以通过 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>
相关推荐
ladymorgana20 分钟前
【spring boot】三种日志系统对比:ELK、Loki+Grafana、Docker API
spring boot·elk·grafana
程序员秘密基地1 小时前
基于html,css,vue,vscode,idea,,java,springboot,mysql数据库,在线旅游,景点管理系统
java·spring boot·mysql·spring·web3
chao_7893 小时前
二分查找篇——搜索旋转排序数组【LeetCode】一次二分查找
数据结构·python·算法·leetcode·二分查找
烛阴4 小时前
Python装饰器解除:如何让被装饰的函数重获自由?
前端·python
noravinsc4 小时前
django 一个表中包括id和parentid,如何通过parentid找到全部父爷id
python·django·sqlite
xdscode4 小时前
SpringBoot ThreadLocal 全局动态变量设置
java·spring boot·threadlocal
ajassi20004 小时前
开源 python 应用 开发(三)python语法介绍
linux·python·开源·自动化
沉默媛5 小时前
如何安装python以及jupyter notebook
开发语言·python·jupyter
天河归来6 小时前
springboot框架redis开启管道批量写入数据
java·spring boot·redis
合作小小程序员小小店6 小时前
web网页,在线%食谱推荐系统%分析系统demo,基于vscode,uniapp,vue,java,jdk,springboot,mysql数据库
vue.js·spring boot·vscode·spring·uni-app