Rootbeer라는 루팅 탐지 앱에서 걸린 루팅 로직을 우회해서 bypass 하도록 만들어봅시다..
Rooting Detect
앱을 실행 후 루팅 체크를 하게 되면 아래와 같이 12개의 체크 로직에서 7개의 로직이 루팅에 걸렸습니다.
공부를 위해 분석하여 우회해 봅시다.
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
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
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
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
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
메서드를 통해 파일 존재 여부를 확인합니다.
exists 메서드를 분석해보면 fopen
으로 전달받은 인자를 통해 해당 파일이 읽어지는지를 통해 0 또는 1를 리턴하여 루팅을 판단합니다.
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)
{
}
});
}