효투의 세상 로딩중...
효투의 세상 로딩중...
반응형

iOS 앱은 소스코드를 직접 볼 수 없기 때문에 리턴값을 변조하는 기존의 후킹 방식도 좋지만

좀 더 다양한 Hooking을 알고있으면 도움이 많이 된다.

라이브러리를 건드리는게 아니라면 대표적으로 3가지 정도의 후킹방식이 사용되는데

1. 특정 메소드의 Return 변조

2. Symbol이 존재하지않는 서브루틴(sub)의 Return 변조

3. 특정 Function 실행주소를 후킹 후 Register 변조

 

위 3가지 정도만 알아도 분석만 잘한다면 웬만한 탈옥 탐지 로직은 우회가 가능하다고 생각한다.

 

특정 메소드의 Return 변조

먼저 메소드의 리턴 변조는 너무 잘알려져있고 기본이 되는 후킹방식이다

분기문에서 탈옥이 탐지되어 True가 반환되면 False로 바꾸는 원리로 동작

Dvia에선 Jailbreak Test 2의 탈옥탐지 방식이다

test 2를 터치했을 때 실행되는 메소드는 아래처럼

Trace로 볼 수 있는데 +[JailbreakDetection isJailbroken] 가 실행된다고 출력되었다.

-m 옵션으로 Trace가 되는 애들 자체가 [클래스 메소드]라서 메소드에서 탈옥탐지를 하는 것이다

 

스크립트 코드 보기

더보기

스크립트 코드 보기

var colors = {
    "resetColor": "\x1b[0m",
    "green": "\x1b[32m",
    "yellow": "\x1b[33m",
	"Cyan": "\x1b[36m",
    "red": "\x1b[31m"
}

function bypassJailbreakDetection() {
	try {
		var className = "JailbreakDetection";
        var funcName = "+ isJailbroken";
        var hook = eval('ObjC.classes.' + className + '["' + funcName + '"]');
        Interceptor.attach(hook.implementation, {
          onLeave: function(retval) {
			console.log(" ");
			console.log(colors.green,"\n[*] Started: Hooking.... ",colors.resetColor);
			console.log(colors.yellow,"[*] Class Name: " + className,colors.resetColor);
			console.log(colors.red,"[*] Method Name: " + funcName,colors.resetColor);
			console.log(" ");
            console.log(colors.Cyan,"\t[-] Type of return value: " + typeof retval,colors.resetColor);
            console.log(colors.Cyan,"\t[-] Original Return Value: " + retval,colors.resetColor);
			console.log(" ");
            retval.replace(0x0);
            console.log(colors.green,"\t[-] Type of return value: " + typeof retval);
            console.log(colors.green,"\t[-] Return Value: " + retval);
          }
        });
        
	} catch(err) {
		console.log("[-] Error: " + err.message);
	}
}

if (ObjC.available) {
	bypassJailbreakDetection();
} else {
 	send("error: Objective-C Runtime is not available!");
}

 

그래서 위 처럼 클래스와 메소드를 후킹하는 기본적인 스크립트로 앱에 붙어서 동작하게되면

리턴값이 1(True)에서 0(False)로 변조되며 탈옥탐지가 우회된다.

 

하지만 Test 2를 제외한 Test 1 ~ Test 5까지는 특정 메소드의 호출이 이루어지지 않는다.

Trace로 살펴보면 

Tapped 라는 메소드뿐 그 아래에선 아무런 메소드 호출을 추적하지 못하는 것을 볼 수있다.

하지만 실제로 디컴파일러 도구로 분석을 해보면

이렇게 BL 명령어로 Test 1 과 관련된 어떤것을 호출하는 것을 볼 수 있고

Function Window 창에도 Test 1과 관련된 기능이 Tapped 메소드를 제외한 1개가 더 검색이 된다.

 

이는 3,4,5 Test에서도 마찬가지이다.

 

 

특정 Function 실행주소를 후킹 후 Register 변조

 

BL로 분명 호출되었음에도 Trace가 캐치하지못하는건 메소드가아니기 때문이며 Function 어떠한 함수로서 호출이 된다.

그래서 Frida Trace에서 i 옵션을 주어 추적을 해야한다.

i 옵션으로 펑션 트레이싱을 하게되면

아까부터 훨씬 더 많은 52개의 펑션에 대해서 트레이싱을 시작하고 

Test 1 , Test 3 등에서 메소드가 아닌 Function의 호출까지 다 트레이싱을 하는것을 볼 수 있다.

 

트레이스에 추적된 Function의 생김새를 보더라도

+나 - 인자가 없어서 클래스 메소드인지 인스턴스 메소드인지 아예 정의가 안되어있어 메소드가 아닌 것을 확인할 수 있고

클래스명이라고 볼 수 있는 요소도 딱히 없다

Test 1 Tapped에서 마지막으로 호출되는 _T07DVIA_v213DVIAUtilitiesC9showAlertySb28forJailbreakTestIsJailbroken_So16UIViewControllerC04viewL0tFZ()

펑션을 IDA에서 보면

어딘가에서 값을 받아와서 W0 레지스터에 저장된 값으로 분기하여 Jailbroken 메시지를 보내는것을 알 수있다.

하지만 

_T07DVIA_v213DVIAUtilitiesC9showAlertySb28forJailbreakTestIsJailbroken_So16UIViewControllerC04viewL0tFZ()

이런식으로 메소드가 아닌것은 위의 메소드 후킹 스크립트로 후킹은 절대 할 수가 없다.

그래서 레지스터 변조를 통하여 W0의 값을 변조해줘야한다.

레지스터 변조를 위해선 아래와 같은 스크립트를 사용할 수 있다.

오픈소스로 공개되어있는 스크립트를 가시성 좋게 수정을 하였다

※ 참고로 W0,W1,W2 .... 레지스터와 X0,X1,X2 ... 레지스터는 같은 값을 가진다 아키텍처에 따라 구분이 달라짐
반응형

스크립트 코드 보기

더보기

스크립트 코드 보기

if(ObjC.available){
    try{
        var module_base = Module.findBaseAddress('DVIA-v2');  // input Application Mach-O or Binary name 
        var sub = module_base.add(0x1CBDD0);   // input Hook Address
	   var moval = "0x00";     // input Modify Value
         Interceptor.attach(sub, {   
            onEnter: function (args) {
				
				console.log("");
				console.log("\x1b[37;1m"+"Sub Address:" + sub);
				console.log("\x1b[36;1m"+"Base Address:" + module_base);
                console.log("\x1b[0m"+"[+] Call Func\n");
			 console.log("\x1b[33m"+"[+] args() Value");
                console.log("\x1b[0m"+"args[2]: " + args[2] + "\nargs[3]: "+args[3] + "\nargs[4]: "+ args[4]+ "\nargs[5]: " + args[5]+ "\nargs[6]: " + args[6]+"\n");
			console.log("\x1b[33m"+"[+] Register x() Value"+"\x1b[0m");	
			 var allRegisters = JSON.parse(JSON.stringify(this.context));
                 for (var i = 0; i <= 30; i++) {
                    if (allRegisters['x' + i] !== undefined) {
                        if (allRegisters['x' + i] == 0x1) {
                            console.log("\x1b[36mx" + i + " : " + allRegisters['x' + i].toString() + "\x1b[0m");
                        } else {
                           // console.log("x" + i + " : " + allRegisters['x' + i].toString());
                        }
                    } else {
                        break;
                    }
                } 
			this.context.x0 = moval;  // input this.context.Modify Register --- ex) this.context.x1
			console.log("");
			console.log("\x1b[31;1m"+"[+] Modify Register : "+moval+"\x1b[0m");
			return 0 ;
            },
      
        });
    }
    catch(e){
        console.log("[!] Error: " + e);
    }
}
else {
    console.log("Objective-C Runtime is not available!");
}

 

스크립트를 동작하게되면 아래와 같이 x0부터 x28까지의 레지스터값들중에 0x1 값이 들어있는 레지스터들을

콘솔에 찍어주고 그 중에 스크립트의 28번 라인에서 지정한 x0 레지스터가 0x0으로 변환되어 Not Jailbroken 알림을

띄우게된다.

 

        var module_base = Module.findBaseAddress('DVIA-v2');  // input Application Mach-O or Binary name 
        var sub = module_base.add(0x1CBDD0);   // input Hook Address
	   var moval = "0x00";     // input Modify Value

