Powered by ThomasKing 2015.04.21. All Rights Reversed
JNINativeInterface Hook
ThomasKing 2015.05.07
0x00 概述
JNINativeInterface Hook指的是:HOOK JNI接口提供的方法,名字取得有点挫,暂时这样吧。HOOK JNI接口方法,trace 函数调用,使得APP JAVA与Native层的交互更加清晰,便于分析。下面,小弟将讲解HOOK的思路来源及实现,并通过2015年ALICTF第四题APK作为实例,感受下trace JNI接口的魅力。限于水平,难免会有疏漏和错误之处,请各位大大斧正,小弟感激不尽。
0x01 思路来源
有过NDK开发的读者都知道,比如NewStringUTF,调用时的C形式是:(*env)->NewStringUTF(env, “xxx”),其汇编形式: mov Ry, [Rx, #0x29c]; blx Ry。不难发现,*env指向了一个JNI接口的函数表:struct JNINativeInterface gNativeInterface(dalvik/vm/Jni.cpp)。这个表中存放了实际各种JNI接口函数指针,那么HOOK的基本思路就是替换这个表中的函数指针。
0x02 JNINativeInterface Hook
1. JNIEnv背后的秘密
在Dalvik虚拟机启动过程中,会调用dvmCreateJNIEnv方法创建JNIEnv:
JNIEnv* dvmCreateJNIEnv(Thread* self) {
JavaVMExt* vm = (JavaVMExt*) gDvmJni.jniVm; //JavaVMExt其实就是Onload方法传入的JavaVM
assert(vm != NULL);
JNIEnvExt* newEnv = (JNIEnvExt*) calloc(1, sizeof(JNIEnvExt)); newEnv->funcTable = &gNativeInterface;
if (self != NULL) {
dvmSetJniEnvThreadId((JNIEnv*) newEnv, self); assert(newEnv->envThreadId != 0); } else {
/* make it obvious if we fail to initialize these later */ newEnv->envThreadId = 0x77777775; newEnv->self = (Thread*) 0x77777779; }
if (gDvmJni.useCheckJni) {
dvmUseCheckedJniEnv(newEnv); }
Powered by ThomasKing 2015.04.21. All Rights Reversed
ScopedPthreadMutexLock lock(&vm->envListLock);
newEnv->next = vm->envList; assert(newEnv->prev == NULL); if (vm->envList == NULL) { // rare, but possible vm->envList = newEnv; } else {
vm->envList->prev = newEnv; }
vm->envList = newEnv;
return (JNIEnv*) newEnv; }
从return的指针转换可知,JNIEnv实际指向的是JNIEnvEx结构体:
struct JNIEnvExt {
const struct JNINativeInterface* funcTable;
const struct JNINativeInterface* baseFuncTable; u4 envThreadId; Thread* self; int critical;
struct JNIEnvExt* prev; struct JNIEnvExt* next; };
newEnv->funcTable = &gNativeInterface;初始化了JNI函数接口指针。另外,在Jni.cpp可以看到声明static const struct JNINativeInterface gNativeInterface,const关键字只是编译器限制,其实际存在放libdvm.so RW segment中,并且其保存的函数指针在加载时还需重定位。故HOOK时无需调用mprotect修改内存权限标示。
此时,JNI函数调用就比较清晰了:(*env)->NewStringUTF(env, “xxx”) -> (env-> funcTable + 0x29c)(env, “xxx”),和汇编代码完美吻合。
//插入双向链表
2. Hook实现
了解了JNIEnv背后的真实类型后,那么Hook就简单了。比如: pOld_NewStringUTF = (*env)->NewStringUTF (*env)->NewStringUTF = TK_ StringUTF
Jstring TK_StringNewStringUTF(JNIEnv* env, const char *str){
LOGD(“TK”, “HOOK!!!”);
Return pOld_NewStringUTF(env, str); }
Hook单个函数可以直接这么替换,不过要HOOK一部分或者全部的话,声明一大堆函数指针比较跪。考虑通过声明一个JNIEnvExt结构体实现来保存。这里不能直接声明一个JNInterface结构体,因为不包含Thread等信息。声明的JNIEnvExt需要拷贝当前的JNIEnv的其他信息,即:
Powered by ThomasKing 2015.04.21. All Rights Reversed
memcpy((char*)(pOldJNIEnvExt) + sizeof(struct TK_JNINativeInterface*), (char*)(env) + sizeof(struct TK_JNINativeInterface*), sizeof(struct TK_JNIEnvExt) - sizeof(struct TK_JNINativeInterface*));
另外,JNIEnvExt结构体在不同版本之间也有区别,这点需要注意。不然HOOK去读取Thread等信息时,由于字段偏移不同造成崩溃。 Android2.x:
typedef struct JNIEnvExt { const struct JNINativeInterface* funcTable; const struct JNINativeInterface* baseFuncTable; struct JavaVMExt* vm; u4 envThreadId; Thread* self; int critical; bool forceDataCopy; struct JNIEnvExt* prev; struct JNIEnvExt* next; }JNIEnvExt;
Android 4.x:
struct JNIEnvExt { const struct JNINativeInterface* funcTable; const struct JNINativeInterface* baseFuncTable; u4 envThreadId; Thread* self; int critical; struct JNIEnvExt* prev; struct JNIEnvExt* next; };
另外,一些JNI接口函数调用时支持变参,现考虑如何获取参数。比如方法: jobject (JNICALL *CallObjectMethod)
(JNIEnv *env, jobject obj, jmethodID methodID, ...); jobject (JNICALL *CallObjectMethodV)
(JNIEnv *env, jobject obj, jmethodID methodID, va_list args);
对于大多数开发者而言,通过习惯使用CallObjectMethod这种变参形式。其编译后实际会被转换为CallObjectMethodV这种形式,这点通过HOOK来验证。通过va_arg(va_list, type)可以获得各个参数,在stdarg.h中可以看到,va_arg其实际通过宏定义实现。不过还是没有解决一个问题,那就是参数个数。
Va_arg虽然可以获取下一个参数,不过并没有说明现在方法有多少个参数,那Dalvik虚拟机执行时是如何知道的呢?熟悉jmethod结构的读者可能已经想到,通过Method结构体的字段来表示。这里选择shorty字段,shorty字段第一个存放了返回类型的字符缩写,其后存放了各个参数类型,非基本类型用’L’表示。通过解析shorty字符串,即可获得参数类型和个数。另外,由于变参通过栈上传递,栈上4字节对其,取short等类型时,通过va_arg(va_list, jint)来取得。在本机测试时,由于float类型和double类型的取得存在问题,通过va_arg(va_list,
Powered by ThomasKing 2015.04.21. All Rights Reversed
jint)和va_arg(va_list, jlong)实现。
这里在啰嗦下,如果需要修改参数的值,需要采用类似调用点检测的方法来实现。虽然args其实际为一个指向栈上指针,由于va_list的宏展开编译时会引起一些异常。这里,通过GETR3汇编宏直接获取R3的值,其实也就是args的值。有了这个指针,即可修改调用参数。 另外,由于这些方法也被系统调用,故需要过滤。根据调用点地址,过滤掉从libdvm.so和libnativehelper.so中的调用,减少log的数量。
0x03 实例分析
在通过log分析ALICTF第四题时,先说明下log的格式:
[0x80c15971] CallObjectMethodV(0x40519d00, 0x427c4110, [I:0x0][L:0x405216f0]) [0x4051fe38] 0x80c15971 调用点
0x40519d00 jclazz or jobject 0x427c4110 jmethod
[I:0x0][L:0x405216f0] 两个参数,第一个参数类型int,第二个为非基本类型 0x4051fe38 返回值
此APK经过dex加固和SO加固,先脱出dex文件打开。其中包含你好中国各种native方法:
Powered by ThomasKing 2015.04.21. All Rights Reversed
图 1
各个类中有都调用了这个类中的native方法,即JAVA层和Native层频繁交互。到这里可以猜测,将java直接调用的方法,通过Native间接调用,模糊调用流程。 不过还是可以锁定btn的onclick方法:
图 2
不难发现,__bb方法即是verify方法。
到这里,基本分析完毕。现在需要解决一个很棘手的问题:这些native函数的地址,即找到Native函数。由于SO被加壳,而且汇编代码存在混淆,直接跟出代码费事费力。现在使用Hook trace下: