什么是动态二进制插桩?
动态二进制插桩(DBI)意味着将外部代码注入到现有的(正在运行的)二进制文件中,使它们能够做一些以前没有做过的事情。这个过程不是利用了漏洞,因为代码注入并不是通过你之前必须搞清楚的一些漏洞所导致的。它也不是调试,因为你没有将调试器附加到二进制文件上,尽管你可以做类似调试的一些事情。你能用DBI做些什么呢?这是一个很酷的东西:)
访问进程的内存
在应用程序运行时覆盖一些功能
从导入的类中调用函数
在堆上查找对象实例并使用这些对象实例
Hook,跟踪和拦截函数等等
当然,你也可以使用调试器完成所有这些操作,不过使用调试器会带来各种麻烦。例如在Android中,你必须反汇编并重新编译应用程序以使其可用于调试。一些应用程序会检测并尝试阻止调试器的调试过程,你需要摆脱这些反调试逻辑。可能你会成功,但是整个过程非常麻烦。使用Frida的DBI可以让你使用黑盒进程快速启动Android应用程序。
FRIDA
Frida可以“将你自己的JavaScript代码片段或代码库注入到Windows,MACOS,Linux, iOS,Android和QNX 的本地应用中”。这是第一款基于谷歌的V8 JavaScript引擎运行的应用程序,在Frida的第九个版本中使用的是Duktape,但它仍然允许你切换回V8引擎,如果你需要这么做的话。Frida有很多可以与二进制文件进行交互(包括在无root权限的设备上对应用程序进行插桩的可能性)的操作模式,不过我们在本文这里会使用最常见的用户操作,并且现在我们不需要关心其内部实现。
在开始本文的破解教程之前,你需要做以下几个事情:
1. Frida(我在本教程中使用的9.1.16版本)
2. frida-server可以二进制发布页面下载到( 在本文发表时为frida-server-9.1.16-android-arm.xz,frida-serve的版本应该与Frida版本一致。)
3. Android模拟器或已ROOT的安卓设备。Frida已经开发了Android 4.4 ARM对应的版本,但它应该适用于更高的版本。我在本教程中成功使用了Android 7.1.1 ARM。对于第二部分的破解,我们需要用到比Android 4.4版本更高的一些东西。
我使用linux系统作为宿主机的操作系统,如果你使用的是Windows或Mac,那你可能需要调整一些命令。
如果你想在解决OWASP Unbreakable Crackme Level 1中的问题,你可以在本系列教程的第二部分中找到破解方法,同时,你也应该下载下面的几个程序:
1. OWASP Unbreakable Crackme Level 1(APK)
2. BytecodeViewer
3. dex2jar
Frida提供各种API和开始破解的方法。你可以使用命令行界面或frida-trace跟踪低级功能的工具(如libc.so中的“open”函数的调用)进行快速运行。你可以使用C,NodeJS或Python绑定更复杂的东西。在Frida内部,会更多的使用Javascript工作,你也将使用这种语言完成大部分插桩工作。所以如果你和我一样,不喜欢使用Javascript(除了XSS功能之外),那Frida或许是让你熟悉JavaScript的另一个原因。
如果没有,请安装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-server:
adb shell
su
cd /data/local/tmp
chmod 755 frida-server
./frida-server
(注1:如果frida-server没有启动,请确保你使用的是root用户,并且该文件已经正确push到设备中。我之前就遇到过文件传输损坏导致的各种奇怪的错误。注2:如果要启动frida-server作为后台进程,可以使用这个命令./frida-server &)
在另一个常规的OS shell终端中,检查Frida是否正在运行,并列出在Android上的进程:
frida-ps -U
参数-U代表USB,并让Frida检查USB设备,但它也可以与模拟器一起工作。你应该得到一个这样的进程列表:
michael@sixtyseven:~$ frida-ps -UPID 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
[...]
你可以看到进程标识(PID)和运行的进程(名称)。使用Frida你现在可以Hook这些进程,并且可以篡改进程。
例如,你可以跟踪Chrome完成的特定通话(如果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会将这些JavaScript文件注入到进程中并跟踪特定的调用。看看生成的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函数(args [0],args [1]等等)的一些调用参数。让我们修改一下这个脚本。如果我们明文输出打开的文件的路径,而不是存储这些路径的内存地址,这样不是更好吗?幸运的是,我们可以直接用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 -f com.android.chrome
这将启动Frida和Chrome应用。但是,它还没有启动Chrome的主进程。这意味着可以在应用程序的主进程启动之前注入Frida代码。不幸的是,在我自己的尝试中总是遇到一个问题——应用程序在启动2秒钟后自动卡死。这不是我们想要的。你可以使用这两秒来键入%resume,如cli输出的建议,让应用程序启动其主进程。或者你直接启动Frida,使用–no-pause参数选择不中断应用程序的启动,仍然将进程的产生留给Frida去做。
在这两种情况下,你都可以获得一个shell(不会被杀死),你现在可以使用其Javascript API向Frida写入命令。按TAB键可以查看可用的命令。shell还支持命令自动完成。
你想做的大多数事情在文档中都是有据可查的。对于Android,特别要检查Javascript-API 的Java部分(我将在此讨论一个“Java API”,尽管在技术上说,是一个用于访问Java对象的Javascript封装)。我们将重点关注Java API,因为这是使用Android应用程序更为方便的方式。我们可以直接使用Java函数和对象,而不是Hook libc中的函数。(注意:如果你对Frida的其他的Java API能做什么很感兴趣,那么你可以使用frida-trace Hook Android里面的更低级的C函数,并且查看文档里的函数部分,我不会按照文档所述那样在本文中进行描述。)
要开始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.enumerateLoadedClassesFridas API 枚举所有加载的类,并将每个匹配到的类使用console.log输出到控制台。这种回调对象的模式你会经常在Frida找到。你需要提供一个回调对象的模板。
{"onMatch":function(arg1, ...){ ... },"onComplete":function(){ ... },
}
一旦Frida匹配到你的请求,就会使用一个或多个参数调用onMatch并且当Frida完成迭代可能的匹配时,将会调用onComplete。
现在我们深入了解了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接收此类的包装对象并访问implementation的onResume函数的属性以提供新的实现。在新的函数主体内,它this.onResume()这样的方式调用原来的onResume的实现,所以应用程序可以继续正常运行。
打开你的模拟器,打开Chrome并用-l选项注入脚本:
frida -U -l chrome.js com.android.chrome
一旦你触发了onResume的执行—— 例如通过在模拟器中切换到另一个应用程序并返回到Chrome后你将得到下面的输出:
[*] onResume() got called!
很好用,不是吗?我们实际上重写了应用程序的一个函数。这给了我们控制目标应用程序行为的很多可能性。但是我们可以做更多的事情:我们也可以在堆上查找实例化的对象Java.choose。
在我们继续之前,我有一个警告:当你的模拟器变得有点慢的时候,Frida有时候会有时间超时的提示。为了防止这种情况,请将脚本包装在setImmediate函数中或将其导出为rpc。RPC在Frida中默认情况下不会超时( 感谢@oleavr的这些提示)。在你修改脚本文件后,setImmediate会自动重新运行Frida脚本,这样很方便。它也会在后台运行你的脚本。这意味着你可以立即得到一个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
所以我们在堆上发现了4个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的绑定和r2frida。但首先会有一点警告。
警告
当你尝试使用Frida时,你会注意到有一些不稳定。首先,将外部代码注入到另一个进程中容易导致崩溃,因为应用程序被以意想不到的方式触发运行。第二,Frida本身仍然让人感觉为一个实验品。有时候它的确在正常工作,但你经常需要尝试多种方式来获得你所需的结果。例如,当我尝试加载一个脚本并在命令行中执行一个命令生成一个进程时,Frida就会一直崩溃。相反,我必须首先启动该进程,然后让Frida去注入脚本。这就是为什么我向你展示了使用Frida的各种方法,并防止超时失败的提示出现。你可能需要弄清楚在你的实际情况下哪个才是最有效的方法。
Python绑定
一旦了解了Frida的工作原理之后,如果你想要使用Frida更自动化的完成你的工作,你应该查看易于使用的Python,C或NodeJS 绑定。例如从Python 中注入chrome.js脚本,你可以使用Frida的Python绑定并创建一个chrome.py脚本:
#!/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的文档。
Frida和Radare2:r2frida
如果我们也可以使用像Radare2这样的反汇编框架来检查我们的应用程序的内存,那不是很好吗?在这里是r2frida。你可以使用r2frida将Radare2连接到Frida,并进行进程内存的静态分析和反汇编。我不会在这里详细介绍r2frida,因为它的使用本身就预先假定了使用者具备Radare2的知识(如果你没有一定的相关知识,那非常值得一看。),但是我仍然想给你一个使用简便的方式。
你可以使用Radare2的包管理器来安装r2frida(假设你已经安装了Radare2):
r2pm install r2frida
回到我们的frida跟踪示例,删除或重命名我们修改过的脚本,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
还可以使用=!前缀检查可用的r2frida命令,以及哪些可以在内存区域快速搜索指定内容或写入任意内存地址的命令等等?
[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
[...]
更多
如果这让你感到好奇,可以看看下面的内容:
1. Frida的项目页面
2. @oleavr在r2con的演讲视频 和David Weinstein对Frida简介的演讲。
3. Frida的Twitter帐号@fridadotre
4. Frida的Telegram频道
5. AppMon——基于Frida的应用程序监视和注入的GUI工具(由@dpnishant提供)
在本教程的第二部分,我们将使用Frida来轻松解决一些小问题。