core-lib / xjar

Spring Boot JAR 安全加密运行工具,支持的原生JAR。
Apache License 2.0
1.65k stars 471 forks source link

运行是输入密码,这样别人也可以通过密码解密代码,请问是否可以避免,谢谢 #1

Open hotcatteam opened 5 years ago

core-lib commented 5 years ago

你有什么好建议呢,或者说你想要达到的效果是怎样?

hotcatteam commented 5 years ago

主要是我考虑到,运行的时候输入了密码,这样对方可以根据密码进行解密操作,起不到保护代码的作用。我目前也没有比较好的方案。

core-lib commented 5 years ago

这个工具的使用场景是运维 实施 或者开发者为客户本地化部署服务时先把jar加密了 然后在客户本地服务端运行 因为密码是自己掌握 所以别人能解密的难度是比较大的 可能你的场景不一样 并不是把加密好的jar和密码都给别人

hotcatteam commented 5 years ago

不知道我理解的是否正确,我看说明中运行时,必须输入密码。谢谢 1、// 命令行运行JAR 因为程序要发布给客户,由于运行时需要输入密码,这样密码如果不给客户,客户无法运行程序 java -jar /path/to/encrypted.jar 2、// 在提示输入密码的时候输入密码后按回车即可正常启动,也可以通过传参的方式直接启动 此种情况,也是需要把密码写入到命令行中的,这样的话是不是密码就泄露了。 java -jar /path/to/encrypted.jar --xjar.password=PASSWORD

core-lib commented 5 years ago

我理解你的担心,所以XJar针对的场景是公司有自己的运维或实施去给客户部署,然后要求只有他们自己知道密码而且采用第一种方式来启动。接下来我也会考虑更优化的方案来避免这种问题。

lineezhang commented 5 years ago

请问下那个XJarLauncher,具体是如何使用的,这边我看到有自定义classloader,然后具体是如何使用呢,请教下哈

lineezhang commented 5 years ago

这样的使用场景,如果公司的运维去客户现场用密码解密jar包的话,得到的是基本明文的class文件,然后客户那边的懂技术的还是可以通过反编译工具来反编译源码,从而达不到保护源码的目的。

core-lib commented 5 years ago

不是这样的哦,并不需要解密jar包,只不过我们在客户的服务器上部署的时候通过一个密码来启动了程序,解密的过程是程序在内存中按需执行的,即加载某个class时当class时加密了的才在内存中解密并加载,源码并不会泄露,因为磁盘中不可能会有解密了的class,客户那边懂技术的人拿到的jar永远是加密了的。

lineezhang commented 5 years ago

哦...终于明白了你这个使用场景了,是针对的是springboot这种可以将应用服务器都打成可执行的文件或者是可执行的jar包,都可以 可以通过定制的过滤器来过滤jar包,来指定不加密或者不解密的资源。

core-lib commented 5 years ago

哈哈哈 都怪我文档写的不好,明白了就好:)

lineezhang commented 5 years ago

这里的密码其实可以修改成 提交数字证书的形式,这样的话就不会给客户看到明文密码了,数字证书了解一下,类似非对称加解密,私钥加密,公钥解密,还可以设置过期时间。

core-lib commented 5 years ago

没想到怎么做,说下你的思路呢

lineezhang commented 5 years ago

这里使用java自带的keystore生成一对自己签名的公钥和私钥,这一对秘钥,公钥给客户解密用,私钥就是我们用来加密需要。这样使用的时候,直接指定私钥文件的路径就可以啦,这样在你的应用上直观上就不用输入明文密码了。体现在这就是在使用的时候不用输入明文密码,保证密钥安全

core-lib commented 5 years ago

那怎么避免客户拿着公钥去解密class呢?

ilaotan commented 5 years ago

哈哈,你俩聊得好欢乐.....

chenjiayi0218 commented 5 years ago

大神,如果springboot后台运行,例如 java -jar /path/to/encrypted.jar --xjar.password=PASSWORD & 。但是这样的话,通过ps -ef 不就可以发现密码了吗?有什么办法可以解决

core-lib commented 5 years ago

启动时不指定密码 运行过程中输入密码

chenjiayi0218 commented 5 years ago

启动时不指定密码 运行过程中输入密码

启动是不指定密码?不太理解,因为我加密jar后,不输入密码直接运行的话,是会报错终止的。能描述大概步骤吗? 我现在的脚本如下:

!/bin/sh

java -jar -Dspring.profiles.active=test /path/to/encrypted.jar --xjar.password=PASSWORD > /path/to//out.x.file 2>&1 & 但是这样执行ps -ef就会出现密码了

core-lib commented 5 years ago

不加 --xjar.password=PASSWORD 参数会报错终止?

chenjiayi0218 commented 5 years ago

不加 --xjar.password=PASSWORD 参数会报错终止?

通过脚本使用后台运行jar,不加--xjar.password=PASSWORD 就会报错的 错误如下: Exception in thread "main" java.lang.NullPointerException at io.xjar.boot.XBootLauncher.(XBootLauncher.java:44) at io.xjar.boot.XBootLauncher.main(XBootLauncher.java:53)

core-lib commented 5 years ago

版本多少

chenjiayi0218 commented 5 years ago

版本多少

maven插件编译,1.0.8

core-lib commented 5 years ago

你这种启动方式 在程序运行时无法获取当前的控制台 java.io.Console 也就无法获取输入的密码 你试试 java -jar -Dspring.profiles.active=test /path/to/encrypted.jar 这样启动

chenjiayi0218 commented 5 years ago

你这种启动方式 在程序运行时无法获取当前的控制台 java.io.Console 也就无法获取输入的密码 你试试 java -jar -Dspring.profiles.active=test /path/to/encrypted.jar 这样启动

如果这样执行的话,无法是程序在后台运行的,还有什么办法吗?

core-lib commented 5 years ago

目前没有

chenjiayi0218 commented 5 years ago

目前没有

那请问你们现在的jar是怎么运行的?业务场景是怎么样的? 打开命令窗口运行jar,然后输入密码,命令窗口不关闭?

core-lib commented 5 years ago

是的 这种方式是最安全的 但也不是绝对安全

core-lib commented 5 years ago

或者你可以尝试 使用 javaw 命令 加密码参数 来启动试试

chenjiayi0218 commented 5 years ago

javaw

用的是centos,没有javaw命令o(╥﹏╥)o

chenjiayi0218 commented 5 years ago

或者你可以尝试 使用 javaw 命令 加密码参数 来启动试试

我重写了XBootLauncher这个类,通过其他方式传入xjar.password参数,从而简单实现了可以后台运行和ps -ef看不到命令进程了(^-^)V

845999248 commented 5 years ago

Scanner sc = new Scanner(System.in); System.out.println("password:"); password = sc.nextLine(); 代替XBootLauncher 中的console类 就可以了

core-lib commented 5 years ago

厉害厉害 go工具方便 提供一下吗 我考虑再升级一个版本 支持这种方式

845999248 commented 5 years ago

加不上你的qq啊 不知道怎么发给你

core-lib commented 5 years ago

646742615

daoyingmingli commented 5 years ago

我重写了XBootLauncher这个类,通过其他方式传入xjar.password参数,从而简单实现了可以后台运行和ps -ef看不到命令进程了(^-^)V

我也打算重写XBootLauncher,读取预先在某个地方的保存有密码的文件,读取完成后删除该文件

core-lib commented 5 years ago

哈哈哈 没想到大家这么想要免密码启动 我就来升级一下吧 大家有什么建议

daoyingmingli commented 5 years ago

哈哈哈 没想到大家这么想要免密码启动 我就来升级一下吧 大家有什么建议

可以固定读取某个路径下的文件,启动是读取里面的密码内容,读取完成后清空密码数据 ? apollo好像也是约定的读取系统某个路径下的文件获取环境信息的。

lvxiaomao commented 5 years ago

哈哈哈 没想到大家这么想要免密码启动 我就来升级一下吧 大家有什么建议

目前支持spring boot开发的应用进行加密部署,能不能也支持war包部署的加密部署?谢谢

core-lib commented 5 years ago

理论上可以 你关注下 我正在研究

core-lib commented 5 years ago

目前最新版本已支持免密码启动,这种危险模式只适合做一个最简单的防反编译,但的确是方便不少。 工具类 XBoot / XJar 中增加几个静态加密方法,提供加密模式(mode)选择,默认为0即普通模式,1为危险模式即免密码启动。可以采用XConstants.MODE_DANGER 常量 使用插件的同学可以设置参数 mode 为 1 即表示使用免密码启动,即危险模式!

GitHubcaowei commented 5 years ago

你好,如果我们提供sdk供第三方调用(其实就是一个普通的jar),但是不想让第三方看到sdk中的源码,所以不能用密码模式,否则就让第三方能解密了。危险加密模式能解决我的需求吗?具体怎么集成? 1.// 危险加密模式,即不需要输入密码即可启动的加密方式,这种方式META-INF/MANIFEST.MF中会保留密钥,请谨慎使用! String password = "io.xjar"; XKey xKey = XKit.key(password); XBoot.encrypt("/path/to/read/plaintext.jar", "/path/to/save/encrypted.jar", xKey, XConstants.MODE_DANGER); 2.第三方调用sdk的时候,我们sdk里面做解密?

core-lib commented 5 years ago

如果你提供的是http请求 这种sdk大可不必防反编译 如果里面的确有知识产权相关的代码 如果混淆能解决 最优方案是混淆 因为加密是可逆的 你让程序无需密码就能解密就代表别的开发者也可以按照这个逻辑解密 xjar的加密场景是自己公司有实施人员 给客户部署整个应用的时候 输入密码启动 这样密码记在自己公司或脑子里 才能做到相对保险 你的需求我暂时觉得是不太适合加密 反而混淆是最佳方案 不过如果你只能用加密方式 混淆毕竟会改变一些代码的定义 我建议你可以这样做 接口是明文 实现是加密的 内部提供一个解密的ClassLoader 让第三方基于接口开发 自定义ClassLoader负责解密并加载 只能加大反编译难度 不是绝对保险

talentqwu commented 5 years ago

实现了一个不需要输入密码的方案: 1、在io.xjar.XLauncher代码中直接设置密码 String password = "XXXXXXXXXXXXXXXX"; 2、在运行XBoot.encrypt()之前对io.xjar.XLauncher.class进行加密,这样打包到xxx-encrypted.jar中的XLauncher就是加密过的,防止防止编译看到密码 3、用C++开发一个JVMTI代理(ClassFileLoadHook),这个代理解密io.xjar.XLauncher 4、运行时输入:java -agentlib:xxx_agent -jar xxx-encrypted.jar

我在Java 端使用AES/ECB/NoPadding对XLauncher.class分块加密;C++端使用mbedTLS对XLauncher.class分块解密。 因为关键的XLauncher解密密码保存在DLL/SO文件,很难通过反编译获取,也就保证了整个系统的安全性亦达到了不需要输入密码的效果。 这个方案比较简单,在使用XJAR所有功能的同时,只需要使用JVMTI对一个文件解密即可(象我这样对于C++连菜鸟对算不上的人很适用,所有资料网上也很容易获取)。 比较关键的一点是因为分块加密的原因,加密后的class文件可能比原文件大,在JVMTI解密时需要注意。

core-lib commented 5 years ago

大神 方便把这个东西发给我吗 我整合在里面

talentqwu commented 5 years ago

C++代码(就一个文件,第一次写C代码,水平有限;使用VS2010编译时需要在C++路径中加入Java及mbedTLS头文件还有mbedTLS的lib) JVMTI请参考:https://github.com/sea-boat/ByteCodeEncrypt mbedTLS请参考:https://www.cnblogs.com/fudong071234/p/6591844.html

#include <iostream>
#include <string.h>

#include "jni.h"
#include <jvmti.h>
#include <jni_md.h>

#include <aes.h>
#pragma comment(lib, "mbedTLS.lib")

void decrypt(int *destLen, char *src, int srcLen, unsigned char** new_class_data)
{
    // 128 bit key
    char *key = "xxxxxxxxxxxxxxxx";

    mbedtls_aes_context aes_ctx; 
    mbedtls_aes_init( &aes_ctx );  

    //设置解密密钥  
    mbedtls_aes_setkey_dec( &aes_ctx, (unsigned char *)key, 128);

    //获取块的数量
    int block = srcLen / 16;

    unsigned char* dest = *new_class_data;

    //清空
    memset(dest, 0, *destLen);

    //分块解密,每块16字节长度
    unsigned char input[16];
    unsigned char outout[16];

    int b = 0;
    while( b < block ){
        int offset = b * 16;
        int len = 0;
        if((srcLen - 16 - offset) > 0 ){
            len = 16;
        }else{
            len = srcLen-offset;
        }

        memset(input, 0 ,16);
        memset(outout, 0, 16);
        memcpy(input, &src[offset], len);

        //解密
        mbedtls_aes_crypt_ecb( &aes_ctx, MBEDTLS_AES_DECRYPT, input, outout ); 
        if (b == 381) {
                        // 分块后剩余字节数
            memcpy(&dest[6096], outout, **9**);
        } else {
            memcpy(&dest[b * 16], outout, 16);
        }
        b++;
    }

    mbedtls_aes_free(&aes_ctx);
}

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
    )
{
    if (name && strncmp(name, "io/xjar/XLauncher", 17) == 0) {
        //动态分配内存(注意:这是XLauncher.class原始长度)
        *new_class_data_len = **6105;**
        jvmti_env->Allocate(*new_class_data_len, new_class_data);
        decrypt((int *)new_class_data_len, (char *)class_data, class_data_len, new_class_data);
    }
}

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 = &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;
}
core-lib commented 5 years ago

哈哈 我也没写过C代码 不过应该能看懂 我有空就整合进去

talentqwu commented 5 years ago

Java代码(因为要替换XLauncher.class,我是将XJAR克隆下来执行的)

public class XjarEncryption {

    public static final String xLauncherPath = "" +
            "/Users/Talent/IdeaProjects/xjar/target/classes/io/xjar/XLauncher.class";

    public static final String sourceJar = "/Users/Talent/IdeaProjects/xxx/target/xxx.jar";

    public static final String targetJar = "/Users/Talent/Downloads/xxx-encrypted.jar";

    public static void main(String[] arg) throws Exception {
        String key = "xxxxxxxxxxxxxxxx"; // 128 bit key
        encryptXLauncher(key);

        XBoot.encrypt(
                new File(sourceJar),
                new File(targetJar),
                XKit.key(key),
                new XEntryFilter<JarArchiveEntry>() {
                    @Override
                    public boolean filtrate(JarArchiveEntry jarArchiveEntry) {
                        String name = jarArchiveEntry.getName();
                        return name.startsWith("xxx/xxxxxxx/") && name.endsWith(".class");
                    }
                }
        );

        System.out.println("Jar was encrypted:" + targetJar);
    }

    private static void encryptXLauncher(String key) throws Exception {
        InputStream inputStream = new FileInputStream(xLauncherPath);
        byte[] data = IOUtils.toByteArray(inputStream);
        inputStream.close();
        new File(xLauncherPath).delete();

        SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
        Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");// 算法/模式/补码方式
        cipher.init(Cipher.ENCRYPT_MODE, skeySpec);

        //分块加密
        int black = data.length / 16 + 1;
        byte[] out = new byte[black * 16];

        // 长度需要与C++代码中的长度保持一致
        System.out.println("加密前io.xjar.XLauncher.class大小:" + data.length);

        int index = 0;
        while (index < black) {
            //计算偏移
            int offset = index * 16;
            //计算剩余长度
            int length = (data.length - 16 - offset) > 0 ? 16 : (data.length - offset);
            byte[] input = new byte[16];
            System.arraycopy(data, offset, input, 0, length);
            byte[] output = cipher.doFinal(input);
            System.arraycopy(output, 0, out, offset, 16);
            index++;
        }

        OutputStream outputStream = new FileOutputStream(xLauncherPath);
        outputStream.write(out);
        outputStream.close();
    }
}
talentqwu commented 5 years ago

