Self-Improvement

[UnCrackable-Level 2] Frida Hooking ( libc.so, Native, Memory.readUtf8String, toInt32() ) 본문

AOS/UnCrackable

[UnCrackable-Level 2] Frida Hooking ( libc.so, Native, Memory.readUtf8String, toInt32() )

JoGeun 2021. 11. 30. 19:32

UnCrackable-Level 2

 

System.exit() 후킹

UnCrackable-Level 2을 실행하면 Level1과 동일하게 루팅을 탐지하는 로직이 존재하여 바로 종료가 되어진다.
APK를 추출한 후 디컴파일하여 해당 로직을 확인해보니 이전 레벨과 동일하게 b 클래스의 a(), b(), c() 메소드를 확인하여 하나라도 참이면 루팅으로 간주하게 된다.

 

그리고는 System.exit()를 실행하여 그대로 종료가 되어짐으로 후킹하여 종료되지 않도록 해본다.

import frida, sys

Hook_package = "owasp.mstg.uncrackable2"


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)

 

Secret 분석

루팅을 탐지하는 로직을 후킹하여 우회하였으니 다음에는 Secret 값 입력하여 검증에 성공해본다.
먼저 로직을 확인해보면 입력한 값을 obj 변수로 받아 m.a() 메소드 인자로 주어 실행하게 된다.

 

m.a(obj) 메소드를 실행하게 되면 native로 선언된 bar 메소드를 실행한 값을 리턴하여 참이면 Secret 값이 정확하다고 출력하게 된다.
이때 Secret가 정확하다고 출력하게 만드는 경우의 수는 여러가지 존재하며 각각 작성해보도록 한다.

 

CASE 1 - m.a(obj) 후킹

첫 번째 케이스로는 m.a(obj) 메소드를 호출하는 CodeCheck 클래스의 a() 메소드를 후킹하여 리턴되는 값을 참으로 만들어 Secret 값에 성공하도록 해본다.
jscode = """
    console.log("[*] Injected JS");
    Java.perform(function() {
        var system = Java.use("java.lang.System");
        system.exit.implementation = function(){
            console.log("[*] System.exit hooking");
        }
        var check = Java.use("sg.vantagepoint.uncrackable2.CodeCheck");
        check.a.implementation = function(arg1){
            var ret = this.a(arg1);
            console.log("[*] Original ret = "+ret);
            ret = true;
            console.log("[*] Replace ret = "+ret);
            return ret;
        }
    });
"""

 

Case 2 - Native 함수 리턴 값 후킹

두 번째 방식으로는 CodeCheck 클래스에 존재하는 bar() 메소드는 Native로 되어 있으며 메인 액티비티에서 libfoo.so 라이브러리를 사용하는 것을 알 수 있다.

 

libfoo.so 라이브러리를 IDA로 열어서 bar 함수를 확인해보면 바로 Secret 값을 알 수 있으나 모른다는 가정하에 해당 함수가 종료되고 리턴되는 result 값을 1로 후킹하여 Secert이 정확하다고 출력해본다.

jscode = """
    console.log("[*] Injected JS");
    Java.perform(function() {
        var system = Java.use("java.lang.System");
        system.exit.implementation = function(){
            console.log("[*] System.exit hooking");
        }
    });
    Interceptor.attach(Module.getExportByName("libfoo.so","Java_sg_vantagepoint_uncrackable2_CodeCheck_bar"),{
        onEnter: function(args){
        },
        onLeave: function(ret){
            console.log("[*] Original ret = "+ret);
            ret.replace(1);
            console.log("[*] Replace ret = "+ret);
        }
    });
"""

 

Case 3 - Strncmp() 후킹

세 번째 방식으로는 앞서 보았던 Native 함수에서 리턴값을 후킹하였다면 이번에는 직접적인 입력값과 Secert 값을 비교하는 strncmp() 함수를 후킹하여 확인해본다.
strncmp() 함수의 인자 두 번째가 secert 값임으로 두 번째 파라미터만 후킹하여 출력해본다.

jscode = """
    console.log("[*] Injected JS");
    Java.perform(function() {
        var system = Java.use("java.lang.System");
        system.exit.implementation = function(){
            console.log("[*] System.exit hooking");
        }
    });
    Interceptor.attach(Module.getExportByName("libfoo.so","strncmp"),{
        onEnter: function(args){
            var secret = args[1];
            
            console.log(secret);
        },
        onLeave: function(ret){
        }
    });
"""

 

수정 1

위 방식대로 하면 수많은 값이 출력됨으로 다시 bar() 함수를 확인해보니 strncmp()를 수행할 때 23byte 길이를 확인함으로 후킹할때도 동일하게 설정을 하여 정확성을 높여준다.
그리고 실행하여 23길이의 아무 값이나 입력하면 hex 값으로 무언가가 출력이 되어진다.

jscode = """
    console.log("[*] Injected JS");
    Java.perform(function() {
        var system = Java.use("java.lang.System");
        system.exit.implementation = function(){
            console.log("[*] System.exit hooking");
        }
    });
    Interceptor.attach(Module.getExportByName("libfoo.so","strncmp"),{
        onEnter: function(args){
            var secret = args[1];
            var len = args[2];
            if(len == 0x17){
                console.log("[*] Secert Val : "+secret);
            }
        },
        onLeave: function(ret){
        }
    });
"""

 

수정 2

메모리에 있는 값을 그대로 읽어오게 됨으로 hex 값을 string으로 읽어도록 후킹 코드를 수정하면 성공적으로 Secert 값을 얻어올 수 있다.
그리고 if 문에서 toInt32()를 사용하면 특정 상황에서 오류가 안나고 정확성이 높아진다고 한다...
jscode = """
    console.log("[*] Injected JS");
    Java.perform(function() {
        var system = Java.use("java.lang.System");
        system.exit.implementation = function(){
            console.log("[*] System.exit hooking");
        }
    });
    Interceptor.attach(Module.getExportByName("libfoo.so","strncmp"),{
        onEnter: function(args){
            var secret = args[1];
            var len = args[2];
            if(len.toInt32() == 0x17){
                console.log("[*] Secert Val : "+Memory.readUtf8String(secret,23));
            }
        },
        onLeave: function(ret){
        }
    });
"""