首页
社区
课程
招聘
[原创]JNINativeInterfaceHook & trace
2015-5-7 19:28 24481

[原创]JNINativeInterfaceHook & trace

2015-5-7 19:28
24481
分享一个之前写的小东西
------------------------------------------------------------------
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相关源码

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

上传的附件:
收藏
点赞0
打赏
分享
最新回复 (25)
雪    币: 219
活跃值: (52)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
少仲 2015-5-7 19:51
2
0
前排膜拜+学习~~~~~~
雪    币: 187
活跃值: (1063)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
vVv一 2015-5-7 21:26
3
0
收藏~以后学习!
雪    币: 233
活跃值: (148)
能力值: ( LV9,RANK:210 )
在线值:
发帖
回帖
粉丝
boyliang 5 2015-5-8 09:19
4
0
思路很好,赞。
雪    币: 5900
活跃值: (1631)
能力值: ( LV7,RANK:150 )
在线值:
发帖
回帖
粉丝
二当家a 2 2015-5-8 10:28
5
0
你好,有个问题想请教一下。

目前我在进行dalvik的插桩,但是apk的每个method对于使用的寄存器数有限制,有的限制在v0 ~v15之间,有的是v0 ~v 255. 这个设置感觉跟编译出来的结果有关,
但是我经常会需要加入比较多的 寄存器, 遇到限制在 v0 ~ v15 之间的限制,就没有办法了, 会重编译出现错误。
不知道托马斯兄有没有比较好的建议或者方案,谢谢!
雪    币: 9
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
有效啊 2015-5-8 11:12
6
0
mark一下
雪    币: 75
活跃值: (21)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
longingfor 2015-5-8 11:23
7
0
tk大大又来分享干货了赞一个~
雪    币: 574
活跃值: (1314)
能力值: ( LV12,RANK:210 )
在线值:
发帖
回帖
粉丝
我是土匪 4 2015-5-8 11:40
8
0
好文,感谢分享。
雪    币: 76
活跃值: (13)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
mingxuan三千 2015-5-8 12:01
9
0
学习  了!
雪    币: 94
活跃值: (1762)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
wangzehua 2015-5-8 12:32
10
0
ThomasKing大大又一篇精华~学习啦
雪    币: 29
活跃值: (499)
能力值: ( LV8,RANK:120 )
在线值:
发帖
回帖
粉丝
万抽抽 2 2015-5-8 14:34
11
0
王总真是吊!那一大波TK_XXX函数的工作量也是杠杠的~~另外参数的个数可以通过系统自带的函数获得:
  dexProtoGetParameterCount(&method->prototype);
雪    币: 333
活跃值: (208)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
peterchen 2015-5-8 14:47
12
0
tk牛,hook殾给你玩难了....
雪    币: 368
活跃值: (1181)
能力值: ( LV9,RANK:310 )
在线值:
发帖
回帖
粉丝
ThomasKing 6 2015-5-8 17:28
13
0
额,调用点宏必须放在HOOK函数入口,只能一个一个写了。。。
多谢指点了!(用shorty正好同时直接解决参数类型和个数)
雪    币: 107
活跃值: (311)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Fido 2015-5-11 14:41
14
0
肿莫没加精华??版主睡觉去了吧??
雪    币: 5900
活跃值: (1631)
能力值: ( LV7,RANK:150 )
在线值:
发帖
回帖
粉丝
二当家a 2 2015-5-11 16:53
15
0
你好,有个问题想请教一下。

目前我在进行dalvik的插桩,但是apk的每个method对于使用的寄存器数有限制,有的限制在v0 ~v15之间,有的是v0 ~v 255. 这个设置感觉跟编译出来的结果有关,
但是我经常会需要加入比较多的 寄存器, 遇到限制在 v0 ~ v15 之间的限制,就没有办法了, 会重编译出现错误。
不知道托马斯兄有没有比较好的建议或者方案,谢谢!
@ThomasKing
雪    币: 368
活跃值: (1181)
能力值: ( LV9,RANK:310 )
在线值:
发帖
回帖
粉丝
ThomasKing 6 2015-5-11 19:05
16
0
额,我没怎么研究过这方面,不好意思。
雪    币: 255
活跃值: (90)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
goabout 2015-5-12 18:43
17
0
请问楼主最后的jInterfaceTrace.so文件还是需要注入到需要监控的apk进程中吧
雪    币: 5900
活跃值: (1631)
能力值: ( LV7,RANK:150 )
在线值:
发帖
回帖
粉丝
二当家a 2 2015-5-12 19:09
18
0
噢! 没事儿, 感谢你的关注!
雪    币: 368
活跃值: (1181)
能力值: ( LV9,RANK:310 )
在线值:
发帖
回帖
粉丝
ThomasKing 6 2015-5-12 22:19
19
0
是的。
雪    币: 9
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
有效啊 2015-6-19 16:03
20
0
楼主 小白想请教一下 关于这个静态库的使用

我是通过动态库掉静态库的方式 生成了一个动态库,然后再apk里直接调用生成的动态库。但是每一次执行到
memcpy(*oldenv,newenv,0x3A4u)就是trace_jInterfaceFunction的最后一句的时候就崩溃了,百思不得其解啊,还望楼主指导一下啊。
万分感谢
雪    币: 368
活跃值: (1181)
能力值: ( LV9,RANK:310 )
在线值:
发帖
回帖
粉丝
ThomasKing 6 2015-6-20 19:39
21
0
多谢反馈。 帖子里面这点没说明白,需要将原jni函数表的地址内存区域mprotect成W模式,再进行拷贝。 后续我重新传一个添加后的版本上来
雪    币: 5
活跃值: (28)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
笨小孩xlz 2015-6-25 23:16
22
0
雪    币: 2
活跃值: (33)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
draytek 2015-6-29 21:20
23
0
mark
雪    币: 10
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
wuca 2015-7-4 12:15
24
0
楼主你是如何打印函数的调用地址的啊?
雪    币: 257
活跃值: (105)
能力值: ( LV8,RANK:130 )
在线值:
发帖
回帖
粉丝
舵手 3 2015-7-6 12:57
25
0
mark
游客
登录 | 注册 方可回帖
返回