[Frida] 안드로이드 루팅 탐지 우회하기 3

Frida를 이용한 루팅 탐지 우회 기법

1. 루팅 관련 바이너리 존재 여부 확인

    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.rooting.apk"
    ];

    var RootBinaries = ["su", "busybox", "supersu", "Superuser.apk", "KingoUser.apk", "SuperSu.apk", "rooting.apk", "Magisk", "temprootremovejb",
        "su-backup"];
        
    PackageManager.getPackageInfo.overload('android.content.pm.VersionedPackage', '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);
    };

    var PackageManager = Java.use("android.app.ApplicationPackageManager");
    var Runtime = Java.use('java.lang.Runtime');
    var NativeFile = Java.use('java.io.File');

    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);
        }
    };

패키지 매니저나 Java.io.File에서 관련 함수를 후킹하는 탐지 기법을 우회한다.

2. "SU" 명령 실행 가능 여부 확인

"SU" 명령은 슈퍼유저 권한을 얻고자 할 때 사용되는 명령이다. 일반 환경에서는 실행이 되지 않는 명령이다. 앱에서는 직접 "SU" 명령을 실행시켜서 명령가 실행되는지 여부를 확인한다.(예외가 발생하는지 여부를 확인한다.) "SU" 명령어가 전달될 경우 임의의 문자열로 변조하여 실행하도록 하는 방법으로 루팅 탐지를 우회한다.

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);
        }
        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 at exec1");
            return exec1.call(this, fakeCmd);
        }
        return exec1.call(this, cmd);
    };

3. Test-Keys 존재 여부 확인

앱을 개발할 때 디버거 모드로 실행되는 지 여부를 확인하는 방법 중 Test-Keys로 실행되는 지 여부를 확인하는 방법을 우회한다.

    var String = Java.use('java.lang.String');
    
    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);
    };

 

4. Lib.so 우회

루팅 탐지 기법을 자바에서 구현하지 않고 JNI(C/C++)로 구현한 경우 관련 함수들을 후킹하여 우회해줘야 한다.

주로 사용하는 함수들 중 "fopen", "system", "strstr", "strtok", "open", "access"에 대해 후킹했다. 앱에서 사용하는 함수를 찾아 우회하면 된다.

Interceptor.attach(Module.findExportByName("libc.so", "fopen"), {
        onEnter: function(args) {
            var path = Memory.readCString(args[0]);
            console.log("fopen >> " + path.toString())
            path = path.split("/");
            var executable = path[path.length - 1];
            
            var shouldFakeReturn = (RootBinaries.indexOf(executable) > -1)
            
            if (shouldFakeReturn && path.indexOf("ahnlab") === -1) {
                Memory.writeUtf8String(args[0], "/notexists");
                send("Bypass native fopen");

            }
        },
        onLeave: function(retval) {

        }
    });

    Interceptor.attach(Module.findExportByName("libc.so", "system"), {
        onEnter: function(args) {

            var cmd = Memory.readCString(args[0]);
            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) {

        }
    });

    Interceptor.attach(Module.findExportByName("libc.so", "strstr"), {
        onEnter: function(args) {
            var cstr = Memory.readCString(args[1]);
            console.log("strstr >> "+ cstr.toString())
            var shouldFakeReturn = (RootBinaries.indexOf(cstr) > -1)
            if (shouldFakeReturn) {
                Memory.writeUtf8String(args[1], "/notexists");
                send("Bypass native strstr");
            }
        },
        onLeave: function(retval) {
        }
    });


    Interceptor.attach(Module.findExportByName("libc.so", "strtok"), {
        onEnter: function(args) {
            var cstr = Memory.readCString(args[1]);
            console.log("strtok >> "+ cstr.toString())
            send(cstr)
        },
        onLeave: function(retval) {

        }
    });

    Interceptor.attach(Module.findExportByName("libc.so", "open"), {
        onEnter: function(args) {
            //console.log("******** OPEN")
            var cstr = Memory.readCString(args[0]);
            console.log("fopen >> " + cstr)

        },
        onLeave: function(retval) {
            //console.log("retval : ", retval);

        }
    });

    Interceptor.attach(Module.findExportByName("libc.so", "access"), {
        onEnter: function(args) {
            //console.log("****** access _______")
            var cstr = Memory.readCString(args[0]);
            console.log("access >> " + cstr);
            //var checkroot = Boolean(0); 
            var shouldFakeReturn = (RootAccess.indexOf(cstr) > -1)
            if (shouldFakeReturn) {
                var buf = Memory.allocUtf8String("/notexists");
                //Memory.writeUtf8String(args[0], "/notexists");
                this.buf = buf;
                args[0] = buf;
                //this.checkroot = Boolean(1);
            }
            
        },
        onLeave: function(retval) {
            /*if(this.checkroot){
                retval = ptr(-1);
            }
            */
            //console.log("retval : ", retval);

        }
    });
반응형