我去年看 RadareCon 的时候,我学习了一个动态二进制插桩框架 Frida 。而且,最初看起来仅仅是有意思的东西,其实是很有趣的。还记得在游戏里的上帝模式么?在原生应用中使用 Frida 就是这样的一种感觉。这是一篇介绍如何使用 Frida 来操作 Android apps 的文章。
什么是动态二进制插桩?
动态二进制插桩(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)
BytecodeViewer
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:
在另外一个终端中,一个正常的 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 其中任意一个进程,并且开始进行修改。
例如:你可以追踪 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@[email protected]@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。)
本文由 看雪翻译小组成员 iromise 编译,来源 Michael Helwig@Notes on IT and Security
戳👇 图片加入看雪翻译小组哦!
❤ 往期热门内容推荐
更多优秀文章,长按下方二维码,“关注看雪学院公众号”查看!
看雪论坛:http://bbs.pediy.com/
微信公众号 ID:ikanxue
微博:看雪安全
投稿、合作:www.kanxue.com