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 VM - use HotSpot instead.
- A release of Zig compatible with your system
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.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>
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 methodmethodName
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 blankI
is the primitive integer typeJ
is the primitive long typeS
is the primitive short typeF
is the primitive float typeD
is the primitive integer typeC
is the primitive char typeB
is the primitive byte typeZ
is the primitive boolean typeLjava/lang/Object;
is the fully qualified classjava.lang.Object
[elementType
is an array ofelementType
.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. 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 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. 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.