UnCrackable-Level1에 이어 Level2를 풀어보자.
UnCrackable-Level2.apk를 다운받고 [NOX 플레이어]로 App을 실행했다.
문제풀이
App을 실행하면 디버깅을 탐지하는 것을 확인할 수 있다.
어떤 방식으로 디버깅을 탐지하는지 확인하기 위해서 코드를 JEB로 분석했다.
○ MainActivity - onCreate()
if((b.a()) || (b.b()) || (b.c())) {
this.a("Root detected!");
}
if(a.a(this.getApplicationContext())) {
this.a("App is debuggable!");
}
onCreate()의 if를 보면 b.a()와 a.a()를 실행하고 결과에 따라 루팅과 디버깅을 탐지하는 것을 확인할 수 있다.
○ MainActivity - a()
private void a(String arg4) {
AlertDialog v0 = new AlertDialog.Builder(this).create();
v0.setTitle(arg4);
v0.setMessage("This is unacceptable. The app is now going to exit.");
v0.setButton(-3, "OK", new DialogInterface.OnClickListener() {
@Override // android.content.DialogInterface$OnClickListener
public void onClick(DialogInterface arg1, int arg2) {
System.exit(0);
}
});
v0.setCancelable(false);
v0.show();
}
this.a()을 보면 System.exit(0)를 실행해서 App을 종료하는 것을 확인할 수 있다.
루팅 감지를 우회하기위해서 System.exit()을 App이 종료되지않도록 후킹하면 쉽게 우회할 수 있다.
디버깅 감지를 우회했으니 본격적으로 풀어보자.
목적은 Secret String을 구하는 것이다.
○ MainActivity - verify()
public void verify(View arg4) {
String v4_1;
String v4 = ((EditText)this.findViewById(0x7F070035)).getText().toString(); // id:edit_text
AlertDialog v0 = new AlertDialog.Builder(this).create();
if(this.m.a(v4)) {
v0.setTitle("Success!");
v4_1 = "This is the correct secret.";
}
else {
v0.setTitle("Nope...");
v4_1 = "That\'s not it. Try again.";
}
v0.setMessage(v4_1);
v0.setButton(-3, "OK", new DialogInterface.OnClickListener() {
@Override // android.content.DialogInterface$OnClickListener
public void onClick(DialogInterface arg1, int arg2) {
arg1.dismiss();
}
});
v0.show();
}
verify()를 보면 m.a()를 통해서 v4를 검증하고 Secret String이 맞을 경우 "Success!"를 출력한다.
m은 onCreate()를 보면 CodeCheck 클래스를 인스턴스로 생성한 변수이므로 m.a()는 CodeCheck 클래스에 정의된 a()일 것이다.
a()를 통해서 입력값이 Secret String인지 검증하므로 CodeCheck 클래스의 a()를 확인해보자.
○ CodeCheck - a()
package sg.vantagepoint.uncrackable2;
public class CodeCheck {
public boolean a(String arg1) {
return this.bar(arg1.getBytes());
}
private native boolean bar(byte[] arg1) {
}
}
bar()를 실행하고 return 값을 받아서 넘기는 것을 확인할 수 있다.
bar()를 분석해야겠다고 생각하고 코드를 보려고하니, 코드가 비어있는 것을 볼 수 있다.
bar()의 선언부를 보면 native라고 써있는 것을 볼 수 있다.
→ 즉, bar()는 so 파일에서 가져오는 native 함수라는 것을 알 수 있다.
○ MainActivity - loadLibrary
static {
System.loadLibrary("foo");
}
MainActivity를 보면 foo.so 파일을 loadLibrary()로 불러오는 것을 확인할 수 있다.
그렇다면 bar()를 분석하기 위해서는 foo.so를 분석할 필요가 있다.
APK로부터 so를 추출하는 방법은 직접 구글링해서 알아보도록 하자.
APK를 분석하고 lib 폴더에 들어가면 libfoo.so를 발견할 수 있다.
○ Java_sg_vantagepoint_uncrackable2_CodeCheck_bar()
_BOOL4 __cdecl Java_sg_vantagepoint_uncrackable2_CodeCheck_bar(int a1, int a2, int a3)
{
const char *v3; // esi
_BOOL4 result; // eax
char s2[24]; // [esp+0h] [ebp-2Ch] BYREF
unsigned int v6; // [esp+18h] [ebp-14h]
v6 = __readgsdword(0x14u);
result = 0;
if ( byte_4008 == 1 )
{
strcpy(s2, "Thanks for all the fish");
v3 = (const char *)(*(int (__cdecl **)(int, int, _DWORD))(*(_DWORD *)a1 + 736))(a1, a3, 0);
if ( (*(int (__cdecl **)(int, int))(*(_DWORD *)a1 + 684))(a1, a3) == 23 && !strncmp(v3, s2, 0x17u) )
return 1;
}
return result;
}
libfoo.so를 ida로 분석하면 많은 함수들이 나오는데, 그 중에서 CodeCheck_bar()라는 함수가 있는 것을 확인할 수 있다.
코드를 분석하면,
strcpy(s2, "Thanks for all the fish")
를 통해 두 인자 값을 비교한다.
이때, s2가 "Thanks for all the fish"면 return 1을 한다.
만약, CodeCheck_bar()를 실행하고 return 1이 된다면?
→ a()도 return 1
→ verify의 if는 true
유추하자면 s2가 입력값이라는 사실을 알 수 있다.
그렇다면 "Thanks for all the fish" 는?
Secret String?
Secret String이 맞는지 확인하기 위해서 루팅 & 디버깅 탐지를 frida를 통해 우회하고 위와 같이 입력했다.
결과는?
○ hook.py
import frida, sys
def on_message(message, data):
if message['type'] == 'send':
print("[*] {0}".format(message['payload']))
else:
print(message)
PACKAGE_NAME = "owasp.mstg.uncrackable2"
jscode = """
console.log("[*] Start Hooking");
Java.perform(function() {
var mainClass = Java.use("sg.vantagepoint.uncrackable2.MainActivity");
var bypassExit = Java.use("java.lang.System"); // system Class Hook
mainClass.init.implementation = function() { // init() overwrite
console.log("[*] init() is called"); // init() 실행 동작 감지
this.init(); // init() 정상적으로 수행하도록 실행
}
bypassExit.exit.implementation = function (){ // exit() overwrite
console.log("[*] System.exit() is called"); // "Root Detect" 우회
}
});
"""
try:
device = frida.get_usb_device(timeout=10)
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 Frida')
script.load()
sys.stdin.read()
except Exception as e:
print(e)