Гайд [JNI] Java Native Interface. Что это, для чего это и зачем.

Начинающий
Начинающий
Статус
Оффлайн
Регистрация
14 Мар 2025
Сообщения
76
Реакции
3

В данном гайде мы разберем, что такое JNI и как же его использовать.



JNI (Java Native Interface) — это интерфейс для связи Java с C++, так называемый мост, который позволяет переносить критичную логику, например авторизацию или проверку лицензий, в нативный код (.dll). Это усложняет реверс-инжиниринг, так как байткод Java легко декомпилируется, в отличие бинарника c++.

Зачем нужен JNI?

  • Защита кода: Логика в .dll сложнее для анализа, чем в .jar. Например, проверка токена на Java легко выдирается декомпилятором, а в C++ — это бинарник, который требует серьёзных усилий для реверса, а так же имеет больше потенциала в сфере защиты и при этом, защитить dll намного проще в отличие от jar.
  • Производительность: Нативный код быстрее для задач вроде криптографии, где JVM добавляет оверхед.

1. Загрузка библиотеки
System.loadLibrary загружает .dll в JVM. Для loadLibrary("mybridge") JVM ищет mybridge.dll в случае windows, mybridge.so - линукс. Если файл не найден или зависимости отсутствуют, вылетает UnsatisfiedLinkError.

Приведу простой пример использования JNI:​


MyBridge.java example:
Expand Collapse Copy
public class MyBridge {
    static { 
       System.loadLibrary("mybridge");
 }
    public native int add(int a, int b);

    public static void main(String[] args) {
        System.out.println(new MyBridge().add(2, 3));  // Вызов C++
    }
}


MyBridge.cpp:
Expand Collapse Copy
#include
#include "MyBridge.h"

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

В этом примере, мы вызываем функцию для сложения Java_MyBridge_add написанную на c++ и использованную в java коде
вызов:
Expand Collapse Copy
public native int add(int a, int b);

Перейдем к ошибкам:
  • UnsatisfiedLinkError: .dll не найдена. Для устранения, нужно положить её рядом с .jar или настроить java.library.path
  • Зависимости: Если .dll требует внешние библиотеки (например, OpenSSL), нужно убедиться, что все они подключены.
Как работает:

  • System.loadLibrary просит JVM найти .dll по имени, добавляя суффикс .dll в Windows, so в Linux.
  • JVM использует ClassLoader для поиска файла в java.library.path или рядом с .jar.
  • ОС мапит .dll в память процесса, разрешает символы и запускает инициализацию.

2. Сигнатуры и заголовки
JNI требует точного соответствия имён функций. Будем использовать javac -h для генерации заголовков.
Пример:
  • Java:
    Expand Collapse Copy
     public native String greet(String name);
    в пакете
    com.example.
  • c++ выходной .h:
    Expand Collapse Copy
    JNIEXPORT jstring JNICALL Java_com_example_MyClass_greet(JNIEnv* env, jobject, jstring);
Процесс:
  • javac -h . MyClass.java создаёт MyClass.h с правильными сигнатурами.
  • JNIEXPORT делает функцию видимой для JVM, JNICALL задаёт соглашение вызова.
  • Статические методы используют jclass, методы экземпляра — jobject.
Несоответствие сигнатур (например, jint вместо jlong) приведёт к UnsatisfiedLinkError или некорректному доступу к памяти.

3. JNIEnv и взаимодействие с JVM
JNIEnv — это указатель на таблицу функций, через который C++ взаимодействует с JVM: создаёт объекты, вызывает методы, бросает исключения.
Пример :
NativeDemo.cpp:
Expand Collapse Copy
#include
#include "NativeDemo.h"

JNIEXPORT jstring JNICALL Java_NativeDemo_greet(JNIEnv* env, jobject, jstring name) {
    const char* s = env->GetStringUTFChars(name, nullptr);
    std::string out = "Hello, " + std::string(s);
    env->ReleaseStringUTFChars(name, s);
    return env->NewStringUTF(out.c_str());
}

  • JNIEnv локален для потока. Нативный поток должен вызвать AttachCurrentThread для получения JNIEnv и DetachCurrentThread после.
  • Неправильное использование JNIEnv (например, кэширование между потоками) вызывает сбои.

4. Типы данных
JNI-типы обеспечивают согласованность между Java и C++.


Java

JNI

C++ эквивалент

int

jint

int32_t

boolean

jboolean

uint8_t

long

jlong

int64_t

float

jfloat

float

double

jdouble

double

char

jchar

uint16_t
Работа с данными:

  • Строки:
    Expand Collapse Copy
    const char* s = env->GetStringUTFChars(jstr, nullptr); → env->ReleaseStringUTFChars(jstr, s);
  • Массивы:
    Expand Collapse Copy
    jint* arr = env->GetIntArrayElements(jarr, nullptr); → env->ReleaseIntArrayElements(jarr, arr, 0);
  • Объекты: Создавайте через env->NewObject с FindClass и GetMethodID.

5. Управление памятью

  • Память: JVM не управляет malloc/new в C++. Освобождайте вручную.
    • Локальные (jobject, jstring) очищаются при выходе из функции или через DeleteLocalRef.
    • Глобальные (NewGlobalRef)сохраняются до DeleteGlobalRef.
  • Пример: Кэширование класса в JNI_OnLoad:
    OnLoad:
    Expand Collapse Copy
    jclass g_cls = env->NewGlobalRef(env->FindClass("com/example/MyClass"));
  • Пропуск освобождения ссылок приводит к утечкам, блокирующим GC (Garbage Collection, сборщик мусора) — это автоматический процесс Java Virtual Machine, освобождающий память, удаляя объекты на которых, нету больше ссылок.

6. Исключения
  • бросайте через:
    Expand Collapse Copy
    env->ThrowNew(env->FindClass("java/lang/IllegalArgumentException"), "msg");
    и сразу
    return.
  • Проверяйте env->ExceptionCheck() после вызовов JNI, чтобы избежать ошибок.
Пример:
Expand Collapse Copy
JNIEXPORT void JNICALL Java_Example_check(JNIEnv* env, jobject, jstring input) {
    if (!input) {
        env->ThrowNew(env->FindClass("java/lang/IllegalArgumentException"), "Null input");
        return;
    }
}


7. Перенос логики для скрытия критичных участков кода:

Пример: проверка лицензии.
SecureCheck.java:
Expand Collapse Copy
public class SecureCheck {
    static {
        System.loadLibrary("secure");
    }
  
    public native boolean verifyLicense(String key);
  
}

SecureCheck.cpp:
Expand Collapse Copy
#include
#include
#include "SecureCheck.h"

JNIEXPORT jboolean JNICALL Java_SecureCheck_verifyLicense(JNIEnv* env, jobject, jstring key) {
    const char* k = env->GetStringUTFChars(key, nullptr);
    if (!k) {
        env->ThrowNew(env->FindClass("java/lang/IllegalArgumentException"), "Invalid key");
        return false;
    }
    unsigned char hash[SHA256_DIGEST_LENGTH];
    SHA256((unsigned char*)k, strlen(k), hash);
    env->ReleaseStringUTFChars(key, k);
    return hash[0] == 0xFF;  // Упростим для демо
}

Защита:
  • Логика в .dll, а не в байткоде.
  • Для усиления обфусцируйте .dll (например, VMProtect).

8. Автоматизация с native-obfuscator

Пожалуйста, авторизуйтесь для просмотра ссылки.
(noad)

Автоматически конвертит .class в .cpp для JNI, сохраняя логику. Доступен исходный код.


На этом гайд подходит к концу, для его написания я очень постарался и надеюсь, что кто-то откроет для себя, что-то новое.

Если многим зайдет данный гайд, то в следующем расскажу, про сам JVM, classFileParser и создадим собственную custom jvm с отключением jvm-ti и правильной оптимизированной сборкой, а так же просто вызов JVM_CreateJava_VM из jvm.dll


гайд написал
Пожалуйста, авторизуйтесь для просмотра ссылки.
 
Последнее редактирование:
Зачем нужен JNI?
  • Защита кода: Логика в .dll сложнее для анализа, чем в .jar. Например, проверка токена на Java легко выдирается декомпилятором, а в C++ — это бинарник, который требует серьёзных усилий для реверса, а так же имеет больше потенциала в сфере защиты и при этом, защитить dll намного проще в отличие от jar.
  • Производительность: Нативный код быстрее для задач вроде криптографии, где JVM добавляет оверхед.
Есть пара моментов, JNI так-же накладывает оверхед и для мелких задач с которыми справится JIT использовать JNI - ну такое, производительность кода после JIT банально может быть лучше из-за оверхеда JNI
На счёт защиты - ну хз, если там 1 нативный вызов который возвращает boolean это не поможет никак, байткод патчится и защиты нет, зависит от того как реализовывать эту защиту, а её можно не только через plain JNI делать но и через агенты же по идее

  • Нативный поток должен вызвать AttachCurrentThread для получения JNIEnv и DetachCurrentThread после.
Как это? Если рассматривать простые примеры по типу вызова нативной функции из-под JVM, то указатель на JNIEnv передаётся в функцию, зачем дополнительно вызывать AttachCurrentThread и DetachCurrentThread? Тип ок если у тебя поток левый сам обращается к JVM, но если JVM обращается и передаёт ENV то это вроде бессмысленно
 
Есть пара моментов, JNI так-же накладывает оверхед и для мелких задач с которыми справится JIT использовать JNI - ну такое, производительность кода после JIT банально может быть лучше из-за оверхеда JNI
Для мелких задач JIT лучше, согласен.Но гайд особо делал именно про jni, крипту и тяжёлые вычисления, где нативка будет получше.
На счёт защиты - ну хз, если там 1 нативный вызов который возвращает boolean это не поможет никак, байткод патчится и защиты нет, зависит от того как реализовывать эту защиту, а её можно не только через plain JNI делать но и через агенты же по идее
Если выбирать где делать авторизацию или другие функции защиты между c++ и jav’ой, то думаю выбор будет очевиден.
Про агенты, тоже в каких то местах имеют плюсы, но агенты работают внутри JVM, их код (если это нативный агент) тоже можно реверсить, как и .dll в JNI, либо, если агент хуево сделан (например, без обфы нормальной или с предсказуемой логикой), его можно обойти, подменить, отключить через JVMTI-хуки или патч JVM
Как это? Если рассматривать простые примеры по типу вызова нативной функции из-под JVM, то указатель на JNIEnv передаётся в функцию, зачем дополнительно вызывать AttachCurrentThread и DetachCurrentThread? Тип ок если у тебя поток левый сам обращается к JVM, но если JVM обращается и передаёт ENV то это вроде бессмысленно
в простых случаях, но если будут левые потоки, то нужны
 
Последнее редактирование:
Для мелких задач JIT лучше, согласен.Но гайд особо делал именно про jni, крипту и тяжёлые вычисления, где нативка будет получше.
Для крипты разница даже 50-100мс вряд-ли будет значительной, если ты конечно не брутфорсишь что-то, для этого достаточно воткнуть сертифицированный инструмент вроде bouncycastle если уж так нужна 'безопасность', в остальном ок

в простых случаях, но если будут левые потоки, то нужны
Ну т.к. это вполне себе достойно пояснения потому-что лишний вызов - лишний оверхед даже несмотря на то что при повторном вызове там no-op происходит
 
Назад
Сверху Снизу