Self-Improvement

[UnCrackable-Level 3] Frida Hooking ( Sequence, Memory.protect , context) 본문

AOS/UnCrackable

[UnCrackable-Level 3] Frida Hooking ( Sequence, Memory.protect , context)

JoGeun 2021. 12. 1. 01:37

Uncrackable Level 3

 

Uncrackable Level 3 을 실행하면 Rooting or tampering이 탐지되었다고 한다.
이를 우회하기 위해서 디컴파일하여 소스코드를 확인해보면 각 검사하는 메소드들 중 참이 하나라도 존재하면 showDialog() 메소드가 동작되어 진다.

 

showDialog() 메소드가 실행되면 System.exit(0)이 실행되어 어플리케이션이 그대로 종료가 되어진다.

 

System.exit() 우회

위에서 분석된 내용을 토대로 어플리케이션이 종료되는 System.exit()를 후킹하여 우회해본다.
import frida, sys

Hook_package = "owasp.mstg.uncrackable3"


def on_message(message, data):
    if message['type'] == 'send':
        print("[*] {0}".format(message['payload']))
    else:
        print(message)


jscode = """
    console.log("[*] Injected JS");
    Java.perform(function() {
        var system = Java.use("java.lang.System");
        system.exit.implementation = function(){
            console.log("[*] System.exit hooking");
        }
    });
"""

try:
    device = frida.get_usb_device(timeout=5)
    pid = device.spawn([Hook_package])
    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 FR1DA!")
    script.load()
    sys.stdin.read()

except Exception as e:
    print(e)

 

Tampering 분석

System.exit() 함수를 제대로 후킹하여 실행하였지만 여전히 우회가 되지 않음으로 tampering 이라는 것에 의하여 안된다는 것으로 추측하여 다른 것을 찾아보기로 하였다.
여러가지 확인하던 중 어플리케이션을 실행하면서 logcat에서 의심할만한 로그가 출력되는 것을 알 수 있었다.

 

또한 frida -Uf owasp.mstg.uncrackable3 --no-pause을 실행하였을 때 backtrace 부분에서 libfoo.so 라이브러리의 goodbye() 함수가 실행된 것을 알 수 있었다.

 

 

해당 문자열은 자바 소스코드에는 존재하지 않으며 MainActivity 을 보면 libfoo.c 라이브러리를 로드하는 것을 보고 grep 명령어를 수행하였더니 libfoo.so 라이브러리 파일에서 사용된다는 것을 알게되어 분석하기로 하였다.

 

libfoo.so 라이브러리의 sub_23C4() 함수에서 프로세스의 maps를 보고 frida와 xposed 문자열이 존재하면 "Tampering detected! Terminating..." 문자열 로그를 출력하고 goodbye() 함수가 수행하는 로직이 존재한다.

 

sub_23C4() 함수를 호출하는 로직은 sub_2468()이며 이는 init_arrary에서 수행되는 함수이다.
그리고 init_arrary는 main() 함수를 실행하기전에 수행됨으로 라이브러리가 로드되고 수행되기전 init_arrary에 의하여 Tampering을 탐지하게 되는 것이다.

 

Tampering 우회 - strstsr()

이제 Tampering을 탐지하는 로직을 우회하기 위해서 strstr()에서 "frida"와 "xposed" 문자열을 검사하는 부분을 후킹하여 로그가 출력되지 않도록 해준다.
이때 후킹하는 순서도 중요하며 system.exit()를 수행하기전에 Tampering 탐지가 이루어짐으로 먼저 작성해주도록 한다.

jscode = """
    console.log("[*] Injected JS");
    Interceptor.attach(Module.findExportByName("libc.so", "strstr"), {
        onEnter: function(args) {
            this.return = 0;
            var haystack = args[0];
            var needle = Memory.readUtf8String(args[1]);
            if(needle == "frida" || needle == "xposed"){
                this.return = 1;
            }
        },
        onLeave: function(retval) {
            if (this.return == 1){
                retval.replace(0);
            }
            return retval;
        }
    });
    Java.perform(function() {
        var system = Java.use("java.lang.System");
        system.exit.implementation = function(){
            console.log("[*] System.exit hooking");
        }
    });
"""

 

Tampering 우회 2 - strstr(), indexOf()

indexOf()를 사용하여 할 수 있다. indexOf에서 인자로 받은 문자열을 존재하는지 찾기되며 해당 포인터? 값을 리턴하는데 존재하지 않다면 -1을 리턴하게 된다.
그리하여 -1이 아닐 경우 즉 "frida"와 "xposed" 문자열이 존재할 경우에 if문이 돌아가도록 하였다.
jscode = """
    console.log("[*] Injected JS");
    Interceptor.attach(Module.findExportByName("libc.so", "strstr"), {
        onEnter: function(args) {
            this.return = 0;
            var haystack = args[0];
            var needle = Memory.readUtf8String(args[1]);
            if(needle.indexOf("frida") != -1 || needle.indexOf("xposed") != -1){
                this.return = 1;
            }
        },
        onLeave: function(retval) {
            if (this.return == 1){
                retval.replace(0);
            }
            return retval;
        }
    });
    Java.perform(function() {
        var system = Java.use("java.lang.System");
        system.exit.implementation = function(){
            console.log("[*] System.exit hooking");
        }
    });
"""

 

Tampering 우회 3 - fopen()

https://hackcatml.tistory.com/105

위의 블로그를 참조하여 작성하였다.

Memory.protect(args[0], 16, 'rwx');을 안하면 frida가 메모리 영역에 write할 수 있는 권한이 없음으로 작성함으로써 가능하게 해준다.
jscode = """
    console.log("[*] Injected JS");
    Interceptor.attach(Module.findExportByName("libc.so", "fopen"), {
        onEnter: function(args) {
            if (Memory.readUtf8String(args[0]) == "/proc/self/maps"){
                Memory.protect(args[0], 16, 'rwx');  // 메모리 접근 권한
                Memory.writeUtf8String(args[0], "/proc/self/stat");
            }
        },
        onLeave: function(retval) {
        }
    });
    Java.perform(function() {
        var system = Java.use("java.lang.System");
        system.exit.implementation = function(){
            console.log("[*] System.exit hooking");
        }
    });
"""

 

문자열을 입력한 후 VERIFY 버튼을 클릭하게 되면 알맞지 않다는 텍스트 박스가 출력되어 진다.

 

MainActivity에서 확인해보면 입력받은 문자열을 obj 변수로 받은 후 check 클래스의 check_code() 메소드의 인자로 사용되고 있다.

 

check_code() 메소드를 확인해보면 네이티브로 정의된 bar()의 리턴된 값을 그대로 리턴하게 되고 참일 경우에는 secert 값이 맞다고 출력되고 아닐 경우에는 틀렸다고 출력하게 된다.

 

check_code() 후킹

위 문제를 풀기 위한 첫 번째 방법은 역시나 check_code() 메소드의 리턴값을 무조건 참으로 후킹하면 될 것 이다.
jscode = """
    console.log("[*] Injected JS");
    Interceptor.attach(Module.findExportByName("libc.so", "strstr"), {
        onEnter: function(args) {
            this.return = 0;
            var haystack = args[0];
            var needle = Memory.readUtf8String(args[1]);
            if(needle.indexOf("frida") != -1 || needle.indexOf("xposed") != -1){
                this.return = 1;
            }
        },
        onLeave: function(retval) {
            if (this.return == 1){
                retval.replace(0);
            }
            return retval;
        }
    });
    Java.perform(function() {
        var system = Java.use("java.lang.System");
        system.exit.implementation = function(){
            console.log("[*] System.exit hooking");
        }
        
        var ck = Java.use("sg.vantagepoint.uncrackable3.CodeCheck");
        ck.check_code.implementation = function(arg1){
            var retval = this.check_code(arg1);
            console.log("[*] return val = "+retval);
            return true;
        }
    });
"""

 

bar() 후킹

https://richong.tistory.com/428
https://m.blog.naver.com/gigs8041/221982471693
https://chp747.tistory.com/362

bar() 함수에서 qword_15038에 있는 값을 출력해보니 XOR key인 "pizzapizza..."가 존재하였다.
XOR Key 값과 입력한 값을 XOR한 후 어떤 특정한 값과 1byte 씩 비교하게 되는데 이때 비교하는 offset 주소 0x3450을 후킹하여 강제로 서로 같게 한 후 W12(=X12)를 출력하게 하면 Secret 값을 얻을 수 있다.

jscode = """
    Interceptor.attach(Module.findExportByName("libc.so", "fopen"), {
        onEnter: function(args) {
            if (Memory.readUtf8String(args[0]) == "/proc/self/maps"){
                Memory.protect(args[0], 16, 'rwx'); 
                Memory.writeUtf8String(args[0], "/proc/self/stat");
            }
        },
        onLeave: function(retval) {
        }
    });


    function native_bar(){
        var foo = Module.findBaseAddress('libfoo.so');
        if(!foo){
            console.log("libfoo not loaded!");
            return 0;
        }
        console.log("[*] libfoo.so = " +foo); // == foo.toString()
        //var base_off = foo.add(0x15038);    // XOR Key
        //console.log("[*] base_off = "+ hexdump(base_off, {
        //    offset: 0,
        //    length: 32,
        //}));
        
        var cmp_key = foo.add(0x3450); // CMP W12, W10
        Interceptor.attach(cmp_key,{
            onEnter: function(args) {
                //console.log("[*] context = "+JSON.stringify(this.context));
                this.context.x12 = this.context.x10; 
                console.log("[*] x12 = "+this.context.x12);
            },
            onLeave: function(retval) {
            }
        });
    }
    
    Java.perform(function() {
        var system = Java.use("java.lang.System");
        system.exit.implementation = function(){
            console.log("[*] exit hooking");
            native_bar();
        }
    });
"""