Disclaimer: This article is for educational and research purposes only. Modifying commercial software may violate licenses or laws. Use these techniques responsibly, the goal here is to understand how Java apps are built, debug issues, or customize behavior in a safe context.

This article follows a previous one about unpacking and repacking Electron apps. The same techniques can be applied to Java apps, which are often distributed as JAR files (Java ARchive). A JAR file is essentially a ZIP archive that contains compiled Java classes, resources, and metadata. By unpacking a JAR file, you can explore its contents, modify it, and then repack it to create a new JAR file.

Locating the right JAR archive

The first step is to locate the JAR file you want to unpack. This could be the main application JAR or a specific library JAR. These are usually found in the application’s Resources folder or within the application’s installation directory. Look for files with the .jar extension, and identify the one containing the app’s core functionality.

You can use ripgrep or a similar tool to search for JAR files within the application’s directory. Once you have identified the correct JAR file, you can proceed to unpack it.

% rg --binary "LicenseVerifier"
***.app/Contents/Resources/core-9.4.0.jar: binary file matches (found "\0" byte around offset 5)

Unpacking a JAR file

To unpack a JAR file, you can use the jar command-line tool that comes with the Java Development Kit (JDK), but I prefer using tar for its simplicity. Here’s how you can do it:

% JAR_FILE="***.app/Contents/Resources/core-9.4.0.jar"
% mkdir core_class
% tar -xf $JAR_FILE -C core_class

The next step is to explore the unpacked contents. You will find a directory structure that mirrors the package structure of the Java classes. The compiled Java classes are stored as .class files, and you may also find resources such as images, configuration files, and metadata.

Decompiling Java classes

Those .class files are compiled Java bytecode, which can be decompiled back into readable Java source code using tools like JD-GUI or CFR. Here I’m going to use fernflower to decompile the classes and explore the source code.

Clone it and build it following the instructions in the repository, then run it to decompile the JAR:

% mkdir core_java
% java -jar fernflower.jar $JAR_FILE core_java
INFO:  Decompiling class ch/cyberduck/ui/pasteboard/PasteboardService
INFO:  ... done
INFO:  Decompiling class ch/cyberduck/ui/pasteboard/PasteboardServiceFactory
INFO:  ... done
........

Fernflower outputs the decompiled source as a new JAR file in the output directory. We need to unpack this JAR to access the .java source files:

% tar -xf core_java/core-9.4.0.jar -C core_java
% rm core_java/core-9.4.0.jar

Now you have the decompiled Java source code in the core_java directory. Using this source code, you can understand how the application works, identify the parts you want to modify, and make your changes. After modifying the source code, you will need to recompile it back into .class files.

For example in the targeted app, I want to modify the LicenseVerifier class to bypass the license check. After decompiling the classes, I can find the DictionaryLicense.java file in the decompiled source code. I can then modify the code to always return true for the license check.

public boolean verify(LicenseVerifierCallback callback) {
    String publicKey = this.getPublicKey();

    try {
        this.verify(this.dictionary, publicKey);
        return true;
    } catch (InvalidLicenseException e) {
        callback.failure(e);
        return false;
    }
}

I can change it to:

public boolean verify(LicenseVerifierCallback callback) {
    String publicKey = this.getPublicKey();

    try {
        this.verify(this.dictionary, publicKey);
        return true;
    } catch (InvalidLicenseException e) {
        callback.failure(e);
        return true; // Always return true to bypass the license check
    }
}

Recompiling the classes

Once you have modified the source code, you need to recompile it back into .class files. You can use the javac command-line tool that comes with the JDK. Make sure to use the same classpath and compiler options that were originally used to compile the application.

Java Version Compatibility

Before recompiling, you should check which Java version the original classes were compiled with. This is important because using a newer Java version to compile might result in a java.lang.UnsupportedClassVersionError when the app tries to load your modified class:

To find out which Java version the original classes were compiled with, run the following command:

% javap -v core_class/<path_to_original_class_file> | grep "major version"
  major version: 65

Here’s the mapping between Java versions and bytecode major versions:

  • Java 8 = 52
  • Java 11 = 55
  • Java 17 = 61
  • Java 21 = 65

Setting up the Classpath

Your modified class likely references other classes from the application’s JAR files (like imports, parent classes, or dependencies). Without telling the compiler where to find these, compilation will fail with “cannot find symbol” errors. You need to include all the application’s JAR files in the classpath.

First, list all the JAR files in the application’s Resources folder to build the classpath:

% ls -1 "***.app/Contents/Resources"/*.jar | tr '\n' ':'

This gives you a colon-separated list of JAR paths. Now compile your modified Java file using this classpath:

% javac --release 21 -cp "<paste_the_jar_list_here>" core_java/<path_to_modified_java_file>

The --release 21 flag targets Java 21 bytecode. Adjust this number based on the major version you found earlier (e.g., use --release 8 for major version 52).

The compiled .class file is generated in the same directory. Now copy it back to replace the original class in the unpacked JAR directory:

% cp core_java/<path_to_modified_class_file> core_class/<path_to_modified_class_file>

Repacking the JAR file

After recompiling the modified classes, you need to repack them into a new JAR file. You can use the jar command-line tool for this purpose. Here’s how you can do it:

% jar cf core_edited.jar -C core_class .

Finally, replace the original JAR file with the modified one:

% mv core_edited.jar $JAR_FILE

Code signing on macOS

After modifying the JAR file, modern macOS will refuse to launch the app if the signature is invalid, even with Gatekeeper disabled. You need to resign the application:

% codesign --force --deep --sign - "***.app"

This command uses an ad-hoc signature (indicated by the - sign), which allows the app to run locally. Note that this removes any previous signature, meaning the app won’t pass verification checks, but it will launch on your machine.

That’s it :)