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 虚拟机的流程。

相关推荐
腾讯云云开发4 分钟前
小程序数据库权限管理,一看就会!——CloudBase新手指南
前端·数据库·微信小程序
多则惑少则明41 分钟前
Vue开发系列——自定义组件开发
前端·javascript·vue.js
用户250694921611 小时前
next框架打包.next文件夹部署
前端
程序猿小蒜1 小时前
基于springboot的校园社团信息管理系统开发与设计
java·前端·spring boot·后端·spring
一叶难遮天1 小时前
开启RN之旅——前端基础
前端·javascript·promise·js基础·es6/ts·npm/nrm
申阳1 小时前
Day 4:02. 基于Nuxt开发博客项目-整合 Inspira UI
前端·后端·程序员
程序猿_极客1 小时前
【期末网页设计作业】HTML+CSS+JavaScript 猫咪主题网站开发(附源码与效果演示)
前端·css·html·课程设计·网页设计作业
IT古董1 小时前
【前端】从零开始搭建现代前端框架:React 19、Vite、Tailwind CSS、ShadCN UI 完整实战教程-第1章:项目概述与技术栈介绍
前端·react.js·前端框架
有点笨的蛋1 小时前
从零搭建小程序首页:新手也能看懂的结构解析与实战指南
前端·微信小程序
爱宇阳1 小时前
Vue3 前端项目 Docker 容器化部署教程
前端·docker·容器