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

안드로이드도 ios와 마찬가지로 프리다를 이용한 후킹으로 루팅탐지를 우회할 수 있다.

2022.05.03 - [모바일/Ios] - [ios] 프리다 후킹 (Frida Hooking)을 통한 탈옥 탐지 우회 방법

 

[ios] 프리다 후킹 (Frida Hooking)을 통한 탈옥 탐지 우회 방법

최근 탈옥우회 트윅들이 퀄리티가 좋지않다... 애용했던 FlyJB X도 개발이 멈춰 지원이 되지않고 ios 14.3 이하버전이 아니라면 호환이 되지않음 그래서 어쩔수 없이 코드 분석을 통해 변조로 우회

hyotwo.tistory.com

 

동작 방식이나 원리는 ios와 동일하며 아래 github에서 스크립트들을 구할 수 있다.

https://github.com/noobpk/frida-android-hook

 

GitHub - noobpk/frida-android-hook: A script that helps you trace classes, functions, and modify the return values of methods on

A script that helps you trace classes, functions, and modify the return values of methods on Android platform - GitHub - noobpk/frida-android-hook: A script that helps you trace classes, functions,...

github.com

 

ios와 마찬가지로 Android에서 루팅 탐지를 하는 대표적인 방법들이 있는데 

그 로직들을 일괄적으로 확인하여 우회해주는 스크립트도 있다

실제로 몇 개의 앱에서 테스트한 결과 매우 잘된다.

 

코드는 아래와 같은데 대표적인 파일이나 패키지 su, superuser, busybox, magisk 등등을 우회대상으로 작성된 것을

볼 수 있다.

// $ frida -l antiroot.js -U -f com.example.app --no-pause
// CHANGELOG by Pichaya Morimoto (p.morimoto@sth.sh): 
//  - I added extra whitelisted items to deal with the latest versions 
// 						of RootBeer/Cordova iRoot as of August 6, 2019
//  - The original one just fucked up (kill itself) if Magisk is installed lol
// Credit & Originally written by: https://codeshare.frida.re/@dzonerzy/fridantiroot/
// If this isn't working in the future, check console logs, rootbeer src, or libtool-checker.so
Java.perform(function() {

    var RootPackages = ["com.noshufou.android.su", "com.noshufou.android.su.elite", "eu.chainfire.supersu",
        "com.koushikdutta.superuser", "com.thirdparty.superuser", "com.yellowes.su", "com.koushikdutta.rommanager",
        "com.koushikdutta.rommanager.license", "com.dimonvideo.luckypatcher", "com.chelpus.lackypatch",
        "com.ramdroid.appquarantine", "com.ramdroid.appquarantinepro", "com.devadvance.rootcloak", "com.devadvance.rootcloakplus",
        "de.robv.android.xposed.installer", "com.saurik.substrate", "com.zachspong.temprootremovejb", "com.amphoras.hidemyroot",
        "com.amphoras.hidemyrootadfree", "com.formyhm.hiderootPremium", "com.formyhm.hideroot", "me.phh.superuser",
        "eu.chainfire.supersu.pro", "com.kingouser.com", "com.android.vending.billing.InAppBillingService.COIN","com.topjohnwu.magisk"
    ];

    var RootBinaries = ["su", "busybox", "supersu", "Superuser.apk", "KingoUser.apk", "SuperSu.apk","magisk"];

    var RootProperties = {
        "ro.build.selinux": "1",
        "ro.debuggable": "0",
        "service.adb.root": "0",
        "ro.secure": "1"
    };

    var RootPropertiesKeys = [];

    for (var k in RootProperties) RootPropertiesKeys.push(k);

    var PackageManager = Java.use("android.app.ApplicationPackageManager");

    var Runtime = Java.use('java.lang.Runtime');

    var NativeFile = Java.use('java.io.File');

    var String = Java.use('java.lang.String');

    var SystemProperties = Java.use('android.os.SystemProperties');

    var BufferedReader = Java.use('java.io.BufferedReader');

    var ProcessBuilder = Java.use('java.lang.ProcessBuilder');

    var StringBuffer = Java.use('java.lang.StringBuffer');

    var loaded_classes = Java.enumerateLoadedClassesSync();

    send("Loaded " + loaded_classes.length + " classes!");

    var useKeyInfo = false;

    var useProcessManager = false;

    send("loaded: " + loaded_classes.indexOf('java.lang.ProcessManager'));

    if (loaded_classes.indexOf('java.lang.ProcessManager') != -1) {
        try {
            //useProcessManager = true;
            //var ProcessManager = Java.use('java.lang.ProcessManager');
        } catch (err) {
            send("ProcessManager Hook failed: " + err);
        }
    } else {
        send("ProcessManager hook not loaded");
    }

    var KeyInfo = null;

    if (loaded_classes.indexOf('android.security.keystore.KeyInfo') != -1) {
        try {
            //useKeyInfo = true;
            //var KeyInfo = Java.use('android.security.keystore.KeyInfo');
        } catch (err) {
            send("KeyInfo Hook failed: " + err);
        }
    } else {
        send("KeyInfo hook not loaded");
    }

    PackageManager.getPackageInfo.overload('java.lang.String', 'int').implementation = function(pname, flags) {
        var shouldFakePackage = (RootPackages.indexOf(pname) > -1);
        if (shouldFakePackage) {
            send("Bypass root check for package: " + pname);
            pname = "set.package.name.to.a.fake.one.so.we.can.bypass.it";
        }
        return this.getPackageInfo.call(this, pname, flags);
    };

    NativeFile.exists.implementation = function() {
        var name = NativeFile.getName.call(this);
        var shouldFakeReturn = (RootBinaries.indexOf(name) > -1);
        if (shouldFakeReturn) {
            send("Bypass return value for binary: " + name);
            return false;
        } else {
            return this.exists.call(this);
        }
    };

    var exec = Runtime.exec.overload('[Ljava.lang.String;');
    var exec1 = Runtime.exec.overload('java.lang.String');
    var exec2 = Runtime.exec.overload('java.lang.String', '[Ljava.lang.String;');
    var exec3 = Runtime.exec.overload('[Ljava.lang.String;', '[Ljava.lang.String;');
    var exec4 = Runtime.exec.overload('[Ljava.lang.String;', '[Ljava.lang.String;', 'java.io.File');
    var exec5 = Runtime.exec.overload('java.lang.String', '[Ljava.lang.String;', 'java.io.File');

    exec5.implementation = function(cmd, env, dir) {
        if (cmd.indexOf("getprop") != -1 || cmd == "mount" || cmd.indexOf("build.prop") != -1 || cmd == "id" || cmd == "sh") {
            var fakeCmd = "grep";
            send("Bypass " + cmd + " command");
            return exec1.call(this, fakeCmd);
        }
        if (cmd == "su") {
            var fakeCmd = "justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled";
            send("Bypass " + cmd + " command");
            return exec1.call(this, fakeCmd);
        }
        if (cmd == "which") {
            var fakeCmd = "justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled";
            send("Bypass which command");
            return exec1.call(this, fakeCmd);
        }
        return exec5.call(this, cmd, env, dir);
    };

    exec4.implementation = function(cmdarr, env, file) {
        for (var i = 0; i < cmdarr.length; i = i + 1) {
            var tmp_cmd = cmdarr[i];
            if (tmp_cmd.indexOf("getprop") != -1 || tmp_cmd == "mount" || tmp_cmd.indexOf("build.prop") != -1 || tmp_cmd == "id" || tmp_cmd == "sh") {
                var fakeCmd = "grep";
                send("Bypass " + cmdarr + " command");
                return exec1.call(this, fakeCmd);
            }

            if (tmp_cmd == "su") {
                var fakeCmd = "justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled";
                send("Bypass " + cmdarr + " command");
                return exec1.call(this, fakeCmd);
            }
        }
        return exec4.call(this, cmdarr, env, file);
    };

    exec3.implementation = function(cmdarr, envp) {
        for (var i = 0; i < cmdarr.length; i = i + 1) {
            var tmp_cmd = cmdarr[i];
            if (tmp_cmd.indexOf("getprop") != -1 || tmp_cmd == "mount" || tmp_cmd.indexOf("build.prop") != -1 || tmp_cmd == "id" || tmp_cmd == "sh") {
                var fakeCmd = "grep";
                send("Bypass " + cmdarr + " command");
                return exec1.call(this, fakeCmd);
            }

            if (tmp_cmd == "su") {
                var fakeCmd = "justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled";
                send("Bypass " + cmdarr + " command");
                return exec1.call(this, fakeCmd);
            }
        }
        return exec3.call(this, cmdarr, envp);
    };

    exec2.implementation = function(cmd, env) {
        if (cmd.indexOf("getprop") != -1 || cmd == "mount" || cmd.indexOf("build.prop") != -1 || cmd == "id" || cmd == "sh") {
            var fakeCmd = "grep";
            send("Bypass " + cmd + " command");
            return exec1.call(this, fakeCmd);
        }
        if (cmd == "su") {
            var fakeCmd = "justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled";
            send("Bypass " + cmd + " command");
            return exec1.call(this, fakeCmd);
        }
        return exec2.call(this, cmd, env);
    };

    exec.implementation = function(cmd) {
        for (var i = 0; i < cmd.length; i = i + 1) {
            var tmp_cmd = cmd[i];
            if (tmp_cmd.indexOf("getprop") != -1 || tmp_cmd == "mount" || tmp_cmd.indexOf("build.prop") != -1 || tmp_cmd == "id" || tmp_cmd == "sh") {
                var fakeCmd = "grep";
                send("Bypass " + cmd + " command");
                return exec1.call(this, fakeCmd);
            }

            if (tmp_cmd == "su") {
                var fakeCmd = "justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled";
                send("Bypass " + cmd + " command");
                return exec1.call(this, fakeCmd);
            }
        }

        return exec.call(this, cmd);
    };

    exec1.implementation = function(cmd) {
        if (cmd.indexOf("getprop") != -1 || cmd == "mount" || cmd.indexOf("build.prop") != -1 || cmd == "id" || cmd == "sh") {
            var fakeCmd = "grep";
            send("Bypass " + cmd + " command");
            return exec1.call(this, fakeCmd);
        }
        if (cmd == "su") {
            var fakeCmd = "justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled";
            send("Bypass " + cmd + " command");
            return exec1.call(this, fakeCmd);
        }
        return exec1.call(this, cmd);
    };

    String.contains.implementation = function(name) {
        if (name == "test-keys") {
            send("Bypass test-keys check");
            return false;
        }
        return this.contains.call(this, name);
    };

    var get = SystemProperties.get.overload('java.lang.String');

    get.implementation = function(name) {
        if (RootPropertiesKeys.indexOf(name) != -1) {
            send("Bypass " + name);
            return RootProperties[name];
        }
        return this.get.call(this, name);
    };

    Interceptor.attach(Module.findExportByName("libc.so", "fopen"), {
        onEnter: function(args) {
            var path1 = Memory.readCString(args[0]);
            var path = path1.split("/");
            var executable = path[path.length - 1];
            var shouldFakeReturn = (RootBinaries.indexOf(executable) > -1)
            if (shouldFakeReturn) {
                Memory.writeUtf8String(args[0], "/ggezxxx");
                send("Bypass native fopen >> "+path1);
            }
        },
        onLeave: function(retval) {

        }
    });

    Interceptor.attach(Module.findExportByName("libc.so", "fopen"), {
        onEnter: function(args) {
            var path1 = Memory.readCString(args[0]);
            var path = path1.split("/");
            var executable = path[path.length - 1];
            var shouldFakeReturn = (RootBinaries.indexOf(executable) > -1)
            if (shouldFakeReturn) {
                Memory.writeUtf8String(args[0], "/ggezxxx");
                send("Bypass native fopen >> "+path1);
            }
        },
        onLeave: function(retval) {

        }
    });

    Interceptor.attach(Module.findExportByName("libc.so", "system"), {
        onEnter: function(args) {
            var cmd = Memory.readCString(args[0]);
            send("SYSTEM CMD: " + cmd);
            if (cmd.indexOf("getprop") != -1 || cmd == "mount" || cmd.indexOf("build.prop") != -1 || cmd == "id") {
                send("Bypass native system: " + cmd);
                Memory.writeUtf8String(args[0], "grep");
            }
            if (cmd == "su") {
                send("Bypass native system: " + cmd);
                Memory.writeUtf8String(args[0], "justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled");
            }
        },
        onLeave: function(retval) {

        }
    });

    /*

    TO IMPLEMENT:

    Exec Family

    int execl(const char *path, const char *arg0, ..., const char *argn, (char *)0);
    int execle(const char *path, const char *arg0, ..., const char *argn, (char *)0, char *const envp[]);
    int execlp(const char *file, const char *arg0, ..., const char *argn, (char *)0);
    int execlpe(const char *file, const char *arg0, ..., const char *argn, (char *)0, char *const envp[]);
    int execv(const char *path, char *const argv[]);
    int execve(const char *path, char *const argv[], char *const envp[]);
    int execvp(const char *file, char *const argv[]);
    int execvpe(const char *file, char *const argv[], char *const envp[]);

    */


    BufferedReader.readLine.overload().implementation = function() {
        var text = this.readLine.call(this);
        if (text === null) {
            // just pass , i know it's ugly as hell but test != null won't work :(
        } else {
            var shouldFakeRead = (text.indexOf("ro.build.tags=test-keys") > -1);
            if (shouldFakeRead) {
                send("Bypass build.prop file read");
                text = text.replace("ro.build.tags=test-keys", "ro.build.tags=release-keys");
            }
        }
        return text;
    };

    var executeCommand = ProcessBuilder.command.overload('java.util.List');

    ProcessBuilder.start.implementation = function() {
        var cmd = this.command.call(this);
        var shouldModifyCommand = false;
        for (var i = 0; i < cmd.size(); i = i + 1) {
            var tmp_cmd = cmd.get(i).toString();
            if (tmp_cmd.indexOf("getprop") != -1 || tmp_cmd.indexOf("mount") != -1 || tmp_cmd.indexOf("build.prop") != -1 || tmp_cmd.indexOf("id") != -1) {
                shouldModifyCommand = true;
            }
        }
        if (shouldModifyCommand) {
            send("Bypass ProcessBuilder " + cmd);
            this.command.call(this, ["grep"]);
            return this.start.call(this);
        }
        if (cmd.indexOf("su") != -1) {
            send("Bypass ProcessBuilder " + cmd);
            this.command.call(this, ["justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled"]);
            return this.start.call(this);
        }

        return this.start.call(this);
    };

    if (useProcessManager) {
        var ProcManExec = ProcessManager.exec.overload('[Ljava.lang.String;', '[Ljava.lang.String;', 'java.io.File', 'boolean');
        var ProcManExecVariant = ProcessManager.exec.overload('[Ljava.lang.String;', '[Ljava.lang.String;', 'java.lang.String', 'java.io.FileDescriptor', 'java.io.FileDescriptor', 
        'java.io.FileDescriptor', 'boolean');

        ProcManExec.implementation = function(cmd, env, workdir, redirectstderr) {
            var fake_cmd = cmd;
            for (var i = 0; i < cmd.length; i = i + 1) {
                var tmp_cmd = cmd[i];
                if (tmp_cmd.indexOf("getprop") != -1 || tmp_cmd == "mount" || tmp_cmd.indexOf("build.prop") != -1 || tmp_cmd == "id") {
                    var fake_cmd = ["grep"];
                    send("Bypass " + cmdarr + " command");
                }

                if (tmp_cmd == "su") {
                    var fake_cmd = ["justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled"];
                    send("Bypass " + cmdarr + " command");
                }
            }
            return ProcManExec.call(this, fake_cmd, env, workdir, redirectstderr);
        };

        ProcManExecVariant.implementation = function(cmd, env, directory, stdin, stdout, stderr, redirect) {
            var fake_cmd = cmd;
            for (var i = 0; i < cmd.length; i = i + 1) {
                var tmp_cmd = cmd[i];
                if (tmp_cmd.indexOf("getprop") != -1 || tmp_cmd == "mount" || tmp_cmd.indexOf("build.prop") != -1 || tmp_cmd == "id") {
                    var fake_cmd = ["grep"];
                    send("Bypass " + cmdarr + " command");
                }

                if (tmp_cmd == "su") {
                    var fake_cmd = ["justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled"];
                    send("Bypass " + cmdarr + " command");
                }
            }
            return ProcManExecVariant.call(this, fake_cmd, env, directory, stdin, stdout, stderr, redirect);
        };
    }

    if (useKeyInfo) {
        KeyInfo.isInsideSecureHardware.implementation = function() {
            send("Bypass isInsideSecureHardware");
            return true;
        }
    }

});

 

역시 ios와 마찬가지로 클래스와 메소드를 찾아서 후킹하기전에 한번 돌려보면 좋을듯하다.

안드로이드 프리다 후킹 코드는 2개 파일이 있는데 

아래 사진에 있는 스크립트들이다.

raptor_frida 스크립트를 살펴보면 아주 간단하게 되어있다.

클래스와 메소드에 후킹을 걸어서 리턴값 변환 후 리턴값 반환

setTimeout(function() { // avoid java.lang.ClassNotFoundException

	Java.perform(function() {

		// Root detection bypass example

		var hook = Java.use("클래스"); // 루팅 탐지 클래스
		console.log("");
		console.log("info: hooking target class");
		
		// 아래에 메소드

		hook.isRooted.overload().implementation = function() {
			console.log("info: entered target method");
			
			// obtain old retval
			var retval = this.isRooted.overload().call(this);
			console.log("old ret value: " + retval);

			// set new retval
			var retnew = false;
			console.log("new ret value: " + retnew);
			return retnew;
		}

	});   

}, 0);

 

반응형

Java.use 뒤 더블쿼터 사이에 Class명을 넣어주면되고

info 로그를 찍고 난 뒤 hook.메소드.overload와 그 아래 this 와 overload 사이에 메소드를 작성하면된다.

