告别RegisterNatives获取JNI函数绑定的地址,迎接最底层的方式获取(3个案例)
2024-8-3 17:33:8 Author: mp.weixin.qq.com(查看原文) 阅读量:1 收藏

很多小伙伴在逆向的时候定位到了Java层的Native函数,如果要进一步进行分析,就需要找到so中注册的Native函数。
第一种情况,函数静态注册,可以直接在so的导出符号表中找到静态注册的函数地址(这里使用的方法是dlsym)。
第二种情况,函数动态注册,在JNI_ONLOAD中使用RegisterNatives这个函数进行注册。
但是出现了一些特殊的情况,hook了这两个函数,却没有找到目标函数的注册方法。
本文章将分多个部分讲解:
1、从AOSP源码的角度讲解RegisterNatives函数具体的流程
2、从AOSP源码出发,探究Java的类加载时,如何注册自己的函数地址
3、讲解函数绑定的地址究竟在哪里,如何从根本上拿到绑定函数的地址
4、如何使用工具拿到属于自己唯一的偏移地址
5、小试牛刀,用学到的知识初步测试
6、利用两个群友遇到问题的例子,一个简单的,一个复杂的,来实战应用技术
◆群友提问

1 .首先用yang的那个dump so脚本hook不到,然后用他那个hook regestive的脚本也hook不到注册函数。

2.为什么我hook了dlsym、jni的RegisterNative、枚举所有模块的所有导出函数都没有找到我要的函数。

◆Fart脱壳王课程、看雪3w课程
脚本部分来源:Fart脱壳王课件
寒冰老师提出的这个方法,我并不是原创,我只是实现了一个小工具以及提供了两个具体案例来实现。
欢迎大家购买看雪2W、3W班,以及FART脱壳王课程来支持寒冰老师,并获得更加充分的售后指导。
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 }
首先我们拿到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不到。
  
ALWAYS_INLINE void SetNativePointer(MemberOffset offset, T new_value, PointerSize pointer_size) {
822 static_assert(std::is_pointer<T>::value, "T must be a pointer type");
823 const auto addr = reinterpret_cast<uintptr_t>(this) + offset.Uint32Value();
824 if (pointer_size == PointerSize::k32) {
825 uintptr_t ptr = reinterpret_cast<uintptr_t>(new_value);
826 *reinterpret_cast<uint32_t*>(addr) = dchecked_integral_cast<uint32_t>(ptr);
827 } else {
828 *reinterpret_cast<uint64_t*>(addr) = reinterpret_cast<uintptr_t>(new_value);
829 }
830 }
在这里 对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。

demo开发原理

在aosp8.0-aosp10的系统上,artmethod的指针就是jmethodid的数值,这里我们可以通过源码来查看 在aosp11的时候这一特性发生了变化,aosp为了安全,将artmetod指针建立了一个数组,并返回了一个id作为index。
从这里看到,jmethoidid只是将artmethod强转了。
所以在aosp10以下,可以直接通过:
来直接获取到手机的偏移地址。
在aosp10以上怎么办?非常好办,frida就可以帮你做内存检索,虽然比app一键获取要来的慢。
下面我们进入下一个篇章,如何用开发的demo获取到你手机目前的偏移地址。

aosp10.0以下:

打开我们自己实现的app。
我们可以看到是4个指针大小(并不是字节,上面打错了)。
如果你的app运行在32位模式下,那么就是4x4(32位指针大小4字节)=16 字节。
adb install --abi armeabi-v7a xxx.apk
这样安装会让你的apk强制运行在32位模式下,其余手机基本默认都运行在64位下。
不确定的可以调用frida的api Proces.pointersize。
我的app是运行在64位模式下,那么就是4X8(64位指针大小8字节)=32字节。

如果你的手机系统在安卓10以上

打开app是另外一个界面:
我们首先获取目标类的artmethod地址。
将frida挂载到demo app上面。
function getHandle(object) {
var handle = null;
try {
handle = object.$handle;
} catch (e) {
}
if (handle == null) {
try {
handle = object.$h;
} catch (e) {
}

}
if (handle == null) {
try {
handle = object.handle;
} catch (e) {
}

}
return handle;
}

Java.perform(function () {
let ReadableNativeMap = Java.use("com.example.getoffsite.MainActivity");
console.log(getHandle(ReadableNativeMap["stringFromJNI"]))

});

不做任何修改的运行。
拿到第一个值,也就是artmethod的地址 0x754d267ed8。
接下来从界面上抄来第二个值,填入下面的脚本。
var startAddress = ptr('0x754d267ed8'); // artmethod地址
var targetValue = ptr('0x74d82ebbb0'); // app界面上的值
var scanLength = 1024; // 扫描长度(字节数)

function scanMemory(address, target, length) {
for (var i = 0; i < length; i++) {
var currentAddress = address.add(i);
var currentValue = Memory.readPointer(currentAddress);

if (currentValue.equals(target)) {
console.log('Found match at address: ' + currentAddress);
console.log("offsite",currentAddress.sub(startAddress));
return;
}
}
console.log('No match found within the specified range.');
}

scanMemory(startAddress, targetValue, scanLength);

注入脚本
就可以获取到你偏移的字节了这里是0x10 也就是16(64位下)。
如果目标app比较老,运行在32位模式下。
adb install --abi armeabi-v7a demo.apk
强制demo app强制运行在32位模式下,即可拿到32位的偏移。
我们目标要获取的类名是
com.example.test_1.MainActivity
方法名是
public native String stringFromJNI();
首先启动好app,frida进行附加。
运行脚本,获取到目标类的artmethod。
function getHandle(object) {
var handle = null;
try {
handle = object.$handle;
} catch (e) {
}
if (handle == null) {
try {
handle = object.$h;
} catch (e) {
}

}
if (handle == null) {
try {
handle = object.handle;
} catch (e) {
}

}
return handle;
}

Java.perform(function () {
let ReadableNativeMap = Java.use("com.example.test_1.MainActivity");
console.log(getHandle(ReadableNativeMap["stringFromJNI"]))

});

之后阅读偏移的16个字节(上一个板块的获取到的)的信息。
 
ptr(0x75480b3ed8).add(16).readPointer();
这就是这个art方法绑定的方法了。我们使用DebugSymbol.fromAddress查看具体符号信息。
简单计算一下偏移:
Process.getModuleByName("libtest_1.so")
使用获取到的地址减去模块的base,得到偏移。
0x1dd80
至此,我们的小试牛刀结束了,下面循序渐进的解决两位群友问题。
问题:为什么我hook了dlsym、jni的RegisterNative、枚举所有模块的所有导出函数都没有找到我要的函数?
app名称:人保e通
目标类型和函数
com.facebook.react.bridge.ReadableNativeMap
第一步,使用脚本拿到artmethod地址:
function getHandle(object) {
var handle = null;
try {
handle = object.$handle;
} catch (e) {
}
if (handle == null) {
try {
handle = object.$h;
} catch (e) {
}

}
if (handle == null) {
try {
handle = object.handle;
} catch (e) {
}

}
return handle;
}
Java.perform(function () {
let ReadableNativeMap = Java.use("com.facebook.react.bridge.ReadableNativeMap");
console.log(getHandle(ReadableNativeMap["importValues"]))

});

拿到了目标地址。
第二步,阅读指针内容。
ptr(0x79b4c96368).add(32).readPointer(); //这里我使用的安卓8.0系统 32是4个指针乘8字节
成功拿到地址:
解析下符号:

样本2: 压轴戏(推荐观看)

问题:.首先用yang的那个dump so脚本hook不到,然后用他那个hook regestive的脚本也hook不到注册函数。
目标样本app:正保会计网校
老套路,获取到目标类型的artmethod:
function getHandle(object) {
var handle = null;
try {
handle = object.$handle;
} catch (e) {
}
if (handle == null) {
try {
handle = object.$h;
} catch (e) {
}

}
if (handle == null) {
try {
handle = object.handle;
} catch (e) {
}

}
return handle;
}

Java.perform(function() {
// 定位类
var targetClass = Java.use('com.cdel.encode.TSEncode');
console.log(getHandle(targetClass.de1))
});

0x7b3ff992c8
拿到目标函数地址:
ptr(0x7b3ff992c8).add(32).readPointer();
奇怪?为什么他绑定在了art里面呢,仔细一看,
art_jni_dlsym_lookup_stub
这不就是第一次统一unregisternative的地址吗?
具体原理请看上面的第三部分。
我们该怎么办?
非常简单,主动调用一次即可!
Java.perform(function() {
// 定位类
var targetClass = Java.use('com.cdel.encode.TSEncode');

// 定义要传递的参数
var param = "7ZvLaMCWJPFQmQX87ZvLaMCWJPEFUzIwJGPZwXlCunyRfQ8xqyCsSt1ADfx3xI3LZkeb.w__X8bMvisv";

// 调用目标方法并获取返回值
var result = targetClass.de1(param);

// 输出结果
console.log("Result: " + result);
});

调用成功后我们再次查看地址。
果然,地址发生了变化。
奇怪的事情来了,他并没有任何符号,仅仅是一个地址。
难道我们的字节读取错误了吗?
使用hexdump 查看一下artmethod在内存中的值。
[Pixel::com.cdel.accmobile ]-> console.log(hexdump(ptr(0x7b3ff992c8)))
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
7b3ff992c8 80 fd e4 14 09 01 00 00 00 00 00 00 2f 1e 00 00 ............/...
7b3ff992d8 03 00 00 00 00 00 00 00 00 b0 10 53 7b 00 00 00 ...........S{...
7b3ff992e8 **80 20 2c 3d 7b** 00 00 00 60 0b b1 68 7b 00 00 00 . ,={...`..h{...
7b3ff992f8 80 fd e4 14 09 01 00 00 00 00 00 00 30 1e 00 00 ............0...
7b3ff99308 04 00 00 00 00 00 00 00 00 b0 10 53 7b 00 00 00 ...........S{...
7b3ff99318 a0 6d b0 68 7b 00 00 00 60 0b b1 68 7b 00 00 00 .m.h{...`..h{...
7b3ff99328 80 fd e4 14 09 01 00 00 00 00 00 00 31 1e 00 00 ............1...
7b3ff99338 05 00 00 00 00 00 00 00 00 b0 10 53 7b 00 00 00 ...........S{...
7b3ff99348 a0 6d b0 68 7b 00 00 00 60 0b b1 68 7b 00 00 00 .m.h{...`..h{...
7b3ff99358 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
7b3ff99368 00 00 00 00 ff ff ff ff 00 00 00 00 00 00 00 00 ................
7b3ff99378 00 00 00 00 00 00 00 00 a0 93 f9 3f 7b 00 00 00 ...........?{...
7b3ff99388 70 08 b1 68 7b 00 00 00 00 00 00 00 00 00 00 00 p..h{...........
7b3ff99398 00 00 00 00 00 00 00 00 30 a3 68 70 00 00 00 00 ........0.hp....
7b3ff993a8 d8 2e 70 70 00 00 00 00 00 00 00 00 00 00 00 00 ..pp............
7b3ff993b8 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 ................
对比了下标横线的地址,我们获取的并没有错误,我们该怎么办?
当然是去map查找他所在的段,查看是不是可执行的,如果是,那么目标so就使用了动态释放内存的操作,将可执行代码用mmap释放到内存中并执行。
获取到了目标进程的pid,我们再开启一个shell。
cat /proc/7129/maps >/data/local/tmp/map.txt
找到了三个可以的段,连名字都没有。
而且发现,目标地址正是在
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。
255|sailfish:/data/local/tmp # ./memdumper64 -m -s 7b3d228000 -e 7b3d453000 -n 123.bin -i 7129 -o /sdcard
进行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次来解决大家的问题。

看雪ID:mb_qzwrkwda

https://bbs.kanxue.com/user-home-945390.htm

*本文为看雪论坛精华文章,由 mb_qzwrkwda 原创,转载请注明来自看雪社区

# 往期推荐

1、Alt-Tab Terminator注册算法逆向

2、恶意木马历险记

3、VMP源码分析:反调试与绕过方法

4、Chrome V8 issue 1486342浅析

5、Cython逆向-语言特性分析

球分享

球点赞

球在看

点击阅读原文查看更多


文章来源: https://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458565630&idx=1&sn=c95d09940c602d284f24653a3cdafa1e&chksm=b18d8b7486fa0262cfd2df288c7babc4044ce675073531a2ae53ee4d59992cbb02ac54cf37e0&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh