Android App Hooking with Frida(3)


이번 포스팅에서는지난시간에 이어서 Frida를 사용한 android app hooking에 대하여 다루도록 하겠습니다.

(도구 설치와 같은 기초 지식은 다루지 않습니다.)


문제파일은 아래 경로를 통하여 다운받을 수 있습니다.

https://github.com/OWASP/owasp-mstg/tree/master/Crackmes



분석환경
OS: Windows10
Tools: Frida, Frida-Server, adb, Python3.7, NOX App Player

문제파일(UnCrackable-Level3.apk)을 다운로드 하여 설치합니다.

[그림 1 apk 설치]



앱 설치 후 실행 시 루팅 탐지가 동작하며 OK 버튼 클릭 시 앱이 강제로 종료됩니다.

[그림 2 루팅탐지]


기존 앱들과 달리 frida-ps -U를 했을 때 앱의 프로세스 두개가 동작 중인것을 확인할 수 있습니다.

[그림 3 frids-ps 확인]


jadx-gui를 이용해 소스코드를 분석합니다.

[그림 4 MainActivity 분석]


Level2와 같이 libfoo.so를 불러와 사용하며 루팅탐지가 되면 OK 버튼 클릭 시 System.exit() 함수를 호출합니다.

[그림 5 MainActivity 분석2]


sub_3250에서 fork와 pthread_create, ptrace 함수를 호출하여 앱의 프로세스가 [그림 3]처럼 하나 더 생성되어 동작하는 것을 알 수 있습니다.

[그림 6 JNI에 의해 라이브러리에서 호출되는 함수]


Level2에서 구현한 코드를 사용했을 때 System.exit()에 대한 후킹에 실패하였고 로그캣에는 앱에 대한 조작행위가 발견되어 앱을 종료한다는 메시지가 남겨져 있습니다.

[그림 7 후킹 실패 후 나타나는 로그캣 정보]



libfoo.so를 다시 살펴보면 sub_3080에서 기기에 frida와 xposed 프로세스가 동작하는지 검사하는 로직이 보입니다.

[그림 8 sub_3080의 frida 프로세스 검사 로직 분석]


따라서 libfoo.so 라이브러리에 후킹방지를 위한 로직이 존재하며 우리는 두가지의 방법으로 로직을 우회할 수 있습니다.

첫번째로 sub_3080의 strstr 함수를 후킹하는 방법입니다.

[그림 9 strstr 함수 분석]


strstr 함수의 구조는 다음과 같습니다.


1
char *strstr(const char *haystack, const char *needle);
cs


strstr 함수의 haystack과 needle 포인터에 대해 후킹을 시도하며 소스코드는 다음과 같습니다.


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
Interceptor.attach(Module.findExportByName("libc.so""strstr"), {
 
    onEnter: function (args) {
 
    this.haystack = args[0];
    this.needle = args[1];
    this.frida = Boolean(0);
 
    haystack = Memory.readUtf8String(this.haystack);
    needle = Memory.readUtf8String(this.needle);
 
    if ( haystack.indexOf("frida"!== -1 || haystack.indexOf("xposed"!== -1 ) {
           this.frida = Boolean(1);
    }
},
 
    onLeave: function (retval) {
 
    if (this.frida) {
       var fakeRet = ptr(0);
       retval.replace(0);
    }
    return retval;
    }
});
cs


두번째 방법은 pthread_create를 이용한 방법입니다.

pthread_create의 구조는 다음과 같습니다.


1
int pthread_create(pthread_t *threadconst pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
cs


함수를 추적해보면 pthread_create는 두 군데에서 호출하는 것을 확인할 수 있습니다.

[그림 10 pthread_create 호출]


또한 첫번째와 세번째 인수가 0인 것을 확인할 수 있습니다.

따라서 pthread_create를 가져와 오버로드하여 조작된 값(0)으로 바꾸고 frida를 탐지할 때 콜백을 조작하여 frida를 사용하지 않는 것 처럼 속일 수 있습니다.

소스코드는 다음과 같습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var p_pthread_create = Module.findExportByName("libc.so""pthread_create");
var pthread_create = new NativeFunction( p_pthread_create, "int", ["pointer""pointer""pointer""pointer"]);
send("NativeFunction pthread_create() replaced @ " + pthread_create);
 
Interceptor.replace( p_pthread_create, new NativeCallback(function (ptr0, ptr1, ptr2, ptr3) {
    send("pthread_create() overloaded");
    var ret = ptr(0);
    if (ptr1.isNull() && ptr3.isNull()) {
        send("loading fake pthread_create because ptr1 and ptr3 are equal to 0!");
    } else {
        send("loading real pthread_create()");
        ret = pthread_create(ptr0,ptr1,ptr2,ptr3);
    }
 
    do_native_hooks_libfoo();
 
    send("ret: " + ret);
 
}, "int", ["pointer""pointer""pointer""pointer"]));
cs


우리는 두가지 방법 중 strstr 함수를 속이는 방식으로 루팅과 후킹 방지에 대한 우회를 시도하겠습니다.

소스코드는 다음과 같습니다.
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
import sys, frida
def on_message(message, data):
    if message['type'== 'send':
        print("[*] {0}".format(message['payload']))
    else:
        print(message)
PACKAGE_NAME = "owasp.mstg.uncrackable3"
jscode = """
send("Starting script");
// char *strstr(const char *haystack, const char *needle);
Interceptor.attach(Module.findExportByName("libc.so", "strstr"), {
onEnter: function (args) {
this.haystack = args[0];
this.needle = args[1];
this.frida = Boolean(0);
haystack = Memory.readUtf8String(this.haystack);
needle = Memory.readUtf8String(this.needle);
if ( haystack.indexOf("frida") !== -1 || haystack.indexOf("xposed") !== -1 ) {
    
            this.frida = Boolean(1);
}
},
onLeave: function (retval) {
if (this.frida) {
    var fakeRet = ptr(0);
    
    retval.replace(0);
}
return retval;
}
});
send("Done with native hooks....");
Java.perform(function () {
    send("Hooking calls to System.exit");
    var sys = Java.use("java.lang.System");
    sys.exit.overload("int").implementation = function(var_0) {
        send("System.exit called");
    };
    send("Done Java hooks installed.");
});
"""
try:
    device = frida.get_usb_device(timeout=5)
    pid = device.spawn([PACKAGE_NAME])
    print("App is Starting.... pid : {}".format(pid))
    process = device.attach(pid)
    device.resume(pid)
    script = process.create_script(jscode)
    script.on('message', on_message)
    print("[*] Running Hook")
    script.load()
    sys.stdin.read()
except Exception as e:
    print(e)
cs

스크립트를 실행시키면 앱이 자동으로 실행되면서 똑같이 루팅이 탐지 되었다는 메시지가 뜨지만 OK를 클릭하면 우회가 됩니다.

[그림 11 System.exit() 후킹]


이제 앱에서 키값을 입력할 수 있게 되었습니다.

[그림 12 루팅 및 후킹 방지 우회된 상태의 앱]


다시 jadx-gui로 돌아와서 CodeCheck 클래스의 소스코드를 살펴보면 그림과 같이 JNI 기법에 의해 libfoo.so의 함수로 입력한 값이 전달됩니다.

[그림 13 CodeCheck 클래스]


libfoo.so에서 Java_sg_vantagepoint_uncrackable3_CodeCheck_bar 함수를 확인할 수 있습니다.

[그림 14 Java_sg_vantagepoint_uncrackable3_CodeCheck_bar]


CodeCheck_bar를 분석해보면 MainActivity에서 보았던 "pizzapizzapizzapizzapizz"와 사용자가 입력한 키값을 XOR하여 실제 키값과 일치하는지 비교하여 성공과 실패를 반환하는 방식입니다. 또한 0x0000344D에서 eax와 18h(24글자)인지 비교하는 로직으로 볼때 키값의 길이는 24자인 것을 확인할 수 있습니다.

[그림 15 키 검증 로직 분석]


위 처럼 분석한 내용을 토대로 hooking 코드를 작성하였을 때 소스코드는 다음과 같습니다.

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
import sys, frida
def on_message(message, data):
    if message['type'== 'send':
        print("[*] {0}".format(message['payload']))
    else:
        print(message)
PACKAGE_NAME = "owasp.mstg.uncrackable3"
jscode = """
send("Starting script");
// char *strstr(const char *haystack, const char *needle);
Interceptor.attach(Module.findExportByName("libc.so", "strstr"), {
    onEnter: function (args) {
        this.haystack = args[0];
        this.needle = args[1];
        this.frida = Boolean(0);
    haystack = Memory.readUtf8String(this.haystack);
    needle = Memory.readUtf8String(this.needle);
    if ( haystack.indexOf("frida") !== -1 || haystack.indexOf("xposed") !== -1 ) {
    this.frida = Boolean(1);
    }
},
    onLeave: function (retval) {
        if (this.frida) {
            var fakeRet = ptr(0);
            retval.replace(0);
        }
    return retval;
    }
});
var address = 0x00003446;
//var address2 = 0x000033ee;
function hook(){
    //libfoo.so Address
    var p_foo = Module.findBaseAddress('libfoo.so');
    send("libfoo.so @ " + p_foo.toString());
    var target_address = p_foo.add(address);
    send("target_Address @ " + target_address.toString());
    Interceptor.attach(target_address, {
        onEnter: function (args) {
        send("onEnter() target_Address");
        send("Context : " + JSON.stringify(this.context));
        var ecx = this.context.ecx;
        send("ecx:"+ecx);
        send(hexdump(ecx,{
            offset: 0,
            length: 24,
            header: false,
            ansi:false
        }));
        var ebx = this.context.ebx;
        send("ebx:"+ebx);
        send(hexdump(ebx,{
            offset: 0,
            length: 24,
            header: false,
            ansi:false
        }));
         var esi = this.context.esi;
        send("esi:"+esi);
        send(hexdump(esi,{
            offset: 0,
            length: 24,
            header: false,
            ansi:false
        }));
        var edi = this.context.edi;
        send("edi:"+edi);
        send(hexdump(edi,{
            offset: 0,
            length: 24,
            header: false,
            ansi:false
        }));
        var ebp = this.context.ebp;
        send("ebp:"+ebp);
        send(hexdump(ebp,{
            offset: 0,
            length: 24,
            header: false,
            ansi:false
        }));
        var esp = this.context.esp;
        send("esp:"+esp);
        send(hexdump(esp,{
            offset: 0,
            length: 24,
            header: false,
            ansi:false
        }));
     },
     onLeave: function (retval) {
        send("onLeave() p_strncmp_xor64");
        send(retval);
     }
    
    });
}
    send("Done with native hooks....");
Java.perform(function () {
    send("Hooking calls to System.exit");
    var sys = Java.use("java.lang.System");
    sys.exit.overload("int").implementation = function(var_0) {
        send("System.exit called");
    };
    hook();
    send("Done Java hooks installed.");
});
"""
try:
    device = frida.get_usb_device(timeout=5)
    pid = device.spawn([PACKAGE_NAME])
    print("App is Starting.... pid : {}".format(pid))
    process = device.attach(pid)
    device.resume(pid)
    script = process.create_script(jscode)
    script.on('message', on_message)
    print("[*] Running Hook")
    script.load()
    sys.stdin.read()
except Exception as e:
    print(e)
cs


위 스크립트를 실행하면 다음과 같이 키 값을 입력받기 위한 대기 상태가 됩니다.

[그림 16 루팅 우회된 상태의 스크립트]


앱에서 24자의 조건을 충족시키기 위해 "012345678901234567890123"을 입력합니다.

[그림 17 임의 값 입력]


VERIFY를 클릭하면 실패했다는 팝업이 뜨고 스크립트에는 그림과 같이 메모리에 적재되어 있는 레지스터의 값들이 보이게 됩니다.

[그림 18 레지스터에 남아있는 키값]


여기서 esp가 키 값 비교에 사용된 실제 키 값, esi가 사용자 입력 값, ecx가 MainActivity에서 보았던 XOR에 쓰이는 키값인 것을 알 수 있습니다. 위 로직 대로 실제 키 값과 XOR에 쓰이는 키 값을 이용해 정답이 무엇인지 확인해보겠습니다.

1
2
3
4
5
6
7
8
9
secret = bytes.fromhex('1d0811130f1749150d0003195a1d1315080e5a0017081314').decode('utf-8')
key = 'pizzapizzapizzapizzapizz'
 
password = ""
 
for i in range(24):
    password += chr((ord(secret[i]) ^ ord(key[i])))
 
print ("password : ",password)
cs

위 소스코드를 실행시키면 다음과 같이 키 값이 추출 됩니다.

[그림 19 추출된 키값]


추출된 키 값을 입력하면 성공 메시지가 출력됩니다.

[그림 20 키 값 검증]


지금까지 Frida를 사용한 Android App Hooking에 대하여 알아봤습니다.

참고 : http://sh3llc0d3r.com/owasp-uncrackable-android-level3/

   https://enovella.github.io/android/reverse/2017/05/20/android-owasp-crackmes-level-3.html



'Mobile > Android' 카테고리의 다른 글

Android App Hooking with Frida(2)  (2) 2018.04.06
Android App Hooking with Frida(1)  (0) 2018.02.26

이 글을 공유하기

댓글