Implementing the Java Native Interface to Harden Native Code


Options



Before You Begin

Purpose

In this tutorial, you apply the Java Native Interface (JNI) secure coding guidelines and implement exception handling and memory management.

Time to Complete

Approximately 120 minutes

What Do You Need?

A virtual machine based on Oracle Linux 6.5 64-bit. The installation guide contains the steps to install and configure the virtual machine (VM). The guide covers the following:  

  • Downloading and Installing VBOX   
  • Downloading the VM from Oracle Technology Network
  • Importing VM on the VBOX
  • Installing JDK in the VM
  • Downloading exercises in the VM

Note: Perform all steps in the installation guide before you start this tutorial.

Introduction to JNI

What Is JNI?

JNI allows Java code that runs inside a Java Virtual Machine (JVM) to interoperate with applications and libraries written in other programming languages, such as C, C++, and assembly. The most important benefit of JNI is that it imposes no restrictions on the implementation of the underlying JVM. Therefore, JVM vendors can add support for JNI without affecting other parts of the JVM. Programmers can write one version of a native application or library and expect it to work with all JVMs that support JNI. Native methods in a shared library (.dll, .so, .dylib) are connected to a Java method definition with the native keyword.  

JNI Development Workflow

When you develop JNI-based libraries, the Java source is transformed into class files using the javac compiler. Then, you generate the native headers (*.h) with the javah utility. These headers are included in the implementation source code (*.c,*.cpp), and are compiled and linked to a platform-dependent shared library using a C/C++ compiler. A Java class contains native methods that typically load the shared JNI library in a static initializer. During runtime, the JVM invokes methods in the shared library when it encounters a JNI method definition during control flow.

 

JNI Threat Model

The Java runtime environment safely manages memory: It performs automatic checks on access within array bounds, and it has no explicit pointer arithmetic. When you compare Java with C and C++programs, you find that C and C++ programs can fail because of memory corruption that is caused by heap and buffer overflows. Because the Java runtime environment often executes nontrusted code, it must protect against unauthorized access to privileged resources, which results in a different set of coding antipatterns than C and C++. To keep Java programs safe when you use JNI, be aware of the typical C and C++ antipatterns during development of JNI code.

JNI Runtime Behavior

A specific behavior of a runtime environment is considered a vulnerability if it allows an attacker to subvert the checking mechanisms that protect the confidentiality, integrity, and availability of an environment. In the context of Java, this applies to bypassing the security manager checks. Native functions are capable of bypassing the Java security architecture. The action of native code is not checked against the security policy or visibility declarations (for example, public, protected). For example, JNI code might alter the values of normally immutable classes such as java.lang.Integer, and it also might allow read and modify (for example, private key data) from arbitrary private fields. If attackers can inject values into a native function that doesn't have appropriate range checks, they could trigger buffer or heap overflows that could lead to arbitrary code execution on behalf of the attackers.

Components of a JNI Application

JNI Source Code

In the JNI program, native language stubs are attributed with the native keyword. Implementation of the native method itself is provided in a native library, and you link the method to the JVM address space with the System.loadLibrary command.

Generated JNI Headers

The javah JDK tool generates native prototypes from those methods that are marked with the native keyword. The jni.h library contains the definitions required for JNI development.

Native JNI implementation

The JNI methods are implemented using the generated prototypes. The JNICALL and JNIEXPORT macros are translated to the platform counterpart declarations. With every typical C and C++ program, memory is explicitly allocated and released.

JNI Makefile File

The Makefile file automates the build process. You execute it by running the make binary.

Creating the CHelloWorld.java File

In this section, you perform the following tasks:

  • Compile the CHelloWorld.java file.
  • Test CHelloWorld.java for security issues.
  • Create a secure version of the program by applying code changes and using secure compile and link flags.
  • Automate the program with Makefile.

Compiling Source Files

  1. Open the directory, click the terminal icon, and browse to the Exercise 0 directory:

    cd ~/HOL6325/LABS/Exercise_0/

  2. Compile the java source file:

    javac CHelloWorld.java

  3. Generate the header files:

    javah CHelloWorld

    The CHelloWorld.h header file is created.

  4. Compile the CHelloWorld.c C code:

    gcc CHelloWorld.c

     Error messages are displayed because the compile command didn't include the directories.
  5. alt description here
  6. Compile again, but include the platform-independent JNI headers (jni.h) and the platform-dependent JNI headers. The header files are in the JDK include directory: /usr/java/latest/include and /usr/java/latest/include/linux.

    gcc –I /usr/java/latest/include  -I /usr/java/latest/include/linux CHelloWorld.c
    alt description here
  7. Link the binary code to a shared library to create a JNI library. In Linux, use the -shared and –fPIE options and add a file name for the library. The file name is specified in the loadLibrary call. Use lib as the prefix and .so as the extension.

  8. gcc –o libCHelloWorld.so –shared –fPIE –I /usr/java/latest/include -I /usr/java/latest/include/linux CHelloWorld.c
    alt description here

     The libCHelloWorld.so file is created.

  9. Load the CHelloWorld.java file:

    java CHelloWorld

    alt description here

    The stack dump shows that the loadLibrary call failed because it didn’t find the native library, even though it’s in the current directory. To successfully load the CHelloWorld.java file, you must specify the directory containing the native library with the java.library.path system property.

  10. Load the CHelloWorld.java file:

    java –Djava.library.path=. CHelloWorld

    alt description here

    The program executes successfully and "Hello World" is displayed.

Testing Code for Security Issues

In this section, you investigate the code for security issues and test the program with arbitrary values.

  1. Add MyParameter to the command:

    java –Djava.library.path=. CHelloWorld MyParameter

    alt description here

    The program executes successfully.

  2. Add special characters as the parameter:

    java –Djava.library.path=. CHelloWorld one%ntwo%nthree%n
    alt description here

    The crash occurs if special characters are passed in the input parameter. The crash occurred in the IO_vfprintf function inside libc. The input parameter, one%ntwo%nthree%n, contains the %n format string. This format string writes the number of successfully written characters to an integer pointer.

Creating Secure Program Versions

In this section, you create a secure version of the program by applying the code changes and using secure compile and link flags.

  1. Find the log file name:

    ls
    alt description here

    The hs_err_pidxxxx.log (replace xxxx with the actual process ID) hotspot log file provides information about the type of crash.

  2. Open the hs_err_pidxxxx.log file:

    vi hs_err_pidxxxx.log
    alt description here

    The siginfo indicates that the reason for the crash is a processor signal 11: A segmentation violation (SIGSEGV,) was detected. The SIGSEGV violation occurred because of an access error (si_code=2) while writing the address specified in si_addr.

  3. Verify printf by using GCC.

    Because printf and the related standard library calls (sprintf) may cause problems, you can check their proper use with GCC during compilation. The –Wformat and –Wformat-security compile flags emit a warning message when a printf allows variable input in its first parameter. The -Wformat -Werror=format-security compile flag ensures that code is compiled even when GCC finds vulnerable use of format strings.

    gcc –o libCHelloWorld.so –shared –fPIE –I /usr/java/latest/include -Wformat -Werror=format-security -I /usr/java/latest/include/linux CHelloWorld.c
    alt description here

    Because the compiler rejects source code in this vulnerable shape, you need a patch. You can find out about the vulnerable code location by analyzing the calls to printf.

  4. Open the CHelloWorld.c file:

    vi CHelloWorld.c
    alt description here
  5. Press Insert to edit the code.

  6. Replace the call to printf with a string variable format:

    printf("%s",nativeString);
    alt description here
  7. Press Esc, and then enter :wq to write and quit.

    alt description here
  8. Add input, one%ntwo%nthree%n, to the command, and verify the output.

    java –Djava.library.path=. CHelloWorld one%ntwo%nthree%n

    alt description here
    After you apply the fix, GCC builds the program binary with the secure compile settings. Running the program with the previous input no longer generates a crash dump.

Automating the Program