스크립트에서 처음에 변수로 지정되는 3개의 부분을 설명하면

 var module_base = Module.findBaseAddress('DVIA-v2');

Frida의 Module 함수는 위처럼 대상앱의 바이너리 파일을 지정해주면 그 실행파일의 Offset값을 찾아준다.

그래서 DVIA-v2 바이너리의 Offset 주소를 현재 module_base 라는 변수에 저장하는 것

그리고 그 아래에 sub 변수는

위에서 module_base 변수에 저장된 바이너리의 Offset값을 어떤 주소에 더하기(+)를 해주는 것이다.

 var sub = module_base.add(0x1CBDD0);

여기서 말하는 어떤 주소는 레지스터 변조를하기위한 주소이다

 

나는 저 TBZ 분기문에서 W0 레지스터를 변조하기를 원했고

IDA에서 알려주는 주소 1CBDD0을 Offset 값과 더해줌으로써 실제로 메모리에서 이 분기문이 실행되는 주소를 

sub 변수에 저장할 수 있다.

offset값을 보기위해선 lldb로 간단히 확인할 수 있다

image list 명령어를 입력하여 나오는 바이너리의 Offset값은 아래 102c5c000이다

실제로

계산기를 이용해서 두 주소값을 더해보면

콘솔에 찍힌 실제 후킹이되었던 주소 Sub Address와 동일한걸 볼수있다.

Base Address는 Offset 값이다.

이렇게 주소구하기가 쉬운데 굳이 Frida를 사용하는 이유는 Offset 값은 실행할 때마다

항상 변경되기 때문에 Frida가 직접 구해주는 Module을 사용하게되면 편하게 후킹을 할 수있다.

어쨌든 이렇게 구해지는 주소값에 

28번 라인의 this.context.x0 = moval; 코드 한줄을 통해서

0x102e27dd0의 주소가 가진 x0이라는 레지스터에 0x0 값을 저장해주면서 우회가 된다.

별거 아닌것같지만 이런 레지스터 변조로 우회가되는 금융앱들도 존재한다.

 

Symbol이 존재하지않는 서브루틴(sub)의 Return 변조

 

서브루틴 변조는 이전에 한번 포스팅을 해뒀어서 간단하게 보고 넘어가면

디컴파일러에서 또는 Frida에서 클래스나 메소드명을 찾지 못하는 기능들이 있다.

알아본 바로는 Symbol이 없어서 그렇다고하는데 잘 모르겠다.

어쨌든 이런 서브루틴 함수들도 후킹을 하기위해선 주소값으로 후킹하여

리턴을 변조할 뿐 그 원리는 레지스터 변조와 동일하다

스크립트 코드 보기

더보기

스크립트 코드 보기

if(ObjC.available){
    try{
        var module_base = Module.findBaseAddress('바이너리');  //앱 Base명 입력
        var sub = module_base.add(주소);   // 오프셋 값 입력

         Interceptor.attach(sub, {   
            onEnter: function (args) {
				
				console.log("");
				console.log("서브루틴 주소:" + sub);
				console.log("베이스주소:" + module_base);
            },
            onLeave: function (retval) {
            console.log("\x1b[31;1m"+"\t[-] Original Return Value: " + retval);
            retval.replace(0x0); // 리턴값 변환
            console.log("\x1b[34;1m"+"\t[-] Modify Return Value: " + retval);
            
            }
        });
    }
    catch(err){
        console.log("[!] Error: " + err.message);
    }
}
else {
    console.log("Objective-C Runtime is not available!");
}

 

DVIA에서는 해당 탈옥 탐지 로직이 존재하지않고

몇몇 상용앱에서는 아래와 같은 방식으로 서브루틴 안에 탈옥탐지 로직이 있다.

 

그럼 이 서브루틴이 시작되는 주소에 위 스크립트로 후킹을 걸면된다

레지스터와 같은 원리로 역시 

시작주소+Offset 주소가 더해져서 

실제로 해당 서브루틴이 동작하는 주소에 후킹이 걸리고

그 끝에 리턴값이 변조된다.

 

반응형
  • hyotwo7658@gmail.com

복사 완료 👍