JNI demo及逆向技巧

JNI 使用语法

官方文档如下:https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/jniTOC.html

JNI 类型

Java类型 本地类型 描述 c类型
int jint signed 32 bits 根据平台不同
long jlong signed 64 bits 根据平台不同
byte jbyte signed 8 bits 根据平台不同
char jchar unsigned 16 bits typedef unsigned short
short jshort singed 16 bits typedef short
boolean jboolean unsigned 8 bits typedef unsigned char
float jfloat 32 bits typedef float
double jdouble 64 bits typedef double
void void N/A N/A

成员域和成员方法ID

成员域和成员方法都被定义为普通的C指针类型:

struct _jfieldID;              /* opaque structure */ 
typedef struct _jfieldID *jfieldID;   /* field IDs */ 
 
struct _jmethodID;              /* opaque structure */ 
typedef struct _jmethodID *jmethodID; /* method IDs */ 

查看 Java 类库签名

# 使用上述命令查看 Java 类库签名
javap -s java.lang.String


# 查看自己编写java 类的签名
javap -s -p /xxxx/SayHello.class [路径]

或者 javap -s -p -classpath . SayHello
 
Compiled from "SayHello.java"
public class com.xiaoxiao.JniHello.SayHello {
  public com.xiaoxiao.JniHello.SayHello();
    descriptor: ()V

  private native void helloJNI();
    descriptor: ()V

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V

  static {};
    descriptor: ()V
}

JNI函数

出处:https://www.jianshu.com/p/35848c03f2d5

接口函数表

所有的JNI函数都是通过传递进来固定的第一个参数 JNIEnv 参数来进行访问到的。JNIEnv 类型是一个指向所有JNI函数指针集合的指针。它的定义如下:

typedef const struct JNINativeInteface *JNIEnv;

虚拟机初始化函数表如下:

const struct JNINativeInterface_ {

    NULL,
    NULL,
    NULL,
    NULL,
    GetVersion,

    DefineClass,
    FindClass,

    FromReflectedMethod,
    FromReflectedField,
    ToReflectedMethod,

	..........
};

操作类

DefineClass
jclass DefineClass(JNIEnv *env, const char *name, jobject loader,const jbyte *buf, jsize bufLen);

参数:

  • env:JNI接口指针
  • name:需要加载的类或接口的短名字,这个字符串使用MUTF-8编码。
  • loader:指派用来加载类的ClassLoader
  • buf:包含 .class 文件数据的 buffer
  • bufLen: buffer的长度

返回值:

返回加载的Java类对象(java class object),或者如果出现错误则返回NULL

FindClass
jclass FindClass(JNIEnv *env, const char *name);

参数:

  • env:JNI接口指针
  • name:全称的类名(包名以 / 作为分隔符, 然后紧跟着类名),如果名字以 [开头(数组签名标识符),则返回一个数组的类,这个字符串也是MUTF-8。

返回值:

指定名称的类的对象(a class object),或者在没有找到对应类时返回 NULL

GetSuperclass
jclass GetSuperclass(JNIEnv *env, jclass clazz);

参数:

  • env:JNI接口指针
  • clazz: Java类对象(java class object)

返回值:

返回传入的 clazz 的父类,或 NULL .

操作对象

IsAssignableForm
jboolean IsAssignableFrom(JNIEnv *env, jclass class1, jclass clazz2);

检查 clazz1 的对象是否能被安全的转型(cast)为 clazz2

参数:

  • env:JNI接口指针
  • clazz1:第一个class参数(需要转型的类)
  • clazz2:第二个class参数(转型的目标类)

返回值:

如果是以下情况则返回 JNI_TRUE :

  • clazz1 和 clazz2 指向同一个java类
  • clazz1 是 clazz2 的子类。(向上转型是安全的)
  • clazz1 是 clazz2(接口)的实现类。(也属于向上转型)
AllocObject
jobject AllocObject(JNIEnv *env, jclass clazz);

在不调用类的任何一个构造器来分配一个新的Java对象。

参数:

  • env :JNI接口指针
  • clazz:需要初始化的类,不能是一个数组类型的类。

返回值:

返回新的尚未初始化的Java对象的引用。如果不能分配对象则返回 NULL

NewObject, NewObjectA, NewObjectV
jobject NewObject(JNIEnv *env, jclass clazz, jmethodID methodID, ...);

jobject NewObjectA(JNIEnv *env, jclass clazz, jmethodID methodID, const jvalue *args);

jobject NewObjectV(JNIEnv *env, jclass clazz, jmethodID methodID, va_list args);

构造(初始化)一个新的Java对象,参数methodID指向需要被调用的构造器函数,这个methodID必须使用 GetMethodID() 来获取,并且必须使用 <init> 作为方法名,void(V) 作为返回类型。

clazz 参数必须不指向一个数组类型的类。

NewObject

可传入将所有需要传入给构造器的参数放置在 methodID 参数之后,NewObject函数将接受三个参数,并将其传入到Java构造器方法内。

NewObjectA:

可将所有需要传入给构造器的参数列表放置在 jvalue 数组里,它们会全部传给Java构造器方法。

NewObjectV:

可将所有构造器参数放置于 va_list 里,它们也会在调用Java构造器方法时作为参数传入。

参数:

  • env :JNI接口指针
  • clazz : 需要初始化对象的类
  • methodID:构造器方法ID
  • const jvalue *args: 构造器参数
  • va_list args : 构造器函数

返回值:

返回一个Java对象,无法被构造时返回 NULL

GetObjectClass
jclass GetObjectClass(JNIEnv *env, jobject obj);

获取一个对象的class。

参数:

  • env :JNI接口指针
  • obj:需要获取class的对象( 必须 不能为 NULL )

返回值:

一个Java类对象(Java class object)

访问对象成员域

GetFieldID
jfieldID GetFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);

返回一个class的指定成员域的fieldID,成员域根据它的名字和签名来指定。返回的fieldID可以用来调用 Get<type>FieldSet<type>Field 来访问具体的成员域。

GetFieldID() 可能会导致一个没有被初始化(uninitialized)的类被初始化。

GetFieldId() 不能用来获取数组的长度,使用 GetArrayLength() 来替代。

参数:

  • env :JNI接口指针
  • clazz :java class object
  • name :成员域的名称(MUTF-8编码)
  • sig :成员域的签名(MUTF-8编码)

返回值:

返回成员域的fieldID,操作失败则返回 NULL

Get<>Field
jobject GetObjectField(JNIEnv *env, jobject obj, jfieldID fieldID);

jboolean GetBooleanField(JNIEnv *env, jobject obj, jfieldID fieldID);

jbyte GetByteField(JNIEnv *env, jobject obj, jfieldID fieldID);

jchar GetCharField(JNIEnv *env, jobject obj, jfieldID fieldID);

jshort GetShortField(JNIEnv *env, jobject obj, jfieldID fieldID);

jint GetIntField(JNIEnv *env, jobject obj, jfieldID fieldID);

jlong GetLongField(JNIEnv *env, jobject obj, jfieldID fieldID);

jfloat GetFloatField(JNIEnv *env, jobject obj, jfieldID fieldID);

jdouble GetDoubleField(JNIEnv *env, jobject obj, jfieldID fieldID);

这一系统Get方法,用于获取对象的非静态成员域。fieldID可以通过 GetFieldId() 函数来获取。

参数:

  • env :JNI接口指针
  • object:java对象(必须不能为 NULL
  • fieldId :一个有效的fieldID

返回值:

返回成员域的内容

Set<>Field
void SetObjectField(JNIEnv *env, jobject obj, jfieldID fieldID, jobject value);

void SetBooleanField(JNIEnv *env, jobject obj, jfieldID fieldID, jboolean value);

void SetByteField(JNIEnv *env, jobject obj, jfieldID fieldID, jbyte value);

void SetCharField(JNIEnv *env, jobject obj, jfieldID fieldID, jchar value);

void SetShortField(JNIEnv *env, jobject obj, jfieldID fieldID, jshort value);

void SetIntField(JNIEnv *env, jobject obj, jfieldID fieldID, jint value);

void SetLongField(JNIEnv *env, jobject obj, jfieldID fieldID, jlong value);

void SetFloatField(JNIEnv *env, jobject obj, jfieldID fieldID, jfloat value);

void SetDoubleField(JNIEnv *env, jobject obj, jfieldID fieldID, jdouble value);

一组设置基本类型成员域的函数。

参数:

  • env :JNI接口指针
  • object:java对象(必须不能为 NULL
  • fieldId :一个有效的fieldID
  • value:设置的新值

返回值:

调用对象方法

GetMethodID
jmethodID GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig);

返回一个类或接口的非静态实例方法MethodID,一个方法可以使 clazz 的方法,也可以使 clazz 从其父类继承而来的方法。这个方法靠方法名或方法签名来指定。

GetMethodID() 会触发一个没有初始化的类被初始化。

通过使用 <init> 作为方法名,void<V> 作为返回值来获取构造器的methodID。

参数:

  • env :JNI接口指针
  • clazz:java class object
  • name:方法名(MUTF-8编码)
  • sig:方法签名(MUTF-8编码)

返回值:

返回方法的MethodID,如果找不到该方法则返回 NULL

Call<>Method(MethodA, MethodV)
void CallVoidMethod(JNIEnv *env, jobject obj, jmethodID methodID, ...);
void CallVoidMethodA(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args);
void CallVoidMethodV(JNIEnv *env, jobject obj, jmethodID methodID, va_list args);

jobject CallObjectMethod(JNIEnv *env, jobject obj, jmethodID methodID, ...);
jobject CallObjectMethodA(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args);
jobject CallObjectMethodV(JNIEnv *env, jobject obj, jmethodID methodID, va_list args);

jboolean CallBooleanMethod(JNIEnv *env, jobject obj, jmethodID methodID, ...);
jboolean CallBooleanMethodA(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args);
jboolean CallBooleanMethodV(JNIEnv *env, jobject obj, jmethodID methodID, va_list args);

jbyte CallByteMethod(JNIEnv *env, jobject obj, jmethodID methodID, ...);
jbyte CallByteMethodA(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args);
jbyte CallByteMethodV(JNIEnv *env, jobject obj, jmethodID methodID, va_list args);

jchar CallCharMethod(JNIEnv *env, jobject obj, jmethodID methodID, ...);
jchar CallCharMethodA(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args);
jchar CallCharMethodV(JNIEnv *env, jobject obj, jmethodID methodID, va_list args);

jshort CallShortMethod(JNIEnv *env, jobject obj, jmethodID methodID, ...);
jshort CallShortMethodA(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args);
jshort CallShortMethodV(JNIEnv *env, jobject obj, jmethodID methodID, va_list args);

jint CallIntMethod(JNIEnv *env, jobject obj, jmethodID methodID, ...);
jint CallIntMethodA(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args);
jint CallIntMethodV(JNIEnv *env, jobject obj, jmethodID methodID, va_list args);

jlong CallLongMethod(JNIEnv *env, jobject obj, jmethodID methodID, ...);
jlong CallLongMethodA(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args);
jlong CallLongMethodV(JNIEnv *env, jobject obj, jmethodID methodID, va_list args);

jfloat CallFloatMethod(JNIEnv *env, jobject obj, jmethodID methodID, ...);
jfloat CallFloatMethodA(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args);
jfloat CallFloatMethodV(JNIEnv *env, jobject obj, jmethodID methodID, va_list args);

jdouble CallDoubleMethod(JNIEnv *env, jobject obj, jmethodID methodID, ...);
jdouble CallDoubleMethodA(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args);
jdouble CallDoubleMethodV(JNIEnv *env, jobject obj, jmethodID methodID, va_list args);

调用一个Java非静态实例方法,methodID 根据 GetMethodID() 来获取。

如果用这些函数来访问private方法或构造器方法,这MethodID必须来至真正的 obj 类,而不能是它们的父类。

Call<type>Method

可传入将所有需要传入给Java方法的参数放置在 methodID 参数之后,NewObject函数将接受三个参数,并将其传入到Java方法内。

Call<type>MethodA:

可将所有需要传入给Java方法的参数列表放置在 jvalue 数组里,它们会全部传给Java方法。

Call<type>MethodV:

可将Java方法的参数放置于 va_list 里,它们也会在调用Java方法时作为参数传入。

参数:

  • env :JNI接口指针
  • obj : 调用方法的java object
  • methodID:构造器方法ID
  • const jvalue *args: 构造器参数
  • va_list args : 构造器函数

返回值:

返回Java方法的返回结果。

访问静态域

GetStaticFieldID
jfieldID GetStaticFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);

返回类的静态域的fieldID,这个静态域通过字段名和签名来指定。GetStatic<type>FieldSetStatic<type>Field 一系列访问函数通过该方法获得的fieldID来获取静态域。

此函数可能引起未初始化(initialized)的类初始化。

参数:

  • env :JNI接口指针
  • clazz :java class object
  • name:静态域的名称
  • sig :静态域的签名

返回值:

返回fieldID,无法指定的静态域没有找到则返回 NULL

GetStaticField
jobject GetStaticObjectField(JNIEnv *env, jclass clazz, jfieldID fieldID);
jboolean GetStaticBooleanField(JNIEnv *env, jclass clazz, jfieldID fieldID);
jbyte GetStaticByteField(JNIEnv *env, jclass clazz, jfieldID fieldID);
jchar GetStaticCharField(JNIEnv *env, jclass clazz, jfieldID fieldID);
jshort GetStaticShortField(JNIEnv *env, jclass clazz, jfieldID fieldID);
jint GetStaticIntField(JNIEnv *env, jclass clazz, jfieldID fieldID);
jlong GetStaticLongField(JNIEnv *env, jclass clazz, jfieldID fieldID);
jfloat GetStaticFloatField(JNIEnv *env, jclass clazz, jfieldID fieldID);
jdouble GetStaticDoubleField(JNIEnv *env, jclass clazz, jfieldID fieldID);

一系列函数用于获取不同类型的静态域。

参数:

  • env :JNI接口指针
  • class:java class对象
  • fieldId: 静态域的fieldId.

返回值:

返回静态域的值。

SetStaticField
void SetStaticObjectField(JNIEnv *env, jclass clazz, jfieldID fieldID, jobject value);
void SetStaticBooleanField(JNIEnv *env, jclass clazz, jfieldID fieldID, jboolean value);
void SetStaticByteField(JNIEnv *env, jclass clazz, jfieldID fieldID, jbyte value);
void SetStaticCharField(JNIEnv *env, jclass clazz, jfieldID fieldID, jchar value);
void SetStaticShortField(JNIEnv *env, jclass clazz, jfieldID fieldID, jshort value);
void SetStaticIntField(JNIEnv *env, jclass clazz, jfieldID fieldID, jint value);
void SetStaticLongField(JNIEnv *env, jclass clazz, jfieldID fieldID, jlong value);
void SetStaticFloatField(JNIEnv *env, jclass clazz, jfieldID fieldID, jfloat value);
void SetStaticDoubleField(JNIEnv *env, jclass clazz, jfieldID fieldID, jdouble value);

一系列设置不同类型静态域的函数。

参数:

  • env :JNI接口指针
  • class:java class对象
  • fieldId: 静态域的fieldId.
  • value:静态域的新值

返回值:

调用静态方法

GetStaticMethodID
jmethodID GetStaticMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig);

返回类的静态方法的methodID,静态方法按方法名和签名来指定。

该方法可能引发未初始化的方法被初始化。

参数:

  • env :JNI接口指针
  • class:java class对象。
  • name:静态方法名(MUTF-8编码字符串)
  • sig:静态方法签名(MUTF-8编码字符串)

返回值:

返回静态方法的methodID,或操作失败返回 NULL

CallStaticMethod(MethodA, MethodV)
NativeType CallStatic<type>Method(JNIEnv *env, jclass clazz, jmethodID methodID, ...);

NativeType CallStatic<type>MethodA(JNIEnv *env, jclass clazz, jmethodID methodID, jvalue *args);

NativeType CallStatic<type>MethodV(JNIEnv *env, jclass clazz, jmethodID methodID, va_list args);

一系列调用不同类型的静态方法的函数。

参数:

  • env :JNI接口指针
  • clazz: Java class对象
  • methodId: 静态方法methodID

返回值:

返回Java静态方法返回的结果。

操作字符串

NewString
jstring NewString(JNIEnv *env, const jchar *unicodeChars, jsize len);

使用unicode字符串数组来构造一个 java.lang.String 对象。

参数:

  • env :JNI接口指针
  • unicodeChars :指向unicode字符串的指针
  • len : unicode字符串的长度

返回值:

返回一个Java String对象,当字符串无法被构造时返回 NULL

GetStringLength
jsize GetStringLength(JNIEnv *env, jstring string);

返回Java String的长度(unicode字符串数组的长度)

参数:

  • env :JNI接口指针
  • string:需要获取长度的字符串

返回值:

Java String的长度

GetStringChars
const jchar * GetStringChars(JNIEnv *env, jstring string, jboolean *isCopy);

返回Java String对象的unicode字符串数组的指针。这个指针一直在调用 ReleaseStringchars() 方法前都有效。

参数:

  • env :JNI接口指针
  • string:目标Java String对象
  • isCopy :NULL或JNI_TRUE表示返回一份copy,JNI_FALSE表示不copy,直接返回指向Java String的原始地址

返回值:

返回unicode 字符串的指针。操作失败而返回 NULL

NewStringUTF
jstring NewStringUTF(JNIEnv *env, const char *bytes);

根据一个MUTF-8编码的字符串数组(an array of characters in modified UTF-8 encodin)来构建一个 java.lang.String对象。

参数:

  • env :JNI接口指针
  • bytes:指向MUTF-8编码字符串的指针。

返回值:

返回一个Java String对象,或失败时返回 NULL

操作数组

GetArrayLength
jsize GetArrayLength(JNIEnv *env, jarray array);

返回数组(array)的元素个数。

参数:

  • env :JNI接口指针
  • array:目标Java数组对象

返回值:

返回数组的长度

使用实例:

NewObjectArray
jobjectArray NewObjectArray(JNIEnv *env, jsize length, jclass elementClass, jobject initialElement);

构造一个新的 elementClass 类型的数组,并设置其初始值。

参数:

  • env :JNI接口指针
  • length:数组长度
  • elementClass :数组元素的class
  • initialElement:初始化数组元素

返回值:

返回一个Java数组对象,如果数组不能被构造则返回 NULL

抛出异常:

  • OutOfMemoryError :如果系统内存不足时。
GetObjectArrayElement
jobject GetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index);

返回数组的index处的元素对象。

参数:

  • env :JNI接口指针
  • array:Java数组
  • index:数组下标

返回值:

返回一个Java对象。

SetObjectArrayElement
void SetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index, jobject value);

设置数组的某个值。

参数:

  • env :JNI接口指针
  • array:一个Java数组
  • index:数组下标
  • value:设置的新值

返回值:

NewArray
jbooleanArray NewBooleanArray(JNIEnv *env, jsize length);
jbyteArray NewByteArray(JNIEnv *env, jsize length);
jcharArray NewCharArray(JNIEnv *env, jsize length);
jshortArray NewShortArray(JNIEnv *env, jsize length);
jintArray NewIntArray(JNIEnv *env, jsize length);
jlongArray NewLongArray(JNIEnv *env, jsize length);
jfloatArray NewFloatArray(JNIEnv *env, jsize length);
jdoubleArray NewDoubleArray(JNIEnv *env, jsize length);

一组函数用于构造各种不同基本类型的数组对象。

参数:

  • env :JNI接口指针
  • length:数组长度

返回值:

返回Java数组,数组无法被构造则返回 NULL

GetArrayElements
jboolean GetBooleanArrayElements(JNIEnv *env, jbooleanArray array, jboolean *isCopy);
jbyte GetByteArrayElements(JNIEnv *env, jbyteArray array, jboolean *isCopy);
jchar GetCharArrayElements(JNIEnv *env, jcharArray array, jboolean *isCopy);
jshort GetShortArrayElements(JNIEnv *env, jshortArray array, jboolean *isCopy);
jint GetIntArrayElements(JNIEnv *env, jintArray array, jboolean *isCopy);
jlong GetLongArrayElements(JNIEnv *env, jlongArray array, jboolean *isCopy);
jfloat GetFloatArrayElements(JNIEnv *env, jfloatArray array, jboolean *isCopy);
jdouble GetDoubleArrayElements(JNIEnv *env, jdoubleArray array, jboolean *isCopy);

一组函数用于获取各种不同基本类型数组的元素。

返回的结构在调用相应的 Release<Type>ArrayElements() 之前一直有效。因为返回的可能是一个复制值,因此对于返回值的修改不一定会对原始数组产生影响,直到调用了相应的 Release<Type>ArrayElements() 才可能影响到原始数组。

参数:

  • env :JNI接口指针
  • array:目标数组
  • isCopy: NULLJNI_FALSE 不COPY,JNI_TRUE 返回COPY值。

返回值:

返回数组某个元素的指针。或失败返回 NULL

ReleaseArrayElements
void ReleaseBooleanArrayElements(JNIEnv *env, jbooleanArray array, jboolean *elems, jint mode);
void ReleaseByteArrayElements(JNIEnv *env, jbyteArray array, jbyte *elems, jint mode);
void ReleaseCharArrayElements(JNIEnv *env, jcharArray array, jchar *elems, jint mode);
void ReleaseShortArrayElements(JNIEnv *env, jshortArray array, jshort *elems, jint mode);
void ReleaseIntArrayElements(JNIEnv *env, jintArray array, jint *elems, jint mode);
void ReleaseLongArrayElements(JNIEnv *env, jlongArray array, jlong *elems, jint mode);
void ReleaseFloatArrayElements(JNIEnv *env, jfloatArray array, jfloat *elems, jint mode);
void ReleaseDoubleArrayElements(JNIEnv *env, jdoubleArray array, jdouble *elems, jint mode);

一组函数通知虚拟机本地代码不再需要访问数组中的元素。这些元素是使用 Get<type>ArrayElements函数来获取的,如果需要,这个函数会把修改后的内容复制到原始的数组中。使用最后一个参数 mode 来控制:

  • mode = 0 , 将内容复制回原始数组,并释放 elems (通常情况下都传入0即可)
  • mode = JNI_COMMIT ,将内容复制回原始数组,但不释放 elems
  • mode = JNI_ABORT ,不将内容复制回原始数组,并释放 elems

参数:

  • env :JNI接口指针

返回值:

GetArrayRegion
void GetBooleanArrayRegion(JNIEnv *env, jbooleanArray array, jsize start, jsize len,  jboolean *buf);
void GetByteArrayRegion(JNIEnv *env, jbyteArray array, jsize start, jsize len,  jbyte *buf);
void GetCharArrayRegion(JNIEnv *env, jcharArray array, jsize start, jsize len,  jchar *buf);
void GetShortArrayRegion(JNIEnv *env, jshortArray array, jsize start, jsize len,  jshort *buf);
void GetIntArrayRegion(JNIEnv *env, jintArray array, jsize start, jsize len,  jint *buf);
void GetLongArrayRegion(JNIEnv *env, jlongArray array, jsize start, jsize len,  jlong  *buf);
void GetFloatArrayRegion(JNIEnv *env, jfloatArray array, jsize start, jsize len,  jfloat *buf);
void GetDoubleArrayRegion(JNIEnv *env, jdoubleArray array, jsize start, jsize len,  jdouble *buf);

一组函数用来复制基本类型数据的一部分值到buffer。

参数:

  • env :JNI接口指针
  • array:java数组
  • start :起始的下标
  • len:需要复制的元素个数
  • buf:目标buffer

返回值:

Invocation API

创建虚拟机

JNI_CreateJavaVM() 函数载入和初始化一个Java虚拟机。调用该函数的线程被视为是主线程(main thread)。

attach到虚拟机

JNI接口指针(JNIEnv)只在当前线程有效,如果需要在另一个线程访问Java虚拟机,必须先调用 AttachCurrentThread() 来将自己 attach 到虚拟机来获得JNI接口指针(JNIEnv)

被acttach到的线程必须有足够的stack空间来执行一定的工作。每个线程分配多少栈空间根据系统而不同。

detach虚拟机

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

unload虚拟机

使用 JNI_DestroyJavaVM() 函数来卸载(unload)一个Java虚拟机

虚拟机会等待(阻塞),直到当前线程成为唯一的非守护进程的用户进程(the only non-daemon user thread),才真正执行 unload 操作。

用户进程(user thread)包括:

  • Java线程(java threads)
  • attached到虚拟机的本地线程(attached native threads)

为什么要做这样的限制(强制等待),是因为Java线程和native线程可能会hold住系统资源,例如锁,窗口等资源,而虚拟机不能自动释放这些资源。通过限制当前线程是唯一的运行中的用户线程才unload虚拟机,则将释放这种系统资源的任务交给程序员自己来负责了。

5.2 库和版本管理

在 JDK/JRE 1.1,一旦本地库被加载,它对所有classLoader都可见。因此两个被不同的classLoader加载的类可能链接到同一个本地方法。这会出现两个问题:

  • 一个类可能错误的链接到了另一个classLoader载入的同名本地库。
  • 本地方法可以轻松的混合不同ClassLoader载入的类,这破坏了由ClassLoader提供的命名空间隔离,会可能引发类型安全问题。

在 JDK/JRE 1.2,每个ClassLoader都有自己的一组本地库。一个本地库一旦被一个ClassLoader加载后,则不允许再被其他ClassLoader重复加载了。否则会抛出 UnsatisfiedLinkError 异常。这样的好处是:

  • 由ClassLoader创建的命名空间隔离在本地库也被保存下来了。一个本地库不能轻易混合不同ClassLoader加载的类。
  • 另外,本地库可以在ClassLoader被垃圾收回时unload。

ClassLoader什么时候会被垃圾收回?

JNI_OnLoad

Java虚拟机在加载本地库(native library)时(即调用 System.loadLibrary() )后,在加载本地库到内存之后,会寻找其内部的 JNI_OnLoad 函数,并执行它。这个函数必须返回本地库使用的 JNI版本号。

如果要使用新的JNI函数,则必须返回高版本的JNI版本号。如果本地库不提供 JNI_ONLoad 函数,则虚拟机默认它使用的是 JNI_VERSION_1_1版本。如果返回一个虚拟机不支持的JNI版本号,则本地库不能被加载。

JNI_OnUnload

虚拟机在本地库被垃圾回收前,调用其 JNI_OnUnload 函数。这个函数用来执行一个清理操作。因为函数在不确定的情况下被调用(例如 finalizer),因此开发者应该保守的使用Java虚拟机服务,并避免任何Java回调函数。

注意:JNI_OnLoadJNI_OnUnload 两个函数是可选的,不是必须要有。

JNI demo

IDEA + JNI

JNI调用C的流程图

image

IDEA 项目最终目录

image

步骤如下:   

  • ①、编写带有 native 声明的方法的java类,生成.java文件;

    package com.xiaoxiao.JniHello;
    
    
    public class SayHello {
    
        //native 关键字告诉 JVM 调用的是该方法在外部定义
        private native void helloJNI();
    
        static{
            System.loadLibrary("SayHello");//载入本地库
        }
        public static void main(String[] args) {
            SayHello jni = new SayHello();
            jni.helloJNI();
        }
    }
    
  • ②、使用 javac 命令编译所编写的java类,生成.class文件;

    # -encoding UTF-8 有中文注释的情况下使用
    
    javac -encoding UTF-8 SayHello.java
    
  • ③、使用 javac -h java类名.java 生成扩展名为 h 的头文件,即生成.h文件;

    # 也可以直接一步到位
    
    javac -encoding UTF-8 -h ./ SayHello.java
    

image

  • ④、使用C/C++实现本地方法。创建.h文件的目的,也为了创建.cpp文件实现.h文件中的方法;

    .h 文件

image

.cpp文件

#include"com_xiaoxiao_JniHello_SayHello.h"


JNIEXPORT void JNICALL Java_com_xiaoxiao_JniHello_SayHello_helloJNI
  (JNIEnv *, jobject){
  
  printf("HelloJNI!\n");

  }
  • ⑤、将C/C++编写的文件生成动态连接库,生成dll文件;

    # -m64 代表64位程序,需要与jdk版本适配
    # -I"" 引用的文件头路径
    
    gcc SayHello.cpp -m64 -I"D:\Java_path\java17path\include" -I"D:\Java_path\java17path\include\win32" -fPIC -shared -o SayHello.dll
    

image

vm 选项设置,使loadLibrary()正确加载库路径。【注意库文件目录不能存在中文

# 添加vm选项

-Djava.library.path=xxxxxxxxxxx [存放dll库文件路径]

image

成功运行!

image

apk Java JNI 逆向技巧

有了上面的JNI 语法的铺垫,我们就可以很好地去逆向apk 中涉及调用静态库的函数了。

[原创]Android逆向新手答疑解惑篇——JNI与动态注册-Android安全-看雪论坛-安全社区|安全招聘|bbs.pediy.com (kanxue.com)

【其实就是ida 无法正确识别,JNI函数类型。导致逆向分析难度加大】

那么对应的,我们只需要根据JNI语法规则,在ida中按y修改参数类型,使其正确解析即可!

案例 babyanti

jadx

jadx 先分析java 代码,及调用关系链。

image

image

这里可以看到,onCreate()后,调用了m19860(),然后使用了makeCRC32_()

ida

JNI_ONload()

image

image

可以看到并未识别出JNI函数正确的参数,这也导致了逆向的难度加大!

int a1 ==> JavaVM *a1
int v1 ==> JNIEnv *v1
int v2 ==> jclass *v2
_DWORD v4[4] ==> JNIEnv *v4[4]

# 这里如何做出准确的修改呢?

显然需要根据JNI函数的语法规则,以及实现功能去相应修改类型。
【同时,也可以猜着改,知道修正完毕】

image

到这里显然可以知道JNI_OnLoad()都做了些什么。

通过JAVAvm 获取 env:JNI接口指针,然后找到com/VNCTF2023/anti_cheat/AntiCheatPlugin类,在进行动态注册来绑定native本地方法。

makeCRC32Digest()
# makeCRC32Digest()声明
# CRC32是循环冗余校验码(Cyclic Redundancy Check,CRC)算法的一种,用于在数据传输或存储过程中检测数据是否发生了改变。

private native int makeCRC32Digest(byte[] bArr, int i);

image

image

这里把第一个参数改为JNIEnv* 即可。函数主要完成读取字节数组a3到v4,然后使用sub_54C40()进行循环冗余校验。

反debug检测函数
is_device_rooted
_DWORD v6[6]==> JNIEnv *v6[6]

image

image

通过获取安卓android/os/Build类中TAGS描述的build的标签,如未签名,debug等等,实现root检测,这里是与test-keys(测试版)进行对比判断。

【release-keys”,代表此系统是正式版。是安全的】

接下来还有sub_53040函数,通过对

/system、/system/bin、/system/sbin、/system/xbin、/vendor/bin、/sys、/sbin、/etc、/proc、/dev

等路径是否具有写权限,来判定是否为root账户。

image

is_device_modified

image

这里通过 dword_CB408 进行检测。

【前面我们已经知道,这个值会有CRC32 循环冗余码进行校验赋值】

is_device_injected

image

注入hook检测,通过读取/proc/self/maps 中是否存在frida字样进行检测!

常见frida检测

1.检测frida-server文件名 
2.检测27042默认端口 
3.检测D-Bus 
4.检测/proc/pid/maps映射文件 
5.检测/proc/pid/tast/tid/stat或/proc/pid/tast/tid/status 
6.双进程保护
反检测函数如何进行调用的呢?

我们进行交叉引用x发现,上面所述的反检测函数,没有直接的调用!

image

发现了

image

image

显然 JNI_Onload 完成了这两个方法的注册!

image

image

可以看到sendValue() 发送的b、c这两个值,赋给了反函数检测修改的变量。

getValue()又将修改过的值设置到b、c

至此 .so 文件就分析完了。

完整调用过程

image

frida 解法

image

学习参考链接:
https://www.jianshu.com/p/35848c03f2d5

【做学习总结之用,相关图片、叙述侵权联系即删】

posted @ 2023-04-06 21:18  Only-xiaoxiao  阅读(386)  评论(0编辑  收藏  举报