基于FRIDA的工具,可在Android应用中追踪JNI API的使用。
Android应用中包含的本机库通常使用JNI API利用Android运行时。通过手动反向工程跟踪这些呼叫可能是一个缓慢而痛苦的过程。 jnitrace是一种动态分析跟踪工具,类似于Frida-Trace或Strace,但对于JNI。
使用jnitrace运行的最简单方法是使用PIP安装:
pip install jnitrace
PIP安装后,很容易运行jnitrace :
jnitrace -l libnative-lib.so com.example.myapplication
jnitrace需要至少两个参数才能运行跟踪:
-l libnative-lib.so用于指定要跟踪的库。该参数可以多次使用, *可以用于跟踪所有库。例如, -l libnative-lib.so -l libanother-lib.so或-l * 。com.example.myapplication是要跟踪的Android软件包。该软件包必须已经安装在设备上。可选论点如下:
-R <host>:<port> - 用于指定远程Frida服务器的网络位置。如果A:未指定,则Localhost:27042由DeAfult使用。-m <spawn|attach> - 用于指定用于使用的Frida附加机构。它可以产生或附着。 Spawn是默认和推荐选项。-b <fuzzy|accurate|none> - 用于控制返回跟踪输出。默认情况下, jnitrace将以accurate模式运行回避器。可以将此选项更改为fuzzy模式,也可以通过使用none选项来阻止返回。有关差异的解释,请参见Frida文档。-i <regex> - 用于指定应追踪的方法名称。这可能有助于减少特别大的JNI应用程序中的噪声。该选项可以多次提供。例如, -i Get -i RegisterNatives只包括以其名称包含获取或登记剂的JNI方法。-e <regex> - 用于指定跟踪中应忽略的方法名称。这可能有助于减少特别大的JNI应用程序中的噪声。该选项可以多次提供。例如, -e ^Find -e GetEnv将从结果中排除所有开始查找或包含getEnv的JNI方法名称。-I <string> - 用于指定应追踪的库中的导出。这对于您只想追踪少量方法的库很有用。 jnitrace认为导出的功能是直接从Java侧呼叫的任何功能,包括使用登记剂限制的方法。该选项可以多次提供。例如, -I stringFromJNI -I nativeMethod([B)V可用于包括来自名为Java_com_nativetest_MainActivity_stringFromJNI的库中的导出,并使用寄存器名称和使用nativeMethod([B)V的签名的寄存器名称绑定的方法。-E <string>用于指定不应追踪的库中的导出。这对于您想要忽略的一组繁忙的本机通话的库很有用。 jnitrace认为导出的功能是直接从Java侧呼叫的任何功能,包括使用登记剂限制的方法。该选项可以多次提供。例如, -E JNI_OnLoad -E nativeMethod将从跟踪中排除JNI_OnLoad函数调用和任何具有名称nativeMethod的方法。-o path/output.json用于指定jnitrace将存储所有跟踪数据的输出路径。该信息以JSON格式存储,以允许以后的跟踪数据进行后处理。-p path/to/script.js提供的路径用于在加载jnitrace脚本之前将FRIDA脚本加载到目标过程中。这可用于在jnitrace开始之前击败抗Frida或反欺骗代码。-a path/to/script.js加载jnitrace后,提供的路径用于将FRIDA脚本加载到目标过程中。--hide-data - 用于减少控制台中显示的输出量。此选项将隐藏显示为hexdumps或字符串去引用的其他数据。--ignore-env使用此选项将隐藏该应用程序使用JNienv struct进行的所有调用。--ignore-vm使用此选项将隐藏使用Javavm struct进行的所有调用。--aux <name=(string|bool|int)value> - 用于传递应用程序时传递自定义参数。例如--aux='uid=(int)10'将催生用户10的应用程序,而不是默认用户0。笔记
请记住,Frida服务器必须在运行jnitrace之前运行。如果已遵循安装FRIDA的默认说明,则以下命令将启动服务器的jnitrace :
adb shell /data/local/tmp/frida-server
为jnitrace提供动力的引擎可作为一个单独的项目提供。该项目允许您以一种熟悉使用Frida Interceptor将功能和地址附加的方法导入JNItrace来跟踪单个JNI API调用。
import { JNIInterceptor } from "jnitrace-engine" ;
JNIInterceptor . attach ( "FindClass" , {
onEnter ( args ) {
console . log ( "FindClass method called" ) ;
this . className = Memory . readCString ( args [ 1 ] ) ;
} ,
onLeave ( retval ) {
console . log ( "tLoading Class:" , this . className ) ;
console . log ( "tClass ID:" , retval . get ( ) ) ;
}
} ) ;更多信息:https://github.com/chame1eon/jnitrace-engine
从源构建jnitrace要求首先安装node 。安装node后,需要运行以下命令:
npm installnpm run watch npm run watch将在背景中运行frida-compile将源汇编为输出文件, build/jnitrace.js 。默认情况下,从build/jnitrace.js加载jnitrace.py ,因此不需要其他更改即可运行更新。
像Frida-trace一样,输出是根据API调用线程进行着色的。
显示屏上的线程ID正下方是JNI API方法名称。方法名称与jni.h标头文件中看到的名称完全匹配。
随后的行包含|-指示的参数列表。 |-字符是参数类型之后,然后是参数值。对于Jmethods,Jfields和Jclasses Java类型将显示在卷曲括号中。这取决于jnitrace看到了原始方法,字段或类查找。对于传递缓冲区的任何方法, jnitrace将从参数中提取缓冲区,并将其显示为参数值以下的六十振。
返回值在列表的底部显示为|=并且对于void方法,则不会存在。
如果启用了回溯,则将在方法调用下方显示FRIDA回溯。请注意,根据Frida文档,模糊的回溯并不总是准确的,准确的回溯可能会提供有限的结果。
该项目的目的是创建一个工具,该工具可以为大多数Android应用程序有效地追踪JNI API调用。
不幸的是,在JNIENV结构中将所有功能指针附加到所有功能指针的最简单方法是超载该应用程序。它会导致基于其他无关libart.so进行的大量功能调用数量的崩溃。
为了应对这种性能障碍, jnitrace创建了一个阴影JNienv,它可以向其想要跟踪的库提供。 JNIENV包含一系列函数蹦床,通过一些自定义的Frida NativeCallbacks向JNI API弹跳,以跟踪这些功能的输入和输出。
通用的Frida API在提供一个平台上做得很好,以最少的努力来构建这些功能蹦床。但是,这种简单的方法对所有JNIENV API都不起作用。追踪所有方法的关键问题是在API中使用variadic参数。不可能提前为这些功能创建NativeCallback,因为事先未知所有不同的Java方法组合。
解决方案是监视呼叫的过程,以获取GetMethodID或GetStaticMethodID ,用于从运行时查找方法标识符。一旦jnitrace看到jmethodID查找,它就会具有已知的ID映射到方法签名。稍后,当进行JNI Java方法调用时,使用初始的NativeCallback来提取呼叫中的方法ID。然后将该方法签名解析以提取方法参数。一旦jnitrace在该方法中提取了参数,它就可以动态创建该方法的NativeCallback。返回了新的NativeCallback,并且一点点架构特定的ShellCode处理了设置堆栈和寄存器,以允许该调用成功运行。这些特定方法的NativeCallbacks被缓存,以使回调在多次调用的方法时更有效地运行。
一个简单的NativeCallback不足以从方法调用提取参数的另一个地方是使用VA_ARGS指针作为最终参数。在这种情况下, jnitrace使用一些代码从提供的指针中提取参数。同样,这是特定于架构的。
这些功能调用中追踪的所有数据都发送到格式化并显示给用户的Python控制台应用程序。
该工具的大多数测试都是在运行棉花糖的Android X86_64模拟器上进行的。在另一台设备上运行的任何问题,请提交问题,但如果可能的话,建议尝试在类似的模拟器上运行。
对于运行jnitrace的任何问题,请在Github上创建一个问题。请在提交的问题中包含以下信息: