(PRACTICE) Uncrackable-Level 1

1. 정보수집

최초 앱 실행 시 “Root Detected!” 메시지 발생하므로 APK 분석 필요
JADX, JEB 등의 도구를 활용하여 APK 디컴파일 후 코드 분석 진행


2. 코드분석

2-1. 루팅 탐지

onCraete 함수 내부를 살펴보면 c.a(), c.b(), c.c(), b.a(…) 를 통해 루팅/디버깅을 탐지하고 있다.

  • c.a() → System.getenv("PATH")로 환경변수 값을 불러와 su 파일이 존재하면 참 값 반환  
  • c.b() → Build.TAGS 값이 test-keys 라면 참 값 반환
  • c.c() → 소스코드 내 배열에 할당된 경로(파일)가 존재하면 참 값 반환

2-2. BYPASS - ROOT DETECTION (c.a())

함수의 리턴 값을 변조하면 간단하게 우회가 가능하겠지만 상용 앱에서 이런 경우는 흔치 않기 때문에 그 외의 방법으로 프리다 후킹 코드를 작성해보자.
File 클래스의 인스턴스 생성 후 exists 함수로 루팅 시 생성되는 바이너리(su) 파일 경로가 존재하는지 확인하고 있는데 이 경로를 확인해보면 아래와 같다.


안드로이드 쉘에서 /system/bin/su 파일이 존재하기 때문에 루팅 기기로 판별되고 있으므로, File.$init(filePath, fileName) 에서 filePath 값을 임의 값으로 덮어 씌우면 우회가 가능할 것으로 보인다.

c1q:/system/bin # ls -alh | grep su
[ . . . ]
lrwxrwxrwx  1 root root     8 2019-01-12 12:21 su -> ./magisk
[ . . . ]

(FRIDA) BYPASS ROOTING DETECTION

Java.perform(function () {
    console.log();
 
    var File = Java.use('java.io.File');
    File.$init.overload('java.lang.String', 'java.lang.String').implementation = function (str, su) {
        var stackTrace = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new());
 
        if (stackTrace.includes("sg.vantagepoint.a.c.a")) {
            console.warn('File.$init(str, "su") in sg.vantagepoint.a.c.a has been called !')
            console.log(`\tFile.$init(str, "su") => File.$init(${str}, "su")`);
            return this.$init("dumptrashdumptrash", su);
        }
 
        return this.$init(str, su);
    }
})

2-3. 플래그 확인 (VERIFY)

위 코드를 실행하면 이제 메인 액티비티 화면으로 접근할 수 있는데, 여기서 임의의 값을 입력하고 “VERIFY” 버튼을 클릭하면 토스트 메시지가 발생한다.


코드를 확인해보면 a.a(obj) 결과에 따라 성공/실패가 분기되는 로직을 확인할 수 있다.


a.a(obj) 함수 내부를 살펴보면 sg.vantagepoint.a.a.a(...)로 바이트 배열 값을 구한 후에 사용자가 입력한 문자열(str)과 new String(bArr) 값이 동일한 경우에 참 값을 반환하고 Success 알림창을 발생시키고 있다. 따라서, sg.vantagepoint.uncrackable1.a.a(...) 리턴 값을 참으로 반환시키거나 new String(bArr) 값을 추출한 뒤 동일한 값을 앱에서 입력하면 Success 화면을 확인할 수 있겠다.


플래그 확인 코드

Java.perform(function () {
    console.log();
 
    var File = Java.use('java.io.File');
    File.$init.overload('java.lang.String', 'java.lang.String').implementation = function (str, su) {
        var stackTrace = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new());
 
        if (stackTrace.includes("sg.vantagepoint.a.c.a")) {
            console.warn('File.$init(str, "su") in sg.vantagepoint.a.c.a has been called !')
            console.log(`\tFile.$init(str, "su") => File.$init(${str}, "su")`);
            return this.$init("dumptrashdumptrash", su);
        }
 
        return this.$init(str, su);
    }
 
    var a = Java.use('sg.vantagepoint.a.a');
 
    a.a.overload('[B', '[B').implementation = function(byteArr1, byteArr2) {    
        console.warn(`sg.vantagepoint.a.a.a(byteArr1, byteArr2) => ${this.a(byteArr1, byteArr2)}`);
 
        var String = Java.use('java.lang.String');
        var bArr_to_String = String.$new(this.a(byteArr1, byteArr2));
        console.log('\tbArr to String => ', bArr_to_String);
        
        return this.a(byteArr1, byteArr2);
    }
})

스크립트 실행 후 앱에서 임의의 값을 입력하면
문자열 비교 로직에 사용되는 문자열(“I want to believe”)을 확인할 수 있다.


다시 메인 액티비티 화면으로 들어가서 위에서 확인한 문자열을 입력하면, “Success” 메시지가 화면에 출력되는 것을 볼 수 있다!


REFERENCE

  1. https://mas.owasp.org/crackmes/