JNI 函数有两种注册方式,也是所有攻防技术的基础:
静态注册:通过 Java_包名_类名_方法名 函数名规则自动匹配
动态注册:通过 RegisterNatives() 手动注册 JNINativeMethod 结构体
静态注册通过IDA总结观察其方法名称即可找到,而动态注册一般情况下需要通过HOOK RegisterNatives即可抓取函数地址。
我们不再乖乖地调用官方 JNI 接口,而是先让系统已经帮我们填好的那部分当“样本”,量一量结构体的布局,然后照着写,后续所有 Java → native 的绑定,全靠手动写指针完成。
想靠写内存实现“注册”,你必须知道写哪儿。
换句话说:在当前 Android 版本里,ArtMethod 结构体里,那个存 native 函数入口地址的字段,到底偏移多少?
于是就有了这套“样本探测”思路:
刚好存在一个函数GetMethodID这个函数的返回值就是ArtMethod *
通过上面这种不直接调用 RegisterNatives 的注册方式,可以很好地隐藏 JNI 函数。攻击者根本无法通过传统 Hook RegisterNatives 的方式,拿到 Java native 函数在 so 里的真实偏移与地址。
难道遇到这种防护,我们就束手无策了吗?
当然不是。这里给大家分享一个通杀思路:不去管它怎么注册、怎么隐藏,我们直接去遍历目标类的 ArtMethod,把里面的关键字段打印出来,就能直接拿到 Java 方法与 Native 函数的对应关系,从虚拟机层面绕过所有注册层防护
原理:Java 函数最终指向 Native 层的 ArtMethod
思路如下:
1、用 Runnable 当“探针”自动探测 methods_ 偏移
通过 env.findClass("java/lang/Runnable") 拿到系统接口 Runnable,再用 getMethodId("run", "()V") 获取它唯一的那个方法的 methodId。
因为 Runnable 只有一个 run(),所以这个 methodId 就是 ArtMethod[0] 的地址,直接减去 LengthPrefixedArray 头部大小(32 位减 4,64 位减 8)就能反推出整块 LengthPrefixedArray<ArtMethod> 的首地址。
把这个地址转成 uint64,在 Runnable 对应的 mirror::Class 内存里,从偏移 0 开始每 4 个字节读一次 U64,找到那个等于我们刚才算出的值的位置,这个 offset 就是当前系统上 mirror::Class::methods_ 字段的真实偏移,这样可以解决不同系统版本中methods_ 字段偏移可能不同的情况。
2、通过枚举 ClassLoader 找到任意 Java 类的 jclass
因为目标类不一定能直接用 env.findClass 找到,所以用 Java.enumerateClassLoaders 把所有 ClassLoader 走一遍。
对每个 loader 调用 Class.forName(targetClassName, false, loader)(这里传全限定名 "com.cr.Lsposed_ShadowHook.MainActivity"),哪一个不抛异常且返回非空,就是正确的 Class。
3、借助 DecodeJObject 将jclass转换为 C++ 的 mirror::Class*
调用 DecodeJObject(thread_self, jclass_ptr) 把 Java 的 Class 解到 ART 里的 mirror::Class*,这是 C++ 层真正的类对象。
用第一步算出来的 methods_offset,在 mirror::Class* 内存上 add(methods_offset).readPointer(),就得到了 LengthPrefixedArray<ArtMethod>*。
LengthPrefixedArray<ArtMethod>内存结构就是ArtMethod数量+ArtMethod数组
4、按你逆出来的ArtMethod偏移拆解 ArtMethod 内容
以 64 位为例,针对你那版结构体:
+0x04 读取 access_flags_ 看方法修饰符。
+0x08 / +0x0c / +0x10 / +0x12 分别读 dex_code_item_offset_、dex_method_index_、method_index_、hotness_count_。
+0x18 / +0x20 / +0x28 分别读取 dex_cache_resolved_methods_、data_、entry_point_from_quick_compiled_code_,其中 entry_point 就是 native/quick 入口地址。
遍历 methods_count 次,每次用 method_array_data.add(i * art_method_size) 定位到当前 ArtMethod 指针,然后按这些偏移把信息全部打印出来。
5、通过 PrettyMethod 把 ArtMethod 转成人类可读的方法名
在 libart.so 里遍历导出符号,模糊匹配名字里同时包含 "art", "ArtMethod", "PrettyMethod" 的符号,拿到它的地址。
结合不同 Android 版本的实现,封装出适配当前 ABI 的 NativeFunction,再在 JS 里准备好 std::string 的临时缓冲区,调用后用你写的 readStdString 从返回的 std::string 结构里把真正的 UTF‑8 字符串读出来。
在遍历 ArtMethod 时,对每一个 currentMethodPtr 调一次 callprettymethod(currentMethodPtr),日志里就能看到“类名.方法名(签名)”和对应的 entry_point,方便后续做 inline hook 或直接改入口指针。
Frida脚本的具体实现如下:
传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 1天前
被0xCodeMaster编辑
,原因: