分享一个之前写的小东西
------------------------------------------------------------------
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);
}
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的其他信息,即:
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, 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方法:
图 1
各个类中有都调用了这个类中的native方法,即JAVA层和Native层频繁交互。到这里可以猜测,将java直接调用的方法,通过Native间接调用,模糊调用流程。
不过还是可以锁定btn的onclick方法:
图 2
不难发现,__bb方法即是verify方法。
到这里,基本分析完毕。现在需要解决一个很棘手的问题:这些native函数的地址,即找到Native函数。由于SO被加壳,而且汇编代码存在混淆,直接跟出代码费事费力。现在使用Hook trace下:
图 3
各种Native方法已经吐出来了。另外,加载过程的调用也已经trace到:
图4
找到了各个native函数,再任意输入查看调用(部分截图):
图 5
调用Base64参数:
[0x80c59851] FindClass(android/util/Base64) [0x40538928]
[0x80c59781] GetStaticMethodID(0x40538928, decode, (Ljava/lang/String;I)[B) [0x4297fef0]
[0x80c1591f] CallStaticObjectMethodV(0x40538928, 0x4297fef0, [L:0x4053a540][I:0x0]) [0x4053a620]
返回的0x4053a620后面被用于验证:
[0x80c8e8a1] FindClass(你好中国) [0x405209f8]
[0x80c8e9cf] GetStaticMethodID(0x405209f8, __x, ([B[B)Z) [0x42974578]
[0x80c31633] CallStaticBooleanMethodV(0x405209f8, 0x42974578, [L:0x4053a220][L:0x4053a620]) [False] //比较
比较的返回值为False,已经很明显了吧。。。
限于篇幅,就不详细分析了吧,直接根据log和简单的动态调试即可获得答案:wsfduj。有兴趣的读者自己试试。
鉴于之前发动态链接库有小伙伴说难用,直接上静态链接库文件。
0x04 参考文献
老罗:Dalvik虚拟机的运行过程分析
Dalvik Jni相关源码
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
上传的附件: