Home Rootbeer bypass
Post
Cancel

Rootbeer bypass

Rootbeer라는 루팅 탐지 앱에서 걸린 루팅 로직을 우회해서 bypass 하도록 만들어봅시다..


Rooting Detect

앱을 실행 후 루팅 체크를 하게 되면 아래와 같이 12개의 체크 로직에서 7개의 로직이 루팅에 걸렸습니다.
공부를 위해 분석하여 우회해 봅시다.

rooting

Root Management Apps && Potentially Dangerous Apps

Root Management Apps의 로직을 보면 Const.knownRootAppsPackages, Const.knownDangerousAppsPackages 배열에 저장된 값을 가져와 v0 배열에 모두 추가합니다.
다음 his.isAnyPackageFromListInstalled(v0) 함수의 인수로 사용합니다.
isAnyPackageFromListInstalled 함수에서 package Manager의 객체를 가져와 전달은 인자의 getPackageInfo 패키지 정보를 가져옵니다.
패키지 정보가 있다면 루팅 되었다고 판단하여 true를 반환하고 아니면 배열이 끝날때 까지 돌고 False를 반환합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
    public boolean detectRootManagementApps() {
        return this.detectRootManagementApps(null);
    }

    public boolean detectRootManagementApps(String[] arg3) {
        ArrayList v0 = new ArrayList(Arrays.asList(Const.knownRootAppsPackages));
        if(arg3 != null && arg3.length > 0) {
            v0.addAll(Arrays.asList(arg3));
        }

        return this.isAnyPackageFromListInstalled(v0);
    }

-----------------------------------------------------------

 public boolean detectPotentiallyDangerousApps() {
        return this.detectPotentiallyDangerousApps(null);
    }

    public boolean detectPotentiallyDangerousApps(String[] arg3) {
        ArrayList v0 = new ArrayList();
        v0.addAll(Arrays.asList(Const.knownDangerousAppsPackages));
        if(arg3 != null && arg3.length > 0) {
            v0.addAll(Arrays.asList(arg3));
        }

        return this.isAnyPackageFromListInstalled(v0);
    }

-----------------------------------------------------------

    private boolean isAnyPackageFromListInstalled(List arg6) {
    PackageManager v0 = this.mContext.getPackageManager();
    Iterator v6 = arg6.iterator();
    boolean v2 = false
    ;
    while(v6.hasNext()) {
            Object v3 = v6.next();
            String v3_1 = (String)v3;
            try {
                v0.getPackageInfo(v3_1, 0);
                QLog.e(v3_1 + " ROOT management app detected!");
            }
            catch(PackageManager.NameNotFoundException unused_ex) {
                continue;
            }

            v2 = true;
        }

    return v2;
    }

bypass Root Management Apps && Potentially Dangerous Apps

Const.knownRootAppsPackages, Const.knownDangerousAppsPackages의 배열을 가져와 이를 빈값으로 변조하여 우회할 수 있습니다.

1
 Const.knownRootAppsPackages = new String[]{"com.noshufou.android.su", "com.noshufou.android.su.elite", "eu.chainfire.supersu", "com.koushikdutta.superuser", "com.thirdparty.superuser", "com.yellowes.su", "com.topjohnwu.magisk", "com.kingroot.kinguser", "com.kingo.root", "com.smedialink.oneclickroot", "com.zhiqupk.root.global", "com.alephzain.framaroot"};
1
Const.knownDangerousAppsPackages = new String[]{"com.koushikdutta.rommanager", "com.koushikdutta.rommanager.license", "com.dimonvideo.luckypatcher", "com.chelpus.lackypatch", "com.ramdroid.appquarantine", "com.ramdroid.appquarantinepro", "com.android.vending.billing.InAppBillingService.COIN", "com.android.vending.billing.InAppBillingService.LUCK", "com.chelpus.luckypatcher", "com.blackmartalpha", "org.blackmart.market", "com.allinone.free", "com.repodroid.app", "org.creeplays.hack", "com.baseappfull.fwd", "com.zmapp", "com.dv.marketmod.installer", "org.mobilism.android", "com.android.wp.net.log", "com.android.camera.update", "cc.madkite.freedom", "com.solohsu.android.edxp.manager", "org.meowcat.edxposed.manager", "com.xmodgame", "com.cih.game_cih", "com.charles.lpoqasert", "catch_.me_.if_.you_.can_"};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Root_Management_Apps(){
    var Const = Java.use("com.scottyab.rootbeer.Const");
    var str_arr = Java.array('java.lang.String',[""]);
    console.log("[+] knownRootAppsPackages : "+Const.knownRootAppsPackages.value);
    Const.knownRootAppsPackages.value = str_arr;
}


function Potenially_Dangerous_Apps(){
    var Const = Java.use("com.scottyab.rootbeer.Const");
    var str_arr = Java.array('java.lang.String',[""]);
    console.log("[+] Potentially Apps : "+Const.knownDangerousAppsPackages.value);
    Const.knownDangerousAppsPackages.value = str_arr;
}

result

bypass1,2

SU Binary

checkForSuBinary 메서드를 보면 checkForBinary 메서드에 su를 인수로 넘겨줍니다. checkForBinary는 getPaths() 메서드의 결과를 가지고 해당 경로에 파일 객체를 만들어 su가 존재하는지 검사합니다.
존재하면 true를 반환하고 아니면 false를 반환합니다.

getPaths() 메서드는 시스템의 PATH 환경 변수에 기반하여 실행 가능한 프로그램 파일의 경로를 가져옵니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
  public boolean checkForSuBinary() {
        return this.checkForBinary("su");
    }

  public boolean checkForBinary(String arg8) {
        String[] v0 = Const.getPaths();
        int v2 = 0;
        boolean v3 = false;
        while(v2 < v0.length) {
            String v5 = v0[v2] + arg8;
            if(new File(v0[v2], arg8).exists()) {
                QLog.v(v5 + " binary detected!");
                v3 = true;
            }

            ++v2;
        }

        return v3;
    }

-------------------------------------------------------------

  static String[] getPaths() {
        ArrayList v0 = new ArrayList(Arrays.asList(Const.suPaths));
        //Const.suPaths = new String[]{"/data/local/", "/data/local/bin/", "/data/local/xbin/", "/sbin/", "/su/bin/", "/system/bin/", "/system/bin/.ext/", "/system/bin/failsafe/", "/system/sd/xbin/", "/system/usr/we-need-root/", "/system/xbin/", "/cache/", "/data/", "/dev/"};
        String v1 = System.getenv("PATH");
        if(v1 != null && !"".equals(v1)) {
            String[] v1_1 = v1.split(":");
            int v4;
            for(v4 = 0; v4 < v1_1.length; ++v4) {
                String v5 = v1_1[v4];
                if(!v5.endsWith("/")) {
                    v5 = v5 + '/';
                }

                if(!v0.contains(v5)) {
                    v0.add(v5);
                }
            }

            return (String[])v0.toArray(new String[0]);
        }

        return (String[])v0.toArray(new String[0]);
    }

SU Binary bypass

getPaths() 리턴 값을 빈 배열로 전달하면 이를 우회할 수 있습니다.

1
2
3
4
5
6
7
function SU_Binary(){
    var Const = Java.use("com.scottyab.rootbeer.Const");
    Const.getPaths.implementation = function(arg1){
        console.log("[+] getPaths() :"+this.getPaths());
        return [];
    }
}

result

subypass

2nd SU Binary check

getRuntime().exec()를 통해 which su 명령을 실행하여 v1에 저장합니다.
v2에 su의 명령어 경로를 저장합니다.
만약 v2가 null이 아니라면 true를 반환되어 명령이 실행되었다고 루팅 여부를 판단합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public boolean checkSuExists() {
        boolean v0 = false;
        Process v1 = null;
        try {
            v1 = Runtime.getRuntime().exec(new String[]{"which", "su"});
            String v2 = new BufferedReader(new InputStreamReader(v1.getInputStream())).readLine();
        }
        catch(Throwable unused_ex) {
            if(v1 != null) {
                v1.destroy();
            }

            return false;
        }

        if(v2 != null) {
            v0 = true;
        }

        if(v1 != null) {
            v1.destroy();
        }

        return v0;
    }

bypass 2nd SU Binary check

exec()메서드의 인자를 후킹하는 방법도 있지만 이번에는 BufferReader의 readLine()메서드를 후킹하여 결과를 빈값으로 후킹하면 루팅을 우회할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function _2nd_SU_Binary_check(){
        console.log("hook2")
        var Runtime = Java.use("java.lang.Runtime");
        Runtime.exec.overload('[Ljava.lang.String;').implementation = function(command){
        var BufferedReader = Java.use("java.io.BufferedReader");
        BufferedReader.readLine.overload().implementation = function() {
            var result = this.readLine();
            console.log("[+] BufferedReader readLine :"+result);
            result = null;
                return result;
            };
        //command = "glasses96";
        return this.exec(command)
    }
    return this.checkSuExists();
}

result

bypass_readbuffer

For RW Paths

해당 로직은 시스템 마운드 정보를 읽어 오는데 안드로이드 OS버전에 따라 공백을 기준으로 각 필드를 구분합니다.
replace로 SDK23 이상 버전은 ()를 지우고 Const.pathsThatShouldNotBeWritable에 저장된 배열에서 rw권한이 존재하는지 확인합니다.
Const.pathsThatShouldNotBeWritable = new String[]{“/system”, “/system/bin”, “/system/sbin”, “/system/xbin”, “/vendor/bin”, “/sbin”, “/etc”};
rw 권한이 있으면 true를 반환하여 루팅 여부를 탐지 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
 public boolean checkForRWPaths() {
        String[] v16;
        String v7_1;
        String v10;
        String[] v0 = this.mountReader();
        if(v0 == null) {
            return false;
        }

        int v2 = Build.VERSION.SDK_INT;
        int v3 = v0.length;
        int v4 = 0;
        boolean v5 = false;
        while(v4 < v3) {
            String v6 = v0[v4];
            String[] v7 = v6.split(" ");
            if(v2 <= 23 && v7.length < 4 || v2 > 23 && v7.length < 6) {
                QLog.e("Error formatting mount line: " + v6);
            }
            else {
                if(v2 > 23) {
                    v10 = v7[2];
                    v7_1 = v7[5];
                }
                else {
                    v10 = v7[1];
                    v7_1 = v7[3];
                }

                String[] v11 = Const.pathsThatShouldNotBeWritable;
                int v13 = 0;
                while(v13 < v11.length) {
                    String v14 = v11[v13];
                    if(v10.equalsIgnoreCase(v14)) {
                        if(Build.VERSION.SDK_INT > 23) {
                            v7_1 = v7_1.replace("(", "").replace(")", "");
                        }

                        String[] v1 = v7_1.split(",");
                        int v8 = 0;
                        while(v8 < v1.length) {
                            v16 = v0;
                            if(v1[v8].equalsIgnoreCase("rw")) {
                                QLog.v(v14 + " path is mounted with rw permissions! " + v6);
                                v5 = true;
                                goto label_80;
                            }

                            ++v8;
                            v0 = v16;
                        }
                    }

                    v16 = v0;
                label_80:
                    ++v13;
                    v0 = v16;
                }
            }

            ++v4;
            v0 = v0;
        }

        return v5;
    }

Bypass For RW Paths

이번에는 String 클래스의 메소드인 equalsIgnoreCase메소드를 후킹하여 우회가 가능합니다.
간단하게 인자를 -> glasses96으로 변조한 코드입니다.

1
2
3
4
5
6
7
8
function For_RW_Paths(){
    var String = Java.use("java.lang.String");
    String.equalsIgnoreCase.implementation = function(anotherString){
        console.log("[+] anotherString : "+anotherString);
        anotherString = "glasses96";
        return this.equalsIgnoreCase(anotherString)
    }
}

build version은 아래의 코드로 확인할 수 있습니다. 저는 24로 어떤 로직이 동작하는지를 쉽게 유추할 수 있습니다.

1
2
3
4
5
function build_sdk_version(){
    var build = Java.use("android.os.Build$VERSION");
    console.log("[+] build version : "+build.SDK_INT.value);
}

result

anotherString

Root via native check

해당 루팅 탐지 로직은 libtool-check.so 바이너리에서 checkForRoot() 메서드에서 판단합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.scottyab.rootbeer;

import com.scottyab.rootbeer.util.QLog;

public class RootBeerNative {
    private static boolean libraryLoaded = false;

    static {
        try {
            System.loadLibrary("tool-checker");
            RootBeerNative.libraryLoaded = true;
        }
        catch(UnsatisfiedLinkError v0) {
            QLog.e(v0);
        }
    }

    public native int checkForRoot(Object[] arg1) {
    }

    public native int setLogDebugMessages(boolean arg1) {
    }

    public boolean wasNativeLibraryLoaded() {
        return RootBeerNative.libraryLoaded;
    }
}


ida를 이용해서 디컴파일 결과를 보면 아래와 같이 exists 메서드를 통해 파일 존재 여부를 확인합니다.

decomfile

exists 메서드를 분석해보면 fopen으로 전달받은 인자를 통해 해당 파일이 읽어지는지를 통해 0 또는 1를 리턴하여 루팅을 판단합니다.

exists

bypass Root via native check

fopen을 후킹하여 해당 인자에 어떤 값이 전달되는지를 후킹하여 루팅과 관련된 인자를 우회하면 루팅 탐지를 우회할 수 있습니다.
fopen의 첫번째 인자가 filename이므로 첫번째 인자를 후킹하면 알 수 있습니다.

1
FILE *fopen(const char *filename, const char *mode);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Root_via_native_check(){
    Interceptor.attach(Module.findExportByName("libtool-checker.so", 'fopen'), {
        onEnter: function (args)
        {
            var su_detect = Memory.readUtf8String(args[0]);
            console.log("args[0]: " + Memory.readUtf8String(args[0]));
            if (su_detect.indexOf("/su")){
                console.log("[+] su detect bypass : " + Memory.readUtf8String(args[0]));
                Memory.writeUtf8String(args[0],"glasses96");
            }
        },
        onLeave: function (retval)
        {
        }
    });

}

result

rootnative_via