当我去年看 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
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)
• dex2jar
Frida提供了一系列的API以及方法来开始你的旅程。你可以使用命令行窗口或者像frida-trace的记录low-level函数(例如libc.so中的'open'调用)的工具来快速运行。你可以使用C,NodeJs或者Python绑定来完成更加复杂的工作。Frida在内部使用了很多JavaScript语言,因此很多时候你可能都需要和这个语言打交道。因此,如果你也像我一样总是有点不喜欢JavaScript(除了它的XSS capabilities),Frida就是一个你需要熟悉它的理由。
如果你还没有安装Frida,那就使用下面的方法来安装(当然你也可以根据 README 采用其他的方法来安装):
pip install frida
npm install frida
然后启动你的模拟器或者连接到你的设备,并且确保adb可以运行,之后列出你的设备:
michael@sixtyseven:~$ adb devices List of devices attached emulator-5556 device
然后安装frida-server。从压缩包中提取对应的文件,并把它push到设备上:
adb push /home/michael/Downloads/frida-server-9.1.16-android-arm /data/local/tmp/frida-server
使用设备的adb来打开一个shell,并且切换到root状态,然后启动frida:
adb shell su cd /data/local/tmp chmod 755 frida-server ./frida-server
note1:
• 如果frida-server没有启动的话,请确保你处于root状态,并且之前传输的那个二进制文件被正确传输了。我曾经由于传输文件不成功而导致了一些奇怪的错误。
• 如果你想要将frida-server作为一个后台程序启动,请使用./frifa-server &。
在另外一个终端中,一个正常的OS的shell,检查frida是否正在运行,并且列出在Android上的进程:
frida-ps -U
-U 代表着USB,并且让Frida检查USB-Device,但是使用模拟器也会有这样的效果,你会得到类似于下面的结果:
michael@sixtyseven:~$ frida-ps -U PID Name ---- -------------------------------------------------- 696 adbd 5828 android.ext.services 6188 android.process.acore 5210 audioserver 5211 cameraserver 8334 com.android.calendar 6685 com.android.chrome 6245 com.android.deskclock 5528 com.android.inputmethod.latin 6120 com.android.phone 6485 com.android.printspooler 8355 com.android.providers.calendar 5844 com.android.systemui 7944 com.google.android.apps.nexuslauncher 6416 com.google.android.gms [...]
你可以看到进程号id(PID),以及正在运行的程序的名字。利用Frida,你现在就可以hook其中任意一个进程,并且开始进行修改。
e.g. 你可以追踪Chrome的某些调用(如果它还没有运行的话,请记得先启动chrome)。
frida-trace -i "open" -U com.android.chrome
然后你会得到如下的结果:
michael@sixtyseven:~$ frida-trace -i open -U -f com.android.chrome Instrumenting functions... open: Loaded handler at "/home/michael/__handlers__/libc.so/open.js" Started tracing 1 function. Press Ctrl+C to stop. /* TID 0x2740 */ 282 ms open(pathname=0xa843ffc9, flags=0x80002) /* TID 0x2755 */ 299 ms open(pathname=0xa80d0c44, flags=0x2) /* TID 0x2756 */ 309 ms open(pathname=0xa80d0c44, flags=0x2) /* TID 0x2740 */ 341 ms open(pathname=0xa80d06f7, flags=0x2) 592 ms open(pathname=0xa77dd3bc, flags=0x0) 596 ms open(pathname=0xa80d06f7, flags=0x2) 699 ms open(pathname=0xa80d105e, flags=0x80000) 717 ms open(pathname=0x9aff0d70, flags=0x42) 742 ms open(pathname=0x9ceffda0, flags=0x0) 758 ms open(pathname=0xa63b04c0, flags=0x0)
frida-trace命令会产生一些javascript 文件,这些文件就是Frida注入到进程中用来记录某些调用的。我们可以简单看看生成的open.js,它的目录为__handlers__/libc.so/open.js。它hook了libc.so中的open函数,然后输出了对应参数。在Frida这样做是非常简单的。
[...] onEnter: function (log, args, state) { log("open(" + "pathname=" + args[0] + ", flags=" + args[1] + ")"); }, [...]
请注意Frida如何提供访问到被chrome在内部调用的open函数的参数的能力。我们来简单改一下这个脚本。如果我们将对应输出文件的路径名修改为可以阅读的文本格式而不是这些路径被存储的内存地址,那岂不是更好?幸运的是,我们可以直接利用Frida来访问内存。简单看一下Frida的API以及Memory 对象。我们可以修改我们的脚本,并将内存地址对应的内容以UTF8的格式输出,这样我们就可以看到一个更加直观的效果了。修改之后,脚本大概是这个样子的:
onEnter: function (log, args, state) { log("open(" + "pathname=" + Memory.readUtf8String(args[0])+ ", flags=" + args[1] + ")"); },
我们仅仅添加了Memory.readUtf8String函数,然后我们得到了:
michael@sixtyseven:~$ frida-trace -i open -U -f com.android.chrome Instrumenting functions... open: Loaded handler at "/home/michael/__handlers__/libc.so/open.js" Started tracing 1 function. Press Ctrl+C to stop. /* TID 0x29bf */ 240 ms open(pathname=/dev/binder, flags=0x80002) /* TID 0x29d3 */ 259 ms open(pathname=/dev/ashmem, flags=0x2) /* TID 0x29d4 */ 269 ms open(pathname=/dev/ashmem, flags=0x2) /* TID 0x29bf */ 291 ms open(pathname=/sys/qemu_trace/process_name, flags=0x2) 453 ms open(pathname=/dev/alarm, flags=0x0) 456 ms open(pathname=/sys/qemu_trace/process_name, flags=0x2) 562 ms open(pathname=/proc/self/cmdline, flags=0x80000) 576 ms open(pathname=/data/dalvik-cache/arm/system@app@Chrome@Chrome.apk@classes.dex.flock, flags=0x42)
Frida输出了对应的路径名,对吧?
另一个需要注意的事情是,你不仅可以在利用Frida注入到某个进程之前启动它,你还可以利用-f参数来使得Frida自己产生对应的进程。
现在我们来看一下Frida的命令行接口frida-cli:
frida -U com.android.chrome
这将会启动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的版本吧
[USB::Android Emulator 5556::['com.android.chrome']]-> Java.androidVersion "7.1.1"
或者列出所有被加载的类(警告:这将会输出一大堆东西,我下面就会对这些代码进行介绍):
[USB::Android Emulator 5556::['com.android.chrome']]-> Java.perform(function() {Java.enumerateLoadedClasses({"onMatch":function(className){ console.log(className) },"onComplete":function(){}})}) org.apache.http.HttpEntityEnclosingRequest org.apache.http.ProtocolVersion org.apache.http.HttpResponse org.apache.http.impl.cookie.DateParseException org.apache.http.HeaderIterator
这里我们输入了一个很长的命令,我们需要明确一下其中嵌入的代码。首先,我们输入的代码的最外层包装是Java.perform(function(){ ... }),这是Fridas Java API的需求。
下面是我们在Java.perform中插入的函数:
Java . enumerateLoadedClasses ( { "onMatch": function(className){ console.log(className) }, "onComplete":function(){} } )
这一步相当简单,我们利用Java.enumerateLoadedClasses来枚举所有被装载的类,然后利用console.log来输出所有的匹配。你将会在Frida中遇到很多这样的回调函数。你会提供下面样子的回调对象:
{ "onMatch":function(arg1, ...){ ... }, "onComplete":function(){ ... }, }
一旦Frida在你的请求中发现了一个匹配,onMatch会被一个有 一个或者多个参数 的函数所调用。然后当Frida枚举完所有的可能的匹配后,就会调用这个函数。
现在,让我们更加深入地了解一下Frida的魔法吧,然后利用Frida来覆盖一个函数,除此之外,我们也同样加载了一个外部分脚本,而不是把它出入到cli中,这样会更加方便一点。把下面的代码保存到一个叫脚本文件中,例如chrome.js:
Java.perform(function () { var Activity = Java.use("android.app.Activity"); Activity.onResume.implementation = function () { console.log("[*] onResume() got called!"); this.onResume(); }; });
这个函数会重写android.app.Activity类的onResume函数。他利用Java.use来接受这个类的一个封装对象,然后访问他的onResume函数的implementation的属性来提供一个新的接口。在函数的内部,他会通过this.onResume()来直接调用原始的onResume接口,因此这个app可以正常地运行。
打开你的模拟器,然后打开chrome,并利用-l来注入对应的脚本:
frida -U -l chrome.js com.android.chrome
一旦你触发了onResume -e.g. 通过切换到其它的应用,然后返回到模拟器中的chrome,你将会得到
[*] onResume() got called!
漂亮,难道不是么?我们确实重写了一个app中的函数。因此,我们就很有可能控制目标app的行为了。但是我们可以做的更多:我们可以利用Java.choose来寻找堆上实例化的对象。
在我们继续之前,需要注意一点:当你的模拟器有点慢的时候,Frida可能会超时。 为了防止这个发生,要么在函数 setImmediate 中给你的脚本添加一层包装,要么 export them as rpc 。Frida中的RPC默认不会超时(这里要感谢@oleavr 给出这些建议)。一旦你修改了脚本,setImmediate就会自动返回它,因此这相当方便。它同时在后台运行你的程序。这也就意味着,你会立即得到一个cli,即使Frida还在执行你的脚本。我们需要做的就是等待,然后不要离开cli,直到Frida已经把你脚本里所有的输出都打印出来了。再次修改一下你的chrome.js脚本:
setImmediate(function() { console.log("[*] Starting script"); Java.perform(function () { Java.choose("android.view.View", { "onMatch":function(instance){ console.log("[*] Instance found"); }, "onComplete":function() { console.log("[*] Finished heap search") } }); }); });
然后通过下面的指令运行:
frida =U -l chrome.js com.android.chrome
应该会输出如下的结果:
[*] Starting script [*] Instance found [*] Instance found [*] Instance found [*] Instance found [*] Finished heap search
可以看出,我们在堆上找到了android.view.View的四个对象。让我们继续来看看我们可以利用这些东西再做些什么。或许我们可以调用这些实例的方法。我们只需要简单地添加instance.toString() 到我们的console.log中(因为我们使用了setImmediate,我们可以直接修改我们的代码,Frida会自动重新加载它):
setImmediate(function() { console.log("[*] Starting script"); Java.perform(function () { Java.choose("android.view.View", { "onMatch":function(instance){ console.log("[*] Instance found: " + instance.toString()); }, "onComplete":function() { console.log("[*] Finished heap search") } }); }); });
这会返回:
[*] Starting script
[*] Instance found: android.view.View{7ccea78 G.ED..... ......ID 0,0-0,0 #7f0c01fc app:id/action_bar_black_background}
[*] Instance found: android.view.View{2809551 V.ED..... ........ 0,1731-0,1731 #7f0c01ff app:id/menu_anchor_stub}
[*] Instance found: android.view.View{be471b6 G.ED..... ......I. 0,0-0,0 #7f0c01f5 app:id/location_bar_verbose_status_separator}
[*] Instance found: android.view.View{3ae0eb7 V.ED..... ........ 0,0-1080,63 #102002f android:id/statusBarBackground}
[*] Finished heap search
Frida确实调用了android.view.View的toString方法。相当酷。因此,在Frida的帮助下,我们可以读取进程的内存,修改函数,然后找到存在的实例化对象,然后使用简单的几行代码来使用它。
现在你应该对Frida有了一个基本的了解,而且应该能够自己去更进一步地挖掘它的文档与API了。在结束这篇文章之前,我想要添加两个主题,Frida' binding 以及r2frida,但是首先,我们需要注意如下的事情。
注意事项
当你在用Frida的时候,你可能会发现不稳定性。首先,将外部代码注入到其它的进程中后,由于app将会以不被期望的方式运行,所以可能会导致崩溃。其次,Frida本身也仍处于试验期阶段。虽然它也是有效的,但通常你需要尝试很多种方法才能够得到结果。例如,当我尝试在命令行中使用一条命令加载一个脚本并且生成一个进程的时候,Frida经常会崩溃。取而代之的,我需要首先启动进程,然后再利用Frida注入代码。这就是我为什么给你展示了使用Frida的不同的方法以及防止延迟。 你需要自己去判断哪一个更加适合自己。
Python bindings
如果你想要利用Frida自动化工作,你可能需要python、C或者NodeJS了。例如,使用python注入chrome.js脚本,你可能需要使用Frida的 Python bindings,然后创建一个脚本。
#!/usr/bin/python import frida # put your javascript-code here jscode= """ console.log("[*] Starting script"); Java.perform(function() { var Activity = Java.use("android.app.Activity"); Activity.onResume.implementation = function () { console.log("[*] onResume() got called!"); this.onResume(); }; }); """ # startup frida and attach to com.android.chrome process on a usb device session = frida.get_usb_device().attach("com.android.chrome") # create a script for frida of jsccode script = session.create_script(jscode) # and load the script script.load()
如果你想要结束你的Frida会话并且销毁在这个会话中插入的脚本,你可以调用session.detach()。
关于更多的例如,请参考Frida的documentation。
Frida和Radare2:r2frida
如果可以使用一个类似于 Radare2的反汇编框架来检查我们的app内存,那岂不是更好。因此有了r2frida。你可以利用r2frida来将Radare2连接到Firda。然后使用静态分析以及反汇编进程内存。由于它需要Radare2(如果你没有看的话,非常值得看一看)作为铺垫,我在这里并不打算详细介绍r2frida。但是,我还是打算告诉你如何简单地使用它。
你可以利用Radare2的包管理器(假设你已经装了Radare2)来装r2frida。
r2pm install r2frida
我们再次回到frida-trace的例子,删除或者重命名我们已经修改的脚本,然后让frida-trace自动生成默认的脚本,然后我们再来简单看一下日志:
michael@sixtyseven:~$ frida-trace -i open -U -f com.android.chrome Instrumenting functions... open: Loaded handler at "/home/michael/__handlers__/libc.so/open.js" Started tracing 1 function. Press Ctrl+C to stop. /* TID 0x2740 */ 282 ms open(pathname=0xa843ffc9, flags=0x80002) /* TID 0x2755 */ [...]
通过r2frida,你可以非常容易地检查内存地址中的内容,并且读取对应的文件名(在这个例子中是/dev/binder):
root@sixtyseven:~# r2 frida://emulator-5556/com.android.chrome -- Enhance your graphs by increasing the size of the block and graph.depth eval variable. [0x00000000]> s 0xa843ffc9 [0xa843ffc9]> px - offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF 0xa843ffc9 2f64 6576 2f62 696e 6465 7200 4269 6e64 /dev/binder.Bind 0xa843ffd9 6572 2069 6f63 746c 2074 6f20 6f62 7461 er ioctl to obta 0xa843ffe9 696e 2076 6572 7369 6f6e 2066 6169 6c65 in version faile 0xa843fff9 643a 2025 7300 4269 6e64 6572 2064 7269 d: %s.Binder dri [...]
使用r2frida来访问对应的内存并且使用它来注入代码的语法如下:
r2 frida://DEVICE-ID/PROCESS
你同样可以使用=!来检查你可以使用那些命令。你还可以快速查找内存中的特定内容或者将内容写到任意地址。
[0x00000000]> =!? r2frida commands available via =! ? Show this help ?V Show target Frida version /[x][j] <string|hexpairs> Search hex/string pattern in memory ranges (see search.in=?) /w[j] string Search wide string [...]
后续
如果这个使你感兴趣的话,你可以看看下面的内容
• Frida的project page
• youtube上的@oleavr在r2con上的讲话,以及David Weinstein的 Frida Intro talk
• Frida的Twitter @fridadotre
• Frida的Telegram channel
• AppMon, 基于Frida的app监视以及注入的可视化工具(by @dpnishant)
在这个教程的第二部分,我们将会利用Frida来解决一个简单的crackme。
如果有任何评论,问题或者批评等等,请在 Twitter 上面联系我。
原文链接:https://www.codemetrix.net/hacking-android-apps-with-frida-1/
本文由 看雪翻译小组成员 iromise 翻译