一、Android 普通app获取root用户权限的原理

Android 普通程序执行su,可以获取root权限。

该过程原理如下:

1.1 手机环境要求:手机已root

首先,该手机已root。这意味着,手机持有者已经完成了以下操作:

(1)保证该手机的/system/bin/下面有su可执行文件

cp /data/local/tmp/su /system/bin/ #copy su 到/system/分区

(2)保证su的所有者是root用户

chown root:root su #su的所有者置成root

(3)保证su的权限位为4775
即,非root用户对su有执行权限,且su文件有SUID权限(rws的s)

chmod 4775 /system/bin/su #把su置成-rwsrwxr-x

rws的s,保证了运行su的进程的EUID,在运行su期间,变成了su的所有者的UID

关于EUID,详情参考:https://blog.csdn.net/qq_39441603/article/details/125013758

1.2 app发起一个shell进程

app执行su命令,在java层通常的实现方式是:

process = Runtime.getRuntime().exec("su");

这里的Runtime.getRuntime().exec,底层原理是发起一个shell进程,去执行"su"命令。

注意,这里的shell进程很关键。后面将su源代码时会涉及这个。

"su"命令就是执行su文件。

1.3 shell进程运行su文件

一个进程有三个UID:RUID,EUID,SUID

详情参考:https://blog.csdn.net/qq_39441603/article/details/125013758

由于su文件的权限位中有rws,所以:运行su的进程的EUID,在运行su期间,变成了su的所有者的UID

而上文已述,su的所有者是root用户,所以运行su的进程的EUID,在运行su期间,变成了root用户的UID

需要特别注意的是,当shell进程开始运行su的时候,shell进程的EUID,就已经是root用户的UID了。换言之,此时的shell进程,已经拥有root用户权限了。

但是,这种EUID变为root用户UID的情况,是有时效性的,在su文件运行完毕后就失效了。

而之所以运行过一次su文件,进程就能持久性地获得root用户权限,归功于su文件的内容。

其实,只要shell进程,运行的是一个owner是root用户,且权限位为4775的可执行文件,shell进程都能获取到(短暂的)root用户权限。之所以必须要运行su文件,而不是其他文件,就是因为su文件中的代码,能赋予shell进程持久性的root用户权限。

1.4 su中代码赋予shell进程持久性的root用户权限

这里需要解读su源代码。

这里对su源码的关键内容解释如下:

(1)su检查当前进程的RUID,发现其等于AID_SHELL,故允许继续执行

(AID_SHELL也就是Shell用户的UID)

su会检查当前进程的RUID,只有当其是Root用户的UID或Shell用户的UID时,才允许继续执行。

uid_t current_uid = getuid(); //返回当前进程的RUID
if (current_uid != AID_ROOT && current_uid != AID_SHELL) error(1, 0, "not allowed");

对于app发起的shell进程而言,其RUID并不因su文件的rws权限位而变化(rws权限位只影响进程的EUID),所以shell进程的RUID仍是shell用户的UID,而不是root用户的UID。

所以,对于app发起的shell进程而言,这里能执行下去,是因为current_uid等于AID_SHELL,而不是current_uid等于AID_ROOT

(2)su检查参数列表,发现无参数,故默认切换当前进程到root用户状态

su是Switch User的简写,用于各种用户切换,并不只用于切换到root用户状态。

根据su的源码,当su不加任何参数时,默认切换当前进程到uid = 0且gid = 0的状态,也就是root用户状态。

int main(int argc, char** argv) {……// The default user is root.// 无参数时,默认切换到rootuid_t uid = 0;gid_t gid = 0;……// If there are any arguments, the first argument is the uid/gid/supplementary groups.// 有参数时,切换到参数argv指定的用户状态if (*argv) {……// 从argv中提取内容,放入uid, gid, gids,覆盖之前uid和gid的默认值extract_uidgids(*argv, &uid, &gid, gids, &gids_count);……++argv;}……
}

(3)su调用setuid函数,将当前shell进程的RUID,设置为root用户进程的UID

这就是为什么su程序能让shell进程持久性地切换到root用户UID。

关于setuid函数,参考https://blog.csdn.net/qq_39441603/article/details/125013758

概括而言:
su文件的rws权限位,让当前的shell进程的EUID,成为了su所有者(Root用户)的UID,也就是AID_ROOT(也就是0)

所以,这里的setuid(uid),会按照setuid的情况1,将当前进程的RUID,EUID和SUID都设置为uid,并返回0。由于无参数,所以这里的uid是缺省值AID_ROOT。

// 根据参数(或缺省默认值)设置当前进程的gid和uid
if (setgid(gid)) error(1, errno, "setgid failed");
if (setuid(uid)) // 由于当前shell进程的EUID为AID_ROOT,// 所以这里的setuid(uid),会按照setuid的情况(1),// 将当前shell进程的RUID,EUID和SUID均设置为uiderror(1, errno, "setuid failed");

如果当前的shell进程的EUID!=AID_ROOT,则属于情况2(当进程的SUID==AID_ROOT时)或情况3(当进程的SUID!=AID_ROOT时),则setuid(uid)至多只影响当前shell进程的EUID,而不影响其RUID和SUID

之后,shell进程会继续执行完su程序。su程序执行完毕后,shell进程的RUID,EUID和SUID均为AID_ROOT,意味着shell进程获得了持久性的Root用户权限。

二、su源码完整解读

下面给出su程序源码的完整解读。

部分参考:https://zhuanlan.zhihu.com/p/47661378

2.1 Android版本

以android-12.0.0_r3为例:
源码参考:http://aospxref.com/android-12.0.0_r3/

2.2 su 二进制&源码位置

su二进制文件一般在/system/bin 目录或/system/xbin 目录

编译安卓系统源代码时,编译好的su二进制文件在/out/target/product/<vendor>/system/xbin中,
system.img镜像文件中没有su二进制文件

su的源代码在/system/extras/su 目录下:
http://aospxref.com/android-12.0.0_r3/xref/system/extras/su/

2.3 su 源码分析(带注释)

Android.mk:

LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)LOCAL_CFLAGS := -Wall -WerrorLOCAL_SRC_FILES:= su.cppLOCAL_MODULE:= su
LOCAL_LICENSE_KINDS:= SPDX-license-identifier-Apache-2.0
LOCAL_LICENSE_CONDITIONS:= notice
LOCAL_NOTICE_FILE:= $(LOCAL_PATH)/NOTICELOCAL_HEADER_LIBRARIES := libcutils_headersLOCAL_MODULE_PATH := $(TARGET_OUT_OPTIONAL_EXECUTABLES)include $(BUILD_EXECUTABLE)

su.cpp源码:


#include <errno.h>
#include <error.h>
#include <getopt.h>
#include <paths.h>
#include <pwd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>#include <private/android_filesystem_config.h>// 根据用户名获取uid和gid
void pwtoid(const char* tok, uid_t* uid, gid_t* gid) {// 根据用户名获取用户登录信息struct passwd* pw = getpwnam(tok);if (pw) {if (uid) *uid = pw->pw_uid;if (gid) *gid = pw->pw_gid;} else {char* end;errno = 0;uid_t tmpid = strtoul(tok, &end, 10);if (errno != 0 || end == tok) error(1, errno, "invalid uid/gid '%s'", tok);if (uid) *uid = tmpid;if (gid) *gid = tmpid;}
}// 调用pwtoid,根据用户名获取uid和gid
// arg1: main的argv[0](逗号分隔)
void extract_uidgids(const char* uidgids, uid_t* uid, gid_t* gid, gid_t* gids, int* gids_count) {char *clobberablegids;char *nexttok;char *tok;int gids_found;if (!uidgids || !*uidgids) {*gid = *uid = 0;*gids_count = 0;return;}clobberablegids = strdup(uidgids);strcpy(clobberablegids, uidgids);nexttok = clobberablegids;tok = strsep(&nexttok, ",");pwtoid(tok, uid, gid);tok = strsep(&nexttok, ",");if (!tok) {/* gid is already set above */*gids_count = 0;free(clobberablegids);return;}pwtoid(tok, NULL, gid);gids_found = 0;while ((gids_found < *gids_count) && (tok = strsep(&nexttok, ","))) {pwtoid(tok, NULL, gids);gids_found++;gids++;}if (nexttok && gids_found == *gids_count) {fprintf(stderr, "too many group ids\n");}*gids_count = gids_found;free(clobberablegids);
}// su的用法:
// su [WHO [COMMAND...]]
// WHO:要切换到的用户,默认为root,逗号分隔
// COMMAND:切换到WHO之后要执行的命令
int main(int argc, char** argv) {uid_t current_uid = getuid(); //返回当前进程的RUID// 当前进程的RUID必须是root用户UID 或shell用户UID// 关于shell用户:// 安卓app 获取shell权限时,通常使用:// Process p = Runtime.getRuntime().exec("su");// 这里的Runtime.getRuntime().exec,// 底层原理是调用操作系统接口,新建一个shell进程异步执行命令if (current_uid != AID_ROOT && current_uid != AID_SHELL) error(1, 0, "not allowed");// Handle -h and --help.++argv;if (*argv && (strcmp(*argv, "--help") == 0 || strcmp(*argv, "-h") == 0)) {fprintf(stderr,"usage: su [WHO [COMMAND...]]\n""\n""Switch to WHO (default 'root') and run the given COMMAND (default sh).\n""\n""WHO is a comma-separated list of user, group, and supplementary groups\n""in that order.\n""\n");return 0;}// The default user is root.// 无参数时,默认切换到rootuid_t uid = 0;gid_t gid = 0;// su的核心部分:由setgroups、setgid、setuid完成,设置当前进程的附加组、gid和uid// If there are any arguments, the first argument is the uid/gid/supplementary groups.// 有参数时,切换到参数argv指定的用户状态if (*argv) {gid_t gids[10];int gids_count = sizeof(gids)/sizeof(gids[0]);// 从argv中提取内容,放入uid, gid, gids,覆盖之前uid和gid的默认值extract_uidgids(*argv, &uid, &gid, gids, &gids_count);// 根据参数 设置当前进程的附加组if (gids_count) {// int setgroups(size_t size, const gid_t * list);// setgroups()用来 将 当前进程的附加组 设置为 参数2 list数组中所标明的group// setgroups()参数1 size 为list数组的gid_t 数目, 最大值为NGROUP(32)if (setgroups(gids_count, gids)) {error(1, errno, "setgroups failed");}}++argv;}// 根据参数(或缺省默认值)设置当前进程的gid和uidif (setgid(gid)) error(1, errno, "setgid failed");if (setuid(uid)) // 由于当前shell进程的EUID为AID_ROOT,// 所以这里的setuid(uid),会按照setuid的情况(1),// 将当前shell进程的RUID,EUID和SUID均设置为uid// 否则按照情况(2)或情况(3),至多只影响当前进程的EUIDerror(1, errno, "setuid failed");// Reset parts of the environment.setenv("PATH", _PATH_DEFPATH, 1);unsetenv("IFS");struct passwd* pw = getpwuid(uid);if (pw) {setenv("LOGNAME", pw->pw_name, 1);setenv("USER", pw->pw_name, 1);} else {unsetenv("LOGNAME");unsetenv("USER");}// Set up the arguments for exec.char* exec_args[argc + 1];  // Having too much space is fine.size_t i = 0;for (; *argv != NULL; ++i) {exec_args[i] = *argv++;}// Default to the standard shell.if (i == 0) exec_args[i++] = const_cast<char*>("/system/bin/sh");exec_args[i] = NULL;execvp(exec_args[0], exec_args);error(1, errno, "failed to exec %s", exec_args[0]);
}

关于Supplementary group(附加组):
参见:https://blog.csdn.net/qq_39441603/article/details/125010004

三、Android设备 具体root方案

前文已述,一个shell进程,要想获得root权限,需要执行下列代码:

cp /data/local/tmp/su /system/bin/ #copy su 到/system/分区
chown root:root su #su的所有者置成root
chmod 4775 /system/bin/su #把su置成-rwsr-xr-x

但问题是,上面的每一行代码,都需要root用户权限才能执行。

而上述代码本身就是用于获取root用户权限的。所以再执行上述代码之前,普通app发起的进程,是无法获取root用户权限的。

那么这个逻辑闭环如何打破呢?这就需要root技术。

3.1 使用提权漏洞

一个办法是,找一个本身有root权限的进程来执行上述代码。这样普通app执行process = Runtime.getRuntime().exec("su");就能获得root权限了。
但是,有root权限的进程,都是预装app发起的,代码写死了,普通app没法控制它去执行特定的代码。
这个时候就需要用提权漏洞,来root手机。比如zergRush漏洞,就利用了一个拥有root权限的进程的栈溢出漏洞。

3.2 修改ROM并刷机

(1)BootLoader:
引导加载程序BootLoader是系统启动时自动运行的一个底层程序。
程序的主要目的是初始化硬件,然后找到并启动主操作系统
Android bootloader一般是锁定的,也就仅仅允许启动或安装一个被OEM签名的操作系统img。

(2)Fastboot:
Fastboot是一种手机状态的名称,也是一个协议的名称。
当手机处于快速启动模式(Fastboot模式)时,若PC与手机通过USB连接,则两者可通过Fastboot协议进行通信。

具体而言,PC上的fastboot命令行工具,通过USB bulk,与手机上的USB Client通信。
PC上的fastboot命令行工具:位于Android SDK中
手机上的USB Client:Bootloader

Fastboot最初的作用是向BootLoader发送分区镜像,来将镜像写入到特定的设备分区中,实现分区清除或者覆盖,以方便Android系统移植(device bring-up)和设备恢复出厂设置。
但是现在,Fastboot多被用于解锁Bootloader。

(3)Recovery:
功能相当于PC中的PE。
用于存放Recovery恢复模式的分区,里面有一套Linux Kernel,但并不是安卓系统里的那个Linux Kernel。

分为原生Recovery和定制Recovery(例如TWRP提供的Recovery)。

(4)Boot:
启动顺序在bootloader之后,与recovery同级。
用于存放安卓系统的Linux Kernel相关内容。

可以参考:
https://blog.csdn.net/fmc088/article/details/90376116

3.2.1 Magisk patch boot.img,fastboot 刷入 patched boot.img(线刷)

整体思路:
用Magisk app 对 ROM的boot.img进行patch,并将patched boot.img存放至PC;
手机重启进入Fastboot;
使用PC上Fastboot工具,通过数据线,将patched boot.img刷入手机。

可以参见:
https://blog.csdn.net/qq_39441603/article/details/124679514

(1)解BootLoader锁
未解锁的BootLoader,不允许刷入非官方签名过的img镜像(包括Recovery.img,Boot.img等)
解锁之后,就可以通过PC上的fastboot程序,刷入Magisk patch过的boot.img

(2)Magisk对boot.img进行patch
获取到当前OS的线刷包的boot.img后,使用Magisk app对boot.img进行patch。

线刷包通常是tgz格式,例如https://xiaomirom.com/download/mi-8-dipper-weekly-9.8.22/#china-fastboot。

Magisk对boot.img进行patch的过程:Magisk通过对boot.img的patch,在boot启动阶段创建钩子,把/data/magisk.img挂载到/magisk,构建出一个在 system 基础上能够自定义替换,增加以及删除的文件系统。
所有操作都在启动时完成,实际上并不修改/system(即所谓systemless方案,以不触动 /system 的方式修改 /system)。/magisk相当于android系统的另一个独立分区。

(3)fastboot 刷入 patched boot.img

重启进入fastboot,并使用PC上的Fastboot命令行工具,通过USB bulk,基于fastboot协议,与手机上的USB Client通信,刷入 patched boot.img

adb reboot bootloader
fastboot flash boot magisk_patched-22100_LMHbQ.img
fastboot reboot

3.2.2 TWRP刷入第三方Recovery + Magisk.zip

详细过程可参考:
使用ADB Sideload方案:https://miuiver.com/install-magisk-via-twrp/
格式化整个Data分区方案:https://forum.butian.net/share/1068

(1)解BootLoader锁

(2)刷入定制Recovery

定制Recovery可从TWRP项目中获得
直接启动定制Recovery:fastboot boot custom-recovery.img
将定制Recovery永久写入设备:fastboot flash custom-recovery.img
将定制Recovery永久写入设备并启动:fastboot flash boot custom-recovery.img

(3)借助定制Recovery刷入Magisk.zip
从界面中的Log来看,好像是,在这个Magisk.zip的刷入过程中,Magisk会去patch boot.img:

使用ADB Sideload:

格式化整个Data分区:

如果是这样的话,这种基于定制Recovery卡刷的root原理,跟基于fastboot线刷的root原理,就是一样的了:都是patch boot.img。

线刷和卡刷区别主要在于:
线刷没动Recovery,在Fastboot模式下手,刷入Magisk app提前patch好的boot.img;
卡刷动了Recovery,在Recovery模式下手,利用Magisk.zip对boot.img进行patch。

四、Magisk原理

参考:https://www.zhihu.com/question/278585502
Magisk的原理,大致是通过修改boot分区,使得手机在启动时,systemless中的文件先作为系统文件加载,然后才加载真正的系统,达到了不修改system分区而实现修改的效果。

比如修改机型或是字体,只需要安装并启用相应的模块,模块存放在systemless里面,就会在手机启动时生效;又因为system分区本身并没有被修改,只需要禁用模块就可以还原,无需备份原有的配置;

而root,也就是把root相关的一些文件放在systemless里,取代掉手机系统原本的su文件(SuperSU就是直接修改system里的su文件,而magisk是把su放在systemless中,手机启动时取代系统原有su)

Magisk通过启动时在 boot 中创建钩子,把 /data/magisk.img 挂载到 /magisk,在 system 基础上构建出了一个能够自定义替换、增加以及删除的文件系统。所有操作都在启动的时候完成,实际上并没有对 /system 分区进行修改(即 systemless方案,以不触动 /system 的方式修改 /system)。

