一、正常Android启动流程二、几种Boot方式三、boot_patch.sh打补丁四、Magisk-Preinit预初始化1.magisk劫持init第一阶段执行2. init第一阶段执行3. Magisk劫持init第二阶段执行4. init第二阶段5. 流程总结五、Magisk-post-fs-data1. init.rc2. magisk启动六、Magisk-late_start 七、Magisk-boot-complete参考:
Magisk的Patch操作和实际的工作流程涉及到Android启动的不同方法,对不同设备不同系统有着不同的启动方式,这是一个很复杂的问题,将会直接决定Magisk的工作流程,有时间需要进行细致分析。在这里仅给出一种实现方式的解析,其他类比起来看就行。
测试设备为RedMI Note7(lavender),非A/B分区设备,刷入Android 12类原生系统。
一、正常Android启动流程
Step1 BootROM
启动ROM代码从预先定义的位置开始执行。它将引导加载程序加载到RAM中并开始执行。
- A.引导ROM代码将使用映射到ASIC上的一些物理球的系统寄存器来检测引导介质。这是为了确定在何处找到引导加载程序的第一阶段。
- B.一旦引导介质序列建立,引导ROM将尝试加载第一阶段的引导加载程序到内部RAM。一旦引导加载程序就位,引导ROM代码将执行跳转并继续在引导加载程序中执行。
Step2 BootLoader
引导加载程序的执行分为两个阶段,第一阶段是检测外部RAM并加载程序以帮助第二阶段的执行。在第二阶段,引导加载程序设置网络、内存等,这些都需要运行内核。引导加载程序能够为特定目的向内核提供配置参数或输入。
- A. 第一阶段引导加载程序将检测并设置外部RAM。
- B. 一旦外部RAM可用并且系统已准备好运行更重要的东西,第一阶段将加载主引导加载程序并将其放置在外部RAM中。
- C. 引导加载程序的第二阶段是第一个将运行的主要程序。它可能包含用于设置文件系统、附加内存、网络支持和其他事项的代码。在手机上,它还可能负责加载调制解调器CPU的代码并设置低级内存保护和安全选项。
- D. 一旦引导加载程序完成了任何特殊任务,它将寻找要引导的Linux内核。它将从引导介质(或其他依赖于系统配置的来源)加载内核并将其放置在RAM中。它还会将一些引导参数放置在内存中,供内核在启动时读取。
- E. 一旦引导加载程序完成,它将执行跳转到Linux内核,通常是一些解压缩例程,然后内核就承担了系统责任。
Step3 Linux kernel
Linux内核在Android系统上的启动方式与其他系统类似。它会设置系统运行所需的所有内容,包括初始化中断控制器、设置内存保护、缓存和调度。
- A. 一旦内存管理单元和缓存已经初始化,系统将能够使用虚拟内存并启动用户空间进程。
- B. 内核将在根文件系统中查找init进程(在Android开源树中的system/core/init目录下)并将其启动为初始的用户空间进程。
Step4 Init Process
init是非常重要的第一个进程,可以说它是所有进程的祖先进程。init进程有两个责任:1. 挂载目录,如/sys,/dev,/proc等;2. 运行init.rc脚本。
Step5 Zygote and Dalvik(ART)
Step6 system server
二、几种Boot方式
由于测试机为RedMI Note7(lavender)(非A/B分区设备,即A-only设备),即使刷入了Android12的类原生系统,但是其出场Android版本API=28,即LV=28,设备属于第三类设备,分区中没有包含有
ramdisk
,但是后面会讲到测试设备刚好是红米,幸运的是它居然可以接受手动添加到boot中的ramdisk
,但是Magisk 实际上不可能知道设备引导加载程序是否接受虚拟硬盘,因此假设始终通知用户使用恢复模式,因此在Magsik界面Ramdisk后面显示否。
理论上来说测设设备启动是采用MethodB: Legacy SAR方式进行启动,内核直接挂载到system分区作为根目录并且直接运行/init进行启动。通过后面日志分析也可以发现,[2.686814] magiskinit: LegacySARInit
这里第一阶段进入了LegacySARInit
分支。因此本文重点分析这种启动方式,其次Method C是现如今Android设备常规启动方式,因此也进行分析。三、boot_patch.sh打补丁
在该Shell脚本中有如下代码,就是使用magiskboot对Android的ramdisk
进行patch,重点替换init
为magiskinit
,然后创建一些文件夹以用于临时存放Magisk相关的文件。在绝大多数设备上,Magisk可以通过直接对boot
分区进行补丁操作或在自定义Recovery中刷入zip包来安装。这是因为这些设备的分区中都包含有ramdisk
,可以在其中安装Magisk。但是,在某些设备类型(Type III)中,因为boot分区
中没有包含有ramdisk
,因此无法在其中安装Magisk。在这种情况下,Magisk只能安装到recovery
分区中,而不能直接安装到boot
分区中。这些设备在正常启动时无法访问Magisk,需要每次重启都进入recovery
模式才能使用Magisk。在下面的节选部分脚本中,除了使用magiskboot
替换init→magiskinit
、创建overlay.d文件夹、放入magisk守护进程和壳APP并且进行备份外,还需要将skip_initramfs
修改为want_initramfs
。linux调用populate_rootfs
默认会并加载boot分区自带的ramdisk(recovery)
,但如果do_skip_initramfs
被设置为1,则会调用default_rootfs
生成一个极小的rootfs,并在其中创建出/dev/console
文件,除此之外没有其他文件。正因如此,当设备正常启动到系统时读取/proc/cmdline
,其中会出现skip_initramfs
的参数,但是启动到recovery时不会有此参数。重点来了,某些Type III设备的引导程序仍然会接受手动添加到boot中的ramdisk
,例如部分小米手机,但许多设备不会,例如三星S10,Note 10等。测试设备刚好是红米,它居然可以接受手动添加到boot中的ramdisk,因此我们无需每次重启到rec模式才能使用Magisk,可以直将Magisk装入ramdisk。
(根据看雪论坛“残页”大佬的说法,小米Type III设备都可以直接修补boot.img,而非需要修补Recovery )
boot_patch.shboot_patch.sh
脚本将kernel中的skip_initramfs
修改为want_initramfs
,因为如果设置了skip_initramfs
代表着Method B启动方式中进入到正常系统而不是recovery
,magisk也就无法启动了,修改为want_initramfs
屏蔽掉skip_initramfs
让系统可以从恢复模式中的ramdisk中启动。但是需要注意:skip_initramfs
修改为want_initramfs
的操作发生了两次,但是patch的位置不同,目的也不同。第一次发生在./magiskboot dtb $dt patch; then
即通过magiskboot对dtb进行patch,在其中调用dtb_patch
对dtb中的skip_initramfs
修改为want_initramfs
,但是注意这里修改的目的是因为三星部分机型不支持 SAR 但是 cmdline 里却有这个参数,正常启动的时候内核会直接忽略,但是 Magisk 会因为有这个参数而判断这台设备是 Legacy SAR,进入 Legacy SAR 流程导致手机循环启动。这里 Patch 的本质是不让 magisk 自己看到错误的参数 https://github.com/topjohnwu/Magisk/pull/4788
(看雪论坛“残雪”)。第二次Patch才是对kernel内部skip_initramfs
进行修改,从而使得在legacy SAR devices
设备中Magisk能正常启动。
从boot.img解出的kernel来看,确实对skip进行了patch,patch为want
因为测试设备是小米手机,所以magisk直接修补boot.img,无法对“Magisk修补Recovery分区的ramdisk,每次启动从rec模式启动,然后init执行再启动到系统”这一情况进行测试。
摘自参考1
:- 如果要启动的是正常系统,直接进入无magisk的原始系统。
- 如果要启动的是recovery模式,会导致
magiskinit
执行,magiskinit
读取/.backup/.magisk
配置文件得到RECOVERYMODE=true
知道它自己是在recovery分区启动,然后调用check_key_combo()
判断是否长按音量键上,如果长按则进入REC系统,否则进入到带有Magisk的正常系统。进入magisk系统仍然进入到init = new LegacySARInit(argv, &config);
进入recovery模式会进入init = new RecoveryInit(argv, &config);
这部分在Magisk官方安装文档中也有提及:Installation | Magisk (topjohnwu.github.io)
Magisk启动整体分为三个阶段
Pre-Init→post-fs-data→late_start→boot_complete
四、Magisk-Preinit预初始化
源码位于native/src/init/init.cpp
,由于在补丁阶段将magiskinit替换为init,所以系统启动时会启动实际上的magiskinit。然后是使用Debug版本的Magisk进行测试时,系统在启动时的日志输出(主要关注标红加粗位置),通过输出分析具体的流程。
以下提到的第一阶段与第二阶段是Magiskinit的阶段而言,而非系统init正常的阶段,千万不要搞混了
1.magisk劫持init第一阶段执行
如果按MethodB的方法启动,整个init过程
实际上分为两个阶段,第一阶段直接执行magiskinit
,即劫持了init
,在main内存在一个if else判断
,如果是第一个阶段则会执行到else内部。如果按MethodB的方法启动,则第一阶段调用流程应当是:
prepare_data
将会将创建data目录并且将magisk挂载到data,然后将关键文件和文件夹复制到/data
目录下。这里的init
实际是patch到ramdisk中的magiskinit,这里cp_afc("/init", "/data/magiskinit");
实际上就是将magiskinit
本身放到/data/magiskinit
- 然后
mount_system_root
尝试挂载系统根目录并且通过apex判断是否进行第二阶段的init,通过日志分析可以发现is_two_stage: [1]
,即需要进入第二阶段init。
first_stage_prep
实际就是将/dev/root
根目录的init
进行patch操作,将其内部的/system/bin/init
字符串改为/data/magiskinit
,这里根目录的init是原始的系统init二进制文件,[2.815494] magiskinit: Patch @ 0001C840 [/system/bin/init] -> [/data/magiskinit]
,输出到/data/init
,并且挂载到/init
- 完成上述操作后
exec_init
通过execv执行init,这里的init是经过patch的系统init
,实际是/data/init
,而非magiskinit
,但是注意这里的参数还是空的,所以在执行系统init时还是第一阶段,相对于在原本的init执行之前magiskinit劫持了一会,但是完事后还是会将控制权交还原本init进行执行。
2. init第一阶段执行
根据日志和前面分析可知,magiskinit打完patch后执行patch后的init
下面两个代码块为Android源码中init部分源码,可以看到init
会查看是否有参数。如果没有参数,也就是当前的这种情况,执行FirstStageMain
,执行原本的第一阶段初始化操作(省略)。然后再次通过execv
执行"/system/bin/init"
,并且将参数设置selinux_setup
, 但是由于对其进行了patch,所以这里执行完初始化操作后会到/data/magiskinit
进行执行,并且带有参数selinux_setup
。
3. Magisk劫持init第二阶段执行
在执行到FirstStageMain
结尾时会带selinux_setup
参数再次执行/system/bin/init
,但是由于对init进行了patch,所以这里字符串实际是/data/magiskinit
,因此magisk会再次劫持。日志也显示magiskinit接着执行并输出SecondStageInit
- 在Magisk的main中new了一个
SecondStageInit
, 接着执行流程应当是
prepare
取消挂载init
,这里的init实际上是/data/init
,而/data/init
实际是经过Patch的系统init ,为了防止 init 的 dmesg 日志被干扰,将 argv[0] 参数设置为/system/bin/init
然后检查当前根文件系统是否为RAMFS
或TMPFS
,如果是,说明当前仍然在rootfs
下,需要在第二阶段重新执行 init,删除/init
链接并创建一个符号链接,指向第二阶段的init
程序,这里的init
程序实际是未经过Patch的系统init
,其位置是/system/bin/init
,以便在第二阶段执行 init。否则返回false。
patch_ro_root
则进行一些初始化任务,主要是在系统启动时对文件系统进行修改和挂载,以便 Magisk 可以正确地运行和隐藏自己。它涉及的操作包括创建临时目录、加载 overlay 文件、修改 init.rc 文件、提取 Magisk 文件、修改 sepolicy 文件等。
patch_init_rc
实现了对 init.rc 文件的修改。具体来说,它使用file_readline
函数逐行读取源文件init.rc
,并根据不同的匹配规则进行不同的修改,然后将修改后的内容写入目标文件 。除了修改 init.rc 文件外,这段代码还向目标文件 dest 中注入了一些自定义的 rc 脚本,这些脚本用于执行一些 Magisk 相关的操作,如在 post-fs-data 阶段启动 Magisk、在系统启动完成后启动 Magisk 等。当然如果在目录下寻找init.rc是找不到的,因为等待注入完成后magisk就会删除rootdir目录以及下面的东西,防止被检测到。如果想要保留就需要native/src/core/daemon.cpp
中patchrm_rf((MAGISKTMP + "/" ROOTOVL).data());
在patch_ro_root
里还调用了一个函数hijack_sepolicy()
, 这个函数就是劫持sepolicy作用。在使用monolithic
策略的设备上,Magisk 直接从/sepolicy
文件中加载 sepolicy 规则。这个文件通常位于系统的根目录下,用于存储 selinux 策略。这种方式比较简单直接,不需要进行额外的 hook 操作。在其他的设备上,Magisk 使用 FIFO(命名管道)劫持 selinuxfs 中的节点
,以实现 selinux hook。具体来说,Magisk 会创建一个 FIFO 文件,并挂载到 selinuxfs 中的 "load" 和 "enforce" 节点上,用于接收 selinux 策略和 enforce 值。这样一来,即使系统中没有 /sepolicy 文件,Magisk 也可以通过劫持 selinuxfs 中的节点,来实现 selinux hook。在 2SI 设备上,由于第二阶段的 init 文件是一个动态可执行文件,而不是静态的 /init 可执行文件,因此 Magisk 还需要协助劫持 selinuxfs。具体来说,Magisk 会在 init 进程启动之前,通过
LD_PRELOAD
的方式,将自己的preload.so
库注入到 init 进程中,并替换 security_load_policy 函数为自己的实现,以实现 selinux hook。然后,Magisk 启动守护程序,等待 init 进程尝试加载 selinux 策略文件。当 init 进程启动时,Magisk 的钩子函数会拦截 security_load_policy 的调用,并将 selinux 策略文件和 enforce 值写入 FIFO 文件中,以实现自定义的 selinux 策略。在selinux.cpp
中,加粗部分较为重要,其中hijack则是通过FIFO劫持 selinuxfs 中的节点
,以实现selinux hook
。之后通过xfork()
创建子进程,父进程直接返回继续执行,执行到exec_init
后继续执行系统未经过patch的init
, 在执行init的过程中会触发security_getenforce
,此时子进程才会从xopen
的阻塞中脱离继续执行,也就是此时以及可以获取到selinux 策略和 enforce 值
。
exec_init
就不再赘述了,还是将执行权归还系统init
继续执行,注意此处的init是系统并未经过patch的init
,前面讲到在SecondStageInit::prepare()
会取消挂载并且重新链接到系统原本的未经过patch的initprepare
取消挂载init
,这里的init实际上是/data/init
,而/data/init
实际是经过Patch的系统init ,为了防止 init 的 dmesg 日志被干扰,将 argv[0] 参数设置为/system/bin/init
然后检查当前根文件系统是否为RAMFS
或TMPFS
,如果是,说明当前仍然在rootfs
下,需要在第二阶段重新执行 init,删除/init
链接并创建一个符号链接,指向第二阶段的init
程序,这里的init
程序实际是未经过Patch的系统init
,其位置是/system/bin/init
,以便在第二阶段执行 init。否则返回false。 ,这次execv
带有参数selinux_setup
因此会执行源码中的SetupSelinux
,于是也就有了日志里init的那些输出(加粗)。
大伙可能会比较好奇为什么中间有两行输出是magiskinit
的输出,其实就是上面讲到的子进程会一直阻塞直到init
触发security_getenforce
,此刻解阻塞后就会输出magiskinit
信息,但是父进程早就执行完成了,到此为止magiskinit
的初始化职责就完成了,再往后的执行都是系统未经过patch的init进行继续初始化。
4. init第二阶段
由于这次execv
带有参数selinux_setup
因此会执行源码中的SetupSelinux
当SetupSelinux
执行完成后,又会再次通过execv到init
执行,参数为second_stage
,这里的init是系统原本的未经过patch的init
,当magiskinit
父进程执行完execv后就会退出,之后的初始化都是系统自己的初始化init
。
5. 流程总结
PS: SELinux劫持和父子进程分叉就不画进去了
五、Magisk-post-fs-data
当/data
被解密和装载时会在post-fs-data
上触发。守护程序magiskd
将被启动,执行 post-fs-data 脚本,并安装模块文件。
1. init.rc
上文讲到magisk在初始化阶段也就是preinit
阶段对init.rc
进行修改,在原本的init.rc
后添加下面的内容。"u:r:magisk:s0 0 0"
表示 Magisk 进程的安全上下文,表示该进程在用户空间中扮演 "magisk" 角色,类型级别为默认级别,用户 ID 和组 ID 均为 0,即以 root 用户身份运行。在不同时机触发magisk进行不同的活动,例如在post-fs-data
阶段运行magisk --post-fs-data
。
2. magisk启动
在native/src/core/magisk.cpp
中magisk_main
即为magisk入口。
- 通过
connect_daemon
创建守护进程daemon进程,由于此时守护进程还未建立,所以if
中的connect
会返回-1
,从而进入if成功的分支
,进入后进行一次fork,父进程会继续向下等待连接,子进程则通过daemon_entry
成为守护进程,之后父进程连接到守护进程后到write_int(fd, req)
,守护进程会一直监听请求。
boot_stage_handler
来处理POST_FS_DATA
LATE_START
BOOT_COMPLETE
三种请求
unlock_blocks
将解锁所有块设备的写保护,mount_mirrors
挂载各种镜像文件系统,prune_su_access
用于在数据库中检查授权的APP并对已经不存在的APP的数据进行删除。magisk_env
对Magisk环境进行初始化。最后load_modules
对模块进行加载。这段代码首先调用了get_prop()
函数来获取系统属性persist.sys.safemode
和ro.sys.safemode
的值。如果get_prop()
函数的第二个参数为true
,表示在没有找到这些属性时返回默认值"1"
。如果获取到的值为"1"
,或者按键组合被触发(通过调用check_key_combo()
函数),则将boot_state
的FLAG_SAFE_MODE
位置1,表示启动处于安全模式,将会禁用所有模块并且禁用禁止列表,这有助于在手机安装一些模块导致手机无法正常启动时对系统进行修复。如果启动不处于安全模式,那么首先调用exec_common_scripts()
函数执行/data/adb/post-fs-data.d
目录下的脚本。接着,定义了一个名为dbs
的db_settings
结构体,调用get_db_settings()
函数将系统属性ZYGISK_CONFIG
的值存储在dbs
中。然后,通过检查dbs[ZYGISK_CONFIG]
的值来确定是否启用了zygisk
。最后,调用initialize_denylist()
函数初始化拒绝列表,并调用handle_modules()
函数处理模块的post-fs-data
脚本、收集模块以及创建模块动态链接库内存描述符为后续模块动态链接库加载做准备mount_mirrors
进行镜像的挂载操作,如果当前系统版本小于24或者安全目录SECURE_DIR(/data/adb)
存在,则执行绑定重新挂载的操作。具体来说,函数创建MAGISKTMP + "/" MODULEMNT(/debug_ramdisk/.magisk/modules)
目录,然后将模块根目录MODULEROOT(/data/adb/modules)
绑定挂载到该目录中,并将挂载点重新挂载为只读文件系统,最后将挂载点设置为私有的,并将/data/adb
的权限设置为0700。Magisk官方文档中提到使用/data/adb
作为安全目录的原因:一些二进制文件和文件应存储在/data
中的非易失性存储中。为了防止检测,所有东西都必须存储在/data
中安全且不可检测的地方。选/data/adb
文件夹是因为其具有以下优点:
- 它是现代安卓系统上的一个现有文件夹,因此不能作为 Magisk 存在的标志。
- 文件夹的权限默认为
700
,所有者为root
,因此非 root 进程无法以任何可能的方式进入、读取和写入文件夹。
- 文件夹 secontext 标记为
u:object_r:adb_data_file:s0
,很少有进程有权与该 secontext 进行任何交互。
- 该文件夹位于设备加密存储区中,因此一旦数据正确装载到 FBE(File-Based Encryption,基于文件的加密)设备中,即可访问该文件夹。
然后检查是否需要挂载预初始化镜像文件系统。如果预初始化镜像文件系统的块设备文件存在,则尝试在当前的挂载信息中查找已经挂载了该块设备的目录,并将目标目录绑定挂载到预初始化镜像文件系统的目录中。然后挂载worker目录,最后函数通过递归绑定挂载的方式将根目录"/"
绑定挂载到一个名为MAGISKTMP + "/" MIRRDIR(/debug_magisk/.magisk/mirror)
的目录中。MagiskV26.0
更新了 Magic Mount 功能的后端,支持将模块加载到系统中并注入 overlayfs 文件。Magic Mount是指通过模块修改系统文件的功能。在这个更新之前,Magisk 通过叠加文件系统来实现 Magic Mount 功能,但是一些设备厂商在他们的系统中注入了 overlayfs 文件,导致 Magic Mount出现问题。需要注意的是,Magisk 的模块挂载是在 Android 系统启动时进行的,因此在安装或卸载 Magisk 模块时需要重启系统才能生效。这里对zygisk
进行挂载 ,成功实现了对/system/bin/app_process32
和/system/bin/app_process64
的替换,替换为对应的magisk
,分析日志也可以发现,通过系统目录的挂载root->mount();
将会实现这种替换。官方之前的说法:尽管安装逻辑非常复杂,但Magic挂载的最终结果实际上非常简单。对于每个模块,目录/system将被递归地合并到真实的/system中;即:将真实system中的现有文件替换为模块“system”中的文件,并将模块“system”中的新文件添加到真实system中。
六、Magisk-late_start
在稍后的引导过程中,将触发late_start
,并启动 Magisk “服务”模式。在此模式下,执行服务(service)脚本。在这个阶段将会对service.d
目录下的以及模块中的service脚本进行执行。
七、Magisk-boot-complete
启动完成时sys.boot_completed=1
触发boot-complete