Android App Hooking with Frida(3)
- Mobile/Android
- 2019. 1. 7. 11:19
이번 포스팅에서는지난시간에 이어서 Frida를 사용한 android app hooking에 대하여 다루도록 하겠습니다.
(도구 설치와 같은 기초 지식은 다루지 않습니다.)
문제파일은 아래 경로를 통하여 다운받을 수 있습니다.
https://github.com/OWASP/owasp-mstg/tree/master/Crackmes
[그림 1 apk 설치]
앱 설치 후 실행 시 루팅 탐지가 동작하며 OK 버튼 클릭 시 앱이 강제로 종료됩니다.
[그림 2 루팅탐지]
기존 앱들과 달리 frida-ps -U를 했을 때 앱의 프로세스 두개가 동작 중인것을 확인할 수 있습니다.
[그림 3 frids-ps 확인]
[그림 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 프로세스 검사 로직 분석]
첫번째로 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의 구조는 다음과 같습니다.
1 | int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg); | cs |
함수를 추적해보면 pthread_create는 두 군데에서 호출하는 것을 확인할 수 있습니다.
[그림 10 pthread_create 호출]
소스코드는 다음과 같습니다.
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 |
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 키 값 검증]
참고 : 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 |
이 글을 공유하기