Advanced Root Detection & Bypass Techniques

移动安全 7个月前 admin
227 0 0

Introduction 介绍

Welcome to another blog post in our series on Advanced Frida Usage. In this blog, we will explore techniques related to root detection on Android devices and methods to bypass it. Our main focus will be on the strategies employed by app developers to protect their applications and prevent them from running on compromised devices. For learning purposes, we will be using a sample root detection application named Root Detector that can be downloaded here (external link).
欢迎阅读我们关于高级弗里达用法系列中的另一篇文章。在本博客中,我们将探讨与Android设备上的根检测相关的技术以及绕过它的方法。我们的主要重点是应用程序开发人员为保护其应用程序并防止它们在受感染设备上运行而采用的策略。出于学习目的,我们将使用一个名为根检测器的示例根检测应用程序,该应用程序可在此处下载(外部链接)。

Analysis 分析

The sample application is already installed on our rooted device, and as we can see it indicates that the device is rooted.
示例应用程序已经安装在我们的 root 设备上,正如我们所看到的,它表明该设备已植根。

Advanced Root Detection & Bypass Techniques

Let us begin the analysis by decompiling the apk using jadx-gui to get an idea of what the Root Detector app is doing once installed on the device. AndroidManifest.xml is the entrypoint for all the android apps where different components and services are defined for the application.
让我们通过使用 jadx-gui 反编译 apk 来开始分析,以了解根检测器应用程序在设备上安装后正在做什么。AndroidManifest.xml 是所有 Android 应用的入口点,其中为应用定义了不同的组件和服务。

Permissions 权限

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<permission android:name="com.8ksec.inappprotections.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION" android:protectionLevel="signature"/>
<uses-permission android:name="com.8ksec.inappprotections.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"/>

Nothing seems to be interesting from the permissions point of view. It is mainly using the storage permission.
从权限的角度来看,似乎没有什么有趣的。它主要使用存储权限。

Other than these defined permissions, let us see what other components are present in this application.
除了这些定义的权限之外,让我们看看此应用程序中还存在哪些其他组件。

<activity android:exported="true" android:name="com.8ksec.inappprotections.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>

We can see that it only has one activity which is the MainActivity that gets launched.
我们可以看到它只有一个活动,即启动的主活动。

Decompiled Code Analysis 反编译代码分析

Let’s see what do we have in this MainActivity.java
让我们看看我们在这个主活动中有什么.java

Advanced Root Detection & Bypass Techniques

We can see that jadx was not able to decompile most of the code. This is because of obfuscation. But let’s try to figure out whatever we can from the decompiled code that we have!
我们可以看到jadx无法反编译大部分代码。这是因为混淆。但是,让我们尝试从我们拥有的反编译代码中找出我们所能弄清楚的任何东西!

It seems that MainActivity has a static block where it is calling System.loadLibrary("inappprotections") which is responsible for loading the native library into the memory.
似乎 MainActivity 有一个静态块,它正在调用 System.loadLibrary("inappprotections") 它负责将本机库加载到内存中。

static {
    String str = "ۧ۠ۧۤ۬۠ۗۛۙۡۦۚۙۡۧۢۛۡۘ۟ۛۥۢ۬۠۬ۨ۬۟ۤۙۙۢۘ۠ۡۛۤۨۨۨۙۜ۟۬ۜۜۙۦۘۤ۠ۨۨ۟ۡ";
    while (true) {
        switch ((((str.hashCode() ^ 436) ^ 234) ^ 912) ^ (-1907612566)) {
            case -1083933248:
                return;
            case 1567145872:
                System.loadLibrary("inappprotections");
                str = "۟ۚۨۘۤۜۧۘۛ۬ۧۖۤ۟ۨ۫ۜۘ۟ۢۥۜۦۦ۫۬ۖۘ۬ۤۢۙ۫ۥۘ";
                break;
        }
    }
}

Since it is in a static block this native library will get loaded as soon as the appl launches.
由于它位于静态块中,因此此本机库将在appl启动后立即加载。

Other than this, we can also observe an interesting function named detectRoot()
除此之外,我们还可以观察到一个名为 detectRoot()
.

public final native int detectRoot();

By looking at the functions signature it looks like a native function that is coming from the inappprotections library and its return value is integer.
通过查看函数签名,它看起来像来自库的 inappprotections 本机函数,其返回值为整数。