In this section, you automate the program with Makefile.

  1. Run the make binary to execute the Makefile, and specify the class name in the CLASSNAME environment variable.

     CLASSNAME=CHelloWorld make

    alt description here

    The Makefile is updated.

    CLASSNAME=CHelloWorld make runjava -verbose:jni -Xcheck:jni -cp . -Djava.library.path=. CHelloWorld

    Note: The Makefile also specifies two options for tracing the linking and execution of JNI methods:
    • The –Xcheck: jni option traces JNI calls and performs a range of basic checks on the sanity of arguments passed to a JNI method. However, these checks don't replace secure programming practices.
    • The –verbose: jni option dumps the registering and linking actions for JNI methods.
    alt description here
  2. Execute the Makefile:

    CLASSNAME=CHelloWorld make run
    alt description here

    The program works as expected. When you execute the Makefile, the output is enhanced with JNI tracing messages. You have now automated the build of your JNI program.

  3. Close the terminal.

Note: You need to be aware of C and C++ secure coding practices. Vulnerabilities caused by bad practices (like allowing attacker-controlled format strings) are still relevant. Follow the documented recommendations for secure compile and link to enable optimal use of platform protection mechanisms.

Applying JNI Secure Coding Guidelines

In this section, you apply JNI secure coding guidelines to a JNI utility class. The class uses native code that reads a string from stdin and concatenates the string with the host name. The class also places the result in an instance of java.lang.String.

Perform the following tasks:

  • Compile the source and link the shared library with secure compiler and linker settings.
  • Test the JNI utility class.
  • Verify that the class is secure. If it isn't, identify the affected code parts and the input values that trigger the bug.
  • Modify the code for a secure solution.

Compiling Source Files

  1. Open the directory, click the terminal icon, and browse to the Exercise 1 directory:

    cd ~/HOL6325/LABS/Exercise_1/

  2. Compile the code with Makefile:

    CLASSNAME=ReadLine make
    alt description here

    If you use the provided Makefile headers, the class file and the dynamic library messages are generated.

  3. Open the ReadLine.c C file:

    vi ReadLine.c

    alt description here
  4. Replace printf()with a call to puts():

    puts(thebuffer);
    puts("\n");
    alt description here
  5. Press Esc, and then enter :wq to write and quit the ReadLine.c C file.

  6. Compile the code:

    CLASSNAME=ReadLine make

    alt description here

    The compilation is successful, and you can execute the program.

Testing the JNI Utility Class

  1. Run the program:

    CLASSNAME=ReadLine make run

    The program starts executing and waits for input from the user.

  2. Enter 123.

    alt description here

    The program displays the entered text.

  3. Enter a longer string. For example, enter:

    01234567890123456789012345678901234567890123456789012345678901234567890.
  4. alt description here

    The program crashed with the following error: The –fstack-protector-all option detected a stack corruption scenario and stopped the program. This segmentation violation could harm security on platforms that don't support this flag.

Creating a Secure Program

  1. Open the ReadLine.c C file and examine the code:

    vi ReadLine.c
    alt description here
  2. Notice the following code:

    • char thebuffer[64];
      Because the input consisted of 70 characters, but the declared array size was smaller, the processing of the entered string wrote past the reserved 64-byte range of the stack and damaged other important data.  

    • gets(thebuffer);
      Because the gets function doesn't check for a maximum size, it's impossible to limit the input to a proper size. The better alternative is to use fgets, which accepts a parameter to specify the input length.
    • In the next steps, you implement the fixes.

  3. Limit the lengths of the entered string.

       char hostnamebuf[32];
      puts("Enter a string [32 chars max]:");   
      fgets(thebuffer,32,stdin); 

    You reduce the size of the string that contains the host name. You want host names that are 32 bytes or fewer; otherwise, the host name isn't appended. Also you limit the length of the input string to 32 bytes by replacing the usage of the unsafe gets function with a call to the length-delimited fgets().
  4. Increase the array length to a safe value. 
  5. To apply the first fix, edit the ReadLine.c file:
    hostnamebuf[31] = '\0';
    int c =  gethostname(hostnamebuf,31 );

    This change adjusts the safety null byte in hostnamebuf and calls gethostname while storing the return value.

    if (!c) {   
        strcat(thebuffer,hostnamebuf);           
    }   

    This change appends only the host name when the gethostname function successfully acquires it. This change prevents a corrupted temporary string from being appended.

  6. The output is as follows:

    alt description here

