动态注册
在库加载时会自动调用JNI_OnLoad()
函数,开发者经常会JNI_OnLoad()
函数做一些初始化操作,我们可以将动态注册放在这里进行。
调用API是env->RegisterNatives(clazz, gMethods, numMethods)
。
env->RegisterNatives(clazz, gMethods, numMethods)
是一个接受三个参数的函数,第一个参数是Java对应的类,第二个参数是JNINativeMethod
数组,第三个参数是JNINativeMethod
数组的长度,也就是需要注册的方法的个数。其中JNINativeMethod
表示的是方法方法的映射关系,它包括Java中的方法名,对应的方法签名和Native映射的函数方法三个信息。
相比静态注册,动态注册的灵活性更高,如果修改了java native函数所在类的包名或类名,仅调整Java native函数的签名信息即可,而且在对抗逆向的时候可玩性更好。
动态注册的例子
java 类
csharp
public class RegisterNative {
public static native void javaTest();
public static native int javaTest2();
}
Jni代码
scss
void test1() {
LOGD("test1 invoked");
}
int test2() {
LOGD("test2 invoked");
return 42;
}
int JNI_OnLoad(JavaVM *vm, void *r) {
JNIEnv *env = nullptr;
int registerResult = vm->GetEnv((void **) &env, JNI_VERSION_1_6);
if (registerResult != JNI_OK) {
return -1;
}
jclass jclazz = env->FindClass("com/aprz/mytestdemo/jni/RegisterNative");
JNINativeMethod methods[] = {
{"javaTest", "()V", (void *) test1},
{"javaTest2", "()I", (void *) test2}
};
env->RegisterNatives(jclazz, methods, sizeof(methods) / sizeof(JNINativeMethod));
env->DeleteLocalRef(jclazz);
return JNI_VERSION_1_6;
}
编译运行
test2 invoked
test1 invoked
IDA 观察
我们使用 IDA 打开so文件,看看 JNI_Onload 方法里面的逻辑:
我们可以很清晰的看到 test1 与 test2 的函数名字。为了避免这种情况,我们可以将函数符号隐藏起来:
scss
__attribute__((visibility ("hidden"))) void test1(JNIEnv *env, jclass clazz) {
LOGD("test1 invoked");
}
再看反编译之后的代码:
发现,函数名字变成了一串数字,这就稍微增加了点逆向难度。
RegisterNatives代码分析
env->RegisterNatives
方法会调用到 Jni.cpp 里面去:
这个方法首先将我们传递的 jclazz 转成了虚拟机的 ClassObject
对象。然后调用 dvmRegisterJNIMethod
方法:
scss
/*
* Register a method that uses JNI calling conventions.
*/
static bool dvmRegisterJNIMethod(ClassObject* clazz, const char* methodName,
const char* signature, void* fnPtr)
{
...
Method* method = dvmFindDirectMethodByDescriptor(clazz, methodName, signature);
if (method == NULL) {
method = dvmFindVirtualMethodByDescriptor(clazz, methodName, signature);
}
if (method == NULL) {
dumpCandidateMethods(clazz, methodName, signature);
throwNoSuchMethodError(clazz, methodName, signature, "static or non-static");
return false;
}
if (!dvmIsNativeMethod(method)) {
ALOGW("Unable to register: not native: %s.%s:%s", clazz->descriptor, methodName, signature);
throwNoSuchMethodError(clazz, methodName, signature, "native");
return false;
}
...
method->fastJni = fastJni;
dvmUseJNIBridge(method, fnPtr);
ALOGV("JNI-registered %s.%s:%s", clazz->descriptor, methodName, signature);
return true;
}
首先,要根据描述符来找方法,先找类的直接方法,没找到的话,再找类的虚方法,这个过程就很像我们之前实现 invoke 指令的过程。如果还没有的话,就要报错了。然后检查方法是不是native方法,不是就要报错。
找到之后呢,会跳到 dvmUseJNIBridge
方法里面:
javascript
/*
* Point "method->nativeFunc" at the JNI bridge, and overload "method->insns"
* to point at the actual function.
*/
void dvmUseJNIBridge(Method* method, void* func) {
...
DalvikBridgeFunc bridge = gDvmJni.useCheckJni ? dvmCheckCallJNIMethod : dvmCallJNIMethod;
dvmSetNativeFunc(method, bridge, (const u2*) func);
}
这里出现了一个变量,useCheckJni
,这个值在启动虚拟机的时候可以设置,默认情况下是 false,所以 bridge
的值是 dvmCallJNIMethod
。
深入到 dvmSetNativeFunc
方法:
vbscript
/*
* Replace method->nativeFunc and method->insns with new values. This is
* commonly performed after successful resolution of a native method.
*
* There are three basic states:
* (1) (initial) nativeFunc = dvmResolveNativeMethod, insns = NULL
* (2) (internal native) nativeFunc = <impl>, insns = NULL
* (3) (JNI) nativeFunc = JNI call bridge, insns = <impl>
*
* nativeFunc must never be NULL for a native method.
*
* The most common transitions are (1)->(2) and (1)->(3). The former is
* atomic, since only one field is updated; the latter is not, but since
* dvmResolveNativeMethod ignores the "insns" field we just need to make
* sure the update happens in the correct order.
*
* A transition from (2)->(1) would work fine, but (3)->(1) will not,
* because both fields change. If we did this while a thread was executing
* in the call bridge, we could null out the "insns" field right before
* the bridge tried to call through it. So, once "insns" is set, we do
* not allow it to be cleared. A NULL value for the "insns" argument is
* treated as "do not change existing value".
*/
void dvmSetNativeFunc(Method* method, DalvikBridgeFunc func,
const u2* insns)
{
ClassObject* clazz = method->clazz;
assert(func != NULL);
/* just open up both; easier that way */
dvmLinearReadWrite(clazz->classLoader, clazz->virtualMethods);
dvmLinearReadWrite(clazz->classLoader, clazz->directMethods);
if (insns != NULL) {
/* update both, ensuring that "insns" is observed first */
method->insns = insns;
android_atomic_release_store((int32_t) func,
(volatile int32_t*)(void*) &method->nativeFunc);
} else {
/* only update nativeFunc */
method->nativeFunc = func;
}
dvmLinearReadOnly(clazz->classLoader, clazz->virtualMethods);
dvmLinearReadOnly(clazz->classLoader, clazz->directMethods);
}
这个方法做的事情在注释里面已经说的很清楚了,就是将 method 的 nativeFunc 这个成员变量的值替换为 func,而这个 func 就是上面说的 bridge 的值。
nativeFunc 可以理解为是 native 函数的入口,从传递的流程来看,我们动态注册的 c 函数的地址会保存到这个变量里面。至于参数是如何传递的,可以继续追踪 dvmCallJNIMethod
函数:
scss
/*
* General form, handles all cases.
*/
void dvmCallJNIMethod(const u4* args, JValue* pResult, const Method* method, Thread* self) {
...
int idx = 0;
Object* lockObj;
if ((accessFlags & ACC_STATIC) != 0) {
lockObj = (Object*) method->clazz;
/* add the class object we pass in */
staticMethodClass = (jclass) addLocalReference(self, (Object*) method->clazz);
} else {
lockObj = (Object*) args[0];
/* add "this" */
modArgs[idx++] = (u4) addLocalReference(self, (Object*) modArgs[0]);
}
...
dvmPlatformInvoke(env,
(ClassObject*) staticMethodClass,
method->jniArgInfo, method->insSize, modArgs, method->shorty,
(void*) method->insns, pResult);
...
}
从这里开始,逻辑就变得稍微抽象了点,但是还是能大致看明白的,这个方法就是在获取一些参数的值,好传递给 method 的 nativeFunc 变量。再往下面,就需要看 dvmPlatformInvoke
代码了,这个方法的实现与平台有关,参数的传递在汇编里面都不太一样,看个例子:
传递的参数在栈里面的分布如下:
scss
[ 8] arg0 JNIEnv (can be left alone)
[12] arg1 clazz (NULL for virtual method calls, non-NULL for static)
[16] arg2 arg info
[20] arg3 argc
[24] arg4 argv
[28] arg5 short signature
[32] arg6 func
[36] arg7 pReturn
位置12是 clazz,如果是静态方法,它就是非空的。
我们看上面的汇编,将12位置的值拿出来,判断是不是 0,如果不是那么就走到 isClass 位置,然后在调用 nativeFunc 函数的时候,传递的参数就是 clazz。如果 12 的位置不是0,那么 eax 的值是 esi的值,而 esi 的值是 argv[0] 的值,这个值就是 this,所以这就很好的解释了为啥静态方法与实例方法在JNI中的参数是不一样的。
scss
Java_you_package_Class_StaticMethod(JNIEnv *env, jclass type, ...) {}
Java_you_package_Class_Method(JNIEnv *env, jobject jthis, ...) {}
有一点疑惑,eax 是方法的第一个参数,但是第一个参数应该是 JNIEnv 才对,可能是汇编代码后面又搞了啥操作,比如 copy 参数之类的,算了,懒得跟了,反正,大致的逻辑就是这样了。
下一篇,分析一下 Art 虚拟机的流程。