[JAVA] Une histoire sur l'effort de décompiler les fichiers JAR

Ceci est l'article sur le 17ème jour du "Calendrier de l'Avent Java 2016".

introduction

Le début des choses

Certains disent

J'ai pensé comme ça

Vous pouvez le démarrer à partir d'un chargeur qui crypte le fichier de classe Java, le déchiffre et le charge dynamiquement. Étant donné que le fichier de classe lui-même est chiffré, vous ne devriez pas pouvoir afficher le code simplement en le plaçant dans le décrypteur.

J'ai essayé quelque chose comme ça

Génération d'un fichier JAR contenant un fichier de classe chiffré

cryptage des fichiers de classe

De manière appropriée comme ça

private Key key;// KeyGenerator#generateKey()De manière appropriée

void encrypt(File file) {
    byte[] inByte = null;
    try {
        inByte = FileUtils.readFileToByteArray(file);

        Cipher cipher = Cipher.getInstance("AES");
        cipher.init(Cipher.ENCRYPT_MODE, key);
        byte[] encrypted = cipher.doFinal(inByte);

        FileUtils.writeByteArrayToFile(file, encrypted);
        decryptTest(file, key, inByte);
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } catch (NoSuchAlgorithmException e) {
        e.printStackTrace();
    } catch (NoSuchPaddingException e) {
        e.printStackTrace();
    } catch (InvalidKeyException e) {
        e.printStackTrace();
    } catch (BadPaddingException e) {
        e.printStackTrace();
    } catch (IllegalBlockSizeException e) {
        e.printStackTrace();
    }
}

Réécriture de mainClass

Spigot lit la classe principale du plug-in dans le fichier de configuration (plugin.yml) et la réécrit dans celle de la classe loader

Yaml yaml = new Yaml();
try {
        String str = FileUtils.readFileToString(new File(appTmpDir, "plugin.yml"), "utf-8");
        Map map = yaml.loadAs(str, Map.class);
        map.put("main", getPackageName(getMainClass()) + ".PluginLoader");
        str = yaml.dumpAsMap(map);
        FileUtils.writeStringToFile(new File(appTmpDir, "plugin.yml"), str, "utf-8");
    } catch (IOException e) {
        e.printStackTrace();
}

Décryptage et chargement dynamique des fichiers de classe

Décryptage

Du fichier à la chaîne d'octets

private byte[] read(InputStream inputStream) {
    byte[] buf = new byte[1024];
    int len;
    BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

    try {
        while ((len = bufferedInputStream.read(buf)) > 0) {
            byteArrayOutputStream.write(buf, 0, len);
        }
        inputStream.close();
    } catch (IOException e) {
        e.printStackTrace();
    }

    return byteArrayOutputStream.toByteArray();
}

Décryptage (la clé est lue par ObjectInputStream)

private byte[] decrypt(byte[] bytes, Key key) {
    byte[] inByte = null;
    try {

        if (key == null) throw new IllegalArgumentException("La clé est nulle.");
        Cipher cipher = Cipher.getInstance("AES");
        cipher.init(Cipher.DECRYPT_MODE, key);
        inByte = cipher.doFinal(bytes);

    } catch (NoSuchAlgorithmException e) {
        e.printStackTrace();
    } catch (NoSuchPaddingException e) {
        e.printStackTrace();
    } catch (InvalidKeyException e) {
        e.printStackTrace();
    } catch (BadPaddingException e) {
        e.printStackTrace();
    } catch (IllegalBlockSizeException e) {
        e.printStackTrace();
    }
    return inByte;
}

Chargement dynamique

Lisez bien la chaîne d'octets en utilisant la réflexion La méthode defineClass0 de ClassLoader est importante