Verifying the Secure Program

  1. Run the program:

    CLASSNAME=ReadLine make run
  2. The program executes and waits for input from the console.

  3. Enter a longer string. For example, enter the following:
    11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111.

    alt description here

    The program works as expected. When you compare native code with Java, you see that built-in array bounds don't check native code. However, compiler flags detect buffers that are written with overlong string values.

  4. Close the terminal.

Violating Visibility Rules with JNI

In this section, you learn how the JNI code can subvert the Java security model, because JNI has access to all information hidden in private fields and methods. You learn how native code changes the internal value of an integer, which breaks the immutability property of the java.lang.Integer type.

Perform the following tasks:
  • Compile the source and link the shared library with secure compiler/linker settings.
  • Test the code.
  • Start the program with a security manager, and define a minimal security policy.
  • Write a Java program that causes the same effect as the native code, and define a minimal security policy.

Compiling Source Files

    1. Open the directory, click the terminal icon, and browse to the Exercise 2 directory:

      cd ~/HOL6325/LABS/Exercise_2/
    2. Open the MutableInteger.java file, review the code, and then close the file.

      vi MutableInteger.java.
      alt description here
    3. Open the C file, review the code, and then close the file.

      vi MutableInteger.c
      alt description here

Testing Code for Security Issues

Use a security manager to determine whether a security policy can help to better control the access facilities of native code. Start the program with a security manager and define a minimal security policy.

  1. Compile the program with the supplied Makefile file:

    CLASSNAME=MutableInteger make

    alt description here
  2. Run the Makefile file:

    CLASSNAME=MutableInteger make run
    alt description here

    The output illustrates the fundamental problem with JNI: You can't prevent native code access to fields that are inaccessible to the Java code.

    java -Xcheck:jni -Djava.security.manager -Djava.security.policy=ex2.policy -cp . -Djava.library.path=. MutableInteger

    alt description here

    The error message indicates that RuntimePermission is missing in the policy file.

Defining a Security Policy

  1. Create the ex2.policy policy file:
    vi ex2.policy

  2. Add RuntimePermission to the policy file:

    grant { permission java.lang.RuntimePermission "loadLibrary.MutableInteger" ; };

    alt description here

    java  -Xcheck:jni  -Djava.security.manager -Djava.security.policy=ex2.policy -cp . -Djava.library.path=. MutableInteger

    alt description here

    The error message indicates that PropertyPermission is missing in the policy file.

  3. Edit the ex2.policy policy file:

    vi ex2.policy
  4. Add PropertyPermission to the policy file:

    permission java.util.PropertyPermission "user.dir", "read" ;

    alt description here
  5. Execute the ex2.policy policy file for the third time:

    java  -Xcheck:jni  -Djava.security.manager -Djava.security.policy=ex2.policy -cp . -Djava.library.path=. MutableInteger

  6. alt description here

    You successfully loaded the native library with an enabled security manager. No further permissions are required to modify fields that are inaccessible to Java code. You can't limit this capability because the security manager can't intercept native code modifications of Java fields.

Developing a Pure Java Solution

In this section, you develop a Java solution for the private field. You replace the native code solution with calls to the Reflection API to modify the private field value. Next, you add a policy that allows the program to run under a security manager. 

  1. Examine the MutIntReflection.java file.

    alt description here
  2. Compile the following commands:

    javac MutIntReflection.java

    java MutIntReflection

    The output is displayed.

    alt description here
  3. Add RuntimePermission to the ex2reflect.policy file:

    grant {
     permission java.lang.RuntimePermission "accessDeclaredMembers";  
     permission java.lang.reflect.ReflectPermission "suppressAccessChecks";
    };

    alt description here

    java -Djava.security.manager -Djava.security.policy=ex2reflect.policy MutIntReflection
    alt description here

    You successfully created a pure Java solution that modifies the private field in an Integer instance.

  4. Close the terminal.

Handling Exceptions

In this section, you implement exception handling in the JNI code.

Perform the following tasks:

  • Compile the source and link the shared library with secure compile and link settings.
  • Test the code.
  • Use a debugger to verify that the JNI code contains security vulnerabilities.
  • Fix the vulnerable code parts, so that the Java program can safely complete the entire loop of array sizes.

In this example, the native code copies a Java-side byte array into a fixed-size native buffer. When the passed byte array is too large to fit in the native buffer, the native code throws an exception back to the Java layer.

Compiling Source Files

    In this section, you compile the source and link the shared library with secure compile and link settings.

    1. Open the directory, click the terminal icon, and browse to the Exercise 3 directory:

      cd ~/HOL6325/LABS/Exercise_3/

      alt description here
    2. Open the VulnerableExceptionHandling.java file:

      vi VulnerableExceptionHandling.java

      alt description here
    3. Open the VulnerableExceptionHandling.c C file:

      vi VulnerableExceptionHandling.c

      alt description here

Testing the Code Using Makefile

  1. Run the VulnerableExceptionHandling file:

    CLASSNAME=VulnerableExceptionHandling make run

    alt description here

    The program copied the byte array for sizes 256 and 512 because they’re smaller than the reserved native array size. The 576-byte array threw a warning when it was copied: One of the –Xcheck:jni checks found a suspicious call to a JNI function while an exception was pending. The crash dump indicates that the corruption caused a later crash in a memory operation, but it was caught by the GCC stack protection.

  2. Edit Makefile:

    SECURECOPTS=-Werror=format-security -Wformat -D_FORTIFY_SOURCE=2 #-fstack-protector-all --param ssp-buffer-size=4

    alt description here

    When you disable the stack protection in Makefile, you can discover the root cause of the stack corruption.

  3. Clean the intermediate file:

    make clean
    alt description here
  4. Execute the VulnerableExceptionHandling class file.

    CLASSNAME=VulnerableExceptionHandling make run

  5. alt description here

    The program fails with an error:

    • From the canary value of 42 (which should be 0), you see that the destination array was written beyond its bounds (the adjacent stack memory location of the canary).
    • The crash occurred in the memcpy standard library function.

Testing Code for Security Issues

  1. Investigate the code with the gdb GNU debugger:

    gdb --args java -Xcheck:jni -cp . -Djava.library.path=. VulnerableExceptionHandling

    The debugger stops at the program's first instruction.

    alt description here
  2. Enter r to continue.

    The following output is displayed:

    alt description here

    Ignore the debugger message about missing debuginfo information.

  3. Disassemble the native instruction at the crash location:

    (gdb) disass $pc,$pc+1

    alt description here

    The $pc and $pc+1 parameters indicate the crash location.

  4. Verify the value of the rsi process register:

    (gdb) i r rsi

    alt description here

    The rsi register is filled with 0x2a bytes, and 0x2a is hexadecimal for 42. Therefore, the rsi register was initialized from a corrupt stack when the array was written beyond its borders.

  5. Execute the bt backtrace command to examine where the crash occurred.

    (gdb)bt

    alt description here

    The crash occurred in the jni_ReleaseByteArrayElements call when it was called into memcpy. The source parameter in rsi was corrupt, and the program subsequently crashed with an illegal read memory access.

  6. Enter quit to exit the(gdb) prompt.

Implementing the Fixes

  1. Add the issafe flag to the VulnerableExceptionHandling.c native code.

    alt description here

    After a JNI exception is thrown, avoid a call to other JNI operations. In this example, you can use the issafe flag to call the dangerous memory-moving code at the end of the native method only for the issafe=True case.

  2. Run the VulnerableExceptionHandling file:

    CLASSNAME=VulnerableExceptionHandling make run
    alt description here

    The loop safely executed. To avoid integer overflows, secure your JNI implementation in Java while testing for valid ranges.

  3. Close the terminal.

Managing Memory

In this section, you learn how a Java program's security can be subverted by inconsistent memory management and false assumptions about integer arithmetic.

Perform the following tasks:
  • Compile the source and link the shared library with secure compiler and linker settings.
  • Test whether the range-checking code is complete.
  • Test whether the memory management of the native array can resist corruptions.

