Magisk分析(未完待续)
**[扩展]**主题表示理解Magisk原理所需要的前提知识, 并不与Magisk直接相关.
[扩展]早期ROOT原理
权限位
文件权限位
文件权限位总共四个:
1 | chmod 4 7 7 5 /system/bin/su |
特殊权限位(3位):
SUID位 (Set User ID)
取值: 4
作用:
- 对于文件: 设置了SUID位时, 在执行过程中将以文件所有者的身份运行. 这样普通用户也可以在执行过程中获得root才能执行的操作.
- 对于目录: 设置了SUID位时, 无论是以哪个用户创建文件或目录, 它们都将继承SUID目录的所有者, 并获得与所有者相同的权限
SGID位 (Set Group ID)
- 取值: 2
- 作用: 当可执行文件设置了 SGID 位时, 在执行过程中将以文件所属组的身份运行. 类似SUID
粘滞位 (Sticky Bit)
- 取值: 1
- 作用: 主要应用于目录, 当目录设置了粘滞位后, 只有目录的所有者, 文件的所有者和超级用户才能删除或移动该目录下的文件. 其他用户虽然对目录有写权限, 但无法删除或移动其他用户的文件.
其他权限位 (所有者, 所属组, 其他用户)
- 读r
- 写w
- 执行x
Linux的UID
Linux内核为每个进程维护三个UID值:
RUID(实际用户ID)
- 含义: 当前登录的UID
EUID(有效用户ID)
- 含义: 当前执行程序的UID
- 一般情况下: EUID = RUID
- 程序设置了SUID权限位[^1]: EUID = 程序的拥有者
SUID(保存的设置用户ID)
- 含义: EUID的一个副本, 跟SUID权限位[^1]有关
早期的ROOT权限
早期对于/system/bin目录下的文件并没有太多的限制, 所以可以直接通过SUID权限位的su文件来获取shell的root权限
进程ROOT
一个进程拥有root权限的标志: EUID = 0
ROOT条件
Magisk的目的之一: 获取ROOT权限.
那也得知道什么是ROOT, 怎样才算获取ROOT:
条件一: 保证手机的/system/bin下有su
条件二: su的所有者是root
条件三: 保证su的权限位[^2]是: 4775
- SUID权限: 1
- 所有者: 读写, 执行
- 所属组: 读写, 执行
- 其他用户: 读, 执行
下面分步解析各条件
环境变量设置su文件
在Linux中, /system/bin为环境变量, 即该目录下的可执行文件在其他路径下仍然可以执行.
理由
- 该目录下可以设置文件的SUID权限位 (最重要的原因)
- /system/bin为环境变量
问题
- 问题: su能否放在/data/tmp目录下, 然后用绝对路径来执行
- 回答: 不行, 因为data目录挂载时规定了文件不能有SUID权限位, 不满足ROOT条件[^3]的条件三
所有者是root
这里需要理解SUID的作用: 当SUID置一时, 其他用户运行该文件时, EUID会设置为文件的所有者.
所以当其他进程运行su的时候, EUID = 所有者 = root. 就相当于获取了root权限
su权限位为4775
最重要的是: 4 -> SUID权限位置一, 其作用在上面也讲了, 用于使进程的EUID设置为root.
原理过程
开启一个shell进程
- EUID: 当前用户
- SUID: 当前用户
- RUID: 当前用户
当shell运行su进程时
- 运行su的期间, 由于su的SUID权限位, shell的EUID = su的所有者 = root.
- 注意: 只在运行su的期间, shell获取了root权限, 所以提权只是暂时的
- 持久提权需要su的逻辑(这也是为什么我们需要使用su文件, 而不是一个权限位为4775的helloworld小程序)
su(switch user)的逻辑:
- su检查当前进程的RUID, SUID权限位只修改了shell的EUID = root, 所以RUID = AID_SHELL
- su发现没有传入参数, 默认切换为root, 即uid = 0, gid = 0;
- su调用setuid(uid), 而uid = 0, 所以设置了shell的RUID = 0, 且SUID = 0
su逻辑执行完毕, 这时shell的RUID, EUID, SUID都等于0
- EUID在su执行完毕后恢复为SUID, 但SUID被设置为0, 所以EUID恢复后仍然等于0
- 至此shell获得了持久root权限
新版Android对root提权的限制
Android4.3 -: 就是通过上面讲的SUID权限位su文件来进行提权
ANdroid4.3 +: Android系统增加了对于root提权的防护
- nosuid目录: /system和/data以
nosuid option
方式挂载, 这在前面也讲过, 即目录下的文件不能有SUID权限位 - Zygote: app进程由Zygote进程fork产生. Zygote进程设置了NO_NEW_PRIVS标志, 其子进程也带有NO_NEW_PRIVS标志, 带有该标识进程的EUID不受SUID影响.
- SELinux: 限制EUID为root的进程的行为
- nosuid目录: /system和/data以
新版Android的root方案
针对新版Android更加强大的防护策略, 就有了其他的root方案:
- 使用提权漏洞
- 修改ROM刷机
提权漏洞
普通app没有root权限, 无法实现上面root的三个条件.
可以利用提权漏洞来root, 比如: zergRush漏洞, 就利用了一个拥有root权限的进程的栈溢出漏洞.
修改ROM刷机
- XPosed
- Magisk
Magisk功能
ROOT权限获取和管理
- 比如/system/xbin(Android文件系统[^4])目录下没有su, 我们可以刷入响应的模块, 在系统启动初期, 讲su映射到/system/xbin下来获取root
挂载功能多样的各种扩展模块
- 替换字体, 指纹特效, 开机动画
Root原理
systemless root: 不修改system分区的情况下, 实现su.
Magisk修补boot.img[^5], 把自己的文件挂在到根目录(Android13)[^6]下的:
- /sbin (Android10-)
- /dev/{random_folder} (Android11+)
磁盘分区表
在系统启动的过程中, 会涉及到磁盘分区表的知识, 分区表将磁盘抽象为一个大的字节数组(跟内存相似), 但是是以扇区为单位(通常: 512 byte)
常见的磁盘分区表:
- 老式: MBR
- 新式: GPT
MBR
全称Master Boot Record(主引导记录), 有两层含义:
- 指开机启动的第一个扇区 (512 byte)
- 指这种扇区分配的方式
第一个扇区由两部分构成:
bootstrap code area: 446 byte
- 功能: 包含启动相关代码
partition table: 64byte
- 功能: 分区表
最后两个字节: 0x55AA
- 功能: MBR的标识
读写逻辑
有两种读写方式:
老式: C/H/S(Cylinder/Head/Sector)
- 以前读写的基本逻辑地址结构
新式: LBA(Logical Block Addressing)
- 当前逻辑地址结构, 抽象的程度更高, 将磁盘视为一个大的字节数组: [LBA0, LBA1, LBA2, …., LBAN]; 每个LBA大小通常为512 byte.
GPT
全称: GUID Partition Table
采用了前面的讲到的LBA逻辑区块地址.
结构图
布局
LBA0: MBR
- 功能: 出于兼容性考虑, LBA0仍然用作MBR
LBA1: 分区表头(Primary GPT Header)
- 功能: 定义了硬盘的可用空间和分区表的项大小和数量. 最多可以创建128个分区.
LBA2 ~ 33: 存储分区表项
每个分区表象的格式
起始(byte) 长度(byte) 内容 0 16 分区类型 16 16 分区GUID 32 8 起始LBA(小端序) 40 8 末尾LBA 48 8 属性标签 56 72 分区名
分区表与启动的关系
bootloader会分析gpt分区结构
Linux内核也会扫描gpt分区表, 生成gendisk的分区表相关结构.
- 检测到sda, sdb设备时会创建/sys/block/sda设备文件
术语
Ramdisk
有两种完全不同的含义:
老式: RAM模拟硬盘技术, 即使用一部分的RAM来模拟一个硬盘, 从而提高文件访问速率. (常见的应用: 作为Web缓存)
Android: 指的是boot.img中的ramdisk文件, 和传统意义上的RAM模拟硬盘技术不是一个东西.
- ramdisk文件中包含了: Android系统启动所需的文件和目录的文件系统映像. 在内核将ramdisk.img从boot.img中解压出来后, 将其放在内存的临时文件系统中. 最后内核将根据ramdisk中的内容创建根文件系统并挂载到’/‘下.
1 | 注意: 为了便于区分两个完全不同的概念, 下文中使用`ramdisk`表示RAM模拟硬盘技术, ramdisk表示一个Android的ramdisk文件. |
ramfs,tmpfs,rootfs
都是虚拟文件系统, 以下是各自的特性:
ramfs(随机访问存储文件系统)
- 基于内存的文件系统, 文件和目录都保存在内存中
- 系统重启则会丢失数据
tmpfs(临时文件系统)
- 基于内存的文件系统, 类似ramfs, 但是存储空间受限.
- 使用虚拟内存, 可以动态调整大小.
- 可以将数据持久化到磁盘中, 防止重启时数据丢失
rootfs(根文件系统)
- 系统启动时的初始文件系统
- 注意跟前面的根文件系统不是一个东西, 只是内核中的一个文件系统.
initrd
全称: init ramdisk
直译: 初始化ramdisk, 但是事实上这只是一种主流的启动方式.
Linux内核启动init进程
方式1
过程
Linux最原始的启动方式, 过程如下:
创建一个格式化为ext4文件系统的disk.img文件, 作为根文件系统
创建一个程序init, 作为内核执行程序, 随便写一个hello world. 然后挂载了disk.img后, 存入该文件系统中.
使用qemu启动指定内核
1
2
qemu-system-x86_64 -kernel arch/x86_64/boot/bzImage -hda my_rootfs/root_disk/disk.img --append "root=/dev/sda init=/init console=ttyS0" -nographic–append: 传递给内核作为命令行参数
- -hda: 指定硬盘映像, 即disk.img
- root: 指定根文件系统所在的设备, dev目录正常还没挂载, 只是习惯上的标识, 实际上内核只会找sda设备
- init=/init: 标识init作为内核执行程序
最后内核将sda作为跟文件系统所在设备, 以img文件作为文件系统, 同时执行init程序
缺点
- 时代发展, 硬件变得复杂. 导致根文件系统可能在各种设备上. 甚至是网络文件系统, 还可能需要加解密. 如果这些都需要在内核驱动中去实现, 实现难度大, 还不一定用得上.
解决方案
增加一个中间层: 初始根文件系统
内核先挂载一个初始根文件系统, 由初始根文件系统去加载合适的驱动并寻找最终根文件系统并挂载.
挂载初始根文件系统的方式:
ramdisk
(ramdisk模拟硬盘技术)- ramfs
- tmpfs
- rootfs
方式2
过程
创建一个格式化为ex2文件系统的ramdisk.img文件
创建linuxrc可执行文件, 作为初始根文件系统的内核执行文件. 挂载img后放入初始根文件系统的根目录下.
在初始根文件系统下创建/dev/console, 用于日志输出.
卸载文件系统
此时ramdisk.img的初始根文件系统结构如下
1
2
3
4--/
--linuxrc
--dev(dir)
--console(dir)
qemu编译
1
qemu-system-x86_64 -kernel arch/x86_64/boot/bzImage -hda my_rootfs/root_disk/disk.img -initrd my_rootfs/old_ramdisk/ramdisk.img --append "root=/dev/sda init=/init console=ttyS0" -nographic
- –hda: 最终根文件目录 = disk.img
- -initrd: 初始根文件目录 = ramdisk.img
结果
结果就是:
内核首先利用
ramdisk
(内存模拟硬盘), 并在内存上挂载了ramdisk.img, 同时执行了linuxrc程序, 等待该程序返回- linuxrc的功能: 加载init程序所需要的模块
随后内核挂载disk.img, 并执行init程序.
方式3
过程
创建一个格式化为ext2的disk.img
创建init可执行文件, 挂载disk.img并存入init
在初始根文件系统下创建/dev/console, 用于日志输出.
卸载文件系统, 得到修改后的disk.img
qemu编译
1
2
qemu-system-x86_64 -kernel arch/x86_64/boot/bzImage -initrd my_rootfs/initrd/disk.img --append "root=/dev/ram0 init=/init console=ttyS0" -nographic- 没有-hda
- -initrd: disk.img
- –append的root改为/dev/ram0 (在ram上挂载根文件目录)
方式4
过程
创建init可执行文件
将init可执行文件打包成压缩的initrd文件
qemu编译
1
2
qemu-system-x86_64 -kernel arch/x86_64/boot/bzImage -initrd my_rootfs/initrd_cpio/simple_initrd.cpio.gz --append "init=/init console=ttyS0" -nographic- -initrd: 压缩的initrd文件
结果
Android的boot.img中的ramdisk启动与此类似
方式5
过程
创建init可执行文件
创建/dev/console设备节点
修改内核配置
CONFIG_INITRAMFS_SOURCE="my_rootfs/initramfs/initramfs_data.cpio.gz"
编译
- ```C++
qemu-system-x86_64 -kernel arch/x86_64/boot/bzImage –append “init=/init console=ttyS0” -nographic1
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* 该方法就是initramfs, **将initramfs_data.cpio.gz文件和内核编译在了一起**, 因此启动时无需额外的参数.
### 源码分析
#### Kernel源码
切入点: BootLoader进入到Kernel层, 此时根文件系统还没有加载.
```C++
start_kernel()
vfs_caches_init()
mnt_init()
init_rootfs()
init_mount_tree() // ->
vfs_kern_mount(&rootfs_fs_type, 0, "rootfs", NULL) // 挂载rootfs
arch_call_rest_init()
rest_init() // 创建1号进程
kernel_thread(kernel_init, NULL, CLONE_FS)
kernel_init_freeable()
do_basic_setup() // 初始化驱动
do_initcalls() --> rootfs_initcall(populate_rootfs)
do_populate_rootfs()
unpack_to_rootfs() // 将initramfs()解压到rootfs
#ifdef CONFIG_BLK_DEV_RAM
populate_initrd_image(err);
console_on_rootfs()
if (init_eaccess(ramdisk_execute_command) != 0) prepare_namespace()
initrd_load()
mount_root()
create_dev("/dev/root", ROOT_DEV)
mount_block_root("/dev/root", root_mountflags)
devtmpfs_mount();
init_mount(".", "/", NULL, MS_MOVE, NULL);
init_chroot(".");
try_to_run_init_process()
- ```C++
init_mount_tree()
不管以哪种方式启动Linux, 都会经过**init_mout_tree()**
**, 随后调用****vfs_kern_mout()(&rootfs_fs_type, 0, "rootfs", NULL)**
.
同时还会:
- 设置current进程的pwd和root (current进程也叫swapper进程: 是内核空间的0号进程, 所有进程的祖先进程)
- 所以不管哪种启动方式都有一个rootfs挂载. 其实现方式可以是ramfs, 也可以是tmpfs.
rest_init()
创建1号进程, 该进程继承了current进程的文件系统信息: rootfs
do_basic_setup()
初始化驱动, 并调用populate_rootfs()
populate_rootfs()
调用链:do_populate_rootfs() -> unpack_to_rootfs()
将initramfs的内容解压到rootfs中.
方式一
回顾上面的qemu编译代码
1 |
|
kernel_init_freeable()
逻辑从populate_rootfs()开始, 方式一的initramfs没有内容, 所以这种编译方式所对应的initrd_start = 0, 回到kernel_init_freeable()
.
在该函数中继续执行:
- 检查rootfs中有没有init可执行文件, 在这里没有进入prepare_namespace()
prepare_namespace()
获取devicename = “dev/sda”, 截断后为”sda”. 然后调用initrd_load()函数
initrd_load()
加载rootfs根目录下的initrd.img文件, rootfs没有该文件, 创建/dev/ram节点.
往后
注: 关于系统启动的挂载可以看 Linux的文件系统和挂载点是什么意思? - 知乎 (zhihu.com)
下面的挂载过程:
- /root: 是一个tmpfs, 即基于内存的fs
- /dev/root: 是一个实际设备的文件系统, 挂载在内存/root上, 实现与内核连接
1 | //创建ROOT_DEV对应的设备节点/dev/root,如果没有指定rootfstype命令行参数就尝试遍历文件系统类型对/dev/root进行挂载,挂载点为/root,并且调用init_chdir("/root")将工作目录切换到/root目录下 |
run_init_process()
执行根系统上的init可执行文件
最后
==/root的挂载点被占用, 但是其只占内存中的一小部分, 所以可以一直存在, 可以理解为内存留给设备文件系统的一个接口==
方式二
qemu编译代码
1 | qemu-system-x86_64 -kernel arch/x86_64/boot/bzImage -hda my_rootfs/root_disk/disk.img -initrd my_rootfs/old_ramdisk/ramdisk.img --append "root=/dev/sda init=/init console=ttyS0" -nographic |
有两个img:
- 初始根文件系统: ramdisk.img
- 最终根文件系统: disk.img
kernel_init_freeable()
逻辑同样从do_populate_rootfs函数, 由于指定了-initrd参数, 所以initrd_start != 0, 会执行unpack_to_rootfs(), 将-initrd选项指定的ramdisk.img解压到rootfs.
但是由于ramdisk.img格式不是cpio, 而是ext2镜像, 所以unpack_to_rootfs()函数会失败, 并进入到populate_initrd_image().
populate_initrd_image()
会在rootfs根目录下创建initrd.image文件, 并将-initrd指定的ramdisk.img写入initrd.image文件.
kernel_init_freeable()
又返回到kernel_init_freeable(), 继续执行prepare_namespace(), 跟进调用initrd_load()
initrd_load() (关键函数)
调用rd_load_image尝试识别出/initrd.image文件格式, 写入的是ext2, 识别出来后会打印日志, 然后将/initrd.image拷贝到ramdisk
设备文件/dev/ram中. (ramdisk
磁盘模拟设备文件, 表示initrd.image被加载到了内存)
随后进入handle_initrd()
1 |
|
handle_initrd() (关键函数)
两个重要的参数:
ROOT_DEV
- 含义: 最终根文件系统
- 值: /dev/sda
ROOT_RAM0
- 含义: initrd_load()中加载initrd.image的
ramdisk
模拟磁盘设备文件 - 值: 一个内存块, 里面加载了initrd.image
- 含义: initrd_load()中加载initrd.image的
判断: 最终根文件系统是否是RAM0, 而RAM0就是initrd.image文件加载的内存
如果最终根文件系统不是RAM0: 说明刚刚加载的initrd.image文件只是一个初始根文件系统, 挂载initrd之后, 还要再挂载真正的根文件系统/dev/sda.
- 所以创建设备文件/dev/root.old表示Root_RAM0(刚刚initrd.image加载的
ramdisk
设备文件), 挂载/initrd.image, 这时文件系统由rootfs变成了initrd.image, 并执行其中的/linuxrc程序. - 随后在调用mount_root, 挂载真正的根文件系统
- 所以创建设备文件/dev/root.old表示Root_RAM0(刚刚initrd.image加载的
mount_root
继续/dev/sda挂载, 跟方式一相同
run_init_peocess
启动位于sda设备上的init进程
总结
启动分为三个过程:
- rootfs
- ramdisk initrd
- sda
方式三
qemu编译
1 | qemu-system-x86_64 -kernel arch/x86_64/boot/bzImage -initrd my_rootfs/initrd/disk.img --append "root=/dev/ram0 init=/init console=ttyS0" -nographic |
与方式二基本一样, 但是ROOT_DEV == Root_RAM0, 直接将RAM中的initrd.image用作最终的根文件系统, 并执行其上的/init进程.
结果
启动分为两个过程:
- rootfs
- ramdisk initrd
方式四
qemu编译
1 |
|
unpack_to_rootfs()
由于文件格式为cpio, 所以可以调用该函数.
直接将simple_initrd.cpio.gz内容解压到rootfs (RAM), 同时释放initrd占据的物理内存.
判断
1 | if (init_eaccess(ramdisk_execute_command) != 0) { |
rootfs中有/init文件, 直接执行该init程序.
挂载文件系统被推迟到init进程启动后
总结
两个过程:
- rootfs
- cpio initrd
方式五
qemu编译
1 | qemu-system-x86_64 -kernel arch/x86_64/boot/bzImage --append "init=/init console=ttyS0" -nographic |
类似方式4, cpio压缩包和内核编译在一起
总结
两个过程:
- rootfs
- initramfs
小结
Android中的boot.img解压出来的ramdisk文件本质上是: cpio格式. 用的不是ramdisk initrd技术而是cpio initrd.
Android启动
对于Magisk, 有三个种启动流程:
Method | Initial rootdir | Fianl rootdir | 描述 |
---|---|---|---|
A | rootfs | rootfs | 老设备的rootfs |
B | system | system | system-as-root |
C | rootfs | system | rootfs -> system |
MethodA
启动方式与前面讲的Linux启动init进程的方法4一致
img与分区
下面是基本的img文件和刷入分区的关系
img文件 | 分区 |
---|---|
boot.img | boot分区 |
cache.img | cache分区 |
recovery.img | recovery分区 |
system.img | system分区 |
bootloader-hammerhead-hhz11k.img | bootloader分区 |
boot.img文件分析
Android系统对于BootLoader有额外的要求:
- 能够解析boot_image_header格式的boot.img
boot_image_header结构体
1 | struct boot_img_hdr |
内容
kernel
- 内核文件
- dtb: 一种数据结构, 用于描述嵌入式系统种硬件设备及其连接关系.
ramdisk文件: 格式为gzip cpio, 前面说明过
- init可执行程序
- init.rx配置文件
- 其他文件…
启动方式
传递initrd
- qemu编译: 通过-initrd参数传递, 内核获取initrd, 并解压到rootfs中
- Android: bootloader通过fdt扁平设备树将initrd交给内核, 配置信息存放在r2寄存器中. (注: 参考kernel.org/doc/Documentation/arm/Booting, 翻译见文章底部)
bootloader在物理内存中加载完ramdisk后修改fdt, 设置/chosen节点的linux,initrd-start和initrd-end属性, 随后内核获取到该信息.
加载ramdisk
随后ramdisk文件会被解压到rootfs中, rootfs挂载点并没有被其他挂载点占据, 系统启动后仍可看到rootfs.
运行init
rootfs中已经有init程序, init启动后加载init.rc配置文件, 其中执行mount_all ./fstab.hammerhead
, 进一步挂载system, userdata分区.
recovery分区
bootloader会决定:
- 加载boot分区镜像, 进入正常系统
- 加载recovery分区镜像, 进入recovery模式
方法B
也叫: Legacy System-as-root,
其中System-as-root意思是system分区作为根文件系统
历史
- A/B区: Android支持A/B分区实现无缝更新概念, 比如: system_a和system_b. (无缝更新: 无线下载期间磁盘上保留一个正常启用的系统)
- Project Treble: Android还有Project Treble计划: 将Android与厂商分开, 且定义二者间稳定的接口. 其中就包含了system-as-root.
- system-as-root: 将init编译进system镜像, 且将system挂载为/
流程
populate_rootfs()
由于do_skip_initramfs = true
, 提前返回.
并不会解压initrd到rootf, 直接调用default_rootfs(), 在rootfs中创建/dev/console文件.
prepare_namespage()
利用device mapper机制创建虚拟块设备dm-0, 将该虚拟块作为根文件系统挂载.
remount_partition()
找到对应的/目录所在设备, 然后重新挂载
方法C
跟方法A有点类似, 又叫: ramdisk system-as-root,
原理
传递并使用initrd
bootloader通过fdt传递initrd给内核, 并解压到rootfs作为初始根文件系统, 并执行init程序, init进程负责挂载并将其作为新的rootdir. 然后执行system/bin/init程序完成系统启动的剩余部分.
Magisk patch ramdisk
Magisk主要目标时boot.img中的ramdisk文件, 做出的修改如下:
用magiskinit程序替换掉init程序
创建出overlay.d/sbin目录
将magisk32和magisk64守护进程拷贝到overlay.d/sbin目录下
将stub.apk的压缩包添加到overlay.d/sbin目录下
备份init程序, 用于修改系统后, 继续启动系统
将配置项保存在**.backup/.magisk**文件中, 内容如下
- ```C++
KEEPVERITY=true
KEEPFORCEENCRYPT=true
PATCHVBMETAFLAG=false
RECOVERYMODE=false
SHA1=ea36c0b1d697814f99d38984d720875274bb17641
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18* patch dtb的skip_initramfs, 该参数影响系统正常启动还是进入recovery模式
* patch内核
### 文件结构
经过修改后的boot文件结构如下
```C++
--/
--magiskinit(替换init)
--overlay.d
--sbin
--magisk32(守护进程)
--magisk64(守护进程)
--stub.xz(stub.apk压缩包)
--.backup
--.magisk(配置文件)
--init(原本的init程序, 修改完成后执行)
- ```C++
Magisk init
bootloader控制权转交给内核, 内核解压ramdisk并运行init程序, 以下为main函数源码
分类
Magisk区分四种类型
Type | Boot Method(引导方式) | Partition(分区数) | 2SI | Ramdisk in boot(引导的Ramdisk文件) |
---|---|---|---|---|
1 | A | A-only | No | boot ramdisk文件 |
2 | B | A/B | Any | recovery ramdisk文件 |
3 | B | A-only | Any | N/A |
4 | D | Any | Yes | Hybird ramdisk |
1 | int main(int argc, char *argv[]) { |
Type 1
分两种情况:
- 启动Android: 执行
init = new RootFSInit(argv, &config);
- 启动recovery: recovery分区并未被magisk修改 直接启动recovery分区 (magisk只修改了boot.img文件)
Type 2
分两种情况:
- 启动Android: 执行
init = new LegacySARInit(argv, &config);
- 启动recovery: 由于针对启动方式B, 所以boot.img中的ramdisk是recovery的影响, 执行
init = new RecoveryInit(argv, &config);
Type 3
分两种情况:
启动Android: 进入无magisk的原始系统
启动recovery: magiskinit读取/.backup/.magisk配置文件, 通过判断音量键决定:
- 进入magisk, 执行
init = new LegacySARInit(argv, &config);
- 进入recovery, 执行
init = new RecoveryInit(argv, &config);
- 进入magisk, 执行
Type 4
分两种情况:
- 启动Android: 执行
init = new FirstStageInit(argv, &config)
- 启动recovery: 仍然执行
init = new FirstStageInit(argv, &config)
修改系统
替换后的magiskinit需要完成的功能就是修改系统, 根据不同的Android启动方式采用不同的修改方式
针对MethodC
仅分析MethodC的Magisk的patch过程
启动过程: rootfs -> system
rootfs过程: 先将原本的init删除, 然后将.backup/init文件移动到根目录, 并进行修改:
- 修改路径字符串: 将”/system/bin/init”改成了”/data/magiskinit”文件 (两个字符串长度都是16)
FirstStageInit
针对MethodC, 创建了FirstStageInit实例, 并调用了start()方法, 查看源码:
继承自BaseInit
start()
- 调用了重写的prepare()
- 调用exec_init()
1 | class FirstStageInit : public BaseInit { |
FirstStageInit::prepare()
逻辑:
- prepare_data()
- restore_ramdisk_init()
- 获取init文件, 并对其进行patch操作, 最后输出日志
1 | void FirstStageInit::prepare() { |
prepare_data()
回顾一下当前的上下文:
- 当前进程: magisk的第一阶段被patch的init
- 文件系统: rootfs, 其中解压的ramdisk文件
逻辑:
- 创建/data目录
- /data作为挂载点, 挂载magisk文件系统, 系统类型为tmpfs(基于内存)
- 拷贝init到/data/magiskinit
- 拷贝/.backup
- 拷贝overlay.d
1 | void BaseInit::prepare_data() { |
restore_ramdisk_init
逻辑:
- 删除原本的根目录下的init
- 获取
"/.backup/init"
文件 - 将
"/.backup/init"
文件移动到根目录
1 | void restore_ramdisk_init() { |
magisk_cpio::patch()
回到了prepare()函数, 调用init.patch对原本的init文件进行patch: 将所有的"system/bin/init"
换成"/data/magiskinit"
1 | for (size_t off : init.patch(INIT_PATH, REDIR_PATH)) { |
BaseInit::exec_init()
完成对init的patch后, 进入该函数, 并执行patch后的init, 该init会执行下列操作.
- 挂载system分区
- 由于运行路径修改, 将运行/data/magiskinit可执行文件.
1 | void BaseInit::exec_init() { |
系统修改
添加服务
magisk对系统的修改主要是为了能启动magisk root守护进程, 这需要修改init.rc文件, 加入magisk的service.
SELinux
[其他]arm平台boot协议(翻译)
接下来的文档基于2.4.18-rmk6以及之前的版本
为了引导启动ARM Linux, 你需要一个BootLoader —- 一个在内核启动之前运行的小程序. BootLoader要求初始化各种设备, 最后调用内核, 并传送信息给内核.
大体上, BootLoader应该最低限度的提供以下功能:
- 启动和初始化RAM
- 初始化一个串口
- 识别机器类型
- 设置内核标记列表(Setup the kernel tagged list)
- 加载initramfs (前面提到过的)
- 调用内核映像
1. 启动和初始化RAM
Bootloader | 性质(是否具有该功能) |
---|---|
现有BootLoader | 强制的 |
新BootLoader | 强制的 |
BootLoader要求找到并初始化所有内核将会用到的RAM. 初始化RAM包括定位和调整RAM的大小, 初始化的具体操作依赖于:
- 内核算法
- 机器预留
- BootLoader设计者任何可行的任何方法
2. 初始化一个串口
Bootloader | 性质(是否具有该功能) |
---|---|
现有BootLoader | 可选的, 推荐使用的 |
新BootLoader | 可选的, 推荐使用的 |
BootLoader应该初始化并启用目标上的一个串口. 这允许内核串口驱动去自动选择哪一个串口应该被内核使用. (通常用于调试, 或者与目标通信)
3. 检测机器类型
Bootloader | 性质(是否具有该功能) |
---|---|
现有BootLoader | 可选的 |
新BootLoader | 强制的(除了dt平台) |
BootLoader应该检查机器类型, 可以通过:
- 硬编码
- 连接硬件的某种算法
以上方法的原理超出了本文档描述的范围.
BootLoader必须项内核提供MACH_TYPE_xxx值, 通过r1寄存器传递给内核.
注: 关于DT平台的描述不常用就没看了
4. 设置引导数据
Bootloader | 性质(是否具有该功能) |
---|---|
现有BootLoader | 可选的, highly |
新BootLoader | 强制的 |
BootLoader必须提供标签表或者dtb映像, 用于将配置数据传递给内核.
boot数据(配置信息)的物理地址在r2寄存器传送给了内核.
tagged list传送方式
BootLoader必须创建并初始化内核标签表.
一个有效标签表以ATAG_CORE开始, 并以ATAG_NONE结束.
ATAG_CORE标签可以为空, 也可以为非空. 一个空ATAG_CORE标签的size字段设置为2, ATAG_NONE必须将size字段设置为0.
标签表中可以防止任意数量的标签.
BootLoader至少要传递传递: 系统内存的大小和位置, 根文件系统的位置. 因此, 最小标记列表应该是如下所示
1 | +-----------+ |
标签表应该存储在系统RAM中.
标签表不能被内核解压缩程序或者bootp覆盖, 所以建议放在内存的头16KiB中.
dtb映像传送方式
BootLoader必须将设备树映像(dtb)到加载到64为对齐地址的系统RAM中, 并用boot数据对其进行初始化.
内核将检查dtb的魔数0xd00dfeed, 确定传递的是dtb而不是标签表
BootLoader至少传递:
- 系统内存的大小和位置
- 根文件系统位置
dtb同样不能被覆盖, 应该放置在内核低内存映射覆盖区域.
RAM开始的128MiB边界上方是一个安全位置.
学习文章
- Magisk原理 - Meansome
- Android su提权的简单实现 - 简书 (jianshu.com)
- [原创] 云手机底层技术揭密 : Android系统启动与Magisk原理-Android安全-看雪-安全社区|安全招聘|kanxue.com
- 全局唯一标识分区表 - 维基百科,自由的百科全书 (wikipedia.org)
- kernel.org/doc/Documentation/arm/Booting
- 引导映像标题 | Android 开源项目 | Android Open Source Project
- Linux的文件系统和挂载点是什么意思? - 知乎 (zhihu.com)
- magisk的原理与检测方法 | CN-SEC 中文网