⚠️
Magisk的Patch操作和实际的工作流程涉及到Android启动的不同方法,对不同设备不同系统有着不同的启动方式,这是一个很复杂的问题,将会直接决定Magisk的工作流程,有时间需要进行细致分析。在这里仅给出一种实现方式的解析,其他类比起来看就行。
💡
测试设备为RedMI Note7(lavender),非A/B分区设备,刷入Android 12类原生系统。
 

一、正常Android启动流程

notion image
Step1 BootROM
启动ROM代码从预先定义的位置开始执行。它将引导加载程序加载到RAM中并开始执行。
  • A.引导ROM代码将使用映射到ASIC上的一些物理球的系统寄存器来检测引导介质。这是为了确定在何处找到引导加载程序的第一阶段。
  • B.一旦引导介质序列建立,引导ROM将尝试加载第一阶段的引导加载程序到内部RAM。一旦引导加载程序就位,引导ROM代码将执行跳转并继续在引导加载程序中执行。
notion image
Step2 BootLoader
引导加载程序的执行分为两个阶段,第一阶段是检测外部RAM并加载程序以帮助第二阶段的执行。在第二阶段,引导加载程序设置网络、内存等,这些都需要运行内核。引导加载程序能够为特定目的向内核提供配置参数或输入。
  • A. 第一阶段引导加载程序将检测并设置外部RAM。
  • B. 一旦外部RAM可用并且系统已准备好运行更重要的东西,第一阶段将加载主引导加载程序并将其放置在外部RAM中。
  • C. 引导加载程序的第二阶段是第一个将运行的主要程序。它可能包含用于设置文件系统、附加内存、网络支持和其他事项的代码。在手机上,它还可能负责加载调制解调器CPU的代码并设置低级内存保护和安全选项。
  • D. 一旦引导加载程序完成了任何特殊任务,它将寻找要引导的Linux内核。它将从引导介质(或其他依赖于系统配置的来源)加载内核并将其放置在RAM中。它还会将一些引导参数放置在内存中,供内核在启动时读取。
  • E. 一旦引导加载程序完成,它将执行跳转到Linux内核,通常是一些解压缩例程,然后内核就承担了系统责任。
notion image
 
Step3 Linux kernel
Linux内核在Android系统上的启动方式与其他系统类似。它会设置系统运行所需的所有内容,包括初始化中断控制器、设置内存保护、缓存和调度。
  • A. 一旦内存管理单元和缓存已经初始化,系统将能够使用虚拟内存并启动用户空间进程。
  • B. 内核将在根文件系统中查找init进程(在Android开源树中的system/core/init目录下)并将其启动为初始的用户空间进程。
notion image
Step4 Init Process
init是非常重要的第一个进程,可以说它是所有进程的祖先进程。init进程有两个责任:1. 挂载目录,如/sys,/dev,/proc等;2. 运行init.rc脚本。
notion image
Step5 Zygote and Dalvik(ART)
notion image
notion image
Step6 system server
notion image
 

二、几种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重点替换initmagiskinit然后创建一些文件夹以用于临时存放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.sh
boot_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
notion image
⚠️
因为测试设备是小米手机,所以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 值
               

              4. init第二阶段

              由于这次execv带有参数selinux_setup 因此会执行源码中的SetupSelinux
              SetupSelinux执行完成后,又会再次通过execv到init执行,参数为second_stage ,这里的init是系统原本的未经过patch的init,当magiskinit父进程执行完execv后就会退出,之后的初始化都是系统自己的初始化init
               

              5. 流程总结

              notion image
              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.cppmagisk_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.safemodero.sys.safemode的值。如果get_prop()函数的第二个参数为true,表示在没有找到这些属性时返回默认值"1"。如果获取到的值为"1",或者按键组合被触发(通过调用check_key_combo()函数),则将boot_stateFLAG_SAFE_MODE位置1,表示启动处于安全模式,将会禁用所有模块并且禁用禁止列表,这有助于在手机安装一些模块导致手机无法正常启动时对系统进行修复。
                    如果启动不处于安全模式,那么首先调用exec_common_scripts()函数执行/data/adb/post-fs-data.d目录下的脚本。接着,定义了一个名为dbsdb_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
                 
                 

                参考:

                1. [原创] 云手机底层技术揭密 : Android系统启动与Magisk原理-Android安全-看雪-安全社区|安全招聘|kanxue.com
                1. 在故事开始之前的故事:Android 启动过程与 magiskinit 分析
                1. Android Booting Shenanigans | Magisk (topjohnwu.github.io)
                1. 内部细节 | Magisk 中文文档 (jesse205.github.io)
                1. (71条消息) Magisk内部实现原理_magisk原理_疯人院的院长大人的博客-CSDN博客
                1. LINUX KERNEL INTERNALS: Android Boot Sequence (learnlinuxconcepts.blogspot.com)
                1. Android操作系统启动过程概览 - 知乎 (zhihu.com)
                1. Magisk源码分析(二) | Shocker (pshocker.github.io)
                1. Magisk源码分析(三) | Shocker (pshocker.github.io)
                1. main.cpp - Android Code Search
                1. selinux.cpp - Android Code Search
                 
                Loading...
                LLeaves
                LLeaves
                Happy Hacking
                最新发布
                eBPF实践之修改bpf_probe_write_user以对抗某加固Frida检测
                2024-11-10
                CVE-2024-31317 Zygote命令注入提权system分析
                2024-11-10
                CVE-2024-0044 Bypassing the "run-as" debuggability check
                2024-10-31
                Frida Interceptor Hook实现原理图
                2024-10-29
                Android Data Encryption-从百草园Patch到三味书屋
                2024-10-16
                bpf_probe_write_user补丁添加对只读内存的修改
                2024-10-16
                公告