(PRACTICE) Uncrackable-Level 3 (IDA-DEBUGGING)

이번 포스팅에서는 IDA의 동적 디버깅 기능을 활용하여 문제를 풀어보도록 하겠습니다.
먼저 앱을 실행하면 아래와 같은 알림창이 발생하는데 JADX로 소스코드를 분석해보면 루팅/디버깅/무결성 변조 탐지 코드에 의한 것임을 알 수 있습니다.


우회 스크립트를 작성한 뒤에 실행해보면 루팅 탐지는 정상적으로 우회가 되는 걸 확인할 수 있는데, 이와 동시에 프로세스가 종료되어 앱이 종료됩니다. 자바 코드 상에서는 앱이 종료되는 원인을 찾을 수 없기 때문에 라이브러리에서 실행되는 코드를 확인해 볼 필요가 있겠습니다.


라이브러리(libfoo.so) 파일을 분석하기 전에 알아야 할 내용이 있는데요. 안드로이드 앱에서는 Android Native Library (즉, .so 파일)의 초기화 과정에서 .init_arrayJNI_OnLoad 가 사용된다고 합니다. 주요 기능을 요약해보면 아래와 같습니다.

.init_array

.init_array는 ELF(Executable and Linkable Format) 파일에서 사용되는 섹션 중 하나로, 라이브러리가 로드될 때 자동으로 실행될 초기화 함수들의 배열을 포함합니다.

역할: 이 섹션에는 라이브러리 로딩 시에 실행되어야 하는 초기화 함수들이 포함됩니다. 이 함수들은 보통 static 초기화나 라이브러리 로딩 시에 필요한 작업을 수행합니다.  

실행 시점: .init_array에 포함된 함수들은 .so 파일이 로드될 때 자동으로 호출됩니다. 일반적으로 해당 라이브러리가 로드된 직후에 실행됩니다. 즉, JNI_OnLoad가 호출되기 전에 먼저 실행될 수 있습니다.  

구체적인 동작: .init_array에 등록된 함수들은 라이브러리가 메모리에 로드되면서 ld.so(Linux/Android의 동적 링커)가 이 함수를 호출하게 됩니다.

JNI_OnLoad

역할: JNI_OnLoad 함수는 Java에서 네이티브 코드와 상호작용할 때 JNI(Java Native Interface)를 사용하여 호출됩니다. 이 함수는 네이티브 라이브러리가 로드될 때 한 번만 호출되며, 네이티브 라이브러리의 초기화 및 JNI 환경 설정을 처리하는 데 사용됩니다. 일반적으로 네이티브 코드에서 Java 클래스나 메소드와 상호작용하기 위한 작업을 초기화하는 데 사용됩니다.  

실행 시점: JNI_OnLoad는 System.loadLibrary 또는 System.load가 호출되어 .so 파일이 로드될 때, 그 라이브러리가 최초로 호출되는 시점에 자동으로 호출됩니다. 이 함수는 JNI 환경을 설정하고, Java와의 네이티브 인터페이스를 준비하는 역할을 합니다.


요약 (.init_array, JNI_OnLoad)

  1. .init_array는 라이브러리가 로드될 때 가장 먼저 실행되는 초기화 함수들의 배열이다.
  2. JNI_OnLoad는 JNI 환경을 설정하고 네이티브 코드와 Java 간의 인터페이스를 설정하는 함수로, .init_array 함수들이 실행된 후에 호출된다.  
  3. 따라서 .so 파일이 로드될 때 .init_array 함수들이 먼저 실행되고, 그 후에 JNI_OnLoad가 호출된다.

자, 위와 같은 배경지식을 준비하고 다시 분석을 시작해보겠습니다.

  1. .init_array 배열 내부에서 sub_31B0 호출
  2. sub_31B0 에서 스레드가 생성되고 있으며, 그 스레드는 sub_30D0 호출


  1. sub_30D0 함수는 /proc/self/maps 파일 내부에 “frida”, “xposed” 문자열이 있는지 검사
  2. (3)번 과정을 통해 안티 프리다, 안티 XPOSED가 수행되고 있음


따라서, 아래와 같이 후킹 코드를 작성하여 fopen 함수의 1번째 인자(“/proc/self/maps”)를 임의의 값으로 변조하면 탐지 로직 우회가 가능합니다. 그런데 이 때, 만약 존재하지 않는 파일 열람을 시도하면 반환 값이 NULL이기 때문에 바로 ELSE문으로 분기되어 goodbye 함수(프로세스 종료)가 실행됩니다. 그러므로, 실제로 접근이 가능한 파일 중 “frida, xposed” 문자열이 없는 파일의 경로로 인자 값을 변조하도록 주의해야 합니다.


필자는 “/proc/self/maps” 경로를 “/proc/self/stat” 으로 변조하여 우회했습니다.

setImmediate(function() {
    Java.perform(function() {
        var RootDetection = Java.use('sg.vantagepoint.util.RootDetection');
        var IntegrityCheck = Java.use('sg.vantagepoint.util.IntegrityCheck');
 
 
        RootDetection.checkRoot1.implementation = function() {
            console.warn('RootDetection.checkRoot1() function has been called !');
            console.log(`\tRootDetection.checkRoot1() => Original Return Value => ${this.checkRoot1()}`);
            console.log(`\tRootDetection.checkRoot1() => Modified Return Value => ${false}`);
            
            return false;
        }
    });
 
    Interceptor.attach(Module.findExportByName(null, 'fopen'), {
        onEnter: function(args) {
            this.path = Memory.readUtf8String(ptr(args[0]));
 
            if (Memory.readUtf8String(ptr(args[0])).includes('/proc/self/maps')) {
                console.warn("\nfopen(...) function has been called !");
                console.log(`\tstrstr(path, 'r') => path => ${this.path}`);
                
                Memory.protect(ptr(args[0]), 512, 'rwx');
                Memory.writeUtf8String(ptr(args[0]), "/proc/self/stat");
 
                console.log(`\tModified args[0] => ${Memory.readUtf8String(ptr(args[0]))}`)
            }
        }
    });
});


메인 액티비티에 정상적으로 접근한 뒤에 임의의 값을 입력하면 아래와 같은 알림창이 또 다시 발생하는데요.
다시 JADX로 돌아가 코드를 분석해보겠습니다.


출력된 문자열을 통해 코드 흐름을 추적해보면 아래와 같은 흐름으로 함수가 호출되고 있습니다.

  • verify(...) → check_code(...) → bar(...)

이 때, bar 함수는 네이티브 함수이기 때문에 라이브러리(libfoo.so)에서의 분석이 필요합니다.
다시 IDA로 돌아가서 코드를 분석해보시죠.


아래 함수(Java_sg_…_bar)의 분석 내용은 다음과 같습니다.

  • (1) v8 배열 값을 0으로 초기화하고, (2) dword_15054 == 2 값이 참이고 (3) if (*(unsigned int (__fastcall **)(__int64, __int64))(*(_QWORD *)a1 + 1368LL))(a1, a3) == 24 ) 값이 참이고 (4) (unsigned __int8 *)(v6 + v7) == (*((unsigned __int8 *)&qword_7AB38AD038 + v7) ^ *((unsigned __int8 *)v8 + v7))) 값이 참일 때 (5) v7 == 24 이면 1을 반환
    그 외에는 모두 0 값을 반환하고 있기 때문에 문제를 풀기 위해서는 마지막 조건문까지 들어가서 v7 == 24 값이 참이 되도록 유도를 해야할 것으로 보입니다.

NATIVE FUNCTOIN - bar(…) 분석

먼저 선언된 v8[3] 배열의 값을 0으로 초기화 시킵니다. 이후 dword_15054 값이 2이라면 조건문 내부로 진입이 가능합니다. IDA에서 BP를 설정한 뒤 앱에서 임의의 값을 입력하여 “verfiy” 버튼을 클릭하면 BP에 붙을 수 있는데 General registers 항목의 W8 레지스터 값을 살펴보면 2인 것을 알 수 있습니다. 따라서, 무조건 조건문 내부로 진입하게 됩니다.


이후 호출되는 sub_7AB38990E0 함수에는 인자로 v8이 사용되는데요. 함수 내부에 진입하여 코드 로직을 분석해보면 값이 0으로 초기화된 v8 배열에 특정 값을 할당해주는 함수인 것을 알 수 있습니다. 이 값들은 추후 while loop 에서 XOR 연산에 활용됩니다.  


v6 = (*(__int64 (__fastcall **)(__int64, __int64, _QWORD))(*(_QWORD *)a1 + 1472LL))(a1, a3, 0LL); 라인은 v6 변수에 사용자가 입력한 문자열을 할당하는 코드로 유추할 수 있습니다. 또한, 조건문의 조건으로 활용되는 (*(unsigned int (__fastcall **)(__int64, __int64))(*(_QWORD *)a1 + 1368LL))(a1, a3) == 24 )는 사용자가 입력한 문자열의 길이가 24(byte)인지 검증하는 로직으로 추측됩니다.


이후에는 반복문(while)에서 사용자가 입력한 문자열과 XOR 연산결과로 획득한 문자열을 비교하게 됩니다. v7은 최초에 0LL로 선언되어 반복문이 진행될 때마다 값이 1씩 증가하고 있습니다. 따라서, 사용자의 입력값과 XOR 연산 결과가 저장된 배열을 0번째 인덱스부터 1byte 씩 증가하여 총 24개의 문자를 차례대로 비교한다는 것을 알 수 있습니다.


여기서 비교되는 문자열들이 어떤 형태를 띄는지 IDA에 BP를 걸어 확인해보겠습니다. 이전과 동일하게 확인하고 싶은 라인에 BP(F2) 설정하고 입력창에 길이가 24인 임의의 문자열을 입력하면 디버깅이 가능합니다.  


아래 이미지에서 볼 수 있듯이, 반복문(while)에서 사용되는 == 는 어셈블리어의 CMP W12, W10 부분에 해당합니다. 이 때, General register에서 W10, W12의 값을 살펴보면 각각 m(0x6d), t(0x74) 임을 알 수 있습니다. (필자는 테스트 시 testtesttesttesttesttest 를 입력했습니다.)  

m으로 시작하는 문자열은 XOR 연산결과로 산출된 문자열의 1번째 문자이고, t는 필자가 입력한 testtesttesttesttesttest의 1번째 문자입니다.


앞서 언급했듯이, 사용자가 입력한 문자열과 라이브러리에서 내부적으로 생성된 특정 문자열이 동일하지 않으면 반복문에서 바로 탈출되며 bar 함수의 리턴 값으로 0이 반환되어 앱이 종료됩니다. 따라서, 코드 패치를 통해 W10과 W12 레지스터 값을 똑같이 만들어줍니다. 비교 문자열의 길이는 24이므로, 총 24번 W10, W12 레지스터 값이 같도록 변조하면 “Success!” 화면이 출력되는 것을 확인할 수 있습니다.


변조 과정에서 W10 레지스터로부터 획득한 비교 문자열(XOR 연산결과)의 HEX 값은 아래와 같았습니다. 이 값을 문자로 변환하면 making owasp great again 이므로, 언크래커블 앱에서 이 문자열을 입력하고 “VERIFY” 버튼을 클릭하면 “Success!” 메시지가 화면에 출력됩니다.

0x6d 0x61 0x6b 0x69 0x6e 0x67 0x20 0x6f 0x77 0x61
0x73 0x70 0x20 0x67 0x72 0x65 0x61 0x74 0x20 0x61
0x67 0x61 0x69 0x6e

# making owasp great again


자, 이렇게 프리다와 IDA 동적 디버깅을 활용해 봤습니다.
취약점 점검 시 상황에 맞게 응용하면 유용하게 활용할 수 있을 것 같습니다.


REFERENCE

  1. https://mas.owasp.org/crackmes/
  2. https://blog.naver.com/koo__oo/222437620168
  3. https://linears.tistory.com/entry/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EC%95%88%ED%8B%B0%EB%94%94%EB%B2%84%EA%B9%85