译者:興趣使然的小胃
预估稿费:150RMB
投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿
一、前言
在上一篇文章中,Rohit向我们介绍了如何使用Frida完成基本的运行时测试任务。简而言之,Frida可以动态改变Android应用的行为,比如可以绕过检测Android设备是否处于root状态的函数。对于在ART(Android Runtime,Android运行时)环境中运行的应用来说,我们可以使用Java.perform来hook函数。
然而,在某些情况下,开发者会使用Android NDK来执行各种操作,比如检测root状态等,这种情况下,开发者就可以使用C++或C语言来开发代码,也可以访问APK中的函数。
在本文中,我们介绍了如何实现使用Android NDK开发的代码的动态插桩,具体而言,我们会介绍如何利用Frida来hook使用C++或C开发的函数。
二、动机
像Xposed之类的框架默认情况下没有提供hook原生函数(native function)的功能,而其他工具,如android eagle eye对初学者来说并不友好,学习曲线非常陡峭。然而,我们可以使用Frida来hook基于Android NDK框架构建的那些函数。接下来我们可以看看具体的操作流程。
三、目标:Rootinspector
在本文中,我们的测试对象为Rootinspector应用,这个应用可以检查设备的root状态,应用由纯C++语言编写的原生代码构建而成。我们的目标是hook这些函数,绕过root检测逻辑。
在Rootinspector中,与root状态检测逻辑有关的代码分为两个部分。APK中的一个封装函数会调用由C++编写的checkifstream()底层函数,这一过程所对应的java函数为checkRootMethodNative12(),如下图所示。
checkRootMethodNative12()是Android APK中使用Java编写的函数,会调用底层的checkifstream()函数,后者使用C++编写。
这个Android APK中声明的所有原生函数如下所示。
检查原生函数的源代码后,我们发现这个函数的具体实现为JavacomdevadvancerootinspectorRootcheckifstream,这个字符串由包名及函数名构成,由“”符隔开。
我们首先尝试hook checkRootMethodNative12()这个Java函数,所使用的代码如下所示:
然而,上述代码没法实现hook任务,出现的错误如下所示。Frida无法获得Root类对应的“localRoot”对象的引用。
在这种情况下,我们无法hook使用C++编写的那些函数,因为这些函数没有运行在Java VM上下文环境中。因此,我们必须做些改变,才能hook到原生的C++代码。
四、Hook原生代码
我们可以使用Frida中的Interceptor函数,深入到设备的底层内存中,hook特定的库或者内存地址。
当APK被封装打包时,编译器会编译C++代码,将其存放在APK文件lib目录中的“libnative.so”,如下所示。
使用Interceptor时,我们需要hook libnative2.so这个.so以及Javacomdevadvance_rootinspectorRootcheckfopen函数。我们需要使用十六进制编辑器或者调试器来读取.so文件,通过逆向工程获取函数名。这里我们耍了点小聪明,因为我们对应用的源代码已经非常熟悉。
现在,我们可以运行如下代码,看看我们是否可以成功拦截到checkfopen这个原生函数。
执行上述代码后,我们又遇到一个错误,错误提示某个指针不存在,这意味着libnative.so文件没有被正确加载,或者应用没有找到这个文件。
然而,再次运行代码,保持应用处于启动状态,我们的代码就能正常执行。具体操作为,先结束第一次运行的脚本,保持应用处于打开状态,再次运行脚本,点击“inspect using native code”按钮后,程序的运行状态如下图所示。
我们有必要了解发生这种情况的具体原因。在Android 1.5中,Android NDK提供了动态链接库(与Windows环境中的DLL类似),以支持NDK中的动态加载特性。当我们第一次启动应用时,dll文件(libnative2.so)没有被加载,因此我们会得到一个“expected a pointer”的错误信息。现在,当我们终止脚本、保持应用处于打开状态时,再次运行脚本,程序发现dll文件已经被加载,因此此时我们就可以hook目标函数。
现在换个思路,不必等待程序加载dll文件,我们可以在“dlopen”函数上设置一个陷阱,这个函数是一个原生系统调用,可以用来加载与应用有关的所有动态链接库。一旦dlopen函数hook成功,我们就可以检查我们的目标dll有没有被加载。如果dll是第一次被加载,我们可以继续运行,hook原生函数。我们使用didHookApi布尔值检查hook过程,避免dlopen被多次hook。
我们使用如下代码来直接hook原生函数。代码可以分为两部分。
在代码第17-30行中,我们首先尝试使用Frida的Module.findExportByName API来hook dlopen函数,然后搜索内存中的dlopen函数(这里只能祈祷该函数没有被覆盖)。
在onLeave事件中,我们首先检查我们的目标DLL有没有被加载,只有DLL已经被加载的情况下,我们才会hook原生函数。
执行最终的脚本后,我们就可以通过原生函数,绕过Rootinspector的root检测机制,过程如下所示。
脚本运行之前如下所示:
现在,关掉应用,在不启动应用的情况下运行脚本。脚本会自己打开这个应用。我们只需要点击“Inspect Using Native”这个按钮即可,如下所示。