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
:指派用来加载类的ClassLoaderbuf
:包含 .class 文件数据的 bufferbufLen
: 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
:构造器方法IDconst 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>Field
和 Set<type>Field
来访问具体的成员域。
GetFieldID()
可能会导致一个没有被初始化(uninitialized)的类被初始化。
GetFieldId()
不能用来获取数组的长度,使用 GetArrayLength()
来替代。
参数:
env
:JNI接口指针clazz
:java class objectname
:成员域的名称(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
:一个有效的fieldIDvalue
:设置的新值
返回值:
无
调用对象方法
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 objectname
:方法名(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 objectmethodID
:构造器方法IDconst jvalue *args
: 构造器参数va_list args
: 构造器函数
返回值:
返回Java方法的返回结果。
访问静态域
GetStaticFieldID
jfieldID GetStaticFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
返回类的静态域的fieldID,这个静态域通过字段名和签名来指定。GetStatic<type>Field
和 SetStatic<type>Field
一系列访问函数通过该方法获得的fieldID来获取静态域。
此函数可能引起未初始化(initialized)的类初始化。
参数:
env
:JNI接口指针clazz
:java class objectname
:静态域的名称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
:数组元素的classinitialElement
:初始化数组元素
返回值:
返回一个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
:NULL
或JNI_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_OnLoad
和 JNI_OnUnload
两个函数是可选的,不是必须要有。
JNI demo
IDEA + JNI
JNI调用C的流程图
IDEA 项目最终目录
步骤如下:
-
①、编写带有 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
-
④、使用C/C++实现本地方法。创建.h文件的目的,也为了创建.cpp文件实现.h文件中的方法;
.h 文件
.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
vm 选项设置
,使loadLibrary()正确加载库路径。【注意库文件目录不能存在中文】
# 添加vm选项
-Djava.library.path=xxxxxxxxxxx [存放dll库文件路径]
成功运行!
apk Java JNI 逆向技巧
有了上面的JNI 语法的铺垫,我们就可以很好地去逆向apk 中涉及调用静态库的函数了。
[原创]Android逆向新手答疑解惑篇——JNI与动态注册-Android安全-看雪论坛-安全社区|安全招聘|bbs.pediy.com (kanxue.com)
【其实就是ida 无法正确识别,JNI函数类型。导致逆向分析难度加大】
那么对应的,我们只需要根据JNI语法规则,在ida中按y
修改参数类型,使其正确解析即可!
案例 babyanti
jadx
jadx 先分析java 代码,及调用关系链。
这里可以看到,onCreate()后,调用了m19860(),然后使用了makeCRC32_()
ida
JNI_ONload()
可以看到并未识别出JNI函数正确的参数,这也导致了逆向的难度加大!
int a1 ==> JavaVM *a1
int v1 ==> JNIEnv *v1
int v2 ==> jclass *v2
_DWORD v4[4] ==> JNIEnv *v4[4]
# 这里如何做出准确的修改呢?
显然需要根据JNI函数的语法规则,以及实现功能去相应修改类型。
【同时,也可以猜着改,知道修正完毕】
到这里显然可以知道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);
这里把第一个参数改为JNIEnv*
即可。函数主要完成读取字节数组a3到v4,然后使用sub_54C40()进行循环冗余校验。
反debug检测函数
is_device_rooted
_DWORD v6[6]==> JNIEnv *v6[6]
通过获取安卓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账户。
is_device_modified
这里通过 dword_CB408 进行检测。
【前面我们已经知道,这个值会有CRC32 循环冗余码进行校验赋值】
is_device_injected
注入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
发现,上面所述的反检测函数,没有直接的调用!
发现了
显然 JNI_Onload 完成了这两个方法的注册!
可以看到sendValue() 发送的b、c
这两个值,赋给了反函数检测修改的变量。
getValue()又将修改过的值设置到b、c
。
至此 .so 文件就分析完了。
完整调用过程
frida 解法
学习参考链接:
https://www.jianshu.com/p/35848c03f2d5
【做学习总结之用,相关图片、叙述侵权联系即删】