Java安全之JNI

介绍

JNI(Java Native Interface)是一种允许Java程序与本地代码(如C或C++)互操作的接口技术。通过JNI,Java程序能够调用本地代码,实现性能和功能上的优化,克服Java在某些场景下的内存管理和执行效率瓶颈。它使得开发者可以在Java应用中集成底层操作系统功能或使用已存在的高效本地库,从而提升应用的执行速度或访问硬件资源的能力。

JNI 基本知识

本地库生命周期

阶段触发条件关键函数用途
加载阶段System.loadLibraryJNI_OnLoad注册动态方法/初始化资源
运行阶段Java调用native方法业务逻辑执行
卸载阶段类加载器回收JNI_OnUnload释放内存/关闭句柄

Native方法命名规范

动态链接器根据特定规则查找Java本地方法对应的Native函数,方法名遵循如下规则:

  • 函数名带有Java_前缀
  • 后跟以下划线完整类名,类的包名分隔符.以_来代替
  • 后跟函数名
  • 对于重载的本地方法,会使用双下划线(__)来分隔参数签名

命名结构:Java_ {全限定类名}_ {方法名}, 示例:Java_com_example_Encryptor_encryptData

重载处理:对重载方法追加双下划线 __ 和参数签名缩写, 示例:encrypt__Ljava_lang_String_2

Native方法参数

  • 第一个参数是JNI接口指针,JNIEnv *
  • 第二个参数是Java对象,具体值取决于当前方法是静态方法还是实例方法,若是静态方法,则表示类对象,若是实例方法,则表示实例对象
  • 其余参数与定义本地方法时的参数一 一对应

Java方法声明

public native void process(
   int count,          // -> jint
   String data,        // -> jstring
   byte[] buffer       // -> jbyteArray
);

C++函数实现

JNIEXPORT void JNICALL Java_Processor_process(
   JNIEnv *env,        // JNI环境指针
   jobject obj,        // 调用对象实例
   jint count,         // 对应Java int
   jstring jstr,       // 对应Java String
   jbyteArray jarray   // 对应Java byte[]
) {
   /* 实现代码 */
}

JNI数据类型

Java类型原生类型原生类型
booleanjbooleanunsigned 8 bits
bytejbytesigned 8 bits
charjcharunsigned 16 bits
shortjshortsigned 16 bits
intjintsigned 32 bits
longjlongsigned 64 bits
floatjfloat32 bits
doublejdouble64 bits
voidvoidnot applicable

类型签名

类型签名Java类型
booleanjboolean
bytejbyte
charjchar
shortjshort
intjint
longjlong
floatjfloat
doublejdouble
voidvoid

例如:Java函数声明是long f (int n, String s, int[] arr);,那么他的类型是(ILjava/lang/String;[I)J

JNI结构函数表

具体可以参考jni.h头文件中的JNINativeInterface 定义, 这里只简单说明几个接口函数。

函数接口说明
GetObjectClass返回某个对象的类型对象
GetMethodID返回某个类或接口的实例方法ID
RegisterNatives为指定类型注册本地方法
UnregisterNatives取消注册某个类中所有的本地方法
Call<type>Method在本地方法中调用Java对象的实例方法

第一个JNI程序

目标

  1. 实现Java调用Native函数
  2. 实现Native函数调用Java实例函数
  3. 实现Java调用JNI动态注册的Native函数

代码实现

Main.java

public class Main {
   static {

       System.loadLibrary("test");
  }

   private native int add(int a, int b);
   private native int sub(int a, int b);
   private native void foo();

   private void sayHi() {
       System.out.println("Hello World");
  }

   public static void main(String[] args) {
       Main mainObj = new Main();
       System.out.println(mainObj.add(1, 2));
       System.out.println(mainObj.sub(2, 1));
       mainObj.foo();
  }
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.10)

project(test)
set(PRODUCT_NAME ${PROJECT_NAME})
set(CMAKE_CXX_STANDARD 17)

set(JAVA_HOME $ENV{JAVA_HOME})
include_directories(${JAVA_HOME}/include)
include_directories(${JAVA_HOME}/include/win32)
link_directories(${JAVA_HOME}/lib)

message(STATUS JAVA_HOME:${JAVA_HOME})

aux_source_directory(. SRC_LIST)

add_library(${PRODUCT_NAME} SHARED ${SRC_LIST})

使用javah -jni Main自动生成Main类的JNI头文件 Main.h

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class Main */

#ifndef _Included_Main
#define _Included_Main
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class:     Main
* Method:   add
* Signature: (II)I
*/
JNIEXPORT jint JNICALL Java_Main_add
(JNIEnv *, jobject, jint, jint);

/*
* Class:     Main
* Method:   foo
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_Main_foo
(JNIEnv*, jobject);


#ifdef __cplusplus
}
#endif
#endif

Main.cpp

#include "Main.h"

JNIEXPORT jint JNICALL Java_Main_add(JNIEnv* env, jobject obj, jint a, jint b)
{
 return  a + b;
}

JNIEXPORT void JNICALL Java_Main_foo(JNIEnv* env, jobject obj)
{
 jclass clazz = env->GetObjectClass(obj);
 jmethodID  sayHi = env->GetMethodID(clazz, "sayHi", "()V");
 env->CallVoidMethod(obj, sayHi);
}

JNIEXPORT jint JNICALL Main_sub(JNIEnv* env, jobject obj, jint a, jint b)
{
 printf("Java_Main_sub\n");
 return  a - b;
}

static const JNINativeMethod methods[] = {
  {"sub", "(II)I", (void*)Main_sub},
};

jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
 printf("JNI_OnLoad\n");
 JNIEnv* env;
 if (vm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK)
{
   return JNI_ERR;
}

 jclass mainClass = env->FindClass("Main");
 if (mainClass == NULL)
{
   return JNI_ERR;  
}

 // 注册本地方法
 int numMethods = sizeof(methods) / sizeof(methods[0]);
 if (env->RegisterNatives( mainClass, methods, numMethods) < 0)
{
   return JNI_ERR;
}

 return JNI_VERSION_1_6;
}

运行结果

JNI_OnLoad      // 加载库时调用JNI_OnLoad
3               // add(1, 2) 的结果
Java_Main_sub   // 调用动态注册的sub方法
1               // sub(2, 1) 的结果
Hello World     // foo函数调用Java的sayHi方法

执行流程解析:

  1. 加载库: JVM加载test库,触发JNI_OnLoad执行,动态注册sub方法。
  2. 调用add: mainObj.add(1, 2)调用Java_Main_add函数。
  3. 返回计算结果3。
  4. 调用sub: mainObj.sub(2, 1)调用动态注册的Main_sub函数, 打印”Java_Main_sub”。
  5. 返回计算结果1。
  6. Java_Main_foo函数调用Java的sayHi方法

代码安全加固策略

Java字节码因其高度可读性和易反编译特性,导致关键业务逻辑和敏感算法面临被逆向分析的风险。通过JNI将核心功能迁移到C/C++实现的本地库中,可显著提高攻击者的逆向门槛:

  1. 逆向难度提升:本地库的二进制代码比Java字节码更难反编译
  2. 调试障碍增加:需要专业的底层调试工具(如IDA Pro、GDB)
  3. 分析成本激增:逆向工程师需要同时精通Java和底层汇编语言
  4. 攻击面转移:从Java层的逆向转向底层的二进制逆向

此时可直接使用Virbox Protector成熟的代码保护工具进行加固。该工具提供多种保护方案(包括Native库代码保护和虚拟化保护VME等),通过组合使用能有效保护核心代码不被轻易泄露或分析。

滚动至顶部
售前客服
周末值班
电话

电话

13910187371