private void loadClass(byte[] bytes, String name) throws ClassFormatError {
    try {
        String packageName = name.replaceAll("/", ".").substring(0, name.length() - 6);
        Method define0Method = ClassLoader.class.getDeclaredMethod("defineClass0", new Class[]{String.class, byte[].class, int.class, int.class, ProtectionDomain.class});
        define0Method.setAccessible(true);
        Class loadedClass = (Class) define0Method.invoke(getClassLoader(), packageName, bytes, 0, bytes.length, null);
        if (packageName.equals(mainClassName)) {
            this.mainClass = loadedClass;
        }
    } catch (NoSuchMethodException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (InvocationTargetException e) {
        Throwable cause = e.getCause();
        if (cause instanceof ClassFormatError) {
            throw (ClassFormatError) cause;
        }
    }
    stub.add(name);
}

Ça arrive

Étant donné que chaque plug-in est géré par le nom du package + le nom de la classe sur le serveur, s'il existe au moins deux plug-ins difficiles à décompiler en raison d'un conflit entre le nom du package du chargeur et le nom de la classe, le deuxième plug-in et les suivants seront chargés. Échouer. Il n'y a aucun problème lors de la distribution en tant que programme Java autonome.

Solution

Le programme de décompilation difficile JAR (provisoire) a le code source du chargeur dans le corps principal, et le nom du paquet est changé dynamiquement afin qu'il soit compilé à chaque fois. L'argument de getTask a été décidé par essais et erreurs, et il devrait fonctionner normalement.

private void addLoader(String packageName, File target, File bukkitJar) {
    try {
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        String pluginLoaderJava = FileUtils.readFileToString(new File(ClassLoader.getSystemResource("PluginLoader.java").getFile()), "utf-8");
        JavaFileObject file = new JavaSourceFromString("PluginLoader", pluginLoaderJava.replace("{{package}}", "package " + packageName + ";"));

        String[] compileOptions = new String[]{"-d", target.getAbsolutePath(), "-classpath", bukkitJar.getAbsolutePath()};
        Iterable<String> compilationOption = Arrays.asList(compileOptions);
        Iterable<? extends JavaFileObject> compilationUnits = Arrays.asList(file);

        JavaCompiler.CompilationTask task = compiler.getTask(
                null,
                null,
                null,
                compilationOption,
                null,
                compilationUnits);

        task.call();
    } catch (IOException e) {
        e.printStackTrace();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

Ce que je n'aime pas

Le point qu'un JDK est nécessaire pour compiler le chargeur lors de l'exécution d'une décompilation difficile. (Est-il possible de se passer de JDK en modifiant directement le fichier de classe?) Même si les grandes entreprises deviennent open source, qu'en est-il de cacher le code source en premier lieu?

Recommended Posts

Une histoire sur l'effort de décompiler les fichiers JAR
Histoire d'essayer de faire fonctionner le fichier JAVA
Une histoire d'essayer de s'entendre avec Mockito
Ajouter un fichier au fichier jar
Introduction aux fichiers JAR
Une histoire sur la réduction de la consommation de mémoire à 1/100 avec find_in_batches
Une histoire sur la création de chemin PKIX a échoué lors de la tentative de déploiement sur Tomcat avec Jenkins
Une histoire sur le fait d'avoir du mal à construire PHP 7.4 sur CentOS 8 de GCE
Une histoire de malentendu sur l'utilisation du scanner Java (mémo)
Une histoire amusante coincée dans le désordre lors de la tentative d'importation de fx-clj
Comment décompiler un fichier de classe Java
Une histoire sur la fabrication d'une calculatrice pour calculer le taux de monticule d'obus
[Rails] J'ai découvert les fichiers de migration! (Ajout d'une colonne au tableau)
Histoire de changer d'emploi d'un pasteur chrétien (apprenti) à un ingénieur web
Une histoire sur la conversion des codes de caractères de UTF-8 en Shift-jis en Ruby
Une histoire sur l'envoi d'une pull request à MinGW pour mettre à jour la version libgr
Une histoire accro aux espaces réservés des modèles JDBC
Notez que Junit 4 a été ajouté à Android Studio
Une histoire accro à EntityNotFoundException de getOne de JpaRepository
Une histoire sur la prise en charge de Java 11 pour les services Web
Une histoire sur le JDK à l'ère de Java 11
Une histoire qui a mis du temps à établir une connexion
Une histoire très utile sur la classe Struct de Ruby
Une histoire sur la création d'un Builder qui hérite du Builder
L'histoire d'un nouvel ingénieur lisant un programmeur passionné
Une histoire sur la création d'un service qui propose des améliorations à un site Web à l'aide d'une API d'apprentissage automatique