위 hook.isRooted.overload가 실제로 메소드를 후킹하는 코드이며

아래 코드는 로그를 찍기위한 코드이다.

overload는 () 사이 인자로 자료형을 정의해준다고 생각하면 이해가 쉽다

만약 com.x.x.t 클래스안에 a라는 메소드가 2개가 있을 때 하나는 Context 형식 하나는 Boolean 형식일 때

a메소드에 후킹을 걸면 어떤걸로 잡아야할 지 혼란스러워하는데

overload에 자료형을 정의해준다. 2개다 걸고싶을 때 2개다 넣으면 됨.

function()은 overload에서 정의해준 자료형의 개수로 지정

오버로드 자료형 참고자료

.overload() 
.overload('int’) 
.overload('boolean') 
.overload('float') 
.overload('[B') // byte array 
.overload('[B', '[B', '[B') 
.overload('java.io.File') 
.overload('java.lang.String') 
.overload('android.view.View') 
.overload('android.app.Activity') 
.overload('android.content.Context') 
.overload('android.content.Context', 'java.lang.String') 
.overload('java.lang.String', 'java.lang.String') 
.overload('android.graphics.Bitmap') 
.overload('java.util.List’) 
.overload('android.content.Context', 'java.lang.String', 'java.lang.String') 
.overload('android.app.Activity', 'int', 'int', 'int', 'boolean')

어찌됐든 클래스와 메소드 기입 후 스크립트가 동작하게 되면

아래와 같이 true가 false로 바뀌며 루팅탐지가 우회된다.

 

두번째 스크립트는 더 간단하게 되어있다.

사실 첫번째 스크립트와 똑같은 방식으로 동작한다

코드는 로그 부분 등 아주 살짝 수정함


Java.perform(function() {
	var class_name = Java.use("클래스"); //클래스 
	class_name.메소드.overload().implementation = function () 
	{		
	
	var value = this.메소드.overload();
		console.log("");		
		console.log("original return value:" + typeof value);
		console.log("original return value:" + value);
		
	//	console.log("[*] CLASS_NAME:FUNC_NAME was called");		
		//  console.log("\t[+] arg1 : "+JSON.stringify(arg3));
	
	
		//console.log("");		
	//	console.log("new return value:" + value);
	
	   return false;
	}
	
});

 

클래스와 메소드를 넣고 스크립트를 동작하면 스크립트가 동작하며 루팅탐지가 우회된다.

아래 발생하는 에러는 boolean 형식으로 return값이 정의되지 않아서 그렇다

사진 찍을 때 코드 끝에 return false; 를 안해놔서 발생한 에러

 

두번째 스크립트에 의문이 생겨서

스크립트를 계속 바꿔보며 여러번 다양한 앱에 테스트해본 결과...

첫번째 스크립트도 두번째 스크립트도

모두 Return값에 대한 변조가 없어도 루팅탐지가 우회되는 기이한 결과가 생겼다.

그냥 루팅탐지코드를 후킹만 하면 루팅탐지 우회가 가능한것인지...

다른 스크립트들은 왜 리턴변조가 있는것인지 좀 더 알아봐야할것같다.

 

--------------------------------------------- 추가 (분석완료) --------------------------------------------------

2022.05.11 - [모바일/Android] - [Android] 안드로이드 Frida Hooking을 통한 루팅탐지우회 2(분석글)

 

[Android] 안드로이드 Frida Hooking을 통한 루팅탐지우회 2(분석글)

2022.05.10 - [모바일/Android] - [Android] 안드로이드 Frida Hooking을 통한 루팅탐지우회 [Android] 안드로이드 Frida Hooking을 통한 루팅탐지우회 안드로이드도 ios와 마찬가지로 프리다를 이용한 후킹으로..

hyotwo.tistory.com

 

반응형
  • hyotwo7658@gmail.com

복사 완료 👍