首页 文章 精选 留言 我的

精选列表

搜索[供应链攻击],共10000篇文章
优秀的个人博客,低调大师

利用FRIDA攻击Android应用程序(一)

前言 直到去年参加RadareCon大会时,我才开始接触动态代码插桩框架Frida。最初,我感觉这玩意还有点意思,后来发现这种感觉是不对的:应该是非常有意思。您还记得游戏中的上帝模式吗?面对本地应用程序的时候,一旦拥有了Frida,也就拥有了这种感觉。在这篇文章中,我们重点介绍Frida在Android应用方面的应用。在本文的第二篇中,我们将会介绍如何利用Frida来应付Android环境下的crackme问题。 什么是动态代码插桩? 动态二进制插桩(DBI)意味着将外部代码注入到现有的(正在运行的)二进制文件中,从而让它们执行以前没有做过的事情。注意,这并非漏洞利用,因为代码的注入无需借助于漏洞。同时,它不是调试,因为你不必将二进制代码附加到一个调试器上面,当然,如果你非要这么做的好也未尝不可。那么DBI可以用来做什么呢?实际上,它可以用来做许多很酷的事情: 访问进程内存 在应用程序运行时覆盖函数 从导入的类调用函数 在堆上查找对象实例并使用它们 Hook、跟踪和拦截函数等。 当然,调试器也能完成所有这些事情,但是会比较麻烦。例如。在Android平台中,应用程序必须先进行反汇编和重新编译处理,才能进行调试。一些应用程序会尝试检测并阻止调试器,这时你必须先克服这一点,才能进行调试。然而,这一切做起来都会非常麻烦。在DBI与Frida的帮助下,这些事情都不是我们要关心的,所以调试会变得更加便捷。 FRIDA入门 Frida“允许您在Windows、macOS、Linux、iOS、Android和QNX的本机应用程序中注入JavaScript或自己的库代码。”最开始的时候,它是基于谷歌的V8 Javascript运行时的,但是从版本9开始,Frida已经开始使用其内部的Duktape运行时了。不过,如果你需要V8的话,仍然可以切换回去。Frida可以通过多种操作模式与二进制程序进行交互(包括在非root的设备上给应用程序“插桩”),但是这里我们只介绍最简单的情形,同时也不关心其内部运行原理。 为了完成我们的实验,你需要 Frida 您可以从这里下载frida服务器的二进制代码(截止写作本文为止,最新版本为frida-server-9.1.16-android-arm.xz) Android模拟器或已经获得root权限的设备。虽然Frida是在Android 4.4 ARM上面开发的,不过应该同样适用于更高的版本。就本文来说,使用Android 7.1 ARM完全没有一点问题。对于第二部分的crackme来说,则需要使用比Android 4.4更高的版本。 这里假设以linux系统作为主机操作系统,所以如果你使用Windows或Mac的话,有些命令可能需要进行相应的调整。 Frida的启动方式花样繁多,包括各种API和方法。您可以使用命令行界面或类似frida-trace的工具来跟踪底层函数(例如libc.so中的“open”函数),以便快速运行。同时,你还可以使用C、NodeJS或Python绑定完成更复杂的任务。但是在其内部,Frida使用Javascript的时候较多,换句话说,你可以通过这种语言完成大部分的插桩工作。所以,如果你像我一样不太喜欢Javascript的话(除了XSS功能),Frida倒是一个让你进一步了解它的理由。 首先,请安装Frida,具体如下所示(此外,您还可以通过查看README了解其他安装方式): pip install fridanpm install frida 启动模拟器或连接设备,确保adb正在运行并列出您的设备: michael@sixtyseven:~$ adb devicesList of devices attachedemulator-5556device 然后,开始安装frida-server。先进行解压,并将二进制文件放入设备中: adb push /home/michael/Downloads/frida-server-9.1.16-android-arm /data/local/tmp/frida-server 在设备上打开一个shell,切换到root用户,并启动frida: adb shellsucd /data/local/tmpchmod 755 frida-server./frida-server (注意事项1:如果frida-server没有启动,请检查当前是否为root用户,以及文件是否在传输过程中发生损坏。当文件传输而导致文件损坏的时候,经常会出现一些让人奇怪的错误提示。注意事项2:如果你想以后台进程的方式启动frida-server的话,则需要使用./frida-server&) 您可以另一个终端的常规操作系统shell中检查Frida是否正在运行,并列出Android上的进程: frida-ps -U -U代表USB,允许Frida检查USB设备,同时还可用于仿真器。这时,您将看到一个如下所示进程列表: michael@sixtyseven:~$ frida-ps -UPID Name---- --------------------------------------------------696 adbd5828 android.ext.services6188 android.process.acore5210 audioserver5211 cameraserver8334 com.android.calendar6685 com.android.chrome6245 com.android.deskclock5528 com.android.inputmethod.latin6120 com.android.phone6485 com.android.printspooler8355 com.android.providers.calendar5844 com.android.systemui7944 com.google.android.apps.nexuslauncher6416 com.google.android.gms[...] 您将看到进程标识(PID)和正在运行的进程(名称)。现在,您可以通过Frida挂钩到任何一个进程并对其进行“篡改”了。 例如,您可以跟踪由Chrome使用的特定调用(如果还没有运行该浏览器的话,请首先在模拟器中启动它): frida-trace -i "open" -U com.android.chrome 输出结果如下所示: michael@sixtyseven:~$ frida-trace -i open -U -f com.android.chromeInstrumenting 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会将其注入到进程中,并跟踪特定的调用。您可以观察一下在__handlers __ / libc.so/open.js路径下面生成的open.js脚本。它将钩住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 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.chromeInstrumenting 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注入它的magic,或者传递-f选项给Frida,让它创建进程。 现在,我们来考察Fridas的命令行接口frida-cli: frida -U -f com.android.chrome 这将启动Frida和Chrome应用。但是,仍启动Chrome的主进程。这是为了让您可以在应用程序启动主进程之前注入Frida代码。不幸的是,在我实验时,它总是导致应用程序2秒后自动终止。这不是我们想要的结果。您可以利用这2秒钟时间输入%resume,并让应用程序启动其主进程;或者,直接使用--no-pause选项启动Frida,这样就不会中断应用程序了,并将生成的进程的任务留给Frida。 无论使用哪种方法,你都会得到一个shell(不会被杀死),这样就可以使用它的Javascript API向Frida写命令了。通过TAB可以查看可用的命令。此外,这个shell还支持命令自动完成功能。 它提供了非常详尽的文档说明。对于Android,请检查JavaScript-API的Java部分(这里将讨论一个“Java API”,虽然从技术上说应该是一个访问Java对象的Javascript包装器)。在下面,我们将重点介绍这个Java API,因为在跟Android应用程序打交道的时候,这是一种更加方便的方法。不同于挂钩libc函数,实际上我们可以直接使用Java函数和对象。 作为使用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.HttpEntityEnclosingRequestorg.apache.http.ProtocolVersionorg.apache.http.HttpResponseorg.apache.http.impl.cookie.DateParseExceptionorg.apache.http.HeaderIterator 我们在这里输入了一个比较长的命令,确切地说是一些嵌套的函数代码。首先,请注意,我们输入的代码必须包装在Java.perform(function(){...})中,这是Fridas的Java API的硬性要求。 下面是我们在Java.perform包装器中插入的函数体: Java.enumerateLoadedClasses( { "onMatch": function(className){ console.log(className) }, "onComplete":function(){} }) 上面的代码非常简单:我们使用Fridas API的Java.enumerateLoadedClasses枚举所有加载的类,并使用console.log将匹配的类输出到控制台。这种回调对象在Frida中是一种非常常见的模式。你可以提供一个回调对象,形式如下所示 { "onMatch":function(arg1, ...){ ... }, "onComplete":function(){ ... },} 当Frida找到符合要求的匹配项时,就会使用一个或多个参数来调用onMatch;当Frida完成匹配工作时,就会调用onComplete。 现在,让我们进一步学习Frida的magic,并通过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实现,所以应用程序依然可以继续正常运行。 打开您的模拟器和Chrome,然后通过-l选项来注入这个脚本: frida -U -l chrome.js com.android.chrome 一旦触发了onResume——例如切换到另一个应用程序并返回到模拟器中的Chrome——您将收到下列输出: [*] onResume() got called! 很好,不是吗?我们实际上覆盖了应用程序中的一个函数。这就给控制目标应用程序的行为提供了可能性。但是,实际上我们可以继续发挥:还能够利用Javaschoose查找堆中已经实例化的对象。 需要注意的是,当你的模拟速度较慢的时候,Frida经常会超时。为了防止这种情况,请将脚本封装到函数setImmediate中,或将它们导出为rpc。RPC在Frida默认情况下不超时(感谢@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 我们在堆上找到了4个android.view.View对象的实例。让我们看看能用这些搞点什么事情。首先,我们可以调用这些实例的对象方法。这里,我们只是为console.log输出添加instance.toString()。由于我们使用了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进一步提升自己工作的自动化程度的话,你应该学习应用性更高的Python、C或NodeJS绑定,当然,前提是你已经熟悉了Frida的工作原理。例如,要从Python注入chrome.js脚本的话,可以使用Frida的Python绑定。首先,创建一个chrome.py脚本: #!/usr/bin/python importfrida #putyourjavascript-codehere jscode=""" console.log("[*]Startingscript"); Java.perform(function(){ varActivity=Java.use("android.app.Activity"); Activity.onResume.implementation=function(){ console.log("[*]onResume()gotcalled!"); this.onResume(); }; }); """ #startupfridaandattachtocom.android.chromeprocessonausbdevice session=frida.get_usb_device().attach("com.android.chrome") #createascriptforfridaofjsccode script=process.create_script(jscode) #andloadthescript script.load() 更多的例子,请参考Frida的文档。 Frida和Radare2:r2frida 如果我们还可以使用类似Radare2之类的反汇编框架来检查应用程序的内存的话,那不是更好吗?别急,我们有r2frida。您可以使用r2frida将Radare2连接到Frida,然后对进程的内存进行静态分析和反汇编处理。不过,我们这里不会对r2frida进行详细的介绍,因为我们假设您已经了解了Radare2的相关知识(如果您对它还比较陌生的话,建议您抽时间学习一下,我认为这是非常值得的)。无论如何,您都没有必要过于担心,因为这个软件的用法非常容易上手,看看下面的例子您就知道此言不虚。 您可以使用Radare2的数据包管理程序来安装r2frida(假设您已经安装了Radare2): r2pm install r2frida 回到我们的frida-trace示例,删除或重命名我们修改的脚本,让frida-trace再次生成默认的脚本,并重新查看日志: michael@sixtyseven:~$frida-trace-iopen-U-fcom.android.chrome Instrumentingfunctions... open:Loadedhandlerat"/home/michael/__handlers__/libc.so/open.js" Startedtracing1function.PressCtrl+Ctostop. /*TID0x2740*/ 282msopen(pathname=0xa843ffc9,flags=0x80002) /*TID0x2755*/ [...] 使用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 0123456789ABCDEF0xa843ffc9 2f64 6576 2f62 696e 6465 7200 4269 6e64 /dev/binder.Bind0xa843ffd9 6572 2069 6f63 746c 2074 6f20 6f62 7461 er ioctl to obta0xa843ffe9 696e 2076 6572 7369 6f6e 2066 6169 6c65 in version faile0xa843fff9 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[...] 小结 在这篇文章中,我们重点介绍Frida在Android应用方面的应用。在本教程的第二篇中,我们将介绍如何通过Frida轻松搞定crackme。

优秀的个人博客,低调大师

利用FRIDA攻击Android应用程序(二)

在本系列文章的第一篇中,我们已经对Frida的原理进行了详细的介绍,现在,我们将演示如何通过Frida搞定crackme问题。有了第一篇的内容作为基础,理论上讲这应该不是什么难事。如果你想亲自动手完成本文介绍的实验的话,请下载 OWASP Uncrackable Crackme Level 1(APK) BytecodeViewer dex2jar 当然,这里假定您已在计算机上成功地安装了Frida(版本9.1.16或更高版本),并在(已经获得root权限的)设备上启动了相应服务器的二进制代码。我们这里将在模拟器中使用Android 7.1.1 ARM映像。 然后,请在您的设备上安装Uncrackable Crackme Level 1应用程序: adb install sg.vantagepoint.uncrackable1.apk 安装完成后,从模拟器的菜单(右下角的橙色图标)启动它: 一旦启动应用程序,您就会注意到它不太乐意在已经获取root权限的设备上运行: 如果单击“OK”,应用程序会立即退出。嗯,不太友好啊。看起来我们无法通过这种方法来搞定crackme。真是这样吗?让我们看看到底怎么回事,同时考察一下这个应用程序的内部运行机制。 现在,使用dex2jar将apk转换为jar文件: michael@sixtyseven:/opt/dex2jar/dex2jar-2.0$ ./d2j-dex2jar.sh -o /home/michael/UnCrackable-Level1.jar /home/michael/UnCrackable-Level1.apkdex2jar /home/michael/UnCrackable-Level1.apk -> /home/michael/UnCrackable-Level1.jar 然后,将其加载到BytecodeViewer(或其他支持Java的反汇编器)中。你也可以尝试直接加载到BytecodeViewer中,或直接提取classes.dex,但是试了一下好像此路不通,所以我才提前使用dex2jar完成相应的转换。 为了使用CFR解码器,需要在BytecodeViewer中依次选择View-> Pane1-> CFR-> Java。如果你想将反编译器的结果与Smali反汇编(通常比反编译稍微准确一些)进行比较的话,可以将Pane2设置为Smali代码。 下面是CFR解码器针对应用程序的MainActivity的输出结果: package sg.vantagepoint.uncrackable1;import android.app.Activity;import android.app.AlertDialog;import android.content.Context;import android.content.DialogInterface;import android.os.Bundle;import android.text.Editable;import android.view.View;import android.widget.EditText;import sg.vantagepoint.uncrackable1.a;import sg.vantagepoint.uncrackable1.b;import sg.vantagepoint.uncrackable1.c;public class MainActivityextends Activity { private void a(String string) { AlertDialog alertDialog = new AlertDialog.Builder((Context)this).create(); alertDialog.setTitle((CharSequence)string); alertDialog.setMessage((CharSequence)"This in unacceptable. The app is now going to exit."); alertDialog.setButton(-3, (CharSequence)"OK", (DialogInterface.OnClickListener)new b(this)); alertDialog.show(); } protected void onCreate(Bundle bundle) { if (sg.vantagepoint.a.c.a() || sg.vantagepoint.a.c.b() || sg.vantagepoint.a.c.c()) { this.a("Root detected!"); //This is the message we are looking for } if (sg.vantagepoint.a.b.a((Context)this.getApplicationContext())) { this.a("App is debuggable!"); } super.onCreate(bundle); this.setContentView(2130903040); } public void verify(View object) { object = ((EditText)this.findViewById(2131230720)).getText().toString(); AlertDialog alertDialog = new AlertDialog.Builder((Context)this).create(); if (a.a((String)object)) { alertDialog.setTitle((CharSequence)"Success!"); alertDialog.setMessage((CharSequence)"This is the correct secret."); } else { alertDialog.setTitle((CharSequence)"Nope..."); alertDialog.setMessage((CharSequence)"That's not it. Try again."); } alertDialog.setButton(-3, (CharSequence)"OK", (DialogInterface.OnClickListener)new c(this)); alertDialog.show(); }} if (sg.vantagepoint.a.c.a() || sg.vantagepoint.a.c.b() || sg.vantagepoint.a.c.c())通过查看其他反编译的类文件,我们发现它是一个小应用程序,并且貌似可以通过逆向解密例程和字符串修改例程来解决这个crackme问题。然而,既然有神器Frida在手,自然会有更方便的手段可供我们选择。首先,让我们看看这个应用程序是在哪里检查设备是否已获取root权限的。在“Root detected”消息上面,我们可以看到: if (sg.vantagepoint.a.c.a() || sg.vantagepoint.a.c.b() || sg.vantagepoint.a.c.c()) 如果你查看sg.vantagepoint.a.c类的话,你就会发现与root权限有关的各种检查: public static boolean a() { String[] a = System.getenv("PATH").split(":"); int i = a.length; int i0 = 0; while(true) { boolean b = false; if (i0 >= i) { b = false; } else { if (!new java.io.File(a[i0], "su").exists()) { i0 = i0 + 1; continue; } b = true; } return b; } } public static boolean b() { String s = android.os.Build.TAGS; if (s != null && s.contains((CharSequence)(Object)"test-keys")) { return true; } return false; } public static boolean c() { String[] a = new String[7]; a[0] = "/system/app/Superuser.apk"; a[1] = "/system/xbin/daemonsu"; a[2] = "/system/etc/init.d/99SuperSUDaemon"; a[3] = "/system/bin/.ext/.su"; a[4] = "/system/etc/.has_su_daemon"; a[5] = "/system/etc/.installed_su_daemon"; a[6] = "/dev/com.koushikdutta.superuser.daemon/"; int i = a.length; int i0 = 0; while(i0 < i) { if (new java.io.File(a[i0]).exists()) { return true; } i0 = i0 + 1; } return false; } 在Frida的帮助下,我们可以通过覆盖它们使所有这些方法全部返回false,这一点我们已经在第一篇中介绍过了。但是,当一个函数由于检测到设备已经取得了root权限而返回true时,结果会怎样呢? 正如我们在MainActivity函数中看到的那样,它会打开一个对话框。此外,它还会设置一个onClickListener,当我们按下OK按钮时就会触发它: alertDialog.setButton(-3, (CharSequence)"OK", (DialogInterface.OnClickListener)new b(this)); 这个onClickListener的实现代码如下所示: package sg.vantagepoint.uncrackable1;class b implements android.content.DialogInterface$OnClickListener { final sg.vantagepoint.uncrackable1.MainActivity a; b(sg.vantagepoint.uncrackable1.MainActivity a0) { this.a = a0; super(); } public void onClick(android.content.DialogInterface a0, int i) { System.exit(0); }} 它的功能并不复杂,实际上只是通过System.exit(0)退出应用程序而已。所以我们要做的事情就是防止应用程序退出。为此,我们可以用Frida覆盖onClick方法。下面,让我们创建一个文件uncrackable1.js,并把我们的代码放入其中: setImmediate(function() { //prevent timeout console.log("[*] Starting script"); Java.perform(function() { bClass = Java.use("sg.vantagepoint.uncrackable1.b"); bClass.onClick.implementation = function(v) { console.log("[*] onClick called"); } console.log("[*] onClick handler modified") })}) 如果你已经阅读了本系列文章的第一篇的话,这个脚本应该不难理解:将我们的代码封装到setImmediate函数中,以防止超时,然后通过Java.perform来使用Frida用于处理Java的方法。接下来,我们将得到一个类的包装器,可用于实现OnClickListener接口并覆盖其onClick方法。在我们的版本中,这个函数只是向控制台写一些输出。与之前不同的是,它不会退出应用程序。由于原来的onClickHandler被替换为Frida注入的函数,因此它绝对不会被调用了,所以当我们点击对话框的OK按钮时,应用程序就不退出了。好了,让我们实验一下:打开应用程序(使其显示“Root detected”对话框) 并注入脚本: frida -U -l uncrackable1.js sg.vantagepoint.uncrackable1 Frida注入代码需要几秒钟的时间,当你看到“onClick handler modified”消息时说明注入完成了(当然,注入完成时你也可以得到一个shell之前,因为可以把我们的代码放入一个setImmediate包装器中,从而让Frida在后台执行它)。 然后,点击应用程序中的OK按钮。如果一切顺利的话,应用程序就不会退出了。 我们看到对话框消失了,这样我们就可以输入密码了。下面让我们输入一些内容,点击Verify,看看会发生什么情况: 不出所料,这是一个错误的密码。但是这并不要紧,因为我们真正要找的是:加密/解密例程以及结果和输入的比对。 再次检查MainActivity时,我们注意到了下面的函数 public void verify(View object) { 它调用了类sg.vantagepoint.uncrackable1.a的方法: if (a.a((String)object)) { 下面是sg.vantagepoint.uncrackable1.a类的反编译结果: package sg.vantagepoint.uncrackable1;import android.util.Base64;import android.util.Log;/** Exception performing whole class analysis ignored.*/public class a { public static boolean a(String string) { byte[] arrby = Base64.decode((String)"5UJiFctbmgbDoLXmpL12mkno8HT4Lv8dlat8FxR2GOc=", (int)0); byte[] arrby2 = new byte[]{}; try { arrby2 = arrby = sg.vantagepoint.a.a.a((byte[])a.b((String)"8d127684cbc37c17616d806cf50473cc"), (byte[])arrby); } catch (Exception var2_2) { Log.d((String)"CodeCheck", (String)("AES error:" + var2_2.getMessage())); } if (!string.equals(new String(arrby2))) return false; return true; } public static byte[] b(String string) { int n = string.length(); byte[] arrby = new byte[n / 2]; int n2 = 0; while (n2 < n) { arrby[n2 / 2] = (byte)((Character.digit(string.charAt(n2), 16) << 4) + Character.digit(string.charAt(n2 + 1), 16)); n2 += 2; } return arrby; }} 注意在a方法末尾的string.equals比较,以及在上面的try代码块中字符串arrby2的创建。arrby2是函数sg.vantagepoint.a.a.a的返回值。string.equals会将我们的输入与arrby2进行比较。所以,我们要追踪sg.vantagepoint.a.a的返回值。 现在,我们可以着手对这些字符串操作函数和解密函数进行逆向工程,并处理原始加密字符串了,实际上它们也包含在上面的代码中。或者,我们还可以让应用程序替我们完成字符串的处理和加密工作,而我们只要钩住sg.vantagepoint.a.a.a函数来捕获其返回值就可以坐享其成了。返回值是我们的输入将要与之比较的解密字符串(它以字节数组的形式返回)。具体可以参考下面的脚本: aaClass = Java.use("sg.vantagepoint.a.a"); aaClass.a.implementation = function(arg1, arg2) { retval = this.a(arg1, arg2); password = '' for(i = 0; i < retval.length; i++) { password += String.fromCharCode(retval[i]); } console.log("[*] Decrypted: " + password); return retval; } console.log("[*] sg.vantagepoint.a.a.a modified"); 其中,我们覆盖了sg.vantagepoint.a.a.a函数,截获其返回值并将其转换为可读字符串。这正是我们要找的解密字符串,所以我们将其打印到控制台。 将上述代码放到一起,就组成了一个完整的脚本: setImmediate(function() { console.log("[*] Starting script"); Java.perform(function() { bClass = Java.use("sg.vantagepoint.uncrackable1.b"); bClass.onClick.implementation = function(v) { console.log("[*] onClick called."); } console.log("[*] onClick handler modified") aaClass = Java.use("sg.vantagepoint.a.a"); aaClass.a.implementation = function(arg1, arg2) { retval = this.a(arg1, arg2); password = '' for(i = 0; i < retval.length; i++) { password += String.fromCharCode(retval[i]); } console.log("[*] Decrypted: " + password); return retval; } console.log("[*] sg.vantagepoint.a.a.a modified"); });}); 现在,我们来运行这个脚本。然后,将其保存为uncrackable1.js,并执行下列命令(如果Frida没有自动重新运行的话) frida -U -l uncrackable1.js sg.vantagepoint.uncrackable1 耐心等待,直到您看到消息sg.vantagepoint.a.a发生变化,然后在Root detected对话框中单击OK,在secret code中输入一些字符,然后按Verify按钮。哎,运气好像不太好啊。 但是,请注意Frida的输出: michael@sixtyseven:~/Development/frida$ frida -U -l uncrackable1.js sg.vantagepoint.uncrackable1 ____ / _ | Frida 9.1.16 - A world-class dynamic instrumentation framework | (_| | > _ | Commands: /_/ |_| help -> Displays the help system . . . . object? -> Display information about 'object' . . . . exit/quit -> Exit . . . . . . . . More info at http://www.frida.re/docs/home/[*] Starting script[USB::Android Emulator 5554::sg.vantagepoint.uncrackable1]-> [*] onClick handler modified[*] sg.vantagepoint.a.a.a modified[*] onClick called.[*] Decrypted: I want to believe 太好了。我们实际上已经得到了解密的字符串:I want to believe。那么,我们赶紧输入这个字符串,看看是否正确: 本文到此结束,但愿读者阅读本文后,能够对学习Frida的动态二进制插桩功能有所帮助。

优秀的个人博客,低调大师

供应链逆向物流系统 | 逆向物流数据分析:退货率归因分析与改进建议

当逆向物流系统跑起来之后,数据开始沉淀——退货原因、质检分级、维修记录、再销售价格、报废处置……这些散落在各个环节的数据,如果只用来"事后查账",那就太浪费了。真正的数据价值在于向前反哺业务:为什么这个月的退货率突然飙升?是某个供应商的批次问题,还是某个运营活动的描述误导了消费者?哪类商品的维修翻新后仍然复检不通过?哪些 SKU 从退货到再销售的全程成本已经超过了商品本身的价值——也就是说,退回来修好再卖,其实是在亏钱?

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册