Documentation

Note: this documentation is for JNIC 3.3.0. Documentation for older versions is available here

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 11 (or newer) JDK installed.

WARNING

There may be issues with the Eclipse OpenJ9 VMopen in new window - use HotSpot instead.

TIP

Zig should be extracted into the same directory as the JNIC jar file. e.g.

jnic/
  zig-...-0.9.0/
  	doc/
  	lib/
    zig.exe
    LICENCE
  jnic-3.0.0.jar

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 for 64-bit Windows (see configuring targets for more).

<jnic>
	<targets>
		<target>WINDOWS_X86_64</target>
	</targets>
	<include>
		<match/>
	</include>
</jnic>

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

Obfuscation

Execute JNIC with 3 arguments:

  • The input JAR file
  • The output JAR file
  • The config file
$ java -jar jnic.jar program.jar program.jnic.jar config.xml

Here, JNIC obfuscates program.jar and saves the output to program.jnic.jar.

Configuration guide

An example top level configuration may be seen below:

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

Configuring targets

You must configure at least 1 target for JNIC to compile native libraries. The possible targets are:

  • WINDOWS_X86_64
  • WINDOWS_AARCH64
  • MACOS_X86_64
  • MACOS_AARCH64
  • LINUX_X86_64
  • LINUX_AARCH64

WARNING

Selecting additional targets increases both obfuscation time and the resulting file size. You should only configure targets that you need for your application.

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.SecureRandomopen in new window.

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>

DANGER

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 SignatureJVM 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. You should also ensure that your obfuscator does not use special characters for class, method or field names, as C linkers tend not to like these.

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).

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.
  • Integer division by zero does not throw ArithmeticException.
  • 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 serveropen in new window.

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. 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

Last Updated:
Contributors: konsolas