JNI-Dalvik下动态注册原理

动态注册

在库加载时会自动调用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 虚拟机的流程。

相关推荐
森叶1 分钟前
Electron 安装包 asar 解压定位问题实战
前端·javascript·electron
drebander4 分钟前
ubuntu 安装 chrome 及 版本匹配的 chromedriver
前端·chrome
软件技术NINI14 分钟前
html知识点框架
前端·html
深情废杨杨17 分钟前
前端vue-插值表达式和v-html的区别
前端·javascript·vue.js
GHUIJS18 分钟前
【vue3】vue3.3新特性真香
前端·javascript·vue.js
众生回避24 分钟前
鸿蒙ms参考
前端·javascript·vue.js
洛千陨24 分钟前
Vue + element-ui实现动态表单项以及动态校验规则
前端·vue.js
GHUIJS1 小时前
【vue3】vue3.5
前端·javascript·vue.js
&白帝&2 小时前
uniapp中使用picker-view选择时间
前端·uni-app
魔术师卡颂2 小时前
如何让“学源码”变得轻松、有意义
前端·面试·源码