# Documentation

# Tutorial

This section will guide you through the process of protecting an application with JNIC.

# Prerequisites

To use JNIC, you need:

  • A 64 bit Java 8 (or newer) JDK installed.

WARNING

There may be issues with the Eclipse OpenJ9 VM - use HotSpot instead.

  • A suitable 64 bit C compiler for your system.

TIP

We recommend GCC on Linux, MinGW/MSYS2 on Windows and clang on macOS. The translated application will only work on platforms that you have compiled binaries for. Make sure you install the 64 bit versions of these compilers.

JNIC generates makefiles that are compatible with GNU style toolchains.

# Activation

# Small developer and per-user standard licences

With your activation key, run the following command:

$ java -jar jnic.jar activate <activationKey>

Upon successful activation, JNIC will create a file in the working directory named jnic.licence. This file must be kept in the same directory as JNIC.

# Per-device standard licences

Email [email protected] for activation instructions.

# Configuration

JNIC uses XML for its configuration files. The following is an sample configuration file that instructs JNIC to translate all methods in the input jar file.

<jnic>
	<include>
		<match/>
	</include>
</jnic>

Save this as config.xml in the same directory as JNIC and the input JAR file.

# Translation

Execute JNIC as follows:

$ java -jar jnic.jar translate config.xml program.jar program_jnic/ program.xml

JNIC will output program.xml and create a folder named program_jnic in the same directory.

  • program.xml is a list of every method that was translated by JNIC. It should not be removed or modified as it is required for relinking.
  • program_jnic contains the translated output code. This must be compiled before JNIC relinks it to the program.

# Compilation

Before continuing, you need to compile each source file inside program_jnic into a correctly named library file. JNIC generates makefiles to streamline this process.

TIP

JNIC's output file will only run on platforms for which you have compiled a native library. If you:

  1. Need your program to run on operating systems other than the one you use
  2. Do not have access to a physical device with them installed

You may need to set up a virtual machine to build native libraries for those platforms.

WARNING

All of the following instructions depend on the JAVA_HOME environment variable. If it is not set on your system, you must set it to the correct value or the compilation will not work. JAVA_HOME should be equal to the fully qualified path to the directory in which your JDK is installed. This varies depending on your platform and JDK version. You should follow online instructions to set JAVA_HOME for your specific situation if it does not already have a value.

# Using the provided Makefiles

First ensure that your system has make. It should be present by default on most linux distros, and is provided with command line developer tools on macOS. On Windows, we recommmend installing MinGW/MSYS2 to use the makefiles. On MSYS2, you will need to install mingw and make separately with pacman -S mingw-w64-x86_64-gcc make. On slow connections, you may wish to also use the --disable-download-timeout option when updating or installing with pacman.

After changing directory to program_jnic, you should now be able to build the required library files with make:

# Linux
$ make -f makefile_linux -j 4
# Windows/MSYS2

Make sure you're using the MinGW-W64 prompt rather than the generic MSYS2 prompt. It should be called 'MinGW-w64 64-bit Shell' and can usually be found in the Start menu. You can check that you have everything set up correctly by running gcc -v.

$ make -f makefile_windows_msys2 -j 4
# macOS

The $JAVA_HOME environment variable usually not available. You should set it beforehand with export JAVA_HOME=`/usr/libexec/java_home`

$ make -f makefile_macos -j 4

# Relinking

To complete the process, JNIC must be executed one more time to relink the generated library with the original program:

java -jar jnic.jar relink program.xml program.jar program.jnic.jar win:64:program_jnic/win32 lin:64:program_jnic/linux mac:64:program_jnic/darwin

win:64:program_jnic/win32 is a library specifier - it indicates that JNIC should link the libraries in the directory program_jnic/win32 to the output JAR for 64-bit windows platforms. All library specifiers as optional, but you must have at least one for the transformed program to run.

The transformed program is saved to program.jnic.jar.

# Configuration guide

An example top level configuration may be seen below:

<jnic>
	<mangle>false</mangle>
	<stringObf>false</stringObf>
	<include>
		<!-- <match> tags -->
	</include>
	<exclude>
		<!-- <match> tags -->
	</exclude>
</jnic>

# Options

# mangle

If <mangle> is set to true, JNIC will use JNI name mangling instead of JNI method registration to link the native library to loaded class files. This strategy can fail if a name obfuscator generates two methods which differ only by return type, resulting in identical JNI mangled names (and hence a name collision).

Additionally, JNIC will not be able to translate the static block of a class if <mangle> is set to true.

If it is not specified, this option defaults to false (recommended).

# stringObf

JNIC is able to encrypt both C and Java string literals in source code. Each literal is converted into two arrays which generate the original string when combined using xor. The decryption stub that JNIC injects into the source code is designed to be inlined into caller methods and vectorised by your compiler. Keys are unique for each encrypted string and are generated by java.security.SecureRandom.

It is important to note that JNIC's string encryption is not fundamentally irreversible, as decryption keys have to be embedded into the native library. The same is true of any obfuscator, be it for Java or native code.

If it is not specified, this option defaults to false.

WARNING

The <stringObf> option complicates reverse engineering, but it also significantly increases compilation time and the file size of the resulting library.

# Inclusions and exclusions

JNIC will only queue a method for translation if both of the following apply:

  • It is matched by at least one <match> tag in <include>
  • It is not matched by any <match> tag in <exclude>

WARNING

Code translated by JNIC will run significantly slower than the same code running directly on the JVM. This is due to the inherent overhead of native function calls which cannot be avoided. It is recommended that you only use JNIC to translate sensitive parts of your application which are not critical to performance.

An example <match> tag follows:

<match className="net/konsolas/jnic/.*" methodName="main" methodDesc="(\[Ljava/lang/String;)V" />
  • className matches the name of the class containing the method
  • methodName matches the name of the method. The static block is named <clinit>
  • methodDesc matches the JVM method descriptor of the method

All <match> attributes are regular expressions. Wildcards and escapes function as they do in Regex.

JVM method descriptors

methodDesc takes this form: (parameterTypes)returnType, where the following are valid types:

  • V is the void type, used only for the return value of a method. If a method takes no parameters, parameterTypes should be left blank
  • I is the primitive integer type
  • J is the primitive long type
  • S is the primitive short type
  • F is the primitive float type
  • D is the primitive integer type
  • C is the primitive char type
  • B is the primitive byte type
  • Z is the primitive boolean type
  • Ljava/lang/Object; is the fully qualified class java.lang.Object
  • [elementType is an array of elementType. elementType may itself be an array for multidimensional arrays.

Note that [ is a regex special character and needs to be escaped with a \

For example:

Method Signature JVM Method Descriptor
void main(String[] args) ([Ljava/lang/String;)V
String toString() ()Ljava/lang/String;
void wait(long t, int n) (JI)V
boolean compute(int[][] k) ([[I)Z

Any omitted attribute matches everything. For example, <match/> matches every method in every class by omitting the className, methodName and methodDesc tags, and

<match methodName="main" />

matches every method named main, regardless of class or descriptor.

# Combining JNIC with Java obfuscators

It's recommended that you first use a suitable Java obfuscator before protecting your application with JNIC.

Broadly, JNIC will be compatible with Java obfuscators that:

  • Emit well-formed Java 8+ bytecode, class files, and JAR files
  • Do not perform static or runtime integrity checks

On Windows, you will additionally need to ensure that your obfuscator does not emit class names that differ only by case, as Windows uses a case-insensitive filesystem.

Additionally, it is important that you do not apply name obfuscation after JNIC, as this will break links to native code.

# Miscellaneous

Other information which doesn't fit neatly in the other categories.

# Java bytecode compatibility

Older Java compilers may emit the JSR and RET instructions, which is a deprecated instruction that is not permitted in Java 7 bytecode or newer. JNIC only supports Java 8 bytecode and above, and is therefore unable to handle the JSR/RET instruction. If you have older libraries shaded in your application which include Java 6 class files, they should be excluded from translation.

Java 11 class files (and newer) permit a new feature known as ConstantDynamic, which allows constant pool entries to be dynamically initialised at runtime by a bootstrap method. At the time of writing, nothing appears to use this feature. As such, class files containing ConstantDynamic entries are currently not compatible with JNIC.

# Performance

JNIC aims to emit fast code, but the limitations of the JNI interface bottleneck the performance of any translated methods. In particular, method invocations, field access and array operations are slow compared to Java. Arithmetic, casting, control flow and local variable access are still very fast in JNI code (and can even be optimised by your C compiler).

# Manual compilation

If you want to use a different toolchain, or don't want to use the auto-generated makefiles, you can compile each source file separately It's possible to infer the required filenames from the provided makefiles:

  • Compiled libraries for each platform should be in their own folder
  • The file source_file.c should be compiled as a shared library to source_file.jnilib

You must first locate the JNI header files on your system. They are typically located at $JAVA_HOME/include and $JAVA_HOME/include/<platform>, where $JAVA_HOME is an environment variable which contains the location where the JDK was installed.

Here are some example compilation commands.

# Linux
$ gcc -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -std=c99 -fPIC -shared -O3 -s source_file.c -o linux/source_file.jnilib
# Windows/MSYS2
$ gcc -I"$JAVA_HOME\include" -I"$JAVA_HOME\include\win32" -std=c99 -fPIC -shared -O3 -s source_file.c -o win32/source_file.jnilib
# Windows/MSVC
> cl /I"%JAVA_HOME%\include" /I"%JAVA_HOME%\include\win32" /LD /O2 source_file.c /Fe"win32/source_file.jnilib"
# macOS
$ clang -I"`/usr/libexec/java_home`/include" -I"`/usr/libexec/java_home`/include/darwin" -std=c99 -fPIC -shared -O3 -s source_file.c -o darwin/source_file.jnilib

You should not change the names of the files or generated binaries - JNIC relinks to them by name.

# Semantics

The vast majority of the behaviour of native Java bytecode is preserved, with a few minor exceptions:

  • Methods with polymorphic signatures (or that are otherwise JNI incompatible), such as MethodHandle#invokeExact, are replaced with JNI compatible alternatives (i.e. MethodHandle#invokeWithArguments)
  • INVOKEDYNAMIC instructions are emulated and the call stack of bootstrap methods is likely to be different from behaviour in a JVM.
  • Any attempt by the code to check if it's running in a native method will likely succeed, although it is unlikely for normal code to do this.

# Writing more secure code

JNIC is a powerful obfuscator, but its effectiveness can be limited by the code that it translates. Consider the following:

Unsafe

public class App {
	public static void main(String[] args) {
		if(!checkLicence()) {
			System.err.println("Invalid licence");
			return;
		}
		initApp();
		runApp();
	}

	private static native boolean checkLicence(); // Protected by JNIC
}

In this example, even though the licence checking code is protected by JNIC, it is easy for an attacker to modify the main method to ignore its result, e.g. by removing the ! from the call to checkLicence.

Better

public class App {
	public static void main(String[] args) {
		checkLicenceAndInitApp();
		runApp();
	}

	private static native void checkLicenceAndInitApp(); // Protected by JNIC
}

Here, removing the call to checkLicenceAndInitApp won't help an attacker because then the application will not function correctly (as it won't be initialised). It's fine to protect an application's initialisation code with JNIC because it only happens once, so it's not performance critical.

# Support

If your question is not answered by the documentation or the FAQs below, email [email protected] or join our discord server.

# Frequently asked questions

# Will JNIC have a significant impact on my application's performance?

Many code protection tools must make a trade-off between performance and security. JNIC exists at the far end of the scale, and we recommend that you only use it on sensitive code or code that is otherwise not performance critical.

# Does JNIC support lambdas/streams/exceptions/threads/locks/...?

JNIC operates on compiled Java bytecode, and hence supports any bytecode compiled for a Java 8 or newer JVM. As a consequence of this, JNIC supports all language features in Java, and additionally supports other programming languages which run on the JVM.

# What advantages does JNIC have over full ahead-of-time compilation?

JNIC is designed to be compatible with existing Java tools, frameworks, and obfuscators. This is particularly important if your application is, for example, a plugin that needs to integrate with a larger Java application.

  • With JNIC, you don't have to bundle an entire java runtime along with your application
  • With JNIC, you don't need to distribute different builds of your application for different platforms
  • JNIC can operate on already obfuscated Java bytecode
  • Code generated by JNIC is interoperable with other java applications

# What advantages does JNIC have over writing JNI methods myself?

It's surprisingly difficult to write code that uses the Java Native Interface, and such code is typically even harder to debug. JNIC lets you write (and test!) all of your code in Java before protecting it. Additionally:

  • JNIC can translate the output of a Java obfuscator like Zelix Klassmaster or Stringer
  • JNIC can translate things in the Java API which have no direct equivalent in C, like lambdas, method references and streams
  • With JNIC, you don't need to know how to use JNI, or C, nor do you need to write code that links the native library to your application at runtime (JNIC injects this automatically)
  • JNIC can translate your existing Java code - you don't need to waste time rewriting parts of your application that you've already finished

# Can I apply additional obfuscation on the native binaries before relinking them with JNIC?

This is of course possible, though we cannot guarantee the compatibility of any particular native code obfuscation tool - we encourage you to request a trial if you want to test one of them. Additionaly, further obfuscating the native binaries may prove to be unnecessary if you had already used a Java obfuscator before running JNIC.

# Can I apply additional obfuscation on the output JAR file after running JNIC?

Using name obfuscation on a translated method, or any method/field/class referenced by a translated method will result in a crash at runtime due to an unsatisfied link. You are free to use string obfuscation, reference obfuscation, resource encryption, etc.

# Open Source

Open source software used by JNIC and associated licences