很多小伙伴在逆向的时候定位到了Java层的Native函数,如果要进一步进行分析,就需要找到so中注册的Native函数。
第一种情况,函数静态注册,可以直接在so的导出符号表中找到静态注册的函数地址(这里使用的方法是dlsym)。
第二种情况,函数动态注册,在JNI_ONLOAD中使用RegisterNatives这个函数进行注册。
但是出现了一些特殊的情况,hook了这两个函数,却没有找到目标函数的注册方法。
本文章将分多个部分讲解:
本帖子是转储notion的,下面如果格式跟不上请查看:
https://fortunate-decimal-730.notion.site/RegisterNatives-JNI-b83f71b4a9dc4b30a00177b71cee242c?pvs=4
获取更好的阅读体验,谢谢大家~
1、从AOSP源码的角度讲解RegisterNatives函数具体的流程
2、从AOSP源码出发,探究Java的类加载时,如何注册自己的函数地址
3、讲解函数绑定的地址究竟在哪里,如何从根本上拿到绑定函数的地址
4、如何使用工具拿到属于自己唯一的偏移地址
5、小试牛刀,用学到的知识初步测试
6、利用两个群友遇到问题的例子,一个简单的,一个复杂的,来实战应用技术
群友提问
1 .首先用yang的那个dump so脚本hook不到,然后用他那个hook regestive的脚本也hook不到注册函数
2.为什么我hook了dlsym、jni的RegisterNative、枚举所有模块的所有导出函数都没有找到我要的函数
脚本部分来源:Fart脱壳王课件
寒冰老师提出的这个方法,我并不是原创,我只是实现了一个小工具以及提供了两个具体案例来实现。
欢迎大家购买看雪2W、3W班,以及FART脱壳王课程来支持寒冰老师,并获得更加充分的售后指导。
首先我们拿到RegisterNative的函数实现部分
有两个重点关注的地方:
http://aospxref.com/android-10.0.0_r47/xref/art/runtime/jni/jni_internal.cc#2459
java对象转artmethod对象的过程
在这里将java的class和签名都传入
从内存中遍历artmethod,匹配出符合条件的artmethod
第二个重要的地方
artmethod调用自己的RegisterNative方法
这里就有些厂商下沉到artmethod的注册方法,导致脚本hook不到。
在这里 对artmethod的指针进行设置,完成对jni函数的绑定
总结一下:RegisterNative的核心就是调用SetNativePointer这个函数,将函数的地址保存到artmethod中
reinterpret_cast<uintptr_t>(this) + offset.Uint32Value();
这一行正是他保存的偏移地址,artmethod指针的偏移32位在源码里体现出来了,当然我们可以通过计算的方式拿到偏移地址。
这个参数就是artmethod存储地址的地方
可以根据结构体计算出data_的偏移
看到这里,可以揭露下本文章的核心了,就是通过frida拿到artmethod结构体,在计算出当前机器的偏移数量,查看data_数据的内容,那么就是该jni地址绑定的artmehod的地址了
在这个板块,我们将从LoadClass这个函数作为切入点
在这个函数里有LoadMethod和Linkcode这两个核心函数
每个函数第一次都要进行一次链接绑定
在这里判断函数是否要在本地实现
重点:根据函数类型走不同的分支,我们查看method->IsNative()
这个分支
发现函数调用了
**UnregisterNative
这个方法**
在函数链接的时候,所有的native函数都会调用一遍unregisternative
SetEntryPointFromJni
http://aospxref.com/android-10.0.0_r47/s?defs=SetEntryPointFromJni&project=art
和registernative一样 调用了设置入口函数 而入口函数来源于[GetJniDlsymLookupStub](http://aospxref.com/android-10.0.0_r47/s?defs=GetJniDlsymLookupStub&project=art)()
这个函数是一段内联汇编
其中内部调用了artFindNativeMethod这个方法
http://aospxref.com/android-10.0.0_r47/xref/art/runtime/entrypoints/jni/jni_entrypoints.cc?fi=artFindNativeMethod#artFindNativeMethod
最终这个函数调用了 真正的RegisterNative函数
在这个函数里
有着寻找函数符号的过程,可以看到静态注册的规则
将long_name和short_name做拼接去寻找符号,如果没找到则保留null,等待开发人员进行绑定
我们可以理解为,jni函数一开始都绑定在一个地址上,程序员需要在jni_onload再去二次绑定上自己的真实的地址(这里在后面有一个坑)
认真阅读的读者心中已经有了答案,就在Artmethod的data_这个属性里,我们只需要拿到函数的artmethod指针以及知道自己系统artmethod的储存绑定地址的偏移即可。
我们可以自己写一个小demo,手动调用registernative,绑定我们自己的地址到函数上,然后拿到对应的artmethod,对内存进行搜索,取出符合条件的index
在aosp8.0-aosp10的系统上,artmethod的指针就是jmethodid的数值,这里我们可以通过源码来查看 在aosp11的时候这一特性发生了变化,aosp为了安全,将artmetod指针建立了一个数组,并返回了一个id作为index
从这里看到,jmethoidid只是将artmethod强转了
所以在aosp10以下,可以直接通过
来直接获取到手机的偏移地址
在aosp10以上怎么办? 非常好办,frida就可以帮你做内存检索,虽然比app一键获取要来的慢
下面我们进入下一个篇章,如何用开发的demo获取到你手机目前的偏移地址
打开我们自己实现的app
我们可以看到是4个指针大小(并不是字节,上面打错了)
如果你的app运行在32位模式下,那么就是4x4(32位指针大小4字节)=16 字节
这样安装会让你的apk强制运行在32位模式下,其余手机基本默认都运行在64位下
不确定的可以调用frida的api Proces.pointersize
我的app是运行在64位模式下,那么就是4X8(64位指针大小8字节)=32字节
打开app是另外一个界面
我们首先获取目标类的artmethod地址
将frida挂载到demo app上面
不做任何修改的运行
拿到第一个值 也就是artmethod的地址 0x754d267ed8
接下来从界面上抄来第二个值,填入下面的脚本
注入脚本
就可以获取到你偏移的字节了这里是0x10 也就是16(64位下)
如果目标app比较老 运行在32位模式下
强制demo app强制运行在32位模式下,即可拿到32位的偏移
我们目标要获取的类名是
com.example.test_1.MainActivity
方法名是
public native String stringFromJNI();
首先启动好app,frida进行附加
运行脚本,获取到目标类的artmethod
之后阅读偏移的16个字节(上一个板块的获取到的)的信息
这就是这个art方法绑定的方法了 我们使用DebugSymbol.fromAddress查看具体符号信息
简单计算一下偏移
使用获取到的地址减去模块的base,得到偏移
0x1dd80
至此,我们的小试牛刀结束了,下面循序渐进的解决两位群友问题
问题:为什么我hook了dlsym、jni的RegisterNative、枚举所有模块的所有导出函数都没有找到我要的函数
app名称:人保e通
目标类型和函数
com.facebook.react.bridge.ReadableNativeMap
第一步,使用脚本拿到artmethod地址:
拿到了目标地址
第二步,阅读指针内容
成功拿到地址:
解析下符号:
问题:.首先用yang的那个dump so脚本hook不到,然后用他那个hook regestive的脚本也hook不到注册函数
目标样本app:正保会计网校
老套路,获取到目标类型的artmethod:
0x7b3ff992c8
拿到目标函数地址:
奇怪? 为什么他绑定在了art里面呢,仔细一看
art_jni_dlsym_lookup_stub
这不就是第一次统一unregisternative的地址吗
具体原理请看上面的第三部分
我们该怎么办?
非常简单,主动调用一次即可!
调用成功后我们再次查看地址
果然 地址发生了变化
奇怪的事情来了,他并没有任何符号,仅仅是一个地址
难道我们的字节读取错误了吗
使用hexdump 查看一下artmethod在内存中的值
对比了下标横线的地址,我们获取的并没有错误,我们该怎么办?
当然是去map查找他所在的段,查看是不是可执行的,如果是,那么目标so就使用了动态释放内存的操作,将可执行代码用mmap释放到内存中并执行
获取到了目标进程的pid,我们在开启一个shel
找到了三个可以的段 连名字都没有
而且发现 目标地址正是在
7b3d228000-7b3d453000 rwxp 00000000 00:00 0
这个段中
并且这个段还有执行权限,非常可疑,我们来进行内存dump
有三种方式可以dump
第一种 使用dd命令 dd if = 具体可以问gpt如何操作
第二种 使用frida脚本 dump下memory 使用file写入文件
第三种 使用开源项目
https://github.com/kp7742/MemDumper
https://github.com/maiyao1988/elf-dump-fix
文章结尾会打包好 所有需要的文件 下面我们开始dump
进行dump后 我们拿到目标文件查看
是一个elf文件
进行修复后我们导入ida
并计算偏移地址
base:7b3d228000
func ptr :0x7b3d2c2080
计算出偏移地址:
0x9a080
发现就是我们想要的函数
小彩蛋:
libproxy.so 在init_proc中 很奔放的写出了释放过程,大家可以去debug学习下
所有用到的文件打包地址:
链接: https://pan.baidu.com/s/1d3Ym-piDQe49A9-XcJVrhA?pwd=euwa 提取码: euwa
第二第三部分写的非常有瑕疵,欢迎大佬来指正,我会及时修改帖子内容!
希望大家能从我的帖子学到一些东西,现在的东西深度不是很够,我会努力学习给大家带来高质量的帖子~
大家有问题可以给我留言,我会每天看3-5次来解决大家的问题!
static jint RegisterNatives(JNIEnv*
env
,
2460 jclass java_class,
2461 const JNINativeMethod* methods,
2462 jint method_count) {
2463
if
(UNLIKELY(method_count < 0)) {
2464 JavaVmExtFromEnv(
env
)->JniAbortF(
"RegisterNatives"
,
"negative method count: %d"
,
2465 method_count);
2466
return
JNI_ERR;
//
Not reached except
in
unit tests.
2467 }
2468 CHECK_NON_NULL_ARGUMENT_FN_NAME(
"RegisterNatives"
, java_class, JNI_ERR);
2469 ScopedObjectAccess soa(
env
);
2470 StackHandleScope<1> hs(soa.Self());
2471 Handle<mirror::Class> c = hs.NewHandle(soa.Decode<mirror::Class>(java_class));
2472
if
(UNLIKELY(method_count == 0)) {
2473 LOG(WARNING) <<
"JNI RegisterNativeMethods: attempt to register 0 native methods for "
2474 << c->PrettyDescriptor();
2475
return
JNI_OK;
2476 }
2477 CHECK_NON_NULL_ARGUMENT_FN_NAME(
"RegisterNatives"
, methods, JNI_ERR);
2478
for
(jint i = 0; i < method_count; ++i) {
2479 const char* name = methods[i].name;
2480 const char* sig = methods[i].signature;
2481 const void* fnPtr = methods[i].fnPtr;
2482
if
(UNLIKELY(name == nullptr)) {
2483 ReportInvalidJNINativeMethod(soa, c.Get(),
"method name"
, i);
2484
return
JNI_ERR;
2485 }
else
if
(UNLIKELY(sig == nullptr)) {
2486 ReportInvalidJNINativeMethod(soa, c.Get(),
"method signature"
, i);
2487
return
JNI_ERR;
2488 }
else
if
(UNLIKELY(fnPtr == nullptr)) {
2489 ReportInvalidJNINativeMethod(soa, c.Get(),
"native function"
, i);
2490
return
JNI_ERR;
2491 }
2492 bool is_fast =
false
;
2493
//
Notes about fast JNI calls:
2494
//
2495
//
On a normal JNI call, the calling thread usually transitions
2496
//
from the kRunnable state to the kNative state. But
if
the
2497
//
called native
function
needs to access any Java object, it
2498
//
will have to transition back to the kRunnable state.
2499
//
2500
//
There is a cost to this double transition. For a JNI call
2501
//
that should be quick, this cost may dominate the call cost.
2502
//
2503
//
On a fast JNI call, the calling thread avoids this double
2504
//
transition by not transitioning from kRunnable to kNative and
2505
//
stays
in
the kRunnable state.
2506
//
2507
//
There are risks to using a fast JNI call because it can delay
2508
//
a response to a thread suspension request
which
is typically
2509
//
used
for
a GC root scanning, etc. If a fast JNI call takes a
2510
//
long
time
, it could cause longer thread suspension latency
2511
//
and GC pauses.
2512
//
2513
//
Thus, fast JNI should be used with care. It should be used
2514
//
for
a JNI call that takes a short amount of
time
(eg. no
2515
//
long-running loop) and does not block (eg. no locks, I
/O
,
2516
//
etc.)
2517
//
2518
//
A
'!'
prefix
in
the signature
in
the JNINativeMethod
2519
//
indicates that it's a fast JNI call and the runtime omits the
2520
//
thread state transition from kRunnable to kNative at the
2521
//
entry.
2522
if
(*sig ==
'!'
) {
2523 is_fast =
true
;
2524 ++sig;
2525 }
2526
2527
//
Note: the right order is to try to
find
the method locally
2528
//
first, either as a direct or a virtual method. Then move to
2529
//
the parent.
2530 ArtMethod* m = nullptr;
2531 bool warn_on_going_to_parent = down_cast<JNIEnvExt*>(
env
)->GetVm()->IsCheckJniEnabled();
2532
for
(ObjPtr<mirror::Class> current_class = c.Get();
2533 current_class != nullptr;
2534 current_class = current_class->GetSuperClass()) {
2535
//
Search first only comparing methods
which
are native.
2536 m = FindMethod<
true
>(current_class, name, sig);
2537
if
(m != nullptr) {
2538
break
;
2539 }
2540
2541
//
Search again comparing to all methods, to
find
non-native methods that match.
2542 m = FindMethod<
false
>(current_class, name, sig);
2543
if
(m != nullptr) {
2544
break
;
2545 }
2546
2547
if
(warn_on_going_to_parent) {
2548 LOG(WARNING) <<
"CheckJNI: method to register \""
<< name <<
"\" not in the given class. "
2549 <<
"This is slow, consider changing your RegisterNatives calls."
;
2550 warn_on_going_to_parent =
false
;
2551 }
2552 }
2553
2554
if
(m == nullptr) {
2555 c->DumpClass(LOG_STREAM(ERROR), mirror::Class::kDumpClassFullDetail);
2556 LOG(ERROR)
2557 <<
"Failed to register native method "
2558 << c->PrettyDescriptor() <<
"."
<< name << sig <<
" in "
2559 << c->GetDexCache()->GetLocation()->ToModifiedUtf8();
2560 ThrowNoSuchMethodError(soa, c.Get(), name, sig,
"static or non-static"
);
2561
return
JNI_ERR;
2562 }
else
if
(!m->IsNative()) {
2563 LOG(ERROR)
2564 <<
"Failed to register non-native method "
2565 << c->PrettyDescriptor() <<
"."
<< name << sig
2566 <<
" as native"
;
2567 ThrowNoSuchMethodError(soa, c.Get(), name, sig,
"native"
);
2568
return
JNI_ERR;
2569 }
2570
2571 VLOG(jni) <<
"[Registering JNI native method "
<< m->PrettyMethod() <<
"]"
;
2572
2573
if
(UNLIKELY(is_fast)) {
2574
//
There are a few reasons to switch:
2575
//
1) We don't support !bang JNI anymore, it will turn to a hard error later.
2576
//
2) @FastNative is actually faster. At least 1.5x faster than !bang JNI.
2577
//
and switching is super easy, remove !
in
C code, add annotation
in
.java code.
2578
//
3) Good chance of hitting DCHECK failures
in
ScopedFastNativeObjectAccess
2579
//
since that checks
for
presence of @FastNative and not
for
!
in
the descriptor.
2580 LOG(WARNING) <<
"!bang JNI is deprecated. Switch to @FastNative for "
<< m->PrettyMethod();
2581 is_fast =
false
;
2582
//
TODO:
make
this a hard register error
in
the future.
2583 }
2584
2585 const void* final_function_ptr = m->RegisterNative(fnPtr);
2586 UNUSED(final_function_ptr);
2587 }
2588
return
JNI_OK;
2589 }
static jint RegisterNatives(JNIEnv*
env
,
2460 jclass java_class,
2461 const JNINativeMethod* methods,
2462 jint method_count) {
2463
if
(UNLIKELY(method_count < 0)) {
2464 JavaVmExtFromEnv(
env
)->JniAbortF(
"RegisterNatives"
,
"negative method count: %d"
,
2465 method_count);
2466
return
JNI_ERR;
//
Not reached except
in
unit tests.
2467 }
2468 CHECK_NON_NULL_ARGUMENT_FN_NAME(
"RegisterNatives"
, java_class, JNI_ERR);
2469 ScopedObjectAccess soa(
env
);
2470 StackHandleScope<1> hs(soa.Self());
2471 Handle<mirror::Class> c = hs.NewHandle(soa.Decode<mirror::Class>(java_class));
2472
if
(UNLIKELY(method_count == 0)) {
2473 LOG(WARNING) <<
"JNI RegisterNativeMethods: attempt to register 0 native methods for "
2474 << c->PrettyDescriptor();
2475
return
JNI_OK;
2476 }
2477 CHECK_NON_NULL_ARGUMENT_FN_NAME(
"RegisterNatives"
, methods, JNI_ERR);
2478
for
(jint i = 0; i < method_count; ++i) {
2479 const char* name = methods[i].name;
2480 const char* sig = methods[i].signature;
2481 const void* fnPtr = methods[i].fnPtr;
2482
if
(UNLIKELY(name == nullptr)) {
2483 ReportInvalidJNINativeMethod(soa, c.Get(),
"method name"
, i);
2484
return
JNI_ERR;
2485 }
else
if
(UNLIKELY(sig == nullptr)) {
2486 ReportInvalidJNINativeMethod(soa, c.Get(),
"method signature"
, i);
2487
return
JNI_ERR;
2488 }
else
if
(UNLIKELY(fnPtr == nullptr)) {
2489 ReportInvalidJNINativeMethod(soa, c.Get(),
"native function"
, i);
2490
return
JNI_ERR;
2491 }
2492 bool is_fast =
false
;
2493
//
Notes about fast JNI calls:
2494
//
2495
//
On a normal JNI call, the calling thread usually transitions
2496
//
from the kRunnable state to the kNative state. But
if
the
2497
//
called native
function
needs to access any Java object, it
2498
//
will have to transition back to the kRunnable state.
2499
//
2500
//
There is a cost to this double transition. For a JNI call
2501
//
that should be quick, this cost may dominate the call cost.
2502
//
2503
//
On a fast JNI call, the calling thread avoids this double
2504
//
transition by not transitioning from kRunnable to kNative and
2505
//
stays
in
the kRunnable state.
2506
//
2507
//
There are risks to using a fast JNI call because it can delay
2508
//
a response to a thread suspension request
which
is typically
2509
//
used
for
a GC root scanning, etc. If a fast JNI call takes a
2510
//
long
time
, it could cause longer thread suspension latency
2511
//
and GC pauses.
2512
//
2513
//
Thus, fast JNI should be used with care. It should be used
2514
//
for
a JNI call that takes a short amount of
time
(eg. no
2515
//
long-running loop) and does not block (eg. no locks, I
/O
,
2516
//
etc.)
2517
//
2518
//
A
'!'
prefix
in
the signature
in
the JNINativeMethod
2519
//
indicates that it's a fast JNI call and the runtime omits the
2520
//
thread state transition from kRunnable to kNative at the
2521
//
entry.
2522
if
(*sig ==
'!'
) {
2523 is_fast =
true
;
2524 ++sig;
2525 }
2526
2527
//
Note: the right order is to try to
find
the method locally
2528
//
first, either as a direct or a virtual method. Then move to
2529
//
the parent.
2530 ArtMethod* m = nullptr;
2531 bool warn_on_going_to_parent = down_cast<JNIEnvExt*>(
env
)->GetVm()->IsCheckJniEnabled();
2532
for
(ObjPtr<mirror::Class> current_class = c.Get();
2533 current_class != nullptr;
2534 current_class = current_class->GetSuperClass()) {
2535
//
Search first only comparing methods
which
are native.
2536 m = FindMethod<
true
>(current_class, name, sig);
2537
if
(m != nullptr) {
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2024-7-31 17:30
被棕熊编辑
,原因: 添加工具apk到附件,原来的aosp10不会直接输出指针数量,修复这个bug