type
status
date
slug
summary
tags
category
icon
password

1) JNI

JNI(Java Native Interface,Java 本地接口)是 Java 平台的核心技术之一,允许 Java 代码与其他编程语言(如 C、C++、汇编)编写的本地代码进行交互,是java层和native层的桥梁
安卓逆向中,涉及native层(.so)的操作,不得不使用JNI
JNI可以分为两种:静态注册和动态注册
静态注册
当我们在 native 中注册一个 java 函数后,一般可以看到函数的声明都像下面这个样子
因此,静态注册的方法,我们都可以在ida中搜索“Java”开头的函数来寻找
我们可以自己实现一个简单的加法:
至于为什么要这样写,可以在后面数据类型和JNI API处get
还要在MainActivity.java里添加这个方法(静态注册)
动态注册
  • 动态注册机制:通过JNI_OnLoad函数在运行时将函数映射到 Java 类的方法
也来实现一个简单的加法
动态注册都需要使用JNI_OnLoad方法

2) 数据类型

基本数据类型

Java 类型
JNI 数据类型
位数
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

引用数据类型

Java 类型
JNI 数据类型
void
void
java.lang.Object
jobject
java.lang.Class
jclass
java.lang.String
jstring
java.lang.Throwable
jthrowable
Object[]
jobjectArray
boolean[]
jbooleanArray
byte[]
jbyteArray
char[]
jcharArray
short[]
jshortArray
int[]
jintArray
long[]
jlongArray
float[]
jfloatArray
double[]
jdoubleArray

类型描述符

看起来和 smali 的类型声明一模一样
Java 类型
类型描述符
boolean
Z
short
S
float
F
byte
B
int
I
double
D
char
C
long
J
void
V
引用类型
L + 全限定名 + ;
数组
[+ 类型描述符
方法
(参数的类型描述符) 返回值的类型描述符
  • 表示一个 string 类
    • L + 全限定名 ,其中 . 换成 / , 最后加上 ;
      java 类型
      类型描述符
      java.lang.String
      Ljava/lang/String;
  • 表示数组
    • java 类型
      类型描述符
      String[]
      [Ljava/lang/String;
      int[][]
      [[I
  • 表示方法
    • java 类型
      类型描述符
      long f (int n,String s,int arr[]);
      (ILjava/lang/String;[I) J
      void f ();
      () V

其他常用类型

3) JavaVm

javaVM 是 java 虚拟机在 jni 层的代表,在 Android 上,一个进程只有一个 JavaVM,所有的线程共用一个 JavaVM, 也就是在 Android 进程中是通过有且只有一个虚拟机对象来服务所有 Java 和 C/C++ 代码。

3. 1 Invocation API

DestoryJavaVM

卸载一个 Java 虚拟机,并收回它拥有的资源。
  • 任何线程,不管是否已经 attached,都可以调用该函数,如果当前线程已经 attached,则虚拟机会等待当前线程作为唯一的非守护用户线程。
  • 如果当前线程没有 attached,则先 attached,再等待当前线程作为唯一的非守护用户线程。

AttachCurrentThread

附加当前线程到 JavaVM , 并返回 JNIEnv
  • 尝试 attach 已经 attached 过的线程不会执行任何操作(no-op)。
  • 一个本地线程不能同时 attach 到两个不同的 Java 虚拟机。
  • 当前一个线程 attach 到虚拟机,它的上下文 ClassLoader 是 Bootstrap ClassLoader。
扩展
在 Java 中,类加载器(ClassLoader)是负责将.class 文件加载到 JVM 中的组件,它构成了一个树形的委托层次结构,主要包含以下三类:
  1. Bootstrap ClassLoader(启动类加载器)
      • 由 C++ 实现,是 JVM 的一部分
      • 负责加载 JRE 核心类库(如rt.jar中的java.lang.*
      • 位于类加载器层次结构的最顶层
  1. Extension ClassLoader(扩展类加载器)
      • 由 Java 代码实现,继承自URLClassLoader
      • 负责加载jre/lib/ext目录或java.ext.dirs系统属性指定的扩展类库
  1. Application ClassLoader(应用类加载器)
      • 也由 Java 代码实现,同样继承自URLClassLoader
      • 负责加载应用程序 classpath 路径下的类
      • 通常是用户自定义类的默认类加载器

AttachCurrentThreadAsDaemon

和 AttachCurrentThread 类似,只是新创建的 java.lang.Thread 被设置为守护线程(daemon)
这里和DestoryJavaVM 有联动

DetachCurrentThread

从 java 虚拟机 detach 当前线程。所有这个线程持有的 Java 监视区 (monitor) 都会被释放。
  • 从 JDK/JRE 1.2 开始,主线程可以从虚拟机 detach。

GetEnv

获取当前线程的 JNI 接口指针 JNIEnv

3. 2 JavaVM 虚拟机加载流程

创建虚拟机

JNI_CreateJavaVM() 函数载入和初始化一个 Java 虚拟机。调用该函数的线程被视为是主线程(main thread)。
  • 在 JDK/JRE 1.2,不允许在同一个进程创建多个 Java 虚拟机。

线程附加到虚拟机

JNI 接口指针 (JNIEnv) 只在当前线程有效,如果需要在另一个线程访问 Java 虚拟机,必须先调用 AttachCurrentThread() 来将自己 attach 到虚拟机来获得 JNI 接口指针 JNIEnv
线程必须有足够的栈空间来执行一定的工作。每个线程分配多少栈空间根据系统而不同。

脱离虚拟机

一个 attach 到虚拟机的本地线程必须在退出前调用 DetachCurrentThread() 来和虚拟机脱离。如果还有 Java 方法在 call stack 中,则这个线程不能被 detach。

卸载虚拟机

使用 JNI_DestroyJavaVM() 函数来卸载一个 Java 虚拟机
虚拟机会等待(阻塞),直到当前线程成为唯一的非守护进程的用户进程,才真正执行卸载操作。
用户进程 (user thread) 包括:
  • Java 线程 (java threads)
  • attached 到虚拟机的本地线程 (attached native threads)
为什么要做这样的限制(强制等待),是因为 Java 线程和 native 线程可能会 hold 住系统资源,例如锁,窗口等资源,而虚拟机不能自动释放这些资源。通过限制当前线程是唯一的运行中的用户线程才 unload 虚拟机,则将释放这种系统资源的任务交给程序员自己来负责了。

3. 3 获取 JavaVM 接口

  • 在 Java VM 虚拟机加载动态链接库时,可以在 JNI_OnLoad 的参数中获取到 JavaVM
  • 通过 JNIEnv 获取 JavaVM

4) JNIEnv

4. 1 定义

JNIEnv 是提供 JNI Native 函数的基础环境,线程相关,不同线程的 JNIEnv 相互独立,并且 JNIEnv 是一个 JNI 接口指针,指向了本地方法的一个函数表,该函数表中的每一个成员指向了一个 JNI 函数,本地方法通过 JNI 函数来访问 JVM 中的数据结构.

4. 2 JNI 函数

4. 2. 1 类操作

  • DefineClass
从原始类数据的缓冲区中加载类。
  • FindClass
该函数用于加载 Java 类。它将在 CLASSPATH 环境变量所指定的目录和 zip 文件里搜索指定的类名。
  • GetObjectClass
通过对象获取这个类。该函数比较简单,唯一注意的是对象不能为 NULL,否则获取的 class 肯定返回也为 NULL。
  • GetSuperclass
获取父类
  • IsAssignableFrom
确定 clazz1 的对象是否可安全地强制转换为 clazz2

4. 2. 2 异常操作

  • Throw
抛出 java.lang.Throwable 对象
  • ThrowNew
利用指定类的消息(由 message 指定)构造异常对象并抛出该异常
  • ExceptionOccurred
确定某个异常是否正被抛出。在本地代码调用 ExceptionClear () 或 Java 代码处理该异常前,异常将始终保持抛出状态。
  • ExceptionDescribe
将异常及堆栈的回溯输出到标准输出(例如 stderr)。该例程可便利调试操作。
  • ExceptionClear
清除当前抛出的任何异常。如果当前无异常,则不产生任何效果。
  • FatalError
抛出致命错误并且不希望虚拟机进行修复。该函数无返回值

4. 2. 3 全局及局部引用

  • DeleteWeakGlobalRef
删除弱全局引用
  • NewWeakGlobalRef
用 obj 创建新的弱全局引用
  • DeleteLocalRef
删除 localRef 所指向的局部引用
  • NewLocalRef
创建 obj 参数所引用对象的局部引用,创建的引用要通过调用 DeleteLocalRef () 来显式删除
  • DeleteGlobalRef
删除 globalRef 所指向的全局引用
  • NewGlobalRef
创建 obj 参数所引用对象的新全局引用,创建的引用要通过调用 DeleteGlobalRef () 来显式撤消

4. 2. 4 对象操作

  • IsSameObject
测试两个引用是否引用同一 Java 对象
  • IsInstanceOf
测试对象是否为某个类的实例
  • GetObjectClass
返回对象的类
  • NewObject
构造新 Java 对象。方法 methodId 指向应调用的构造函数方法。注意:该 ID 特指该类 class 的构造函数 ID,必须通过调用 GetMethodID () 获得,且调用时的方法名必须为 <init> ,而返回类型必须为 void (V),clazz 参数务必不要引用数组类。
  • AllocObject
分配新 Java 对象而不调用该对象的任何构造函数,返回该对象的引用;clazz 参数务必不要引用数组类。

4. 2. 5 字符串操作

  • Get/ReleaseStringCritical
这两个函数的语义与 Get/ReleaseStringChars 函数类似,但 VM 会尽量返回一个指针。但是使用这一对函数时必须有严格限制:在这对函数调用之间绝对不能调用其他 JNI 方法,否则将导致当前线程阻塞。
  • GetStringUTFRegion
将 str 偏移位置 start 开始的 len 长度 unicode 字符转换为 C char 字符,并放在 buf 中
  • GetStringRegion
从 str 的偏移位置 start 开始,复制 len 长度的 unicode 字符到 buf 中
  • ReleaseStringUTFChars
通知本地代码不要再访问 utf。utf 参数是一个指针,可利用 GetStringUTFChars () 获得
  • GetStringUTFChars
返回指向字符串的 UTF-8 字符数组的指针。该数组在被 ReleaseStringUTFChars () 释放前将一直有效。 如果 isCopy 不是 NULL,*isCopy 在复制完成后即被设为 JNI_TRUE。如果未复制,则设为 JNI_FALSE。
  • GetStringUTFLength
以字节为单位返回字符串的 UTF-8 长度
  • NewStringUTF
利用 UTF-8 字符数组构造新 java.lang.String 对象
  • ReleaseStringChars
通知本地代码不要再访问 chars。参数 chars 是一个指针,可通过 GetStringChars () 从 string 获得
  • GetStringChars
返回指向字符串的 Unicode 字符数组的指针。该指针在调用 ReleaseStringchars () 前一直有效。如果 isCopy 非空,则在复制完成后将 *isCopy 设为 JNI_TRUE。如果没有复制,则设为 JNI_FALSE
  • GetStringLength
返回 Java 字符串的长度(Unicode 字符数)
  • NewString
利用 Unicode 字符数组构造新的 java.lang.String 对象

4. 2. 6 数组操作

  • SetObjectArrayElement
设置 Object 数组的元素
  • GetObjectArrayElement
返回 Object 数组的元素
  • NewObjectArray
构造新的数组,它将保存类 elementClass 中的对象。所有元素初始值均设为 initialElement
  • GetArrayLength
返回数组中的元素数
  • New[PrimitiveType]Array Routines
用于构造基本类型数组对象
下表说明了特定的基本类型数组构造函数。用户应把 New [PrimitiveType] Array 替换为某个实际的基本类型数组构造函数例程名,然后将 ArrayType 替换为该例程相应的数组类型:
New[PrimitiveType]Array
ArrayType
NewBooleanArray()
jbooleanarray
NewByteArray()
jbytearray
NewCharArray()
jchararray
NewShortArray()
jshortarray
NewIntArray()
jintarray
NewLongArray()
jlongarray
NewFloatArray()
jfloatarray
NewDoubleArray()
jdoublearray
  • Get[PrimitiveType]ArrayElements
一组返回基本类型数组体的函数。结果在调用相应的 Release [PrimitiveType] ArrayElements () 函数前将一直有效。由于返回的数组可能是 Java 数组的副本,因此对返回数组的更改不必在基本类型数组中反映出来,直到调用了 Release [PrimitiveType] ArrayElements ()。
Get[PrimitiveType]ArrayElements
NativeType
ArrayType
GetBooleanArrayElements()
jboolean
jbooleanArray
GetByteArrayElements()
jbyte
jbyteArray
GetCharArrayElements()
jchar
jcharArray
GetShortArrayElements()
jshort
jshortArray
GetIntArrayElements()
jint
jintArray
GetLongArrayElements()
jlong
jlongArray
GetFloatArrayElements()
jfloat
jfloatArray
GetDoubleArrayElements()
jdouble
jdoubleArray
  • Release[PrimitiveType]ArrayElements
释放 elems,通知本地代码不要再访问 elems
Release [PrimitiveType] ArrayElements 惯用法里的类型参数与 Get [PrimitiveType] ArrayElements 对应,不再列出
  • Get[PrimitiveType]ArrayRegion
将基本类型数组某一区域复制到缓冲区中的一组函数,使用时替换 PrimitiveType, ArrayType,和 NativeType,如 GetBooleanArrayRegion () ,jbooleanArray 和 jboolean
  • Set[PrimitiveType]ArrayRegion
将基本类型数组的某一区域从缓冲区中复制回来的一组函数,使用时替换 PrimitiveType, ArrayType,和 NativeType,如 SetBooleanArrayRegion () ,jbooleanArray 和 jboolean
  • GetPrimitiveArrayCritical 与 ReleasePrimitiveArrayCritical
作用同 Get/Release [PrimitiveType] ArrayElements 相同,但是 VM 尽可能返回原 java 数组的指针,否则返回一份拷贝。
这两组调用之间不能调用其他 JNI 函数或进行其他系统调用,否则会导致线程阻塞。

4. 2. 7 访问对象的属性和方法

  • GetStaticMethodID
获取类对象的静态方法 ID
  • GetFieldID
返回 Java 类(非静态)域的属性 ID。该域由其名称及签名指定。访问器函数的 Get [type] Field 及 Set [type] Field 系列使用域 ID 检索对象域。GetFieldID () 不能用于获取数组的长度域。应使用 GetArrayLength ()。
  • GetMethodID
返回类或接口实例(非静态)方法的方法 ID。方法可在某个 clazz 的超类中定义,也可从 clazz 继承。该方法由其名称和签名决定。 GetMethodID () 可使未初始化的类初始化。要获得构造函数的方法 ID,应将 <init> 作为方法名,同时将 void (V) 作为返回类型
  • GetStaticFieldID
获取类的静态域 ID 方法
  • Get[type]Field
该例程系列返回对象的实例(非静态)域的值。要访问的域由通过调用 GetFieldID () 而得到的域 ID 指定。
Get[type]Field
NativeType
GetObjectField()
jobject
GetBooleanField()
jboolean
GetByteField()
jbyte
GetCharField()
jchar
GetShortField()
jshort
GetIntField()
jint
GetLongField()
jlong
GetFloatField()
jfloat
GetDoubleField()
jdouble
  • Set[type]Field
该惯用法设置对象的实例(非静态)属性的值。要访问的属性由通过调用 SetFieldID () 而得到的属性 ID 指定。
Set[type]Field
NativeType
SetObjectField()
jobject
SetBooleanField()
jboolean
SetByteField()
jbyte
SetCharField()
jchar
SetShortField()
jshort
SetIntField()
jint
SetLongField()
jlong
SetFloatField()
jfloat
SetDoubleField()
jdouble
  • Call[type]Method
这三个操作的方法用于从本地方法调用 Java 实例方法。它们的差别仅在于向其所调用的方法传递参数时所用的机制。
这三个操作将根据所指定的 methodID 调用 Java 对象的实例(非静态)方法。参数 methodID 必须通过调用 GetMethodID () 来获得。当这些函数用于调用私有方法和构造函数时,methodID 必须从 obj 的真实类派生而来,而不应从其某个超类派生。当然,附加参数可以为空 。
Call[type]Method<A/V>
NativeType
CallVoidMethod()
CallObjectMethod()
jobect
CallBooleanMethod ()
jboolean
CallByteMethod()
jbyte
CallCharMethod()
jchar
CallShortMethod()
jshort
CallIntMethod()
jint
CallLongMethod()
jlong
CallFloatMethod()
jfloat
CallDoubleMethod()
jdouble

4. 2. 8 注册本地方法

  • RegisterNatives
向 clazz 参数指定的类注册本地方法。
  • UnregisterNatives
反注册类的本地方法。类将返回到链接或注册了本地方法函数前的状态。该函数不应在本地代码中使用。相反,它可以为某些程序提供一种重新加载和重新链接本地库的途径。

5) 动态注册

动态注册都是调用 RegisterNatives 来实现的 JNINativeMethod 内数组的三个参数分别代表 函数名称 , 函数签名 , 主体函数
 

6) native 层调用 java 层函数

native 调用 java 层的函数只需要四步就可以完成
  1. 获取 class
  1. 获取函数签名
  1. 获取 class 的实例
  1. 调用函数
 
APP启动流程Frida学习
Loading...
Sh4d0w
Sh4d0w
漫长学习路ing
最新发布
360加固复现学习
2025-6-15
java反射机制
2025-6-14
classLoader机制
2025-6-14
dex文件结构
2025-6-14
APP启动流程
2025-6-14
JNI学习
2025-6-14
公告
Welcome to Sh4dw’s blog!
敬请指导,Q 467194403