【Android安全】Android root原理及方案 | Magisk原理相关推荐

  1. 【Android】Pixel 2 Android 9 系统 ROOT 操作 ( TWRP 下载 | Magisk Manager 下载 | 线刷包下载 | 线刷 9.0 系统 | ROOT 操作 )

    文章目录 一.下载 TeamWin - TWRP 二.下载 Magisk Manager 三.下载 Android 9.0 镜像 四.线刷 Android 9.0 系统 五.ROOT 操作 六.可能用 ...

  2. Android R user root + remount 修改方案

    众所周知,Android在大版本更新上对权限要求越来越严格,AndroidR上user版本包含remount权限也需要进行比较大的修改,如果只需要有root 权限,只需要如下修改即可: 修改源码 sy ...

  3. android 手机获取root权限(刷入magisk面具方式)_获取刷入模块_MIUI_android7/android12实践

    文章目录 预备环节 基础参考内容 视频教程★\bigstar★ 基础知识准备 推荐具有的技能(optional) 工具/材料准备 硬件和软件 magisk app注意事项/刷入面具 关于刷进入magi ...

  4. 【Android】Android获取root原理

    [Android]Android获取root原理 背景 逆向爱好者绕不开的操作,root权限才能最大范围对调用链的控制. 相关知识介绍 root权限 在 Android 操作系统中,root 权限是指 ...

  5. Android手机一键Root原理分析(作者:非虫,文章来自:《黑客防线》2012年7月)

    之前几天都在做Android漏洞分析的项目,万幸发现了这篇文章.废话不多说,上文章! <Android手机一键Root原理分析> (作者:非虫,文章来自:<黑客防线>2012年 ...

  6. Android中apk加固完善篇之内存加载dex方案实现原理(不落地方式加载)

    一.前言 时隔半年,困扰的问题始终是需要解决的,之前也算是没时间弄,今天因为有人在此提起这个问题,那么就不能不解决了,这里写一篇文章记录一下吧.那么是什么问题呢? 就是关于之前的一个话题:Androi ...

  7. Android App罕见错误和优化方案

    本文来自http://blog.csdn.net/liuxian13183/ ,引用必须注明出处! 1.App如果被定义一个有参数构造函数,那么需要再定义一个无参数的,如果不则会在某些情况下初始化失败 ...

  8. 基于Android RIL层实现来电拦截的技术原理(一)

    引入 目前市面上,Android上的防骚扰类应用非常多,比如腾讯手机管家.360手机卫士.金山手机卫士等.由于受Android OS设计框架,他们的来电拦截实现,都是通过接受com.android.p ...

  9. Android应用加固的简单实现方案

    个人博客 http://www.milovetingting.cn Android应用加固的简单实现方案 概述 Android应用加固的诸多方案中,其中一种就是基于dex的加固,本文介绍基于dex的加 ...

  10. Android 10.0 PackageManagerService(一)工作原理及启动流程-[Android取经之路]

    摘要:PackageManagerService是Android系统核心服务之一,在Android中的非常重要,主要负责APK.jar包等的管理. 阅读本文大约需要花费50分钟. 文章的内容主要还是从 ...

最新文章

  1. C#中Action与delegate、EventHandler的差异
  2. 985 211 PHP,985 211是什么意思
  3. BJUI怎样对input添加自定义验证规则
  4. just have a view of the open source project i contributed!!!
  5. python基础之语句_P009 python基础之控制语句01
  6. 学习日记-类继承中的上下转换
  7. 【Linux】用户与权限
  8. Python已成美国顶尖高校中最受欢迎的入门编程语言
  9. 递归应用:折半查找法
  10. 阿里巴巴技术总监全解中台架构
  11. oracle查询语句转sql,将sql server查询语句转换为oracle查询语句[紧急]
  12. sap系统搭建教程_SAP基础教程
  13. websphere设置共享库
  14. C++类内初始值的初始化形式
  15. 为什么你还没有买新能源汽车? 1
  16. 西安80转换成北京独立计算机,WGS84经纬度坐标转换为西安80高斯投影坐标.
  17. win10系统资源管理器频繁崩溃重启的解决思路
  18. CSS3特效-自定义checkbox样式
  19. 贪吃机器人DIY(二)
  20. java8(三)Stream API

热门文章

  1. Ticket Lock的Relaxed Atomics优化
  2. oracle 按汉字拼音顺序排序
  3. 中国在计算机领域取得的成就,厉害了我的国——盘点中国科学近年有哪些成就...
  4. java实现图片合成功能,两张图片合成一张
  5. 小猫钓鱼纸牌游戏java_java实现纸牌游戏之小猫钓鱼算法
  6. 一条挨踢老狗的2017年终总结
  7. 如何做到阿里云 Redis 开发规范中的拒绝 bigkey
  8. 微信小程序实现扫一扫功能
  9. json 格式字符串
  10. Spring validation框架简介