Compiling Source Files

  1. Open the directory, click the terminal icon, and browse to the Exercise 4 directory:
     cd ~/HOL6325/LABS/Exercise_4/

    alt description here
  2. Open the ByteArray.java Java file:

    vi ByteArray.java
    alt description here
  3. Open the ByteArray.c C file:

    vi ByteArray.c
    alt description here
  4. Compile and run the ByteArray.java file with the following parameters:
    ARGV="50 10 20 42 25 15 84" CLASSNAME=ByteArray make run

    alt description here

    The values in the ARGV parameters initiate an array of size 50. They fill the array from offset 10 to entry 30 (length 20) with value 42, and afterwards from offset 25 to offset 40 (length 15) with value 84.

Testing Code for a Range of Values

In this section, you test two cases. For case 1, you catch negative edge values for offset and length. For case 2, you test with the offset value.
  1. Execute the ByteArray.java file with the following parameters:

    ARGV="50 10 20 42 -250 230 84" CLASSNAME=ByteArray make run

    alt description here

    An error occurs. The program seems to be missing checks against negative offset values.

  2. Replace the if condition in ByteArray.java:

    if ((length >=0)  && (offset >=0) &&  (offset + length <= size)){

    alt description here
  3. Execute the ByteArray.java file with the following parameters:

    ARGV="50 10 20 42 -250 230 84" CLASSNAME=ByteArray make run

    alt description here

    The fix catches negative edge values for offset and length.

  4. Test the code with offset set as Integer.MAX VALUE-164 and length set as 170. Verify the output.

    ARGV="1250 10 20 42 2147483500 170 84" CLASSNAME=ByteArray make run
    alt description here

    The program fails with an error.

  5. Execute the program with a debugger:

    gdb --args java -Xcheck:jni  -cp . -Djava.library.path=. ByteArray 1250 10 20 42  2147483500 170  84
    alt description here

  6. Execute the bt backtrace command and examine where the crash occurred:

    (gdb)bt
    alt description here

    The crash occurred in ByteArray.fillArray0, proving that the conditions which you set in the fill_array Java method aren’t sufficient. 

  7. Compile IntegerMath.java:
    javac IntegerMath.java
    java IntegerMath Integer.MAX_VALUE -147 170
    alt description here

    In Java Platform, Standard Edition (Java SE) 7 and 8, integer sums can easily overflow, and the vulnerable pattern occurs in fill_array. Notice that the secure compile flags didn't detect the integer overflow.

    if ((length >=0) && (offset >=0) (offset + length <= size))  

    The Offset + length(Integer.MAX_VALUE-147 + 170) -2147483626 is smaller than the size (1250).

Testing Code for Memory Corruptions

With Java SE 8, you can catch the integer overflow by using the new Math.addExact library method instruction.

  1. Edit ByteArray.java:

    if ((length >=0) && (offset >=0) && (Math.addExact( offset , length) <= size ))

  2. Execute the ByteArray.java file with the following parameters:
    ARGV="1250 10 20 42 2147483500 170 84" CLASSNAME=ByteArray make run

    alt description here
  3. Java SE 8 also catches the overflow case.

    Note: With Java SE 7 and earlier, rearrange the comparison to prevent the overflow:
    if ((length >=0) && (offset >=0) && (offset <= size - length))

    To prevent manipulation, store native memory addresses in private fields and treat them as read-only in Java. To avoid integer overflows, secure your JNI implementation in Java while testing for valid ranges.  

  4. Close the terminal.

Want to Learn More?

Credits

  • Curriculum Developers: Anjana Shenoy, Shilpa Chetan
  • Contributor: Marc Schönefeld
  • To navigate this Oracle by Example tutorial, note the following:

    Topic List:
    Click a topic to navigate to that section.
    Expand All Topics:
    Click the button to show or hide the details for the sections. By default, all topics are collapsed.
    Hide All Images:
    Click the button to show or hide the screenshots. By default, all images are displayed.
    Print:
    Click the button to print the content. The content that is currently displayed or hidden is printed.

    To navigate to a particular section in this tutorial, select the topic from the list.