首页
社区
课程
招聘
[分享]记录如何追踪native函数动态注册地址
发表于: 1天前 356

[分享]记录如何追踪native函数动态注册地址

1天前
356

分析jni动态注册流程并用frida获取函数名与地址。发现只学不练记忆不够深刻,于是记录一下学习过程使自己有更深刻的理解。

开发者在JNI_OnLoad中调用env->RegisterNatives后底层做了什么。

  • 序言

例:



  • 分析RegisterNatives


进入RegisterNatives源码:


上图可以看到,获取JNINativeMethod的fnPtr , 然后调用m->RegisterNative。

JNINativeMethod结构体如下。

name为Java 中的方法名, signature为签名,fnPtr为native函数地址。

然后进入到RegisterNative,在

Runtime::Current()->GetRuntimeCallbacks()->RegisterNativeMethod(this, native_method, /*out*/&new_native_method);中做的事情其实就是把native_method赋值给new_native_method


然后再进入到SetEntryPointFromJni,我们知道entrypoint就是函数地址,而kRuntimePointerSize其实是个固定值,见下图,一个常量,它代表了当前正在运行的 ART 虚拟机所在环境的“真实指针大小”(32位系统是 4 字节,64位系统是 8 字节)

进入SetEntryPointFromJniPtrSize

进入SetDataPtrSize

看一下DataOffset

先看

OFFSETOF_MEMBER(PtrSizedFields, data_) / sizeof(void*) * static_cast<size_t>(pointer_size),

sizeof(void*) 是编译安卓源码时所在机器的指针大小,static_cast<size_t>(pointer_size)是运行安卓系统所在手机的指针大小

再看一下OFFSETOF_MEMBER,即计算出一个结构体(或类)中某个特定成员变量,距离该结构体首地址的字节偏移量。

也就是data_在PtrSizedFields的偏移是多少。


综上,

OFFSETOF_MEMBER(PtrSizedFields, data_) / sizeof(void*) * static_cast<size_t>(pointer_size),

sizeof(void*)  ,假设sizeof(void*)和static_cast<size_t>(pointer_size)都是8,我们可以算一下,OFFSETOF_MEMBER(PtrSizedFields, data_)是0,然后 (0/8 )* 8还是0


然后看,PtrSizedFieldsOffset返回的就是hotness_count_变量在ArtMethod类的偏移+hotness_count_大小,再进行内存对齐。


先看一下ArtMethod类的结构


手动计算一下hotness_count_变量在ArtMethod类的偏移。declaring_class_的偏移是4字节(不说为什么了,篇幅太大太乱了),access_flags_,dex_code_item_offset_,dex_method_index_都是四字节,method_index_和union {uint16_t hotness_count_;uint16_t imt_index_; };是两个字节。union共用内存。所以hotness_count_的偏移是20字节,然后再加上sizeof(hotness_count_),总共是22字节。


再看RoundUp,现在已知参数1是22,参数2是8。即RoundUp(22,8),我们看看会返回什么

调用RoundDown(22+8-1,8)=RoundDown(29,8)



代入计算一下,29 & -8 =24



综上,PtrSizedFieldsOffset(pointer_size)=24, 

OFFSETOF_MEMBER(PtrSizedFields, data_) / sizeof(void*) * static_cast<size_t>(pointer_size)   = 0;




最后进入SetNativePointer,即SetNativePointer(24, 函数地址, 8);



ok了老铁们,也就是 RegisterNative在背后做的事就是 把函数地址赋值给ArtMethod类偏移为24的变量(在arm64是24,32位偏移为20,可以自己算一下),也就是data_变量。


  • 分析std::string

接下来分析std::string类,下面有用


再看basic_string


找一下该类中占内存的变量,即非静态成员变量,找下来只有一个变量是占内存的,且没有虚函数,且父类中没有占内存的变量(不截图了)

然后看一下__compressed_pair类,发现没有占内存的成员变量,不是不占内存,是不占该类的实例的内存。

然后看一下父类__compressed_pair_elem,发现有一个变量占内存,是模板参数_Tp,也就是上图的_T1  ,  _T2, 也就是上上图的__rep 和 allocator_type




allocator_type类中没有占内存的变量(不截图了)。但__rep是占内存的。

结构体__long占24字节

结构体__short占24字节,解释: value_type即char 占1字节,__min_cpp=23


__raw占24字节

综上,__rep占24字节,即std::string占24字节。



然后来看看上面的结构体里的变量都有什么含义。

我们知道,std::string类下的c_str方法能够获取到字符串数据。我们就从从此入手。

c_str调用data方法,然后调用_VSTD::__to_raw_pointer(__get_pointer()),看下图便知最终返回的是__get_pointer()

__get_pointer

看__is_long


结合上面的图片可以看出__r_.first()返回的是

然后__r_.first().__s的意思是  因为 __rep 里面是一个 union,这 24 个字节就像薛定谔的猫,它既可以是长模式,也可以是短模式,取决于你怎么“看”它。当你写下 .__s 的那一瞬间,别管这 24 个字节到底是什么,就把它当成 __short 结构体来解析。


然后__r_.first().__s.__size_取出__short结构体中的第一个字节,然后& 0x01 ,最后转为bool值。


然后看__get_long_pointer和__get_short_pointer就很清晰了。

这就是c_str的底层实现。


  • 问题


那么问题来了。如果我在JNI_Onload中不使用env->RegisterNatives进行注册,而是直接把函数地址赋值给ArtMethod的data_变量,那这样的话传统的hook RegisterNative的方法就会失效,此时阁下如何应对 。如下图

结合上面分析的,于是就有了如下脚本来获取native函数对应的c层函数地址


解释:PrettyMthod方法将 ART 内部复杂的 ArtMethod 对象转换成人类可读的字符串。PrettyMthod的返回值由于是std::string, 上面已经分析出该大小为24字节。c_str方法上面也已经分析。


参考资料:看雪3w班-内核模块绕过frida章节。


只是为了自己记忆深刻,故写篇文章。




传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 1
支持
分享
最新回复 (2)
雪    币: 76
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
两种方法,一种用yang神的hookregister脚本;另一种直接在unidbg搭个空架子也能看到入口偏移
20小时前
0
雪    币: 9
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
3
温泉划水鱼 两种方法,一种用yang神的hookregister脚本;另一种直接在unidbg搭个空架子也能看到入口偏移
谢谢指正
11小时前
0
游客
登录 | 注册 方可回帖
返回