Magisk原理之Boot补丁
通过xml文件追踪到修补boot.img的入口为
com/topjohnwu/magisk/ui/install/InstallViewModel.kt
// 对原boot.img开始补丁
fun install() {
when (method) {
R.id.method_patch -> FlashFragment.patch(data.value!!).navigate(true)
R.id.method_direct -> FlashFragment.flash(0).navigate(true)
R.id.method_inactive_slot -> FlashFragment.flash(1).navigate(true)
R.id.method_direct_system -> FlashFragment.flash(2).navigate(true)
else -> error("Unknown value")
}
}
明显这里我们要走FlashFragment.patch(data.value!!).navigate(true)
fun patch(uri: Uri) = MainDirections.actionFlashFragment(
action = Const.Value.PATCH_FILE,
additionalData = uri
)
最终会来到com/topjohnwu/magisk/ui/flash/FlashFragment.kt
进入页面后的代码
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
defaultOrientation = activity?.requestedOrientation ?: -1
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR
if (savedInstanceState == null) {
viewModel.startFlashing()
}
}
fun startFlashing() {
...
Const.Value.PATCH_FILE -> {
uri ?: return@launch
showReboot = false
MagiskInstaller.Patch(uri, outItems, logItems).exec()
}
...
}
class Patch(
private val uri: Uri,
console: MutableList<String>,
logs: MutableList<String>
) : MagiskInstaller(console, logs) {
override suspend fun operations() = patchFile(uri)
}
protected fun patchFile(file: Uri) = extractFiles() && handleFile(file)
其实这里就简单了修补boot.img就分两个步骤
extractFiles 导出相关环境
handleFile 真正修补img
/**
* 这个相当于构建一个安装的工作目录,将相关的环境copy过去
*
*
*/
private fun extractFiles(): Boolean {
// 输出设备的 CPU 架构和版本信息到控制台
console.add("- Device platform: ${Const.CPU_ABI}")
console.add("- Installing: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
// 创建安装目录并清理旧数据
installDir = localFS.getFile(context.filesDir.parent, "install")
installDir.deleteRecursively() // 删除现有的安装目录及其内容
installDir.mkdirs() // 创建新的安装目录
try {
// 判断当前是否作为 Stub(占位符)运行
if (isRunningAsStub) {
// 如果是 Stub 模式,从当前 APK 文件中提取二进制文件
val zf = ZipFile(StubApk.current(context))
// 定义是否提取 32 位的 magisk 库(适用于非纯 64 位设备)
val is32lib = Const.CPU_ABI_32?.let {
{ entry: ZipEntry -> entry.name == "lib/$it/libmagisk32.so" }
} ?: { false }
// 遍历 ZIP 文件中的条目,筛选出需要的库文件
zf.entries().asSequence().filter {
!it.isDirectory && (it.name.startsWith("lib/${Const.CPU_ABI}/") || is32lib(it))
}.forEach {
// 提取文件名
val n = it.name.substring(it.name.lastIndexOf('/') + 1)
val name = n.substring(3, n.length - 3) // 去掉前缀 "lib" 和后缀 ".so"
val dest = File(installDir, name) // 目标文件路径
// 将文件写入目标路径
zf.getInputStream(it).writeTo(dest)
dest.setExecutable(true) // 设置文件可执行权限
}
zf.close()
} else {
// 如果不是 Stub 模式,直接从 nativeLibraryDir 中加载库文件
val info = context.applicationInfo
// 获取库文件列表
var libs = File(info.nativeLibraryDir).listFiles { _, name ->
name.startsWith("lib") && name.endsWith(".so")
} ?: emptyArray()
// 如果设备支持 32 位,添加 32 位的 magisk 库
val lib32 = info.javaClass.getDeclaredField("secondaryNativeLibraryDir")
.get(info) as String?
if (lib32 != null) {
libs += File(lib32, "libmagisk32.so")
}
// 为每个库文件创建符号链接到安装目录
for (lib in libs) {
val name = lib.name.substring(3, lib.name.length - 3) // 去掉前缀 "lib" 和后缀 ".so"
Os.symlink(lib.path, "$installDir/$name") // 创建符号链接
}
}
// 提取脚本文件到安装目录
for (script in listOf("util_functions.sh", "boot_patch.sh", "addon.d.sh", "stub.apk")) {
val dest = File(installDir, script)
context.assets.open(script).writeTo(dest) // 从 assets 文件夹中读取并写入目标路径
}
// 提取 Chrome OS 工具到子目录 "chromeos"
File(installDir, "chromeos").mkdir() // 创建子目录
for (file in listOf("futility", "kernel_data_key.vbprivk", "kernel.keyblock")) {
val name = "chromeos/$file"
val dest = File(installDir, name)
context.assets.open(name).writeTo(dest) // 从 assets 文件夹中读取并写入目标路径
}
} catch (e: Exception) {
// 捕获异常,记录错误信息到控制台并返回失败状态
console.add("! Unable to extract files")
Timber.e(e)
return false
}
// 如果需要使用 root 目录,将安装文件移动到 tmpfs
if (useRootDir) {
rootFS.getFile(Const.TMPDIR).also {
arrayOf(
"rm -rf $it", // 删除目标目录中的现有内容
"mkdir -p $it", // 创建目标目录
"cp_readlink $installDir $it", // 复制安装目录内容到目标目录
"rm -rf $installDir" // 删除原始安装目录
).sh() // 执行 Shell 命令
installDir = it // 更新安装目录为目标目录
}
}
// 返回成功状态
return true
}
创建一个/data/user_de/0/io.github.huskydg.magisk/install目录
.sh文件来自asset 二进制可执行文件来自于lib 它以lib***.so的方式存储 所以extractFiles的工作还将修改会正确的名称
将执行的.sh和magiskinit magiskboot拷贝到上面的工作目录
private fun handleFile(uri: Uri): Boolean {
val outStream: OutputStream
val outFile: MediaStoreUtils.UriFile
// 处理输入文件
try {
uri.inputStream().buffered().use { src ->
...
// 生成唯一的随机文件名
val alpha = "abcdefghijklmnopqrstuvwxyz"
val alphaNum = "$alpha${alpha.uppercase(Locale.ROOT)}0123456789"
val random = SecureRandom()
val filename = StringBuilder("magisk_patched-${BuildConfig.VERSION_CODE}_").run {
for (i in 1..5) {
append(alphaNum[random.nextInt(alphaNum.length)])
}
toString()
}
// 检查文件是否为 tar 格式
srcBoot = if (tarMagic.contentEquals("ustar".toByteArray())) {
...
} else {
// 处理其他格式(如 img 或 zip)
outFile = MediaStoreUtils.getFile("$filename.img", true)
outStream = outFile.uri.outputStream()
try {
if (magic.contentEquals("CrAU".toByteArray())) {
processPayload(src)
} else if (magic.contentEquals("PK\u0003\u0004".toByteArray())) {
// 处理 zip 文件
processZip(ZipInputStream(src))
} else {
// 将文件直接复制到缓存
console.add("- Copying image to cache")
// 处理 img 文件
installDir.getChildFile("boot.img").also {
src.copyAndCloseOut(it.newOutputStream())
}
}
} catch (e: IOException) {
...
}
}
}
} catch (e: IOException) {
...
}
// 执行补丁处理 处理成新的new-boot.img 真正的补丁位置
if (!patchBoot()) {
outFile.delete() // 如果失败,删除输出文件
return false
}
// 输出补丁后的文件 将new-boot.img 输出到最后的magisk_patched-${BuildConfig.VERSION_CODE}_"文件中
try {
val newBoot = installDir.getChildFile("new-boot.img")
if (outStream is TarOutputStream) {
// 根据文件路径设定名称
val name = with(srcBoot.path) {
when {
contains("recovery") -> "recovery.img"
contains("init_boot") -> "init_boot.img"
else -> "boot.img"
}
}
outStream.putNextEntry(newTarEntry(name, newBoot.length())) // 写入新条目
}
newBoot.newInputStream().copyAndClose(outStream) // 复制新文件
newBoot.delete() // 删除临时文件
} catch (e: IOException) {
...
}
// 清理和修复二进制文件
srcBoot.delete()
"cp_readlink $installDir".sh()
return true
}
这里可以看出handleFile主要就是为补丁后的boot.img取一个新的名称这个也是我们在Magisk修补的输出文件magisk_patched-${BuildConfig.VERSION_CODE}_.img
主要的修补工作是在patchBoot()中
private fun patchBoot(): Boolean {
val newBoot = installDir.getChildFile("new-boot.img")
if (!useRootDir) {
// Create output files before hand
newBoot.createNewFile()
File(installDir, "stock_boot.img").createNewFile()
}
Log.i("SharkChilli", "cd $installDir ")
Log.i(
"SharkChilli", "KEEPFORCEENCRYPT=${Config.keepEnc} " +
"KEEPVERITY=${Config.keepVerity} " +
"PATCHVBMETAFLAG=${Info.patchBootVbmeta} " +
"RECOVERYMODE=${Config.recovery} " +
"LEGACYSAR=${Info.legacySAR} " +
"sh boot_patch.sh $srcBoot"
)
// cd /data/user_de/0/io.github.huskydg.magisk/install
// KEEPFORCEENCRYPT=true KEEPVERITY=true PATCHVBMETAFLAG=false RECOVERYMODE=true LEGACYSAR=true sh boot_patch.sh /data/user_de/0/io.github.huskydg.magisk/install/boot.img
val cmds = arrayOf(
"cd $installDir",
"KEEPFORCEENCRYPT=${Config.keepEnc} " +
"KEEPVERITY=${Config.keepVerity} " +
"PATCHVBMETAFLAG=${Info.patchBootVbmeta} " +
"RECOVERYMODE=${Config.recovery} " +
"LEGACYSAR=${Info.legacySAR} " +
"sh boot_patch.sh $srcBoot"
)
val isSuccess = cmds.sh().isSuccess
shell.newJob().add("./magiskboot cleanup", "cd /").exec()
return isSuccess
}
实际就是使用shell脚本进入到前面创建的install工作目录,使用boot_patch.sh进行修补,所以可以了解到真正的patch的工作都是在boot_patch.sh脚本中
用于boot_patch.sh过长我截取一些关键的部分进行说明
...
./magiskboot unpack "$BOOTIMAGE" # 解包 boot 镜像
...
# 检查并恢复 ramdisk 状态
if [ -e ramdisk.cpio ]; then
./magiskboot cpio ramdisk.cpio test
STATUS=$?
SKIP_BACKUP=""
else
# Stock A only legacy SAR, or some Android 13 GKIs
STATUS=0
SKIP_BACKUP="#"
fi
...
case $((STATUS & 3)) in
0 ) # Stock boot 镜像
ui_print "- Stock boot image detected"
SHA1=$(./magiskboot sha1 "$BOOTIMAGE" 2>/dev/null)
cat $BOOTIMAGE > stock_boot.img
cp -af ramdisk.cpio ramdisk.cpio.orig 2>/dev/null
...
SKIP32="#"
SKIP64="#"
# 压缩文件以节省 ramdisk 空间
if [ -f magisk64 ]; then
$BOOTMODE && [ -z "$PREINITDEVICE" ] && PREINITDEVICE=$(./magisk64 --preinit-device)
./magiskboot compress=xz magisk64 magisk64.xz
unset SKIP64
fi
if [ -f magisk32 ]; then
$BOOTMODE && [ -z "$PREINITDEVICE" ] && PREINITDEVICE=$(./magisk32 --preinit-device)
./magiskboot compress=xz magisk32 magisk32.xz
unset SKIP32
fi
./magiskboot compress=xz stub.apk stub.xz
# 将代码中的参数输出配置文件
echo "KEEPVERITY=$KEEPVERITY" > config
echo "KEEPFORCEENCRYPT=$KEEPFORCEENCRYPT" >> config
echo "RECOVERYMODE=$RECOVERYMODE" >> config
if [ -n "$PREINITDEVICE" ]; then
ui_print "- Pre-init storage partition: $PREINITDEVICE"
echo "PREINITDEVICE=$PREINITDEVICE" >> config
fi
[ -n "$SHA1" ] && echo "SHA1=$SHA1" >> config
# 更新 ramdisk 内容 此处其实就是真正的修补处
./magiskboot cpio ramdisk.cpio \
"add 0750 $INIT magiskinit" \
"mkdir 0750 overlay.d" \
"mkdir 0750 overlay.d/sbin" \
"$SKIP32 add 0644 overlay.d/sbin/magisk32.xz magisk32.xz" \
"$SKIP64 add 0644 overlay.d/sbin/magisk64.xz magisk64.xz" \
"add 0644 overlay.d/sbin/stub.xz stub.xz" \
"patch" \
"$SKIP_BACKUP backup ramdisk.cpio.orig" \
"mkdir 000 .backup" \
"add 000 .backup/.magisk config" \
|| abort "! Unable to patch ramdisk"
...
# 重新打包 boot 镜像
ui_print "- Repacking boot image"
./magiskboot repack "$BOOTIMAGE" || abort "! Unable to repack boot image"
这个脚本其实就是干了先将boot.img解压出来得到ramdisk,往ramdisk中修改替换我们的文件,然后重新打包会boot.img。这样就完成了patch
所以真正的修补逻辑应该就是
./magiskboot cpio ramdisk.cpio \
"add 0750 $INIT magiskinit" \
"mkdir 0750 overlay.d" \
"mkdir 0750 overlay.d/sbin" \
"$SKIP32 add 0644 overlay.d/sbin/magisk32.xz magisk32.xz" \
"$SKIP64 add 0644 overlay.d/sbin/magisk64.xz magisk64.xz" \
"add 0644 overlay.d/sbin/stub.xz stub.xz" \
"patch" \
"$SKIP_BACKUP backup ramdisk.cpio.orig" \
"mkdir 000 .backup" \
"add 000 .backup/.magisk config"
这些修改文件都是项目工程中的执行文件和内置apk,以及上面刚刚通过参数输出的配置文件,打包压缩的执行文件
我们接下来主要来关注一下magiskboot cpio
它的编译文件在此处
native/src/Android.mk
include $(CLEAR_VARS)
LOCAL_MODULE := magiskboot
LOCAL_STATIC_LIBRARIES := \
libbase \
libcompat \
liblzma \
liblz4 \
libbz2 \
libz \
libzopfli \
libboot-rs
LOCAL_SRC_FILES := \
boot/main.cpp \
boot/bootimg.cpp \
boot/compress.cpp \
boot/format.cpp \
boot/boot-rs.cpp
include $(BUILD_EXECUTABLE)
那他的入口应该就是在boot/main.cpp
int main(int argc, char *argv[]) {
...
else if (argc > 2 && action == "cpio") {
return rust::cpio_commands(argc - 2, argv + 2) ? 0 : 1;
}
...
}
bool cpio_commands(::std::int32_t argc, char const *const *argv) noexcept {
return rust$cxxbridge1$cpio_commands(argc, argv);
}
这里可以看到这部分的逻辑使用了rust编写
对应的rust源码就是在native/src/boot/cpio.rs
/// 该函数是处理 `magiskboot` 中 CPIO 命令的主要入口。
/// 它接收命令行参数(`argc` 和 `argv`),并对它们进行处理以操作 CPIO 文件。
///
/// # 参数
/// - `argc`: 命令行参数的数量。
/// - `argv`: 指向命令行参数数组的指针。
///
/// # 返回值
/// - `true`: 如果函数成功处理了所有命令。
/// - `false`: 如果执行过程中发生错误。
pub fn cpio_commands(argc: i32, argv: *const *const c_char) -> bool {
/// 内部函数,执行实际的 CPIO 命令处理。
/// 返回一个 `LoggedResult`,表示成功 (`Ok(())`) 或包含日志信息的错误。
fn inner(argc: i32, argv: *const *const c_char) -> LoggedResult<()> {
// 确保至少有一个参数;否则记录错误并返回。
if argc < 1 {
return Err(log_err!("没有提供任何参数"));
}
// 将命令行参数解析为结构化形式。
let cmds = map_args(argc, argv)?;
// 初始化用于处理 CPIO 命令的 CLI,设置早期退出时打印用法信息。
let mut cli =
CpioCli::from_args(&["magiskboot", "cpio"], &cmds).on_early_exit(print_cpio_usage);
// 从 CLI 参数中获取 CPIO 文件路径,并检查是否存在。
let file = Utf8CStr::from_string(&mut cli.file);
let mut cpio = if Path::new(file).exists() {
// 如果文件存在,则加载 CPIO 文件。
Cpio::load_from_file(file)?
} else {
// 否则,创建一个新的 CPIO 对象。
Cpio::new()
};
// 处理 CLI 命令列表中的每个命令。
for cmd in cli.commands {
// 忽略以 '#' 开头的命令(视为注释)。
if cmd.starts_with('#') {
continue;
}
// 解析特定的 CPIO 子命令及其参数。
let mut cli = CpioCommand::from_args(
&["magiskboot", "cpio", file],
cmd.split(' ')
.filter(|x| !x.is_empty())
.collect::<Vec<_>>()
.as_slice(),
)
.on_early_exit(print_cpio_usage);
// 匹配解析后的子命令,并执行对应的操作。
match &mut cli.command {
CpioSubCommand::Test(_) => exit(cpio.test()), // 测试 CPIO 文件的完整性。
CpioSubCommand::Restore(_) => cpio.restore()?, // 从备份中恢复 CPIO。
CpioSubCommand::Patch(_) => cpio.patch(), // 对 CPIO 文件应用补丁。
CpioSubCommand::Exists(Exists { path }) => {
// 检查 CPIO 中是否存在特定路径。
if cpio.exists(path) {
exit(0);
} else {
exit(1);
}
}
CpioSubCommand::Backup(Backup {
origin,
skip_compress,
}) => cpio.backup(Utf8CStr::from_string(origin), *skip_compress)?, // 备份 CPIO。
CpioSubCommand::Remove(Remove { path, recursive }) => cpio.rm(path, *recursive), // 从 CPIO 中移除文件/目录。
CpioSubCommand::Move(Move { from, to }) => cpio.mv(from, to)?, // 在 CPIO 中移动/重命名文件。
CpioSubCommand::MakeDir(MakeDir { mode, dir }) => cpio.mkdir(mode, dir), // 在 CPIO 中创建目录。
CpioSubCommand::Link(Link { src, dst }) => cpio.ln(src, dst), // 在 CPIO 中创建符号链接。
CpioSubCommand::Add(Add { mode, path, file }) => cpio.add(mode, path, file)?, // 将文件添加到 CPIO。
CpioSubCommand::Extract(Extract { paths }) => {
// 从 CPIO 中提取文件,处理可选的源路径和目标路径。
if !paths.is_empty() && paths.len() != 2 {
return Err(log_err!("无效的参数"));
}
cpio.extract(
paths.get(0).map(|x| x.as_str()),
paths.get(1).map(|x| x.as_str()),
)?;
}
CpioSubCommand::List(List { path, recursive }) => {
// 列出 CPIO 中的文件,可选择递归列出。
cpio.ls(path.as_str(), *recursive);
exit(0);
}
};
}
// 处理完所有命令后,将更新的 CPIO 保存到文件中。
cpio.dump(file)?;
Ok(())
}
// 执行内部函数,并记录遇到的任何错误。
inner(argc, argv)
.log_with_msg(|w| w.write_str("处理 CPIO 失败"))
.is_ok()
}
当解压的文件中有cpio在这个cpio基础上操作,没有的话就创建一个新的文件
"add 0750 $INIT magiskinit" \
"mkdir 0750 overlay.d" \
"mkdir 0750 overlay.d/sbin" \
"$SKIP32 add 0644 overlay.d/sbin/magisk32.xz magisk32.xz" \
"$SKIP64 add 0644 overlay.d/sbin/magisk64.xz magisk64.xz" \
"add 0644 overlay.d/sbin/stub.xz stub.xz" \
"patch" \
"$SKIP_BACKUP backup ramdisk.cpio.orig" \
"mkdir 000 .backup" \
"add 000 .backup/.magisk config"
对应的代码就是
CpioSubCommand::Add(Add { mode, path, file }) => cpio.add(mode, path, file)?, // 将文件添加到 CPIO。
CpioSubCommand::MakeDir(MakeDir { mode, dir }) => cpio.mkdir(mode, dir), // 在 CPIO 中创建目录。
CpioSubCommand::Patch(_) => cpio.patch(), // 对 CPIO 文件应用补丁。
CpioSubCommand::Backup(Backup {
origin,
skip_compress,
}) => cpio.backup(Utf8CStr::from_string(origin), *skip_compress)?, // 备份 CPIO。
这里Add MakeDir就是对目录文件进行的操作,我们就不详细看了
所以补丁的核心就是
# 将名为 magiskinit 的文件添加到 ramdisk 中命令为init,并设置权限为 0750。 此处尤为重要这个就是上一部文章中第一阶段的init
# 参数含义:0750 表示文件拥有者有读、写、执行权限,同组用户有读和执行权限,其他用户无权限。
"add 0750 $INIT magiskinit" \
# 在 ramdisk 中创建名为 overlay.d 的目录,并设置权限为 0750。
# overlay.d 目录可能用于覆盖或扩展现有文件系统的某些功能。
"mkdir 0750 overlay.d" \
# 在 overlay.d 目录中创建名为 sbin 的子目录,并设置权限为 0750。
# overlay.d/sbin 目录可能用于存放补丁工具或可执行文件。
"mkdir 0750 overlay.d/sbin" \
# 如果 SKIP32 未定义,则将压缩的 magisk32.xz 添加到 overlay.d/sbin 目录中,设置权限为 0644。
# 参数含义:0644 表示文件拥有者有读写权限,其他用户只有读取权限。
"$SKIP32 add 0644 overlay.d/sbin/magisk32.xz magisk32.xz" \
# 如果 SKIP64 未定义,则将压缩的 magisk64.xz 添加到 overlay.d/sbin 目录中,设置权限为 0644。
"$SKIP64 add 0644 overlay.d/sbin/magisk64.xz magisk64.xz" \
# 将压缩的 stub.xz 文件添加到 overlay.d/sbin 目录中,并设置权限为 0644。
# stub.xz 可能是一个占位文件或轻量级工具文件。
"add 0644 overlay.d/sbin/stub.xz stub.xz" \
# 调用 patch 命令,修补 ramdisk 文件。
# patch 通常会应用特定逻辑修改 fstab 或其他重要文件,移除 `verity` 校验和强制加密等机制。
"patch" \
# 如果 SKIP_BACKUP 未定义,则备份当前的 ramdisk 文件到 ramdisk.cpio.orig。
# 备份是为了确保在修补出问题时可以还原。
"$SKIP_BACKUP backup ramdisk.cpio.orig" \
# 在 ramdisk 中创建名为 .backup 的隐藏目录,并设置权限为 000(不可读、写、执行)。
# 这种目录可能用于存放内部工具或配置文件,防止被直接访问。
"mkdir 000 .backup" \
# 在 .backup 目录中添加名为 .magisk 的隐藏文件,权限设置为 000。
# 该文件可能是用于存储 Magisk 的特定配置信息。
"add 000 .backup/.magisk config"
总结
总的来说boot补丁的核心就解压boot.img,得到ramdisk如果没有就new一个,然后将自己的文件都放进去,最后打包回去成一个新的boot.img
最重要的是将magiskinit替换到了ramdisk的init中,这个就是启动第一阶段的init,这样将新的boot.img刷入后启动入口就到了magiskinit