当我去年看 RadareCon 的时候,我学习了一个动态二进制插桩框架 Frida 。而且,最初看起来仅仅是有意思的东西,其实是很有趣的。还记得在游戏里的上帝模式么?在原生应用中使用Frida就是这样的一种感觉。这是一篇介绍如何使用Frida来操作Android apps的博客。 此外,由于我们从事这一方面的工作,我们也打算在在这个博客的第二部分来解决一个简单的Android Crackme。
动态二进制插桩(Dynamic Binary Instrumentation)意味着我们要在已经存在(或者运行)的二进制文件中注入外部代码来使得它们去做一些它们之前没有做的事情。它并不需要我们写exp,因为代码注入的发生并不依赖于你是否已经找到了漏洞。它也并不是在debugging,因为我们并不需要使用debugger调试binary,但是我们仍然可以做一些类似的事情。那么我们可以使用DBI做什么呢?有以下一些炫酷的事情:
• 访问进程内存
• 在应用运行的时候覆盖函数
• 调用导入的类中的函数
• 寻找堆上的对象实例,并且利用它们
• Hook,记录以及拦截函数等等
当然你也可以利用debugger来做上面的这些事情,但是却会遇到一系列的问题。例如,在Android中,你需要反汇编并且重新编译一个应用之后,它才可以被debug。一些app还会检测,并且会试图阻止debugger,因此,你还要去想办法跳出对应的逻辑。当然,这是很有可能的,但是确实比较麻烦的。使用Frida的DBI使得我们不需要知道其中的细节就可以快速开始。
Frida允许你将JavaScript的部分代码或者你自己的库注入到windows,macos,linux,IOS,Android,以及QNX的原生应用中。它最初是基于Google的V8 Javascript runtime,到了版本9之后,Frida就开始使用内部的Duktape了。但在你需要的情况下,你仍然可以切换到V8。Frida有很多模式的操作来和二进制文件进行交互(也有可能在没有root的设备上的app中插桩),但是在这里我们就是举一些常用的例子,并且不关心内部的情况。
首先,你需要如下软件
• Frida,在这个教程中我是用的是9.1.16版本
• 一个release page上的Frida-server可执行文件(在写这篇文章的时候,我使用的是frida-server-9.1.16-android-arm.xz,这个可执行文件的版本应该和你的Frida版本匹配)
• 一个安卓模拟器或者被root的设备。Frida是在Android4.4上开发的,但它应该也会支持之后的版本。我在这篇教程中成功地在Android7.1.1中使用了它。对于第二部分的crackme,我们无论如何也需要Android4.4之后的版本。
同样,我假设你已经有了一个linux为主机的操作系统。当然,如果你正在使用windows或者mac,你需要调整一下命令。
如果你想要跟着我在第二部分的讲解中解决OWASP中的Unbreakable Crackme Level 1,你应该也需要下载
• OWASP Uncrackable Crackme Level 1(APK)
• BytecodeViewer
• dex2jar
Frida提供了一系列的API以及方法来开始你的旅程。你可以使用命令行窗口或者像frida-trace的记录low-level函数(例如libc.so中的'open'调用)的工具来快速运行。你可以使用C,NodeJs或者Python绑定来完成更加复杂的工作。Frida在内部使用了很多JavaScript语言,因此很多时候你可能都需要和这个语言打交道。因此,如果你也像我一样总是有点不喜欢JavaScript(除了它的XSS capabilities),Frida就是一个你需要熟悉它的理由。
如果你还没有安装Frida,那就使用下面的方法来安装(当然你也可以根据 README 采用其他的方法来安装):
然后启动你的模拟器或者连接到你的设备,并且确保adb可以运行,之后列出你的设备:
然后安装frida-server。从压缩包中提取对应的文件,并把它push到设备上:
使用设备的adb来打开一个shell,并且切换到root状态,然后启动frida
note1:
• 如果frida-server没有启动的话,请确保你处于root状态,并且之前传输的那个二进制文件被正确传输了。我曾经由于传输文件不成功而导致了一些奇怪的错误。
• 如果你想要将frida-server作为一个后台程序启动,请使用./frifa-server &。
在另外一个终端中,一个正常的OS的shell,检查frida是否正在运行,并且列出在Android上的进程
-U 代表着USB,并且让Frida检查USB-Device,但是使用模拟器也会有这样的效果,你会得到类似于下面的结果
你可以看到进程号id(PID),以及正在运行的程序的名字。利用Frida,你现在就可以hook其中任意一个进程,并且开始进行修改。
e.g. 你可以追踪Chrome的某些调用(如果它还没有运行的话,请记得先启动chrome)。
然后你会得到如下的结果
frida-trace命令会产生一些javascript 文件,这些文件就是Frida注入到进程中用来记录某些调用的。我们可以简单看看生成的open.js,它的目录为__handlers__/libc.so/open.js。它hook了libc.so中的open函数,然后输出了对应的参数。在Frida这样做是非常简单的。
请注意Frida如何提供访问到被chrome在内部调用的open函数的参数的能力。我们来简单改一下这个脚本。如果我们将对应输出文件的路径名修改为可以阅读的文本格式而不是这些路径被存储的内存地址,那岂不是更好?幸运的是,我们可以直接利用Frida来访问内存。简单看一下Frida的API以及Memory 对象。我们可以修改我们的脚本,并将内存地址对应的内容以UTF8的格式输出,这样我们就可以看到一个更加直观的效果了。修改之后,脚本大概是这个样子的
我们仅仅添加了Memory.readUtf8String函数,然后我们得到了
Frida输出了对应的路径名,对吧?
另一个需要注意的事情是,你不仅可以在利用Frida注入到某个进程之前启动它,你还可以利用-f参数来使得Frida自己产生对应的进程。
现在我们来看一下Frida的命令行接口frida-cli:
这将会启动Frida和Chrome app。但是它并不会启动chrome的主进程。这也就意味着,这样就给了你机会使得你可以在主线程启动之前注入Frida代码。不幸的是,在我这里,这样做总会使得app在两秒之后被自动杀死。这并不是我们想要的。正如cli输出所建议的,你可以利用这两秒去输入%resume,使得app启动其主线程。或者你也可以直接使用 --no-pause参数来使得无论如何也不要中断app的启动,同时,将生成进程的工作交给Frida。
无论你是用哪一种方法,你都会得到一个不会被kill的shell,利用这个你就可以使用Frida的JavaScript的API向其中写命令。你还可以利用TAB键来看一下一些可以使用的命令。这个shell是支持补全命令。
大部分你想要做的事情都会有对应的文档。对于Android,你可以看一下Javascript API 部分的 Java section( 尽管从技术角度出发,应该说是访问java对象的Javascript外壳,但我这里就直接说是“Java API”)。我们将在下面关注Java API,因为这是一个相对来说比较容易与app进行交互的方法了。我们并不需要直接hook libc函数,我们可以直接与java函数以及对象进行交互。(注意,如果你对使用Frida做其它的事情感兴趣的话,你可以hook更低一级的C代码,也就是我们使用的frida-trace,你可以看一看文档中的 functions 。这里我不打算将这一部分。)
为了熟悉Java API的访问,简单利用Frida的命令行来看一下Android的版本吧
或者列出所有被加载的类(警告:这将会输出一大堆东西,我下面就会对这些代码进行介绍):
这里我们输入了一个很长的命令,我们需要明确一下其中嵌入的代码。首先,我们输入的代码的最外层包装是Java.perform(function(){ ... }),这是Fridas Java API的需求。
下面是我们在Java.perform中插入的函数
这一步相当简单,我们利用Java.enumerateLoadedClasses来枚举所有被装载的类,然后利用console.log来输出所有的匹配。你将会在Frida中遇到很多这样的回调函数。你会提供下面样子的回调对象:
一旦Frida在你的请求中发现了一个匹配,onMatch会被一个有 一个或者多个参数 的函数所调用。然后当Frida枚举完所有的可能的匹配后,就会调用这个函数。
现在,让我们更加深入地了解一下Frida的魔法吧,然后利用Frida来覆盖一个函数,除此之外,我们也同样加载了一个外部分脚本,而不是把它出入到cli中,这样会更加方便一点。把下面的代码保存到一个叫脚本文件中,例如chrome.js:
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)