追踪Android方法调用2
youncyb11 发表于 湖北 移动安全 848浏览 · 2025-05-28 12:34

1. 前言

本文是对《追踪 Android 方法调用 1》所学知识的实践,通过 frida hook ArtMethod::InvokeArtInterpreterToInterpreterBridgeArtInterpreterToCompiledCodeBridge 等函数,追踪以下调用关系:

Java->Java

Java-JNI

JNI-Java

JNI-JNI

通过前文的分析可知,无论哪种调用方式,其最终都会进入 ArtMethod::Invoke 函数,但对该函数 hook 后,发现其存在一些缺陷。然后我们将目光转向更深入的解释执行逻辑,其中 DocallCommonPerformCall 都是内联函数,没办法 hook,所以只能选择 ArtInterpreterToInterpreterBridgeArtInterpreterToCompiledCodeBridge,对这两个函数的 hook,可以解决 Java-JavaJava-JNI 的 caller 和 callee 关系,但对于 JNI-xxx 的方式无能为力,通过搜索资料后,我们发现通过 frida 打印调用栈的方式(比较耗时)和线程的顺序执行方式确认函数的调用关系。

2. hook ArtMethod:: Invoke

ArtMethod::Invoke 函数签名如下,其中有用的是传入参数:args,短签名:shorty,通过这两个参数,我们就可以对传入的参数进行解析。

解析参数代码可根据 aosp 源码 BuildArgArrayFromVarArgs 函数进行解析:

ArtMethod 对象本身,则是其第 0 个参数,通过调用 ArtMethod::PrettyMethod(bool with_signature) 则可以拿到被调用方法的名称。

通过 ida 反编译 64 位的 libart.so,我们可以用 ts 轻松的写出其调用代码。

由于其返回类型是 std::string,根据 libc++'s implementation of std:: string,可以使用如下代码获取 js 可用的字符串:

完整的 hook ArtMethod:: Invoke 代码即如下所示:

测试代码如下,MainActivity 调用 stringFromJNIstringFromJNI 调用 Java 层函数 Java_method_hello 和 JNI 函数 testFromJNI

图片加载失败


从执行结果来看,hook ArtMethod::Invoke 并没有给出 Java_method_hello2("HHHHHH", 1) 的调用,与《追踪 Android 方法调用 1》分析一致,在 Java->Java 流程中,caller 在调用 callee 时,直接通过 ArtInterpreterToInterpreterBridge 回到 Execute 执行,而不会走 ArtMethod::Invoke

3. 处理 Java 层调用追踪

3.1 选择 hook 对象

其次,可以比较清晰的从上图的结果中看出,MainActivity 中发生了哪些函数的调用,但无法知晓 caller。回顾《追踪 Android 方法调用 1》,我们可以使用 DoCallDoCallCommonPerformCallArtInterpreterToCompiledCodeBridgeArtInterpreterToInterpreterBridge 拿到 Java->JNIJava->Java 的 caller 和 callee。(PS:原理上看,ExecuteMterpImpl(self, accessor.Insns(), &shadow_frame, &result_register) 或者 ExecuteSwitchImpl<false, false>(self, accessor, shadow_frame, result_register, false) 都可以通过 shadow_frame,拿到 caller 和 callee。)

但非内联函数,只剩下 DoCallArtInterpreterToCompiledCodeBridge, ArtInterpreterToInterpreterBridge,而 DoCall 还没有构造 callee_shadow_frame,不利于我们获取 callee 的传入参数,所以最终选择只剩下:ArtInterpreterToInterpreterBridgeArtInterpreterToCompiledCodeBridge,其分别代表 Java->JavaJava-JNI

3.2 解析 ShadowFrame

ShadowFrame 的内存布局如下:

其中较为有用的信息是:

method_: 用于获取函数名

number_of_vregs_:虚拟寄存器数量,可以用于获取传入参数

vregs_[0]:虚拟寄存器,实际参数所在

但需要注意的是,vregs_不仅包含了我们所需要的参数,还包含了函数局部变量所需的寄存器。我们知道参数实际数量是 CodeItem 中定义的 ins_size_ 决定,所以 vregs[number_of_vregs_ - ins_size: number_of_vregs] 才是实际的参数位置。

图片加载失败


在上文中,通过 shorty 解析 ArgArray 的参数时,可知 double、long 类型的参数占用 8 个字节,其他的参数占用 4 个字节。所以也需要知道 shorty 才方便解析 vregs_

通过 ArtMethod 解析 DexFile 对象,可以获取到 shorty,但对于 frida 来说比较麻烦。所以我们可以选择直接对 PrettyMethod 返回的的函数名进行解析。

我们可以通过如下 js 代码获取 shadow_frame 包含的信息:

再对 callee_artmethod_name 进行解析,获取参数数量与 shorty

此时便可以获取正确的参数:

图片加载失败


3.3 hook ArtInterpreterToInterpreterBridge

回过头观察 ArtInterpreterToInterpreterBridge 调用,其 accessorcallee_frame 都只有被调用函数的信息。

但回顾上文 ShadowFrame 的内存布局,可知其第一个成员指向了 caller_shadow_frame。所以我们可以使用如下代码获取 caller 信息:

图片加载失败


3.4 hook ArtInterpreterToCompiledCodeBridge

由于 ArtInterpreterToCompiledCodeBridge 参数中自带 caller,所以无需再通过 ShadowFrame->link_ 解析。

图片加载失败


4. 处理 JNI 层调用追踪

ArtInterpreterToInterpreterBridgeArtInterpreterToCompiledCodeBridge 无法监测到 JNI 层发起的函数调用。在 ArtMethod::Invoke 中其会通过 art_quick_invoke_stubart_quick_invoke_static_stub,这两个函数也没有参数包含了栈信息。

而 Java 层调用和 JNI 层调用都会执行到 ArtMethod::Invoke,为了方便区分 JNI 层发起的调用,我们可以选择在 JNI 层的前一个环节:InvokeWithArgArray,其签名如下,我们可以使用上文解析 ArtMethod::Invoke 的代码对其进行解析。

图片加载失败


5. 读取内存字符串

当参数类型是 java.lang.String,则可以将其解析为可读的字符串。根据 art/runtime/mirror/string.h 定义,字符串分为 compressed 类型和非 compressed 类型,compressed 是单个字节表示字符,采用 ascii 码编码;非 compressed 是 2 个字节编码,采用 utf16 编码。

所以可以用以下 frida 代码解析内存中的字符串,修改 parse_arg_arrays 函数,增加:

图片加载失败


完整的代码:https://github.com/youncyb/trace_android_call

参考

1纯 frida 实现 smali 追踪.md

2使用 Frida 打印 Java 类函数调用关系

3libc++'s implementation of std:: string

1 条评论
某人
表情
可输入 255
用户WVcyiV06X7
2025-06-06 05:26 0 回复
18839873669