Documentation

Note: this documentation is for the latest version of JNIC. 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.10.1/
  	doc/
  	lib/
    zig.exe
    LICENCE
  jnic-3.6.0.jar

Activation

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.

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>
	<options>
		<stringObf>true</stringObf>
		<flowObf>true</flowObf>
		<fastCompile>true</fastCompile>
		<useIntrinsics>true</useIntrinsics>
	</options>
  <includeAnnotation>some/Annotation</includeAnnotation>
  <excludeAnnotation>some/other/Annotation</excludeAnnotation>
	<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

The following options are available in the <options> tag.

stringObf

JNIC is able to encrypt both C and Java string literals in source code. Keys are generated by java.security.SecureRandomopen in new window. The encryption algorithm is a variant of ChaCha20open in new window, an industry standard algorithm used in protocols such as QUIC. JNIC's string encryption also hides references to Java methods, as these will otherwise be present as strings in the native library.

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.

flowObf

JNIC can apply additional control flow obfuscation to the native code it generates. This significantly complicates reverse engineering, but it also further increases file size and reduces performance of the resulting binary. Note that, due to the additional complex control flow added by this option, the obufscation process will be slower with it enabled. JNIC applies Control Flow Flatteningopen in new window with an encrypted dispatch table, significantly complicating reverse engineering.

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

WARNING

The <flowObf> option complicates reverse engineering, but it also increases compilation time, particularly for methods that have many nested try-catch blocks.

fastCompile

This option adjusts the optimisation flags given to the C compiler to significantly improve compilation speeds for large projects, particularly those that have already been processed by a Java obfuscator. Note that this option does not improve the speed of the generated native code, and the resulting native code may be easier to reverse.

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

useIntrinsics

This option allows JNIC to replace calls to certain Java API methods with handwritten optimised replacements. In addition to improving performance, this complicates reverse engineering by preventing the instrumentation of these method calls at the JVM level. Supported methods include:

  • java.lang.Object.getClass()
  • java.lang.String.equals(java.lang.Object)
  • java.lang.String.isEmpty()
  • java.lang.String.length()

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

Inclusions and exclusions

Using annotations

The annotation system is the most flexible way of selecting methods for translation. JNIC does not supply an annotation library to compile against - you must create your own annotations with @Retention(RetentionPolicy.RUNTIME) and supply JNIC with their names through the <includeAnnotation> and <excludeAnnotation> tags (both of which are optional).

These annotations should not be used for any purpose other than marking methods for obfuscation by JNIC, as JNIC will delete them from the input JAR as part of the obfuscation process. An example configuration is shown below:

package some.package;
public @interface Include {}
package some.package;
public @interface Exclude {}
<jnic>
    <includeAnnotation>some/package/Include</includeAnnotation>
    <excludeAnnotation>some/package/Exclude</excludeAnnotation>
</jnic>

These annotations can then be used to mark classes and methods for translation. Annotations on methods override annotations on classes. All annotations override <match> tags, which are described in the next section.

@Include
class Example {
    void foo() { ... }
    @Exclude void bar() { ... }
}

In this example, JNIC will only translate foo, because Example is marked for inclusion. The exclude annotation on bar overrides the include annotation on the class.

Using <match> tags

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
  • JNIC can apply additional obfuscation, such as string encryption and control flow flattening, without any additional effort.

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: Vincent