Usually, such functions return true in case root is detected otherwise it returns false. Our goal here is to somehow bypass the root detection so that we can run the app on a compromised device.
通常,如果检测到根,则此类函数返回 true,否则返回 false。我们的目标是以某种方式绕过根检测,以便我们可以在受感染的设备上运行该应用程序。

Dynamic Analysis 动态分析

Given the preceding assumption, we can employ Frida to intercept the function at the Java layer. This will allow us to inspect the function’s return value. If the return value happens to be a boolean, we can easily modify it to consistently return false.
给定前面的假设,我们可以使用 Frida 在 Java 层拦截函数。这将允许我们检查函数的返回值。如果返回值恰好是 boolean ,我们可以很容易地修改它以一致地返回 false 。

Frida Hooking 弗里达·胡金

We can hook the detectRoot() function using the following Frida script:
我们可以使用以下 Frida 脚本钩住 detectRoot() 函数:

let MainActivity = Java.use("com.8ksec.inappprotections.MainActivity");

MainActivity["detectRoot"].implementation = function () {
            let ret = this.detectRoot();
            console.log('detectRoot ret value is ' + ret);
            return ret;
};

In this hook, we are just printing the return value of detectRoot() function. Let’s run this script using frida.
在这个钩子中,我们只是打印函数的 detectRoot() 返回值。让我们使用 frida 运行此脚本。

Before you run frida, make sure that frida-server is already running on your Android device. You can use a utility like FridaLoader (https://github.com/dineshshetty/FridaLoader/) to run a frida server on your rooted device. 
在运行 frida 之前,请确保 frida-server 已经在您的 Android 设备上运行。您可以使用像FridaLoader(https://github.com/dineshshetty/FridaLoader/)这样的实用程序在root设备上运行frida服务器。

frida -U -l root_bypass.js -f com.8ksec.inappprotections
   . . . .   Connected to Pixel 4a (id=0B151JEC202420)
Spawned `com.8ksec.inappprotections`. Resuming main thread!
[Pixel 4a::com.8ksec.inappprotections ]-> detectRoot is called
detectRoot ret value is 404

In the console we can see that the return value is 404. Okay, so its not just 0 or 1.
在控制台中,我们可以看到返回值为 404。好的,所以它不仅仅是 0 或 1。

Let’s run the script again to see if we are getting the same value:
让我们再次运行脚本以查看我们是否获得相同的值:

. . . .   Connected to Pixel 4a (id=0B151JEC202420)
Spawned `com.8ksec.inappprotections`. Resuming main thread!
[Pixel 4a::com.8ksec.inappprotections ]-> detectRoot is called
detectRoot ret value is 500

This time we get 500 which is not consistent. Unfortunately, it seems that manipulating the return value alone may not be sufficient to bypass this specific root detection case. Additionally, the code is obfuscated, making it challenging to understand the underlying logic of the root detection mechanism solely based on this class.
这次我们得到 500 个,这是不一致的。不幸的是,仅操作返回值似乎不足以绕过此特定的根检测案例。此外,代码经过模糊处理,因此很难理解仅基于此类的根检测机制的底层逻辑。

Hence, we need to look into the native library where this function is defined i.e this inappprotections library.
因此,我们需要研究定义此函数的本机库,即此 inappprotections 库。

Extracting APK 提取APK

Let’s quickly extract this APK using apktool so that we can access these internal files and resources of the APK.
让我们使用 apktool 快速提取此 APK,以便我们可以访问 APK 的这些内部文件和资源。

apktool d root_detector.apk
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
I: Using Apktool 2.5.0-dirty on root_detector.apk
I: Loading resource table...
I: Decoding AndroidManifest.xml with resources...
I: Loading resource table from file: /home/kali/.local/share/apktool/framework/1.apk
I: Regular manifest package...
I: Decoding file-resources...
I: Decoding values */* XMLs...
I: Baksmaling classes.dex...
I: Copying assets and libs...
I: Copying unknown files...
I: Copying original files...
I: Copying META-INF/services directory

Inside the lib folder we can find the libinappprotections.so library.
在 lib 文件夹中,我们可以找到库 libinappprotections.so 。

Analyzing Inappprotections Library
分析应用保护库

Ghidra 吉德拉

To examine this library, we will utilize the Ghidra disassembler. Our objective is to identify imported functions within the library, that can serve as a starting point for further analysis of potentially interesting functions.
为了检查这个库,我们将使用 Ghidra 反汇编器。我们的目标是识别库中的导入函数,这些函数可以作为进一步分析潜在有趣函数的起点。

Advanced Root Detection & Bypass Techniques

It seems we don’t have many functions and the first function we can see is our detectRoot function. Let’s quickly navigate to this function to see the disassembly and try to understand the logic.
似乎我们没有很多函数,我们可以看到的第一个函数是我们的功能 detectRoot 。让我们快速导航到此函数以查看反汇编并尝试理解逻辑。

Advanced Root Detection & Bypass Techniques

Okay so the function itself is not so big, but by having a quick look at this disassembly we can say that it is using indirect branching to break the control flow by analyzing this disassembly. The X8 register is dynamically loaded at runtime and this subsequent branch instruction will invoke the function based on the address pointed out by this X8 register. So just by looking at this disassembly we cannot predict what function will be called. Similar kind of indirect branching instructions are spread through out the code. So it is quite difficult here to understand the logic just by simply doing a static analysis on this function.
好的,所以函数本身不是那么大,但是通过快速浏览此反汇编,我们可以说它正在使用间接分支通过分析此反汇编来中断控制流。X8 寄存器在运行时动态加载,此后续分支指令将根据此 X8 寄存器指出的地址调用该函数。因此,仅通过查看此反汇编,我们无法预测将调用什么函数。类似的间接分支指令遍布整个代码。因此,仅仅通过简单地对这个函数进行静态分析来理解逻辑是相当困难的。

Root Detection #1 根检测 #1

Before moving forward, let’s get some idea about how root detection is performed in general on any Android device. When we root a device it places an executable with the name su in system directory. In order to detect su binary, app has to check for su in a default path like system/bin/su. These paths have to be hardcoded somewhere in this library. It is most likely hardcoded in the read only section of the binary because all the hardcoded constant values are present in that section.
在继续之前,让我们了解一下如何在任何Android设备上执行根检测。当我们根设备时,它会在系统目录中放置一个具有该名称 su 的可执行文件。为了检测 su 二进制文件,应用程序必须在默认路径(如系统/bin/su)中检查。 su 这些路径必须在此库中的某个位置进行硬编码。它很可能在二进制文件的只读部分中进行硬编码,因为所有硬编码的常量值都存在于该部分中。

Advanced Root Detection & Bypass Techniques

Unfortunately, nothing related to su paths have been found in the strings and we can see that lot of random characters are present in the text section. This indicates that the strings have been encrypted and stored into text section. We cannot do much at this point.

What should we consider trying next? We’ve noticed that there are several imported functions related to file handling, including access()fopen(), fclose(), and stat(). To confirm this, let’s apply a Frida interceptor to these functions.

var arg0 = null;
Interceptor.attach(Module.findExportByName("libc.so", "fopen"), {
    onEnter: function (args) {
        arg0 = args[0];
        console.log(`fopen: ${args[0].readCString()}`);
    }
})

Interceptor.attach(Module.findExportByName("libc.so", "stat"), {
    onEnter: function (args) {
        console.log(`stat: ${args[0].readCString()}`);
    }
})

Interceptor.attach(Module.findExportByName("libc.so", "access"), {
    onEnter: function (args) {
        console.log(`access: ${args[0].readCString()}`);
    }
})

Let’s run the script now and see what do we get:

. . . .   Connected to Pixel 4a (id=0B151JEC202420)
Spawned `com.8ksec.inappprotections`. Resuming main thread!
[Pixel 4a::com.8ksec.inappprotections ]-> stat: /data/app/~~MxHO_xqdQgeBWi4_Wpoj4A==/com.8ksec.inappprotections-1XZhJp18cue2EZWsYq4xjA==/base.apk
stat: /data/app/~~MxHO_xqdQgeBWi4_Wpoj4A==/com.8ksec.inappprotections-1XZhJp18cue2EZWsYq4xjA==/base.apk
stat: /data
stat: /data
stat: /data/dalvik-cache/arm64
access: /data/app/~~MxHO_xqdQgeBWi4_Wpoj4A==/com.8ksec.inappprotections-1XZhJp18cue2EZWsYq4xjA==

We’re currently receiving a substantial amount of output, much of which appears to be related to default paths that typical application processes access during execution. In order to refine our results and obtain a more meaningful output, we should invoke these hooks only after our libinappprotections library has been successfully loaded. To achieve this, we’ll be hooking into linker64 since it plays a crucial role in initially loading the library into memory.
我们目前收到大量输出,其中大部分似乎与典型应用程序处理在执行期间访问的默认路径有关。为了完善我们的结果并获得更有意义的输出,我们应该仅在成功加载 libinappprotections 库后调用这些钩子。为了实现这一点,我们将挂接到 linker64,因为它在最初将库加载到内存中时起着至关重要的作用。

var do_dlopen = null;
var call_constructor = null;
Process.findModuleByName('linker64').enumerateSymbols().forEach(function (symbol) {
    if (symbol.name.indexOf('do_dlopen') >= 0) {
        do_dlopen = symbol.address;
    } else if (symbol.name.indexOf('call_constructor') >= 0) {
        call_constructor = symbol.address;
    }
})

We can easily find the linker64 module by using Process.findModuleByName() API of frida and then we need to enumerate all the symbols in linker64 to get the addresses of do_dlopen() and call_constructor functions. These functions will be invoked when linker64 tries to load a library into the memory.
我们可以通过使用 frida 的 Process.findModuleByName() API 轻松找到 linker64 模块,然后我们需要枚举 linker64 中的所有符号来获取 do_dlopen() 和 call_constructor 函数的地址。当 linker64 尝试将库加载到内存中时,将调用这些函数。

Once we have these addresses, we can attach the hooks and trace all the libraries loaded by the linker.
获得这些地址后,我们可以附加钩子并跟踪链接器加载的所有库。

var lib_loaded = 0;
Interceptor.attach(do_dlopen, function () {
    var library_path = this.context.x0.readCString();
    if (library_path.indexOf('libnative-lib.so') >= 0) {
        console.log(`Target library is loading...`);

        Interceptor.attach(call_constructor, function () {
            if (lib_loaded == 0) {
                var native_mod = Process.findModuleByName('libinappprotections.so');
                console.log(`Target library loaded at ${native_mod.base}`);

            }
            lib_loaded = 1;
        })
    }
})

Let’s run the script again and see the intercepted data:
让我们再次运行脚本并查看截获的数据:

frida -U -l root_bypass.js -f com.8ksec.inappprotections
   . . . .   Connected to Pixel 4a (id=0B151JEC202420)
Spawned `com.8ksec.inappprotections`. Resuming main thread!
[Pixel 4a::com.8ksec.inappprotections ]-> Target library is loading...
Target library loaded at 0x6d5d956000
stat: /data/app/~~MxHO_xqdQgeBWi4_Wpoj4A==/com.8ksec.inappprotections-1XZhJp18cue2EZWsYq4xjA==/base.apk
stat: /data/resource-cache/product@overlay@NavigationBarModeGestural@NavigationBarModeGesturalOverlay.apk@idmap
stat: /product/overlay/NavigationBarModeGestural/NavigationBarModeGesturalOverlay.apk
access: /data/user/0/com.8ksec.inappprotections
access: /data/user/0/com.8ksec.inappprotections/cache
access: /data/user_de/0/com.8ksec.inappprotections
access: /data/user_de/0/com.8ksec.inappprotections/code_cache
stat: /system/framework/framework-res.apk
stat: /data/resource-cache/product@overlay@GoogleConfigOverlay.apk@idmap
stat: /product/overlay/GoogleConfigOverlay.apk
stat: /data/resource-cache/product@overlay@GoogleWebViewOverlay.apk@idmap
stat: /product/overlay/GoogleWebViewOverlay.apk
stat: /data/resource-cache/product@overlay@NavigationBarModeGestural@NavigationBarModeGesturalOverlay.apk@idmap
stat: /product/overlay/NavigationBarModeGestural/NavigationBarModeGesturalOverlay.apk
access: /system/xbin/su
access: /system/bin/su
detectRoot ret value is 669
access: /dev/hwbinder
stat: /vendor/lib64/hw/gralloc.sm6150.so

We can observe the execution of our detectRoot() function. The function is invoked, and subsequently, the application attempts to access certain system paths using the access() function. It’s important to note that these specific paths will exist on any rooted device. Following the path access, the function then determines whether root access has been detected based on a constant value.

Now, let’s explore potential strategies to bypass this root detection mechanism.

One viable approach is to manipulate the path being checked by the access() function and provide a fake path instead. This alteration will mislead the app into believing that the `su` binary path is not present, prompting it to return a non-rooted status. To implement this workaround, let’s modify the script accordingly:

Interceptor.attach(Module.findExportByName("libc.so", "access"), {
    onEnter: function (args) {
        var path = args[0].readCString();
        if(path.indexOf("/su") >= 0){
            console.log(`Manipulating su path...`);
            args[0].writeUtf8String("/system/nonexisting");
        }
        console.log(`access: ${args[0].readCString()}`);
    }
})

Run the script again.
再次运行脚本。

Manipulating su path...
access: /system/nonexisting
access: ing
Manipulating su path...
access: /system/nonexisting
access: onexisting
Manipulating su path...
access: /system/nonexisting
fopen:
stat: /sys/fs/selinux/class/security/index
detectRoot ret value is 572

This time we can observe that access function is now trying to access our modified non existent path instead of su paths right. But the app UI still indicates that a root device was detected.
这一次我们可以观察到访问函数现在正在尝试访问我们修改后的不存在的路径而不是正确的 su 路径。但应用 UI 仍指示检测到根设备。

Advanced Root Detection & Bypass Techniques

This may be because the app could be searching for additional artifacts related to root detection.
这可能是因为应用可能正在搜索与根检测相关的其他项目。

Root Detection #2 根检测 #2

Coming back to the output log we can observe that before returning the value it is calling a stat() function which is trying to access selinux policies file. We can assume that the app is trying to access these selinux policy to detect root. To bypass this check again we can use the same approach of altering the input parameter being passed to stat function using frida:
回到输出日志,我们可以观察到,在返回值之前,它正在调用一个 stat() 尝试访问 selinux policies 文件的函数。我们可以假设应用程序正在尝试访问这些 selinux policy 以检测根。要再次绕过此检查,我们可以使用相同的方法来更改传递给 stat frida 的函数的输入参数:

Interceptor.attach(Module.findExportByName("libc.so", "stat"), {
    onEnter: function (args) {
        var path = args[0].readCString();
        if(path.indexOf("/selinux") >= 0){
            console.log(`Manipulating selinux path...`);
            args[0].writeUtf8String("/non/existing");
        }
        console.log(`stat: ${args[0].readCString()}`);
    }
})

And its time to run the script again to see the changes in the console log.

access: /system/nonexisting
fopen:
Manipulating selinux path...
Error: access violation accessing 0x6d5d9568f8
    at <anonymous> (frida/runtime/core.js:147)
    at onEnter (/home/kali/Documents/trainings_2023/root_detection_bypass/root_bypass.js:58)
detectRoot ret value is 628

In the output, an error was reported: “Access violation accessing 0x6d5d9568f8.” This error log suggests that there may be an issue with Frida attempting to tamper with or overwrite the value at this specific memory location. Fortunately, there is a solution available. We can utilize an alternative Frida API, Memory.protect(), to modify the permissions of the memory space in question.

Memory.protect(args[0],Process.pointerSize, 'rwx');

Let’s run the script now and see whether this has fixed the issue or not.

Manipulating su path...
access: /system/nonexisting
access: ing
Manipulating su path...
access: /system/nonexisting
access: onexisting
Manipulating su path...
access: /system/nonexisting
fopen:
Manipulating selinux path...
stat: /non/existing
Manipulating selinux path...
stat: /non/existing
Manipulating selinux path...
stat: /non/existing
Manipulating selinux path...
stat: /non/existing
fopen: /proc/self/attr/prev
detectRoot ret value is 560

It worked! Awesome. In the console log we can observe that stat input paths have been modified and if we have a look at the app, it still says root detected. So clearly, just bypassing this is not enough and we need to analyze it more to identify other checks present in the app.
成功了!棒。在控制台日志中,我们可以观察到输入路径已被修改, stat 如果我们查看该应用程序,它仍然显示检测到root。很明显,仅仅绕过这一点是不够的,我们需要对其进行更多分析以识别应用程序中存在的其他检查。

Root Detection #3 根检测 #3

In the console log after these stat calls this time we have another imported function being called i.e fopen() and its trying to access /proc/self/attr/prev. Let’s access this file from adb shell to see what is there in this file.
在这些统计信息调用之后的控制台日志中,这次我们调用了另一个导入的函数,即 fopen() 它试图访问 /proc/self/attr/prev 让我们从 adb shell 访问此文件以查看此文件中的内容。

adb shell
cat /proc/self/attr/prev
u:r:zygote:s0

 It says zygote here. Let’s check this same file on a non rooted device and see its contents:
这里写着受精卵。让我们在无根设备上检查相同的文件并查看其内容:

cat /proc/self/attr/prev
u:r:untrusted_app:s0:c7,c257,c512,c768

Okay this means that something is getting changed when we are on a rooted device as compared to non rooted one. And after doing some research it was found that this file will contain zygote when we have magisk zygisk module running. So to bypass this check we can either disable zygisk module or we can try to bypass this using Frida.
好的,这意味着当我们在有根设备上与无根设备相比,某些东西正在发生变化。经过一些研究,发现当我们运行magisk zygisk模块时,该文件将包含 zygote 。因此,要绕过此检查,我们可以禁用zygisk模块,也可以尝试使用Frida绕过它。

Let’s go with later approach first. Based on our understanding about this file we can assume that the app has to make a comparison at some point of time to to check whether this file contains “zygote” or not. Let’s look at the strings section again in Ghidra:
让我们先采用稍后的方法。根据我们对该文件的理解,我们可以假设应用程序必须在某个时间点进行比较以检查此文件是否包含“受精卵”。让我们再次看一下 Ghidra 中的字符串部分:

Advanced Root Detection & Bypass Techniques

In the strings listed, we do have zygote present. Next we have to figure out the function responsible for doing this string comparison. Let’s head back to the imported functions in Ghidra:
在列出的字符串中,我们确实存在 zygote 。接下来,我们必须找出负责进行此字符串比较的函数。让我们回到 Ghidra 中导入的函数:

Advanced Root Detection & Bypass Techniques

Here, we have the strstr() function available, that could be used for string comparison. Let’s attach our hook to this function and intercept its arguments.

Interceptor.attach(Module.findExportByName("libc.so", "strstr"), {
    onEnter: function (args) {
        console.log(`strstr: haystack -> ${args[0].readCString()} & needle -> ${args[0].readCString()}`);
    }
})

Examine the output:

Manipulating selinux path...
stat: /non/existing
Manipulating selinux path...
stat: /non/existing
fopen: /proc/self/attr/prev
strstr: haystack -> u:r:zygote:s0 & needle -> zygote
detectRoot ret value is 571

As we have anticipated, after fopen function it is calling strstr() function and is looking for zygote in this string.

Now, to bypass this let’s modify this second argument so that the comparison will fail.

Interceptor.attach(Module.findExportByName("libc.so", "strstr"), {
    onEnter: function (args) {
        var needle = args[1].readCString();
        if(needle.indexOf("zygote") >= 0){
            args[1].writeUtf8String("blabla");
            console.log(`strstr: haystack -> ${args[0].readCString()} & needle -> ${args[1].readCString()}`);
        }
    }
})

Running the script again and see what do we get this time:

Manipulating su path...
access: /system/nonexisting
access: ing
Manipulating su path...
access: /system/nonexisting
access: onexisting
Manipulating su path...
access: /system/nonexisting
Manipulating selinux path...
stat: /non/existing
Manipulating selinux path...
stat: /non/existing
Manipulating selinux path...
stat: /non/existing
Manipulating selinux path...
stat: /non/existing
fopen: /proc/self/attr/prev
strstr: haystack -> u:r:zygote:s0 & needle -> blabla
fopen: /proc/self/mountinfo
detectRoot ret value is 532

We can observe now that this second argument is modified with blabla now but the app is still not convinced, and still detects rooted device.
我们现在可以观察到第二个参数现在使用 blabla 进行了修改,但应用程序仍然不相信,并且仍然检测到 root 设备。

After the strstr() function call, there is another fopen() call which then tries to access mountinfo.
strstr() 在函数调用之后,还有另一个 fopen() 调用,然后尝试访问 mountinfo 。

Root Detection #4? 根检测#4?

What Is Mountinfo? 什么是Mountinfo?

When we have a rooted device, many additional paths are mounted, such as those for Magisk. Many paths that are supposed to be read-only, such as /system that are supposed to be readonly, become writable.
当我们有一个有根设备时,会安装许多其他路径,例如 Magisk 的路径。许多应该是只读的路径(如应该是只读的 /system)变得可写。

So it is possible that the app is trying to look for these changes.
因此,该应用程序可能正在尝试查找这些更改。

Since the app might be looking for the changes we identified in the strstr() function, it’s possible that it’s using the same function to find those strings in the mountinfo file. Another option is to point fopen() to a non-existent path, but let’s try the first approach first. As we already have the hook attached to the strstr() function, we just need to observe the output.
由于应用可能正在查找我们在函数中标识的更改,因此它可能使用相同的 strstr() 函数在 mountinfo 文件中查找这些字符串。另一种选择是指向 fopen() 不存在的路径,但让我们先尝试第一种方法。由于我们已经将钩子附加到 strstr() 函数,我们只需要观察输出。

fopen: /proc/self/mountinfo
strstr: haystack -> 20533 20532 253:5 / / ro,relatime master:1 - ext4 /dev/block/dm-5 ro,seclabel
 & needle -> magisk
strstr: haystack -> 20534 20533 0:18 / /dev rw,nosuid,relatime master:2 - tmpfs tmpfs rw,seclabel,size=2852132k,nr_inodes=713033,mode=755
 & needle -> magisk
strstr: haystack -> 20535 20534 0:20 / /dev/pts rw,relatime master:3 - devpts devpts rw,seclabel,mode=600,ptmxmode=000
 & needle -> magisk
strstr: haystack -> 20536 20534 0:19 / /dev/ltgnxkz rw,relatime master:4 - tmpfs magisk rw,seclabel,size=2852132k,nr_inodes=713033,mode=755
 & needle -> magisk
detectRoot ret value is 655

As we suspected – the app is accessing the contents of the mountinfo file to search for this specific string magisk. Here in the last iteration we have this magisk present in mountinfo. Let’s bypass this using the same approach we used earlier, modifying the second argument of this function:
正如我们所怀疑的 – 该应用程序正在访问 mountinfo 文件的内容以搜索此特定字符串 magisk .在上次迭代中,我们在mountinfo中提供了这个 magisk 礼物。让我们使用之前使用的相同方法绕过它,修改此函数的第二个参数:

if(needle.indexOf("magisk") >= 0){
    args[1].writeUtf8String("blabla");
}

After rerunning the script, we noticed that the app now displays “suspicious” instead of “rooted.” This suggests that we have successfully bypassed the root detection to some extent.
重新运行脚本后,我们注意到该应用程序现在显示“可疑”而不是“rooted”。这表明我们已经在某种程度上成功地绕过了根检测。

Root Detection #5 根检测 #5

There are still some missing checks causing the app to believe the device environment is not clean. Upon reexamining the output console, we couldn’t identify any other noteworthy function calls.

So, what else could it be?

Let’s launch Ghidra again and try to find some other low hanging fruit.

On randomly analyzing the subroutines we can see an interesting instruction : SVC 0x0.

Advanced Root Detection & Bypass Techniques

And these instructions are spread throughout the binary as you can see from the above screenshot.

What are these SVC instructions?

These instructions are used to call functions using system call number. Since we are dealing with Arm64 binary let’s open up the system call mapping table for this architecture. You can find a complete list of syscalls here: https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md#arm64-64_bit

Advanced Root Detection & Bypass Techniques

In this first column we have the name of the function, then in the 3rd column we have system call number and as specified here it gets stored in X8 register and then the arguments to the functions will be stored in registers starting from X0 to X5Let’s analyze this in the disassembly we just saw:
在第一列中,我们有函数的名称,然后在第三列中我们有系统调用号,正如这里指定的,它存储在寄存器中 X8 ,然后函数的参数将存储在从 to X5 开始 X0 的寄存器中。让我们在刚刚看到的反汇编中分析一下:

0x00001994      08078052       mov w8, 0x38
0x00001998      010000d4       svc 0

0x38 is stored in w8 register here, we can match this syscall number with this table to figure out what function is getting called. In the table the corresponding function to this syscall number is openat(). This function is similar to open() function that will open the file in memory for reading or writing purpose. This is a good candidate to look for and developers use such techniques to hide the function names from being analyzed statically.
0x38存储在寄存器中 w8 ,我们可以将此 syscall 编号与此表匹配,以确定正在调用的函数。在表中,与此系统调用号对应的函数是 openat() 此函数类似于将在内存中打开文件以进行读取或写入的 open() 函数。这是一个很好的候选者,开发人员使用此类技术来隐藏函数名称,使其不被静态分析。

To attach the Frida interceptor on all these SVC instructions, we first need to find out the offsets where all these instructions are present. We can do this easily with the help of Ghidra search instruction pattern feature.
为了在所有这些 SVC 指令上附加 Frida 拦截器,我们首先需要找出所有这些指令存在的偏移量。我们可以在 Ghidra 搜索指令模式功能的帮助下轻松做到这一点。

Advanced Root Detection & Bypass Techniques

And we found all the SVC instructions as shown below:
我们找到了所有 SVC 指令,如下所示:

Advanced Root Detection & Bypass Techniques

Let’s now intercept these supervisor calls and see what file the openat functions invoke.

function hookSVC(base_addr){
    Interceptor.attach(base_addr.add(0x00001998), function(){
        var path = this.context.x1.readCString();
        console.log(`svc: ${path}`);
    })

    Interceptor.attach(base_addr.add(0x000019bc), function(){
        var path = this.context.x1.readCString();
        console.log(`svc: ${path}`);
    })

    Interceptor.attach(base_addr.add(0x000019dc), function(){
        var path = this.context.x1.readCString();
        console.log(`svc: ${path}`);
    })

    Interceptor.attach(base_addr.add(0x00001a00), function(){
        var path = this.context.x1.readCString();
        console.log(`svc: ${path}`);
    })

    Interceptor.attach(base_addr.add(0x00001a20), function(){
        var path = this.context.x1.readCString();
        console.log(`svc: ${path}`);
    })
}

Let’s run the script and see whether it shows anything interesting or not.
让我们运行脚本,看看它是否显示任何有趣的内容。

svc 56: /system/xbin/su
svc 56: /system/bin/su
svc 56: /sbin/su
svc 56: /system/bin/.ext/su
svc 56: /system/sd/xbin/su
detectRoot ret value is 566

From the output we can clearly observe that these SVC instructions are trying to access su binary paths. Great! Now we can bypass this easily by passing a non-existing path in the X1 register.
从输出中我们可以清楚地观察到这些 SVC 指令正在尝试访问 su 二进制路径。伟大!现在,我们可以通过在 X1 寄存器中传递不存在的路径来轻松绕过它。

Interceptor.attach(base_addr.add(0x000025a0), {
    onEnter: function (args) {
        var path = Memory.readCString(this.context.x1);
        this.context.x1.writeUtf8String("/non/exist");
        console.log(`svc ${this.context.x8.toInt32()}: ${path}`);
    }
})
Advanced Root Detection & Bypass Techniques

Alright, the app is no longer displaying any complaints, and it now indicates that the device environment is clean. We have successfully bypassed all the checks!
好的,该应用程序不再显示任何投诉,现在指示设备环境干净。我们已经成功绕过了所有的检查!

Conclusion 结论

In this blog post, we have learned about various root detection techniques used in Android apps. One technique was to check for the presence of the su binary path. Another technique was to check the selinux policy. We found that even after bypassing these checks, the app was still detecting root. 
在这篇博文中,我们了解了Android应用程序中使用的各种根检测技术。一种技术是检查 su 二进制路径是否存在。另一种技术是检查selinux策略。我们发现,即使在绕过这些检查后,该应用程序仍在检测root。

Further analysis revealed some new detections based on zygote and mountinfo, which were being performed using string comparison. Finally, we discovered a very interesting detection that was based on su binary detection, but was being done stealthily with the help of supervisor instructions (SVC). SVC instructions are used to call a function using a system call number. We saw how to identify these system calls and how to figure out the function names mapped to the system call number. 
进一步的分析揭示了一些基于 和 mountinfo 的新 zygote 检测,这些检测是使用字符串比较执行的。最后,我们发现了一个非常有趣的检测,它基于 su 二进制检测,但在主管指令的帮助下秘密完成( SVC )。SVC 指令用于使用系统调用号调用函数。我们了解了如何识别这些系统调用以及如何找出映射到系统调用号的函数名称。

In the end, we were able to bypass all of these checks using Frida as our hooking framework!
最后,我们能够使用 Frida 作为我们的钩子框架绕过所有这些检查!

GET IN TOUCH 取得联系

Visit our training page if you’re interested in learning more about these techniques and developing your abilities further. Additionally, you may look through our Events page and sign up for our upcoming Public trainings. 
如果您有兴趣了解有关这些技术的更多信息并进一步发展您的能力,请访问我们的培训页面。此外,您可以浏览我们的活动页面并注册我们即将举行的公开培训。

Please don’t hesitate to reach out to us through out Contact Us page or through the Button below if you have any questions or need assistance with Penetration Testing or any other Security-related Services. We will answer in a timely manner within 1 business day.
如果您有任何问题或需要渗透测试或任何其他安全相关服务的帮助,请随时通过“联系我们”页面或通过下面的按钮与我们联系。我们将在1个工作日内及时回复。

We are always looking for talented people to join our team. Visit out Careers page to look at the available roles. We would love to hear from you.
我们一直在寻找有才华的人加入我们的团队。访问职业页面查看可用职位。我们很乐意听取您的意见。

原文始发于Advanced Root Detection & Bypass Techniques

版权声明:admin 发表于 2023年9月21日 上午9:09。
转载请注明:Advanced Root Detection & Bypass Techniques | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...