기술보안/Android

[Android] Uncrackable level 2 문제 풀이

kimcogi 2024. 3. 3. 18:05

● Uncrackable란?

Uncrackable는 정보보안을 연구하는 그룹(OWASP)에서 모바일 리버싱 연습용으로 개발한 앱이다

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

 

MAS Crackmes - OWASP Mobile Application Security

MAS Crackmes Welcome to the MAS Crackmes aka. UnCrackable Apps, a collection of mobile reverse engineering challenges. These challenges are used as examples throughout the OWASP MASTG. Of course, you can also solve them for fun.

mas.owasp.org

총 7개의 앱으로 구성되어 있으며(android 5개, ios 2개) 이번에는 android 중 level 2에 대한 풀이 과정을 적은 글이다

 

● 환경 구성

- 환경 : 에뮬레이터(녹스) 루팅 상태
- os : android 9 (64bit)
- Tool : frida 16.1.4, jadx, IDA

 

● 루팅 탐지 우회

Uncrackable level 1번 문제와 동일하여 별도의 루팅 탐지 우회 구문 관련해서 작성하지 않음

(메소드나 함수명은 다를지라도 방법은 거의 동일하기 때문)

 

Uncrackable level 1 문제

 

● Secret key(String) 찾기

먼저 소스코드를 살펴보자

이전 1번 문제와 동일하게 verify() 함수 내 우리가 입력창에 입력하는 문자열이 "obj"라는 변수로 들어가고, if 문을 통해 a.a() 함수 내 "obj" 변수를 전달해서 정상적인 Secret key(String)이 입력될 경우 "Success!"를 아닐 경우 "Nope..."을 반환하도록 되어있다

 

그렇다면 먼저 a.a() 함수에 대해 후킹을 해보자

위 코드는 a.a() 함수에 대한 결과값을 확인해 볼 수 있는 후킹 코드이다

 

입력창에 아무 문자열(kimcogi)를 입력한 후 결과를 살펴보자

위 사진을 보았을때는 당연하게 "Nope..." 메시지가 등장한다

그렇다면 후킹 코드에서는 어떻게 결괏값이 표출이 되는지 살펴보자

후킹 코드 상에도 이전 1번과 동일하게 a.a() 함수에 대한 결과값이 false로 떨어지는 것을 볼 수 있다

 

그렇다면 이번에도 동일하게 return 값을 "true"로 변경하게 된다면 어떻게 될까

위 사진은 return 값을 true 값으로 변조한 후킹 코드이다

 

"Success!" 라는 메시지 창과 함께 정답이라고 표기가 되는 것을 볼 수 있다

하지만 이번에도 앱을 만든 제작자가 이렇게 표기가 되기를 원치는 않을것같다

소스코드를 조금 더 살펴보자

 

jadx로 소스코드를 조금 더 살펴본 결과 CodeCheck() 함수가 선언되어 있고 라이브러리파일을 작동하도록 System.loadLibrary가 선언되어 있다

 

CodeCheck() 함수에 대해 조금 더 살펴보니 a 메서드로 선언이 되어있으며, String 값을 byte 형식으로 전환해서 bar 메소드로 넘겨주는 모습이 보인다

bar 메소드는 네이티브로 선언이 되어 있으므로 JNI로 볼 수 있으며, 라이브러리를 호출하는 걸로 볼 수 있다

라이브러리 파일을 호출시켜주는 건 위위 사진을 살펴보면 된다

 

그러면 lib 폴더 내에 존재하는 라이브러리 파일을 IDA로 분석해 보자

IDA로 "libfoo.so" 라이브러리 파일을 확인해 본 결과 여러 함수가 존재하며 우리가 주로 살펴볼만한 함수로는 아래와 같다

Java_sg_vantagepoint_uncrackable2_CodeCheck_bar
Java_sg_vantagepoint_uncrackable2_MainActivity_init
strncmp

 

여러 종류의 함수가 존재하지만 Export 탭을 이용해서 어떤 함수를 상세히 살펴봐야 되는지 살펴보자

Export 탭에서는 "Java_sg_vantagepoint_uncrackable2_CodeCheck_bar"와 "Java_sg_vantagepoint_uncrackable2_MainActivity_init"이 Export 되어있음을 볼 수 있다

우리가 살펴보려 하는 값은 bar에 관련된 값이므로 bar에 해당되는 "Java_sg_vantagepoint_uncrackable2_CodeCheck_bar"를 살펴보자

 

위 사진은 " Java_sg_vantagepoint_uncrackable2_CodeCheck_bar"를 보기 쉽게 변환을 시켜둔 상태이다 (단축키 F5)

 

해당 코드들에 상세히 분석을 해보자

v3 = (const char *)(*(__int64 (__fastcal **)(__int64, __int64, _QWORD))(*(_QWORD *)a1 + 1472LL))(a1, a3, 0LL);

 -> v3는 상수 문자열 포인터(const char *)로 이루어져 있으며, 3개의 인자를 받고 있다 (__int64, __int64, _QWORD)

 -> __int64, __int64, _QWORD 인자는 메모리주소 a1의 위치에 저장되어 있다

 -> a1에 저장된 주소값에 1472LL을 더한 위치에 있는 함수를 호출해 온다

 -> 함수가 호출될 때 3개의 인자를 가지고 오는데(a1, a3, 0LL), a1과 a3는 __int64, 0LL은 _QWORD이다

 

※ QWORD는 64비트 크기의 데이터를 나타내는 데이터 타입으로 16진수로 나타낼 경우 16자리에 해당됨, 정확히는 부호 있는 정수, 부호 없는 정수, 포인터 등을 저장할 때 쓰임

 

if ( (*(unsigned int (__fastcall **)(__int64, __int64))(*(_QWORD *)a1 + 1368LL))(a1, v3) == 23 && !strncmp(v4, (const char *)&v6, 0x17uLL) ) { result = 1; }

 -> a1에 저장된 주소값에 1368LL를 더한 위치에 있는 함수를 호출함

 -> 위 함수는 (__int64, __int64) 형식의 매개변수를 가짐

 -> 해당 결과가 23인지 검사 및 비교를 하고 같을 경우 result에 1을 저장함

 ---> 위 분석을 보아 조건문 내 "strncmp" 함수를 이용하여 문자열 비교를 하고 있음을 알 수 있다 = 즉 v4, v6 사이에 23byte가 일치하는지를 확인함

 

strncmp 함수가 뭐지?라고 생각이 들 수가 있다

strncmp는 C언어에서 두 개의 문자열을 비교하는 함수로 아래의 형식으로 가지고 있다

int strncmp(const char *str1, const char *str2, size_t n);

1. str1, str2 두 문자열이 같으면 0을 반환하도록 되어있음

2. str1이 str2 보다 크거나 앞에 있을 경우 음수를 반환함

3. str1이 str2 보다 작거나 뒤에 있을 경우 양수를 반환함

 

strncmp는 문자열을 바이트 단위로 비교하는 함수로 알고 있으면 좋을 듯하다

 

그렇다면 위 코드를 보고 우리가 알 수 있는 것은 입력창에 23개의 문자열이 입력되어야 하며, 23개의 문자열이 입력되기 전까지는 result 값이 1로 반환되지 않음을 알 수 있다

그렇다면 위 사진과 같이 23개의 문자열을 입력창에 입력했을 때 strncmp의 첫 번째 인자 값(str1 = set1)이 들어가도록 하는 코드이며, 만약 첫번째값에 23개의 문자열인 "12345678901234567890123"이 삽입되었다면 첫번째 인자값(우리가 입력한 값), 두 번째 인자값(Secret key)이 출력이 되도록 하는 코드이다

 

※ 결괏값이 더욱 정확하게 나올 수 있도록 메모리에 해당 문자열이 포함되어 있는지 검사를 하는 indexOf 함수를 사용하는 것이 가장 정확하다

 

작동을 시켜보자

먼저 입력창에 우리가 입력하려고 하는 값인 "12345678901234567890123"을 삽입하고 verify를 시켜보자

 

후킹 코드를 살펴본 결과 정상적으로 첫 번째 인자값에 "12345678901234567890123"이 삽입된 것으로 보이며 또한 두 번째 인자 값인 secret key(String) 값도 보인다

 

해당 secret key를 입력하게 되면 우리가 원하는 "Success!" 구문을 얻을 수 있다

 

 

참고 사이트

indexof 정의 : https://learn.microsoft.com/ko-kr/dotnet/api/system.string.indexof?view=net-8.0

 

String.IndexOf 메서드 (System)

이 인스턴스에서 맨 처음 발견되는 지정된 유니코드 문자 또는 문자열의 0부터 시작하는 인덱스를 보고합니다. 이 인스턴스에 해당 문자나 문자열이 없으면 이 메서드는 -1을 반환합니다.

learn.microsoft.com

strncmp() 정의 : https://learn.microsoft.com/ko-kr/cpp/c-runtime-library/reference/strncmp-wcsncmp-mbsncmp-mbsncmp-l?view=msvc-170

 

strncmp, wcsncmp, _mbsncmp, _mbsncmp_l

자세한 정보: strncmp, wcsncmp, _mbsncmp, _mbsncmp_l

learn.microsoft.com

strncmp() - 스트링 비교 : https://www.ibm.com/docs/ko/i/7.4?topic=functions-strncmp-compare-strings

 

strncmp() — 스트링 비교

0보다 작음 string1이 string2보다 작음 0 string1이 string2와 같음 0보다 큼 string1이 string2보다 큼

www.ibm.com

Uncrackable-level 2 정답 코드
// 루팅 탐지 우회(1번 문제와 동일)
Java.perform(function () {
    let AnonymousClass1 = Java.use("sg.vantagepoint.uncrackable2.MainActivity$1");
    AnonymousClass1["onClick"].implementation = function (dialogInterface, i) {
        console.log("[!] rooting bypass");
    };
});

// libfoo.so 라이브러리 파일 내 strncmp 함수 관련
Interceptor.attach(Module.findExportByName('libfoo.so','strncmp'), {
    onEnter : function(args) {
        var set1 = Memory.readUtf8String(args[0]);
        var set2 = Memory.readUtf8String(args[1]);
        if(set1.indexOf("12345678901234567890123") !== -1) {
            console.log("[!] string :", set1);
            console.log("[!] secret_key :", set2);
        }
    },
    onLeave : function(){
    }
});
반응형