Magisk原理分析(未完待续)

Magisk分析(未完待续)

**[扩展]**主题表示理解Magisk原理所需要的前提知识, 并不与Magisk直接相关.

[扩展]早期ROOT原理

权限位

文件权限位

文件权限位总共四个:

1
2
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的进程的行为

新版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逻辑区块地址.

结构图

image

布局

  • 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” -nographic
      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
      * 该方法就是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()
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
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
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
2
3
4
5
6
7
8
9
//创建ROOT_DEV对应的设备节点/dev/root,如果没有指定rootfstype命令行参数就尝试遍历文件系统类型对/dev/root进行挂载,挂载点为/root,并且调用init_chdir("/root")将工作目录切换到/root目录下

mount_root();

//将当前工作目录(/root)移动挂载至/目录下
init_mount(".", "/", NULL, MS_MOVE, NULL);

//切换当前进程的根目录至当前目录
init_chroot(".");
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
2
3
4
5
6
7
8

内存

--------------------------
rootfs文件系统 |
--------------------------
装载initrd.image到RAM内存中|
--------------------------
handle_initrd() (关键函数)

两个重要的参数:

  • ROOT_DEV

    • 含义: 最终根文件系统
    • 值: /dev/sda
  • ROOT_RAM0

    • 含义: initrd_load()中加载initrd.image的ramdisk​模拟磁盘设备文件
    • 值: 一个内存块, 里面加载了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, 挂载真正的根文件系统
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
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
unpack_to_rootfs()

由于文件格式为cpio, 所以可以调用该函数.

直接将simple_initrd.cpio.gz内容解压到rootfs (RAM), 同时释放initrd占据的物理内存.

判断
1
2
3
4
if (init_eaccess(ramdisk_execute_command) != 0) {
ramdisk_execute_command = NULL;
prepare_namespace();
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct boot_img_hdr
{
uint8_t magic[BOOT_MAGIC_SIZE];
uint32_t kernel_size; /* size in bytes */
uint32_t kernel_addr; /* physical load addr */

uint32_t ramdisk_size; /* size in bytes */
uint32_t ramdisk_addr; /* physical load addr */

uint32_t second_size; /* size in bytes */
uint32_t second_addr; /* physical load addr */

uint32_t tags_addr; /* physical addr for kernel tags */
uint32_t page_size; /* flash page size we assume */
uint32_t unused;
uint32_t os_version;
uint8_t name[BOOT_NAME_SIZE]; /* asciiz product name */
uint8_t cmdline[BOOT_ARGS_SIZE];
uint32_t id[8]; /* timestamp / checksum / sha1 / etc */
uint8_t extra_cmdline[BOOT_EXTRA_ARGS_SIZE];
};
内容
  • 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=ea36c0b1d697814f99d38984d720875274bb1764
      1
      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程序, 修改完成后执行)

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
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
int main(int argc, char *argv[]) {
umask(0);

auto name = basename(argv[0]);
if (name == "magisk"sv)
return magisk_proxy_main(argc, argv);

if (getpid() != 1)
return 1;

BaseInit *init;
BootConfig config{};

if (argc > 1 && argv[1] == "selinux_setup"sv) {
rust::setup_klog();
init = new SecondStageInit(argv);
} else {
// This will also mount /sys and /proc
load_kernel_info(&config);

if (config.skip_initramfs)
init = new LegacySARInit(argv, &config);
else if (config.force_normal_boot)
init = new FirstStageInit(argv, &config);
else if (access("/sbin/recovery", F_OK) == 0 || access("/system/bin/recovery", F_OK) == 0)
init = new RecoveryInit(argv, &config);
else if (check_two_stage())
init = new FirstStageInit(argv, &config);
else
init = new RootFSInit(argv, &config);
}

// Run the main routine
init->start();
exit(1);
}

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);

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
2
3
4
5
6
7
8
9
10
11
12
class FirstStageInit : public BaseInit {
private:
void prepare();
public:
FirstStageInit(char *argv[], BootConfig *config) : BaseInit(argv, config) {
LOGD("%s\n", __FUNCTION__);
};
void start() override {
prepare();
exec_init();
}
};
FirstStageInit::prepare()

逻辑:

  • prepare_data()
  • restore_ramdisk_init()
  • 获取init文件, 并对其进行patch操作, 最后输出日志
1
2
3
4
5
6
7
8
9
void FirstStageInit::prepare() {
prepare_data();
restore_ramdisk_init();
auto init = mmap_data("/init", true);
// Redirect original init to magiskinit
for (size_t off : init.patch(INIT_PATH, REDIR_PATH)) {
LOGD("Patch @ %08zX [" INIT_PATH "] -> [" REDIR_PATH "]\n", off);
}
}
prepare_data()

回顾一下当前的上下文:

  • 当前进程: magisk的第一阶段被patch的init
  • 文件系统: rootfs, 其中解压的ramdisk文件

逻辑:

  • 创建/data目录
  • /data作为挂载点, 挂载magisk文件系统, 系统类型为tmpfs(基于内存)
  • 拷贝init到/data/magiskinit
  • 拷贝/.backup
  • 拷贝overlay.d
1
2
3
4
5
6
7
8
9
void BaseInit::prepare_data() {
LOGD("Setup data tmp\n");
xmkdir("/data", 0755);
xmount("magisk", "/data", "tmpfs", 0, "mode=755");

cp_afc("/init", "/data/magiskinit");
cp_afc("/.backup", "/data/.backup");
cp_afc("/overlay.d", "/data/overlay.d");
}
restore_ramdisk_init

逻辑:

  • 删除原本的根目录下的init
  • 获取"/.backup/init"​文件
  • "/.backup/init"​文件移动到根目录
1
2
3
4
5
6
7
8
9
10
11
12
13
void restore_ramdisk_init() {
unlink("/init");

const char *orig_init = backup_init();
if (access(orig_init, F_OK) == 0) {
xrename(orig_init, "/init");
} else {
// If the backup init is missing, this means that the boot ramdisk
// was created from scratch, and the real init is in a separate CPIO,
// which is guaranteed to be placed at /system/bin/init.
xsymlink(INIT_PATH, "/init");
}
}

magisk_cpio::patch()

回到了prepare()函数, 调用init.patch对原本的init文件进行patch: 将所有的"system/bin/init"​换成"/data/magiskinit"

1
2
3
for (size_t off : init.patch(INIT_PATH, REDIR_PATH)) {
LOGD("Patch @ %08zX [" INIT_PATH "] -> [" REDIR_PATH "]\n", off);
}
BaseInit::exec_init()

完成对init的patch后, 进入该函数, 并执行patch后的init, 该init会执行下列操作.

  • 挂载system分区
  • 由于运行路径修改, 将运行/data/magiskinit可执行文件.
1
2
3
4
5
6
7
8
9
void BaseInit::exec_init() {
// Unmount in reverse order
for (auto &p : reversed(mount_list)) {
if (xumount2(p.data(), MNT_DETACH) == 0)
LOGD("Unmount [%s]\n", p.data());
}
execv("/init", argv);
exit(1);
}

系统修改

添加服务

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
2
3
4
5
6
7
	+-----------+
base -> | ATAG_CORE | |
+-----------+ |
| ATAG_MEM | | increasing address
+-----------+ |
| ATAG_NONE | |
+-----------+ v

标签表应该存储在系统RAM中.

标签表不能被内核解压缩程序或者bootp覆盖, 所以建议放在内存的头16KiB中.

dtb映像传送方式

BootLoader必须将设备树映像(dtb)到加载到64为对齐地址的系统RAM中, 并用boot数据对其进行初始化.

内核将检查dtb的魔数0xd00dfeed, 确定传递的是dtb而不是标签表

BootLoader至少传递:

  • 系统内存的大小和位置
  • 根文件系统位置

dtb同样不能被覆盖, 应该放置在内核低内存映射覆盖区域.

RAM开始的128MiB边界上方是一个安全位置.

学习文章

文章作者: LamのCrow
文章链接: http://example.com/2023/06/09/MagiskPrinciple/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 LamのCrow