Uncrackable2 write-up

Uncrackable2 write-up

앱 출처: https://github.com/OWASP/owasp-mstg/tree/master/Crackmes

Uncrackable1과 마찬가지로 루팅된 디바이스에서 앱 실행 시 아래와 같은 에러메시지와 함께 앱이 종료된다.


Uncrackable1과 동일하게 v0.setCancelable(false);를 smali 코드를 이용하여 v0.setCancelable(true);로 변경한 후 recompile하고 재설치하여 우회한다.


verify 함수를 분석해보면 위에서 생성된 CodeCheck 클래스의 a 함수의 결과에 의해 Success 문이 출력된느 것을 확인할 수 있다.


CodeCheck 클래스의 a함수를 분석해보면 사용자가 입력한 문자열을 bar 함수로 전송하는 것을 확인할 수 있으나 아래의 bar 함수에는 코드가 존재하지 않는다. bar 함수 앞에 native가 붙은 것으로 보아 라이브러리에 있는 코드를 사용하는 것으로 확인된다. MainActivity를 재확인 해보면 System.loadLibrary("foo"); 구문을 통해 라이브러리를 불러오는 것을 확인할 수 있다.

해당 라리브러리 파일은 apk를 디컴파일한 폴더에 lib/[해당 platform]/libfoo.so로 존재한다.

라이브러리 파일을 IDA를 통해 연후 Exports 탭에 가보면 CodeCheck_bar 함수를 확인할 수 있다.

해당 함수를 분석해보면 문자열을 처리하는 코드들과 비교하는 코드들이 보인다. 좀 더 내려가다 보면 strncmp 함수를 이용하여 문자열을 비교하는 것을 확인할 수 있다.


해당 부분을 통해 Secret String이 23자리인 것으로 확인할 수 있고 strncmp 함수를 통해 사용자 입력값과 평문의 Secret String과 비교하는 것으로 추측할 수 있다. 

다음은 strncmp의 인수를 출력하여 secret string을 확인하는 후킹 코드이다.

setImmediate(function() {

    Java.perform(function() {

        console.log("[*] Hooking calls to System.exit");

       

        var strncmp = undefined;

        imports = Module.enumerateImportsSync("libfoo.so");


        for(i = 0; i < imports.length; i++) {

        if(imports[i].name == "strncmp") {

                strncmp = imports[i].address;

                break;

            }


        }


        Interceptor.attach(strncmp, {

            onEnter: function (args) {

               if(args[2].toInt32() == 23 && Memory.readUtf8String(args[0],23) == "01234567890123456789012") {

                    console.log("[*] Secret string at " + args[1] + ": " + Memory.readUtf8String(args[1],23));

                }

             },

        });

        console.log("[*] Intercepting strncmp");

    });

});


Module.enumerateImportsSync()를 이용하여 libfoo.so 라이브러리에 존재하는 함수를 불러오고 해당 함수명이 strncmp일 경우 해당 함수의 주소 값을 저장한다. 

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



strcmp를 보게되면 총 3개의 인자를 갖는 것을 알 수 있다. arg[0], arg[1]은 비교할 문자열이고 arg[2]은 비교할 문자열의 길이이다.


Memory.readUtf8String(args[0],23)



JAVA 문자열은 NULL로 끝나지 않는다.  Frida의 Memory.readUtf8String 메소드로 문자열 포인터의 메모리 위치에 엑세스할 때 

길이를 제공하지 않으면 Frida는 문자열이 끝나는 곳을 알 수 없어 에러가 발생하게 된다. 따라서 두번째 인수로 읽을 문자의 양을 지정하면 에러가 해결이 된다.


   Interceptor.attach(strncmp, {

            onEnter: function (args) {

               if(args[2].toInt32() == 23 && Memory.readUtf8String(args[0],23) == "01234567890123456789012") {

                    console.log("[*] Secret string at " + args[1] + ": " + Memory.readUtf8String(args[1],23));

                }

             },

        });


arg[0], arg[1] 둘 중 어느 것이 Secret String인지는 알 수 없으나 한번씩 해보면 확인할 수 있다. "01234567890123456789012" 문자열은 특별한 의미를 가지고 있는 것이 아니고 23자리의 사용자 입력 값이다.

즉, 첫번째 인자가 사용자가 입력한 값과 동일하고 세번째 인자가 23인 strncmp 함수만을 후킹하기 위한 if문이다.



frida -U -f 옵션을 이용하여 uncrackable2 앱을 실행시킨 후 위의 코드를 입력하여 실행시킨다.

(python을 이용하여 후킹 코드를 작성하여도 되나  에러메시지가 지속적으로 발생하여 아래의 방법을 이용하였다.)





해당 코드를 실행시킨 후 코드의 IF 문을 만족할 수 있도록 "01234567890123456789012"를 입력하고 VERIFY 버튼을 눌러주자.


다시 시도해보라는 메시지가 출력됐지만 stncmp 문의 두번째 인자를 출력하는 후킹 코드로 인해 Secret string을 확인할 수 있다.



확인한 문자열을 입력한 결과 success 메시지가 출력되는 것을 확인할 수 있다.



TAGS.

Comments