用Frida入侵Android App III – OWASP UNCRACKABLE 2

2020-12-23
1342
 

 

用Frida入侵Android App III – OWASP UNCRACKABLE 2

在我发完第二篇关于Frida的博文之后,@muellerberndt 立即就决定公布另一个OWASP  Android crackme。我想试试看我是否依然可以用Frida来解决这个问题。如果你也想跟着我一起,那么你需要:

如果你需要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 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'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]

我们看到调用了forkpthread_create,getppid,ptracewaitpid. 无需花太多时间来反编译我们就可以猜到,当调试器用ptrace的时候,主进程会fork一个子进程来关联它。这是很简单的反调试技术,你可以从这里了解到更多细节。

因为Fridaptrace来初始化注入,所以这就解释了为什么我们不能连接到父进程:因为已经连接了一个进程来作为调试器,再来一个进程关联调试将被阻塞。

 

反反调试方案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:

Uncrackable root

但我们在这输入了什么?来看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来反编译它:

Uncrackable root

仔细观察这些汇编代码,我们可以看到有一些字符串的比较操作,还看到一个很有意思的明文字符串 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函数,输出它的参数。我们期望这样能给出解密后的字符串。我们要做的是:

  1. 找到strncmp在libfoo.so中的内存地址。

  2. 用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));
               }
            }
});

 

在这段代码中有几点需要注意的:

  1. 这段代码调用了Module.enumerateImportsSync来检索对象数组,这些对象中包含了从libfoo.so导入的信息(具体请看文档 )。我们迭代这个数组直至我们找到strncmp和它的地址。然后我们给它关联一个拦截器(Interceptor)。

  2. Java中的字符串不是以null来终止的。当我们用Frida的Memory.readUtf8String方法且不提供长度来读取strncmp内存中的字符串时,Frida会以为有\0来终止,不然就一直返回一些内存垃圾,因为它不知道字符串的终点在哪里。如果我们在第二个参数中明确给出要读取的字符串长度,我们就不会遇到这个问题。

  3. 如果我们不在判断条件那里作限制,限制我们要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:

Uncrackable input

在控制台,你将会看到:

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。把它填入输入框就能看到成功的消息啦。

Uncrackable input

解决修改的方案

最后,一些关于修改值得注意的事以及为什么我们不能用修改过的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动态改变内存。

因此,我们需要:

  1. 获取已经加载好的foo库的基址。

  2. 找到那个变量相对于库基址的偏移量(我们从反编译后的代码中可以看到这个偏移量是0x400C位)

  3. 设置这个变了为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 编译

转载时必须以链接形式注明原始出处及本声明

扫描关注公众号