修改XLauncher(最直接、最笨的办法)

public class XLauncher implements XConstants {
    public final String[] args;
    public final XDecryptor xDecryptor;
    public final XEncryptor xEncryptor;
    public final XKey xKey;

    public XLauncher(String... args) throws Exception {
        this.args = args;
        String algorithm = DEFAULT_ALGORITHM;
        int keysize = DEFAULT_KEYSIZE;
        int ivsize = DEFAULT_IVSIZE;
        **String password = "xxxxxxxxxxxxxxxx";**
        String keypath = null;
 ... ...
talentqwu commented 5 years ago

水平有限,目前只在Windows平台上实现了。因为DLL中的密码不容易被看到(也可能是我认知有限),所以生成一个DLL后可供所有项目使用,也没有必须为每个项目都做一个DLL;如果有大神可以破解一个做再多也没有用。

hei66 commented 5 years ago

C++代码(就一个文件,第一次写C代码,水平有限;使用VS2010编译时需要在C++路径中加入Java及mbedTLS头文件还有mbedTLS的lib) JVMTI请参考:https://github.com/sea-boat/ByteCodeEncrypt mbedTLS请参考:https://www.cnblogs.com/fudong071234/p/6591844.html

#include <iostream>
#include <string.h>

#include "jni.h"
#include <jvmti.h>
#include <jni_md.h>

#include <aes.h>
#pragma comment(lib, "mbedTLS.lib")

void decrypt(int *destLen, char *src, int srcLen, unsigned char** new_class_data)
{
  // 128 bit key
  char *key = "xxxxxxxxxxxxxxxx";

  mbedtls_aes_context aes_ctx; 
  mbedtls_aes_init( &aes_ctx );  

  //设置解密密钥  
  mbedtls_aes_setkey_dec( &aes_ctx, (unsigned char *)key, 128);

  //获取块的数量
  int block = srcLen / 16;

  unsigned char* dest = *new_class_data;

  //清空
  memset(dest, 0, *destLen);

  //分块解密,每块16字节长度
  unsigned char input[16];
  unsigned char outout[16];

  int b = 0;
  while( b < block ){
      int offset = b * 16;
      int len = 0;
      if((srcLen - 16 - offset) > 0 ){
          len = 16;
      }else{
          len = srcLen-offset;
      }

      memset(input, 0 ,16);
      memset(outout, 0, 16);
      memcpy(input, &src[offset], len);

      //解密
      mbedtls_aes_crypt_ecb( &aes_ctx, MBEDTLS_AES_DECRYPT, input, outout ); 
      if (b == 381) {
                        // 分块后剩余字节数
          memcpy(&dest[6096], outout, **9**);
      } else {
          memcpy(&dest[b * 16], outout, 16);
      }
      b++;
  }

  mbedtls_aes_free(&aes_ctx);
}

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
  )
{
  if (name && strncmp(name, "io/xjar/XLauncher", 17) == 0) {
      //动态分配内存(注意:这是XLauncher.class原始长度)
      *new_class_data_len = **6105;**
      jvmti_env->Allocate(*new_class_data_len, new_class_data);
      decrypt((int *)new_class_data_len, (char *)class_data, class_data_len, new_class_data);
  }
}

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

通过JVMTI来加密 XLauncher,但VMTI 只对类是由系统类加载器才能找到对应的类,而XLauncher 不是由系统加载器加载的,请问您是如何解决这个问题。

image

image