Android Root 检测原理

Android Root 检测原理

  • [1. Android Root 检测的基本思路](#1. Android Root 检测的基本思路)
  • [2. 什么是 Android Root](#2. 什么是 Android Root)
    • [Linux/Android 权限模型里的 root](#Linux/Android 权限模型里的 root)
    • [Root 不是一个布尔开关](#Root 不是一个布尔开关)
  • [3. 为什么 Root 检测永远不可能 100% 准确](#3. 为什么 Root 检测永远不可能 100% 准确)
  • [4. 具体检测项](#4. 具体检测项)
    • [检测 Root 管理类 App](#检测 Root 管理类 App)
    • [检测危险 App](#检测危险 App)
    • [检测 Root 隐藏类 App(需要观察下误报情况)](#检测 Root 隐藏类 App(需要观察下误报情况))
    • [检测 `test-keys`](#检测 test-keys)
    • [通过文件路径检查 `su`](#通过文件路径检查 su)
      • [什么是 `su`?](#什么是 su?)
      • 检测策略
        • 硬编码路径
        • [Path 拼接](#Path 拼接)
        • [通过 `which su` 再查一次](#通过 which su 再查一次)
        • [单独做一遍 Native 检测](#单独做一遍 Native 检测)
    • [检测 `magisk`](#检测 magisk)
      • [同 su 文件的查询方式](#同 su 文件的查询方式)
      • [查 Magisk 文件痕迹](#查 Magisk 文件痕迹)
      • [查 Unix Domain Socket(需要观察下误报情况)](#查 Unix Domain Socket(需要观察下误报情况))
    • 检测危险系统属性
    • 检测关键系统路径是否可写

1. Android Root 检测的基本思路

Android Root 检测不是去读取某个系统开关,然后得到"这台手机已经 root"或"这台手机没有 root"的绝对结论。正确的思路是:

  • 收集很多和 root 高度相关的信号

  • 只要其中一个或多个信号命中,就把设备判定为存在 root 迹象

  • 把这些迹象组合成一个最终结果

原因很简单:一旦攻击者拿到 root,他在本机上的权限就非常高,理论上可以反过来欺骗你的检测逻辑。所以本地 root 检测从来都是一个对抗问题,不是一个静态判断


2. 什么是 Android Root

Linux/Android 权限模型里的 root

Android 底层是 Linux。Linux 中最特殊的用户是超级用户,也就是 root,对应 UID 0。拥有 root 身份后,进程可以:

  • 访问本来不允许访问的文件

  • 修改系统分区

  • 注入或替换系统组件

  • 干预别的进程

  • 绕过很多普通 App 的安全边界

Android 在 Linux 之上又加了一层更严格的应用沙箱:

  • 每个 App 默认有自己的 UID

  • 每个 App 的数据目录彼此隔离

  • 权限受 Android framework 和 SELinux 共同约束

所以正常 App 即使拿到了危险权限,也不等于拿到了 root。Root 的本质,是让某个进程获得超出普通应用模型的超级权限。

Root 不是一个布尔开关

Root 并不是系统里有一个公开字段,例如:

Plaintext 复制代码
device.rooted = true

现实里 root 可能表现为很多不同形态:

  • 系统里有 su 二进制文件

  • 安装了 Magisk、SuperSU 一类 root 管理器

  • 某些系统分区变成可写

  • 设备运行的是带 test-keys 的工程构建

  • 系统属性处于不安全配置

  • 存在 Xposed、RootCloak 这类隐藏或注入框架


3. 为什么 Root 检测永远不可能 100% 准确

root == god

一旦对方拿到 root,他就有能力修改、拦截、伪造你的检测路径

  • 你可以查 File.exists(),但对方可以 Hook 它

  • 你可以执行 which su,但对方可以伪造 PATH 或拦截命令执行

  • 你可以查 getprop,但对方可以改输出

  • 你可以查 mount,但对方可以隐藏挂载信息

  • 你可以下沉到 Native,攻击者还可以继续 Hook libc 或 JNI

因此,本地 root 检测的目标不是绝对正确,而是:

  • 降低攻击者绕过检测的成功率

  • 提高攻击者隐藏 root 的成本

  • 给风控系统提供一个设备/账号可信度


4. 具体检测项

检测 Root 管理类 App

安装某些 App,本身就是非常强的 root 相关信号。检查设备上是否安装了已知 root 管理类应用:

Plain 复制代码
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

可以遍历包名列表,如果能拿到 Root 管理类应用包信息,说明这个包存在,返回 true

局限性

  • 包名库是静态的,查不到未知工具

  • 攻击者可以隐藏、卸载或改包名

  • 某些工具存在但不代表 root 当前可用,但高度可疑(正常用户不会装这些软件)


检测危险 App

这里的名单比 root 管理器更宽,包括 Lucky Patcher、ROM Manager、部分模块框架管理器等。

在真实风控场景中,部分精明的攻击者会对 root 管理器做一些隐藏,但常常会安装一些周边工具。周边工具未必说明已经 root,但经常能作为很好的辅助证据。

Plain 复制代码
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_

检测 Root 隐藏类 App(需要观察下误报情况)

这里会查一些典型的 root 隐藏或注入框架 App。如果一台设备安装了专门隐藏 root 的框架,那么从安全角度讲,它已经高度可疑了。

Plain 复制代码
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

检测 test-keys

Android 固件在构建和签名时,常见会出现类似标签:

  • release-keys

  • test-keys

通常来说:

  • release-keys 更接近正式量产发布构建

  • test-keys 更常见于 AOSP、自编译 ROM、工程构建或某些测试环境

此处直接读取:

Java 复制代码
android.os.Build.TAGS

如果里面包含 test-keys,代表设备不是标准官方量产系统,系统可能被修改过,与刷机、调试、root 环境的相关性更高。返回 true


通过文件路径检查 su

什么是 su

su 是 Unix/Linux 里非常经典的提权入口。在 Android root 场景中,通常意味着设备存在从普通身份切换到超级用户身份的能力。

这类检测的本质,是设备文件系统里,是否能找到 root 之后常见的二进制落点。

检测策略

硬编码路径

第一部分是硬编码的常见路径,此部分是:收集历史上各种 root 方案、刷机包、定制 ROM、工具链常见的落点目录,然后在目录后面拼接文件名:su

示例代码:

java 复制代码
for (String path : pathsArray) {
    String completePath = path + filename;
    File f = new File(path, filename);
    boolean fileExists = f.exists();
}
Plain 复制代码
/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/
/system_ext/bin/
/cache/
/data/
/dev/
Path 拼接

PATH 代表的是:当前进程执行命令时默认会去哪些目录里找可执行文件。

如果只查固定路径,攻击者把 su 放到一个不在列表中的可执行目录,就可能绕过静态规则。

PATH 动态并进来,相当于扩大了搜索面。

具体策略是:PATH: 拆开,逐个加入路径列表,然后拼接 su ,检测文件是否存在

局限性

  • su 可以被隐藏、改名、按需挂载

  • 某些 root 方案可能不在经典路径落盘

  • Java 文件访问 API 可以被 Hook

通过 which su 再查一次

实现方式是执行系统命令:

Java 复制代码
Runtime.getRuntime().exec(new String[] { "which", "su" });

然后读取标准输出,只要有结果行,就认定 su 存在。

局限性

  • which 本身可能不存在或被替换

  • 命令执行结果可以被 Hook

  • PATH 可以被操控

单独做一遍 Native 检测

把检查 su 文件是否存在,除了在 Java 层做一次,还在 Native 层再做一次

因为 Java 层常见的几个点很容易成为 Hook 目标:

  • File.exists()

  • Runtime.exec()

  • PackageManager.getPackageInfo()

而把检查放到 Native 后,攻击者如果还想无痕绕过,就需要更进一步:

  • 拦截 JNI 调用

  • 干预动态库加载

  • Hook libc 的 fopen

  • 或直接屏蔽/替换 Native 库

绕过成本就变高了


检测 magisk

同 su 文件的查询方式

逻辑和查 su 文件一样,只不过查的是 magisk 这个二进制或相关落点。

现代 Android root 生态里,Magisk 的地位非常高。即使攻击者对 su 做了更多隐藏,相关的 Magisk 痕迹仍然可能留下来。

策略:路径复用检查 su 文件的路径,只不过二进制文件改为:magisk

查 Magisk 文件痕迹

即使 Magisk App 图标不见了、Java 层也看不到,它在系统里留下的安装残留、启动脚本、镜像文件、日志文件,仍然可能暴露存在

bash 复制代码
/dev/.magisk.unblock
/sbin/magiskinit
/sbin/magisk
/sbin/.magisk
/data/adb/magisk.img
/data/adb/magisk.db
/data/adb/.boot_count
/data/adb/magisk_simple
/data/adb/magisk
/cache/.disable_magisk
/cache/magisk.log
/init.magisk.rc

查 Unix Domain Socket(需要观察下误报情况)

/proc/net/unix 里列的是系统当前的 Unix Domain Socket。看 socket 名字是不是符合一种"很像被刻意隐藏过"的形态,重点关注:

  • 名字前缀是 @
  • 去掉前缀后,不包含 /、空格、. 这些普通 socket 常见字符
  • 如果剩下的名字长度达到 32 位以上,就认为非常可疑

检测危险系统属性

重点看两项属性:

  • ro.debuggable = 1表示系统更偏调试构建

  • ro.secure = 0 表示系统安全边界更松

直接执行:

Java 复制代码
getprop

然后把整段输出按行拆开,逐行匹配目标键值。不能只查数字 10,而是按 Android getprop 典型输出格式,检查是否包含(防止误报):

  • [1]

  • [0]

局限性

  • 一些非官方系统会带这些值,但不一定等价于 root

  • 属性输出可以被伪造

  • 现代 root 环境可能把这些属性清理得很干净


检测关键系统路径是否可写

执行 mount 命令,然后解析挂载表,去看如下关键路径是否被挂载成 rw

Bash 复制代码
/system
/system/bin
/system/sbin
/system/xbin
/vendor/bin
/sbin
/etc

正常量产设备中,这些关键系统路径通常不应该被轻易写入。如果它们以 rw 方式挂载,说明:

  • 系统分区可被修改

  • 攻击者可能已经获得足够高的系统控制力

  • 设备更可能处于 root、刷机、篡改或工程态

注意:不同 Android 版本下,mount 命令的输出格式不一样。

Android 6 及以下 老格式:

Plaintext 复制代码
<device> <mount_point> <type> <options>

Android 6 以上 新格式:

Plaintext 复制代码
<device> on <mount_point> type <fs_type> (<options>)

因此代码需要根据 SDK_INT 决定:

  • 挂载点在哪个字段

  • 挂载选项在哪个字段

Android 6 以上的挂载选项外层还有括号,所以需要做好括号清理,再按逗号拆分,最后精确匹配 rw,参考代码:

java 复制代码
public boolean checkForRWPaths() {

    boolean result = false;
   
    //Run the command "mount" to retrieve all mounted directories
    String[] lines = mountReader();

    if (lines == null){
        // Could not read, assume false;
        return false;
    }

    //The SDK version of the software currently running on this hardware device.
    int sdkVersion = android.os.Build.VERSION.SDK_INT;
    
       /**
         *
         *  In devices that are running Android 6 and less, the mount command line has an output as follow:
         *
         *   <fs_spec_path> <fs_file> <fs_spec> <fs_mntopts>
         *
         *   where :
         *   - fs_spec_path: describes the path of the device or remote filesystem to be mounted.
         *   - fs_file: describes the mount point for the filesystem.
         *   - fs_spec describes the block device or remote filesystem to be mounted.
         *   - fs_mntopts: describes the mount options associated with the filesystem. (E.g. "rw,nosuid,nodev" )
         *
         */

        /** In devices running Android which is greater than Marshmallow, the mount command output is as follow:
         *
         *      <fs_spec> <ON> <fs_file> <TYPE> <fs_vfs_type> <(fs_mntopts)>
         *
         * where :
         *   - fs_spec describes the block device or remote filesystem to be mounted.
         *   - fs_file: describes the mount point for the filesystem.
         *   - fs_vfs_type: describes the type of the filesystem.
         *   - fs_mntopts: describes the mount options associated with the filesystem. (E.g. "(rw,seclabel,nosuid,nodev,relatime)" )
         */
    
    for (String line : lines) {

        // Split lines into parts
        String[] args = line.split(" ");

        if ((sdkVersion <= android.os.Build.VERSION_CODES.M && args.length < 4)
                || (sdkVersion > android.os.Build.VERSION_CODES.M && args.length < 6)) {
            // If we don't have enough options per line, skip this and log an error
            QLog.e("Error formatting mount line: "+line);
            continue;
        }

        String mountPoint;
        String mountOptions;

        /**
         * To check if the device is running Android version higher than Marshmallow or not
         */
        if (sdkVersion > android.os.Build.VERSION_CODES.M) {
            mountPoint = args[2];
            mountOptions = args[5];
        } else {
            mountPoint = args[1];
            mountOptions = args[3];
        }

        for(String pathToCheck: Const.pathsThatShouldNotBeWritable) {
            if (mountPoint.equalsIgnoreCase(pathToCheck)) {

                   /**
                     * If the device is running an Android version above Marshmallow,
                     * need to remove parentheses from options parameter;
                     */
                    if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.M) {
                        mountOptions = mountOptions.replace("(", "");
                        mountOptions = mountOptions.replace(")", "");

                    }
                
                // Split options out and compare against "rw" to avoid false positives
                for (String option : mountOptions.split(",")){

                    if (option.equalsIgnoreCase("rw")){
                        QLog.v(pathToCheck+" path is mounted with rw permissions! "+line);
                        result = true;
                        break;
                    }
                }
            }
        }
    }

    return result;
}

局限性

  • 现代 systemless root 方案不一定真的把 /system 变成可写

相关推荐
kp000008 小时前
如何平衡模型输出的“有用性”和“安全性
人工智能·安全·网络安全·信息安全·ai安全
长风2308 小时前
Day 18:自动备份和恢复自定义UI —— 构建Dashboard自动恢复脚本
人工智能·安全
jctech9 小时前
让「小程序」跑起真正的原生代码:ComboNative 多进程原生小程序运行时 · Alpha 首发
android·设计模式·架构
tmacfrank9 小时前
Android 17 重点新特性介绍
android
●VON9 小时前
鸿蒙 PC Markdown 编辑器文件系统:Core File Kit 与安全保存
安全·华为·编辑器·harmonyos·鸿蒙
YM52e9 小时前
鸿蒙 Flutter 渐变效果详解:LinearGradient、RadialGradient、SweepGradient
android·学习·flutter·华为·harmonyos·鸿蒙
-SOLO-10 小时前
Android 内存压力工具
android
世界尽头与你10 小时前
iOS 越狱检测原理
安全·网络安全·ios·信息安全·渗透测试
雅客李10 小时前
2026云手机延迟数据测评 安卓云手机远程操控实测哪个最好
android·智能手机