Java安全之JNI

介绍

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

JNI 基本知识

本地库生命周期

阶段 触发条件 关键函数 用途
加载阶段 System.loadLibrary JNI_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类型 原生类型 原生类型
boolean jboolean unsigned 8 bits
byte jbyte signed 8 bits
char jchar unsigned 16 bits
short jshort signed 16 bits
int jint signed 32 bits
long jlong signed 64 bits
float jfloat 32 bits
double jdouble 64 bits
void void not applicable

类型签名

类型签名 Java类型
boolean jboolean
byte jbyte
char jchar
short jshort
int jint
long jlong
float jfloat
double jdouble
void void

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

JNI结构函数表

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

函数接口 说明
GetObjectClass 返回某个对象的类型对象
GetMethodID 返回某个类或接口的实例方法ID
RegisterNatives 为指定类型注册本地方法
UnregisterNatives 取消注册某个类中所有的本地方法
CallMethod 在本地方法中调用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 
/* 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等),通过组合使用能有效保护核心代码不被轻易泄露或分析。

你可能感兴趣的:(java软件安全)