用Frida入侵Android App III – OWASP UNCRACKABLE 2
用Frida入侵Android App III – OWASP UNCRACKABLE 2
在我发完第二篇关于Frida的博文之后,@muellerberndt 立即就决定公布另一个OWASP Android crackme。我想试试看我是否依然可以用Frida来解决这个问题。如果你也想跟着我一起,那么你需要:
Android SDK 和 模拟器 (我用的是Android 7.1 x64镜像)
radare2 (或者你自己选择用其他的反编译器)
如果你需要Frida的安装教程,请查看Frida的官方文档. 至于Frida的使用,请查看这个教程的 第一部分 。现在我就当你已经准备好所有东西,在继续往前走之前,你还需要稍微熟悉Frida的使用,还有,确保Frida可以连接你的设备/模拟器。(比如,通过使用frida-ps -U 命令)。
说在前面的话:这不仅仅是一篇解决那个crackme的攻略。相反的,我打算向你展示几种不同的方法来解决这个具体的问题。如果你只是想看解决方法,可以直接翻到本教程的最后面,那里有Frida脚本。
注意:如果你在使用Frida时收到下面的错误
1 | Erro: access violation accessing 0xebad8082 |
或者其他类似的错误,它可能会清除模拟器上所有的用户数据,所以你要重启然后重新安装apk。
做好得多试好几次的思想准备,程序会崩溃,模拟器会需要重启,一切都可能变得很乱七八糟的,但最终,我们会成功。
第一次运行
和在UnCrackable 1做的一样,我们第一步是运行那个app。也和它UnCrackable 1 一样,当我们在模拟器上运行这个app时,它会被检测到说是在已经root过的设备上运行的。
我们会像在UnCrackable 1那里一样,钩住 OnClickListener函数。但在这之前,我们得看看我们是否已经连上Frida以便我们修改。
1 2 3 4 5 6 7 8 9 10 11 | michael@sixtyseven:~ /Development $ frida -U sg.vantagepoint.uncrackable2 ____ / _ | Frida 9.1.27 - 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/ Failed to attach: ambiguous name; it matches: sg.vantagepoint.uncrackable2 (pid: 5184), sg.vantagepoint.uncrackable2 (pid: 5201) |
这是啥?有两个同名进程。我们可以 frida-ps -U
来验证:
1 2 | 5184 s.vantagepoin.uncrackable2 5201 s.vantagepoin.uncrackable2 |
好奇怪,让我们把Frida注入到父进程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | michael@sixtyseven:~ /Development $ frida -U 5184 ____ / _ | Frida9..2 - A world class dynamic instrumentation framewor | (_| | > _ | Commands: /_ |_| help -> Displays the help system . . . . object? -> Display information about 'object' . . . . exit /quit -> Exit . . . . . . . . More info at http: // www.frida.r /docs/homeFailed to attach: unable to access process with pid 518due to system restrictions; try' sudo sysctl kernel.yama.ptrace_scope=`,or run Frida as root |
没用。因为我们是在以root运行Frida时得到这样的结果的,所以这个方案没什么效果。这是怎么了?我们得好好研究这个app。解压apk,用字节码查看器(例如CFR-Decompiler)反编译classes.dex
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | package sg.vantagepoint.uncrackable2; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.os.AsyncTask; import android.os.Bundle; import android.support.v7.app.c; import android.text.Editable; import android.view.View; import android.widget.EditText; import sg.vantagepoint.a.a; import sg.vantagepoint.a.b; import sg.vantagepoint.uncrackable2.CodeCheck; import sg.vantagepoint.uncrackable2.MainActivity; public class MainActivity extends c { private CodeCheck m; static { System.loadLibrary( "foo" ); //[1] } 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 /* Unavailable Anonymous Inner Class!! */); alertDialog.setCancelable(false); alertDialog.show(); } static /* synthetic */ void a(MainActivity mainActivity, String string) { mainActivity.a(string); } private native void init(); //[2] protected void onCreate(Bundle bundle) { this.init(); //[3] if (b.a() || b.b() || b.c()) { this.a("Root detected!"); } if (a.a((Context)this.getApplicationContext())) { this.a("App is debuggable!"); } new /* Unavailable Anonymous Inner Class!! */.execute((Object[])new Void[]{null, null, null}); this.m = new CodeCheck(); super.onCreate(bundle); this.setContentView(2130968603); } public void verify(View view) { String string = ((EditText)this.findViewById(2131427422)).getText().toString(); AlertDialog alertDialog = new AlertDialog.Builder((Context)this).create(); if (this.m.a(string)) { 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 /* Unavailable Anonymous Inner Class!! */ ); alertDialog.show(); } } |
我们注意到static块里调用了System.load
来加载foo库(参看【1】)。这个app还在OnCreate函数里的第一行就调用了this.init(),而这个函数被声明为native函数(参看【2】),所以它应该是foo的一部分。
让我们来看看这个foo库。在radare2中打开这个库(你会在lib文件夹里看到几个不同的架构,我这里用的是lib/x86_64 ),分析它并列出它的输出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | michael@sixtyseven:~ /Development/UnCrackable2/lib/x86_64 $ r2 libfoo.so -- Don 't look at the code. Don' t look . [0x000007a0]> aaa [x] Analyze all flags starting with sym. and entry0 (aa) [x] Analyze len bytes of instructions for references (aar) [x] Analyze function calls (aac) [ ] [*] Use -AA or aaaa to perform additional experimental analysis. [x] Constructing a function name for fcn.* and sym.func.* functions (aan)) [0x000007a0]> iE [Exports] vaddr=0x00001060 paddr=0x00001060 ord=004 fwd=NONE sz=183 bind=GLOBAL type =FUNC name=Java_sg_vantagepoint_uncrackable2_CodeCheck_bar vaddr=0x00001050 paddr=0x00001050 ord=006 fwd=NONE sz=15 bind=GLOBAL type =FUNC name=Java_sg_vantagepoint_uncrackable2_MainActivity_init vaddr=0x00004008 paddr=0x00003008 ord=014 fwd=NONE sz=0 bind=GLOBAL type =NOTYPE name=__bss_start vaddr=0x00004008 paddr=0x00003008 ord=015 fwd=NONE sz=0 bind=GLOBAL type =NOTYPE name=__bss_start vaddr=0x0000400d paddr=0x0000400d ord=016 fwd=NONE sz=0 bind=GLOBAL type =NOTYPE name=_end 5 exports [0x000007a0]> |
我们看到这个库导出了两个很有意思的函数:Java_sg_vantagepoint_uncrackable2_MainActivity_init
和 Java_sg_vantagepoint_uncrackable2_CodeCheck_bar
(关于这些函数的命名,请查看 Java nativ interface JNI ),我们要看的是:
Java_sg_vantagepoint_uncrackable2_MainActivity_init
1 2 | [0x000007a0]> s 0x00001050 [0x00001050]> V |
这是一个蛮短的函数:
1 2 3 4 5 6 7 8 9 | [0x00001050 29% 848 libfoo.so]> pd $r @ sym.Java_sg_vantagepoint_uncrackable2_MainActivity_init / (fcn) sym.Java_sg_vantagepoint_uncrackable2_MainActivity_init 15 | sym.Java_sg_vantagepoint_uncrackable2_MainActivity_init (); | 0x00001050 50 push rax | 0x00001051 e8caf7ffff call sub.fork_820 ;[1] | 0x00001056 c605af2f0000. mov byte [0x0000400c], 1 ; [0x400c:1]=58 ; ": (GNU) 4.9.x 20150123 (prerelease)" | 0x0000105d 58 pop rax \ 0x0000105e c3 ret 0x0000105f 90 nop |
它调用了另一个函数sub.fork_820
,这个要做的事就比较多了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | [0x00000820 14% 265 libfoo.so]> pd $r @ sub.fork_820 / (fcn) sub.fork_820 242 | sub.fork_820 (); | ; var int local_8h @ rsp+0x8 | ; var int local_10h @ rsp+0x10 | ; CALL XREF from 0x00001051 (sym.Java_sg_vantagepoint_uncrackable2_MainActivity_init) | 0x00000820 4156 push r14 | 0x00000822 53 push rbx | 0x00000823 4883ec18 sub rsp, 0x18 | 0x00000827 64488b042528. mov rax, qword fs:[0x28] ; [0x28:8]=0x3180 ; '(' | 0x00000830 4889442410 mov qword [local_10h], rax | 0x00000835 e806ffffff call sym.imp.fork ;[1] | 0x0000083a 8905c8370000 mov dword loc.__bss_start, eax ; [0x4008:4]=0x43434700 ; loc.__bss_start | 0x00000840 85c0 test eax, eax | ,=< 0x00000842 741a je 0x85e ;[2] | | 0x00000844 488d15a5ffff. lea rdx, 0x000007f0 ; 0x7f0 | | 0x0000084b 488d7c2408 lea rdi, [local_8h] ; 0x8 | | 0x00000850 31f6 xor esi, esi | | 0x00000852 31c9 xor ecx, ecx | | 0x00000854 e8f7feffff call sym.imp.pthread_create ;[3]; ssize_t read (int fildes, void *buf, size_t nbyte) | ,==< 0x00000859 e990000000 jmp 0x8ee ;[4] | || ; JMP XREF from 0x00000842 (sub.fork_820) | |`-> 0x0000085e e8fdfeffff call sym.imp.getppid ;[5] | | 0x00000863 89c3 mov ebx, eax | | 0x00000865 bf10000000 mov edi, 0x10 | | 0x0000086a 31d2 xor edx, edx | | 0x0000086c 31c9 xor ecx, ecx | | 0x0000086e 31c0 xor eax, eax | | 0x00000870 89de mov esi, ebx | | 0x00000872 e8f9feffff call sym.imp.ptrace ;[6] | | 0x00000877 4885c0 test rax, rax | |,=< 0x0000087a 7572 jne 0x8ee ;[4] | || 0x0000087c 4c8d742408 lea r14, [local_8h] ; 0x8 | || 0x00000881 31d2 xor edx, edx | || 0x00000883 89df mov edi, ebx | || 0x00000885 4c89f6 mov rsi, r14 | || 0x00000888 e883feffff call sym.imp.waitpid ;[7] |
我们看到调用了fork,pthread_create
,
getppid
,ptrace
和waitpid
. 无需花太多时间来反编译我们就可以猜到,当调试器用ptrace的时候,主进程会fork一个子进程来关联它。这是很简单的反调试技术,你可以从这里了解到更多细节。
因为Frida用ptrace来初始化注入,所以这就解释了为什么我们不能连接到父进程:因为已经连接了一个进程来作为调试器,再来一个进程关联调试将被阻塞。
反反调试方案1:Frida
Frida救援。相比于注入Frida到一个正在运行的进程,我们可以让它自己spawn出一个进程来给我们。用-f选项,我们告诉Frida注入Zygote然后启动该应用程序。在我们启动Frida后关掉这个应用程序,看看发生什么:
1 | frida -U -f sg.vantagepoint.uncrackable2 |
我们得到:
1 2 3 4 5 6 7 8 9 10 11 12 | michael@sixtyseven:~ /Development/UnCrackable2/lib/x86_64 $ frida -U -f sg.vantagepoint.uncrackable2 --no-pause ____ / _ | Frida 9.1.27 - 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/ Spawned `sg.vantagepoint.uncrackable2`. Resuming main thread! [USB::Android Emulator 5554::[ 'sg.vantagepoint.uncrackable2' ]]-> |
呼啦啦!Frida注入到Zygote了,spawn 我们的进程并等待输入。(我承认,有很多教程都告诉你们要在Frida加-f选项,但你也被警告过……)
我们现在已经做好准备了。但是在继续往下走之前,我们再来看另一种针对这个crackme的反反调试方案。
反反调试方案2:修改
除去让Frida来spawn,我们也可以通过修改这个app来解决这个问题。这意味着,我们要反编译这个app,重新打包和签名修改过的apk。然而,在这个crackme中,这样做会在后面给我们带来麻烦。就算这样,我还是决定告诉你怎样做,后面的问题后面解决。
我们可以用apktool来修改:
1 2 3 4 5 6 7 | michael@sixtyseven:~ /Disassembly $ /opt/apktool/apktool .sh -r d UnCrackable-Level2.apk I: Using Apktool 2.2.0 on UnCrackable-Level2.apk I: Copying raw resources... I: Baksmaling classes.dex... I: Copying assets and libs... I: Copying unknown files... I: Copying original files... |
(我用-r来跳过了提取资源,因为这会在重新编译apk时出错。而且,在这里我们也不需要这些资源。)
来看看在smali/sg/vantagepoint/uncrackable2/MainActivity.smali
的smali代码。你可以看到调用inti的操作是在82行附近,你可以把它注释掉。
1 2 3 4 5 6 | # virtual methods .method protected onCreate(Landroid /os/Bundle ;)V .locals 4 const /4 v3, 0x0 # invoke-direct {p0}, Lsg/vantagepoint/uncrackable2/MainActivity;->init()V invoke-static {}, Lsg /vantagepoint/a/b ;->a()Z |
重新打包(忽略叼那个fatal error……):
1 2 3 4 5 6 7 8 9 10 | michael@sixtyseven:~ /Disassembly/UnCrackable-Level2 $ /opt/apktool/apktool .sh b I: Using Apktool 2.2.0 I: Checking whether sources has changed... I: Smaling smali folder into classes.dex... [Fatal Error] AndroidManifest.xml:1:1: Content ist nicht zulässig in Prolog. I: Checking whether resources has changed... I: Copying raw resources... I: Copying libs... ( /lib ) I: Building apk file ... I: Copying unknown files /dir ... |
align(优化):
1 2 3 4 5 6 7 | michael@sixtyseven:~ /Disassembly/UnCrackable-Level2 $ zipalign - v 4 dist /UnCrackable-Level2 .apk UnCrackable2.recompiled.aligned.apk Verifying alignment of UnCrackable2.recompiled.aligned.apk (4)... 49 AndroidManifest.xml (OK - compressed) 914 classes.dex (OK - compressed) 269899 lib /arm64-v8a/libfoo .so (OK - compressed) 273297 lib /armeabi-v7a/libfoo .so (OK - compressed) 279346 lib /armeabi/libfoo .so (OK - compressed) |
签名(注意:在这一步你要有一个key和keystore,你可以在 OWASP 手机安全测试指南 看到更多介绍。):
1 2 3 4 5 6 7 8 9 10 11 12 | michael@sixtyseven:~ /Disassembly/UnCrackable-Level2 $ jarsigner -verbose -keystore ~/.android /debug .keystore UnCrackable2.recompiled.aligned.apk signkey Enter Passphrase for keystore: adding: META-INF /MANIFEST .MF adding: META-INF /SIGNKEY .SF adding: META-INF /SIGNKEY .RSA signing: AndroidManifest.xml signing: classes.dex signing: lib /arm64-v8a/libfoo .so signing: lib /armeabi-v7a/libfoo .so signing: lib /armeabi/libfoo .so signing: lib /mips/libfoo .so [...] |
卸载原来的apk,安装这个修改过的apk:
1 2 | adb uninstall sg.vantagepoint.uncrackable2 adb install UnCrackable2.recompiled.aligned.apk |
启动这个app,运行frida-ps我们会看到只有一个进程了:
1 | 29996 sg.vantagepoint.uncrackable2 |
然后连接Frida也没有问题:
1 2 3 4 5 6 7 8 9 10 11 12 | michael@sixtyseven:~ /Disassembly/UnCrackable-Level2 $ frida -U sg.vantagepoint.uncrackable2 ____ / _ | Frida 9.1.27 - 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/ [USB::Android Emulator 5554::sg.vantagepoint.uncrackable2]-> |
相比于只是在Frida里加-r选项是比较麻烦啦,但这也更普遍。
就像前面提过的,如果我们使用这个修改过的版本,那后面要提取那个Secret String就不会那么容易(尽管如此,我还是会告诉你怎样解决的,所以不要放弃哦)。但是现在我们要用的是原来的版本来进行后面的操作。确保你下面安装的是原来的版本。
继续解决难题
在我们找到摆脱反调试的可能性后,我们来看看要怎么处理。这个app会做一个root检测,当我们在模拟器上运行的时候,只要我们按下OK按钮,就会退出。我们已经从UnCrackable1 看到过这样的情况。同样,我们可以修改这个行为,删掉对System.exit的调用。但这次我们打算用Frida来解决。查看反编译后的代码,我们可以看到并没有OnClickListener类,只有一个匿名的内部类。因为OnClickListener实现System.exit的调用,我们可以简单的hook这个函数,然后让它失效。
这是做这些操作的Frida脚本:
1 2 3 4 5 6 7 8 9 10 | setImmediate(function() { console.log( "[*] Starting script" ); Java.perform(function() { exitClass = Java.use( "java.lang.System" ); exitClass.exit.implementation = function() { console.log( "[*] System.exit called" ); } console.log( "[*] Hooking calls to System.exit" ); }); }); |
再次关掉UnCrackable 2,然后用Frida来打开它:
1 | frida -U -f sg.vantagepoint.uncrackable2 -l uncrackable2.js --no-pause |
等,直到App启动并且Frida在控制台显示Hooking calls……信息。然后按下“OK”。你会得到类似下面的信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 | michael@sixtyseven:~ /Development/frida $ frida -U -f sg.vantagepoint.uncrackable2 --no-pause -l uncrackable2.js ____ / _ | Frida 9.1.27 - 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/ Spawned `sg.vantagepoint.uncrackable2`. Resuming main thread! [USB::Android Emulator 5554::[ 'sg.vantagepoint.uncrackable2' ]]-> [*] Hooking calls to System. exit [*] System. exit called |
这样,这个app就不会被退出来了。我们可以输入一个secret string:
但我们在这输入了什么?来看MainActivity里的Android代码是如何检查正确的输入的:
1 2 3 4 5 6 7 | this .m = new CodeCheck(); [...] //in method: public void verify if ( this .m.a(string)) { alertDialog.setTitle((CharSequence) "Success!" ); alertDialog.setMessage((CharSequence) "This is the correct secret." ); } |
用到了CodeCheck类:
1 2 3 4 5 6 7 | package sg.vantagepoint.uncrackable2; public class CodeCheck { private native boolean bar( byte [] var1); public boolean a(String string) { return this .bar(string.getBytes()); //Call to a native function } } |
我们可以看到我们在文本框输入的信息—我们的“secret string”会被传送到一个名为bar的native函数中。我们在libfoo.so库中再次找到这个函数。查找这个函数的地址(像我们之前找init函数那样),然后用radare2来反编译它:
仔细观察这些汇编代码,我们可以看到有一些字符串的比较操作,还看到一个很有意思的明文字符串 Thanks for all t. 我们在文本框里输入这个字符串发现并不是这个crackme的答案,所以我们还得继续。
查看在0x000010d8
的汇编代码,我们可以看到:
1 2 | 0x000010d8 83f817 cmp eax, 0x17 0x000010db 7519 jne 0x10f6 ;[1] |
所以,这里比较了eax和0x17,也就是十进制的23。如果比较不成功,就不会调用strncmp。我们也注意到在0x00010e1处,0x17作为strncmp的一个参数:
1 | 0x000010e1 ba17000000 mov edx, 0x17 |
要知道,按照64位linux的调用惯例,函数参数是放在——至少参数1到6——寄存器中的。尤其是前三个参数是按序放在RDI,RSI和RDX中(具体的可以看这里 [PDF], p. 20 )。strncmp的头部是这样的:
1 | int strncmp ( const char * str1, const char * str2, size_t num ); |
所以我们的strncmp函数会比较0x17=23个字符。我们可以推断出我们的secret string长度应该是23.
最后让我们尝试这去hook这个strncmp函数,输出它的参数。我们期望这样能给出解密后的字符串。我们要做的是:
找到strncmp在libfoo.so中的内存地址。
用Interceptor.attach来hook libfoo.so中的strncmp函数,并dump它的参数。
如果你这么做了,你会发现很多地方都有调用strncmp,所以我们要进一步限制输出。这是一段Frida代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | var strncmp = undefined; imports = Module.enumerateImportsSync( "libfoo.so" ); for (i = 0 ; i < imports.length; i++) { if (imports[i].name == "strncmp" ) { strncmp = imports[i].address; break ; } } Interceptor.attach(strncmp, { onEnter: function (args) { if (args[ 2 ].toInt32() == 23 && Memory.readUtf8String(args[ 0 ], 23 ) == "01234567890123456789012" ) { console.log( "[*] Secret string at " + args[ 1 ] + ": " + Memory.readUtf8String(args[ 1 ], 23 )); } } }); |
在这段代码中有几点需要注意的:
这段代码调用了
Module.enumerateImportsSync
来检索对象数组,这些对象中包含了从libfoo.so导入的信息(具体请看文档 )。我们迭代这个数组直至我们找到strncmp和它的地址。然后我们给它关联一个拦截器(Interceptor)。Java中的字符串不是以null来终止的。当我们用Frida的Memory.readUtf8String方法且不提供长度来读取strncmp内存中的字符串时,Frida会以为有\0来终止,不然就一直返回一些内存垃圾,因为它不知道字符串的终点在哪里。如果我们在第二个参数中明确给出要读取的字符串长度,我们就不会遇到这个问题。
如果我们不在判断条件那里作限制,限制我们要dump的strncmp参数,我们会看到很多输出。所以我们只在strncmp的第三个参数size_t 是23,且第一个参数指向我们的输入框的时候输出。在输入框中我们会输入01234567890123456789012 (这个字符串有23个字符)。
我是怎么知道args[0]指向我们的输入,args[1]指向那个secret string的?事实上,我并不知道。我只是测试,然后在满屏的输出中找到我的输入。如果你不想跳过这部分,你可以把上面代码中的if语句删掉,然后使用Frida的hexdump输出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | buf = Memory.readByteArray(args[ 0 ], 32 ); console.log(hexdump(buf, { offset: 0 , length: 32 , header: true , ansi: true })); buf = Memory.readByteArray(args[ 1 ], 32 ); console.log(hexdump(buf, { offset: 0 , length: 32 , header: true , ansi: true })); |
这样每次调用strncmp都会输出很多hexdump,要小心哦。
这是代码的完整版本,用这个版本输出那些参数会更直观一些:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | setImmediate(function() { Java.perform(function() { console.log( "[*] Hooking calls to System.exit" ); exitClass = Java.use( "java.lang.System" ); exitClass.exit.implementation = function() { console.log( "[*] System.exit called" ); } var strncmp = undefined; imports = Module.enumerateImportsSync( "libfoo.so" ); for (i = 0 ; i < imports.length; i++) { if (imports[i].name == "strncmp" ) { strncmp = imports[i].address; break ; } } Interceptor.attach(strncmp, { onEnter: function (args) { if (args[ 2 ].toInt32() == 23 && Memory.readUtf8String(args[ 0 ], 23 ) == "01234567890123456789012" ) { console.log( "[*] Secret string at " + args[ 1 ] + ": " + Memory.readUtf8String(args[ 1 ], 23 )); } }, }); console.log( "[*] Intercepting strncmp" ); }); }); |
现在,打开Frida然后加载这个脚本:
1 | frida -U -f sg.vantagepoint.uncrackable2 --no-pause -l uncrackable2.js |
输入字符串并按下verify:
在控制台,你将会看到:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | michael@sixtyseven:~ /Development/frida $ frida -U -f sg.vantagepoint.uncrackable2 --no-pause -l uncrackable2.js ____ / _ | Frida 9.1.27 - 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/ Spawned `sg.vantagepoint.uncrackable2`. Resuming main thread! [USB::Android Emulator 5554::[ 'sg.vantagepoint.uncrackable2' ]]-> [*] Hooking calls to System. exit [*] Intercepting strncmp [*] System. exit called [*] Secret string at 0x7fffa628f010: Thanks for all the fish |
很直观,我们可以看到secret string是Thanks for all the fish。把它填入输入框就能看到成功的消息啦。
解决修改的方案
最后,一些关于修改值得注意的事以及为什么我们不能用修改过的apk得到secret string。libfoo.so中的init函数包含一些初始化逻辑,这些逻辑会阻止我们去查看secret string。
如果我们再认真看看反编译后的init函数,我们会看到一行很有意思的代码:
1 | 0x00001056 c605af2f0000. mov byte [0x0000400c], 1 |
这个变量在后面libfoo.so的bar函数里也有用到,如果它未曾设置,代码就会跳过strncmp。
1 2 | 0x0000107d 803d882f0000. cmp byte [0x0000400c], 1 ; [0x1:1]=69 0x00001084 7570 jne 0x10f6 ;[1] |
所以在它之后应该是一些布尔变量记录init函数有没有运行。如果我们希望修改过的版本能够调用strncmp,我们应该设置这个变量,或者至少阻止它跳过strncmp调用。
现在我们要重新修改,反编译apk,重写jmp指令,然后重新编译。好麻烦。因为这是Frida教程,我们将会用Frida动态改变内存。
因此,我们需要:
获取已经加载好的foo库的基址。
找到那个变量相对于库基址的偏移量(我们从反编译后的代码中可以看到这个偏移量是0x400C位)
设置这个变了为1.
所以,在Frida这边:
1 2 3 4 5 6 | //Get base address of library var libfoo = Module.findBaseAddress( "libfoo.so" ); //Calculate address of variable var initialized = libfoo.add(ptr( "0x400C" )); //Write 1 to the variable Memory.writeInt(initialized,1); |
下面是针对这个apk修改后版本的完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | setImmediate(function() { Java.perform(function() { console.log( "[*] Hooking calls to System.exit" ); exitClass = Java.use( "java.lang.System" ); exitClass.exit.implementation = function() { console.log( "[*] System.exit called" ); } var strncmp = undefined; imports = Module.enumerateImportsSync( "libfoo.so" ); for (i = 0 ; i < imports.length; i++) { if (imports[i].name == "strncmp" ) { strncmp = imports[i].address; break ; } } //Get base address of library var libfoo = Module.findBaseAddress( "libfoo.so" ); //Calculate address of variable var initialized = libfoo.add(ptr( "0x400C" )); //Write 1 to the variable Memory.writeInt(initialized, 1 ); Interceptor.attach(strncmp, { onEnter: function (args) { if (args[ 2 ].toInt32() == 23 && Memory.readUtf8String(args[ 0 ], 23 ) == "01234567890123456789012" ) { console.log( "[*] Secret string at " + args[ 1 ] + ": " + Memory.readUtf8String(args[ 1 ], 23 )); } }, }); console.log( "[*] Intercepting strncmp" ); }); }); |
现在运行这个app,用Frida加载上面的脚本,然后再次输入01234567890123456789012
。按下Verify。app会调用strncmp,然后我们就能够看到那个secret string。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | root@sixtyseven: /home/michael/Development/frida # frida -U sg.vantagepoint.uncrackable2 -l uncrackable2-final.js ____ / _ | Frida 9.1.27 - 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/ [USB::Android Emulator 5554::sg.vantagepoint.uncrackable2]-> [*] Hooking calls to System. exit [*] Intercepting strncmp [*] System. exit called [*] Secret string at 0x7fffd52c6570: Thanks for all the fish |
愿你能从中获得乐趣。
评论、批评、建议等请移步 Twitter 。感谢阅读。
注:感谢 @oleavr 帮我指出一个bug已经告诉我在Frida中正确处理指针的方法。
原文链接:https://www.codemetrix.net/hacking-android-apps-with-frida-3/
本文由 看雪翻译小组 lumou 编译
扫描关注公众号