MENU

Catalog

从启动XBL到启动Linux kernel 安卓底层启动的剖析

这个博客我也鸽了将近有四个月有余,这几个月作为一个咸鱼,偶然间,网上冲浪看到了这个:点击链接
突然间对几个月前在探讨的如何在 SDM(SnapDragon)设备上自定义启动系统有了一个比较清晰的头绪。顾把这篇文章写下来,记录一下那几十个通宵,苦苦寻找资料的夜晚。

从小就对设备跨平台启动有着极大的兴趣,小的时候看到国外大神实现了在 iPad2 和 iPhone3Gs 以及 iPhone4 设备上启动 android,研究后发现他这个是通过启动 Bootlace-一个第三方的 Bootloader 启动 android.

UEFI 在高通 2016 年之后的机器的实现
2017 年,那年的冬天看到 IT 之家,@imbushuo 在 ARM64(Lumia950,严谨的来说启动还是以 32 位的方式启动的)的设备上运行 Windows,且并非虚拟机的这种格式,我震惊了。

阅读这篇文章前,您可能需要掌握这几个词汇,PBL,XBL,ABL,Linux kernel,UEFI,EFI

  • Bootloader:启动链中具有特定作业的链接的通用术语,该作业在每次冷启动时运行
  • 冷启动:从关机状态重新启动
  • QFUSE:集成在 SoC 中的微观硬件保险丝-一旦物理烧断,就无法重置或更换 SoC:片上系统(您手机的“主板”之类)
  • EFUSE:基于软件的保险丝,其数据存储在 QFPROM 中 QFPROM:高通的保险丝区域
  • TrustZone:Qualcomm ARM 芯片组的“Secure World”实现
  • QSEECOM:一种 Linux 内核驱动程序,可让我们与 TrustZone 通信,并向 TrustZone 发出 SCM 调用以执行保险丝之类的操作。它仅允许进行签名的程序和少数规范化的 Call
  • SCM:安全通道管理器(注意:与 Linux 的 SMC 调用无关)
  • DTB:设备树 Blob。其目的是“为 Linux 提供一种描述不可发现的硬件的方法”,在此处阅读更多内容。 Android Verified
  • Boot(AVB):在 aboot / ABL 级别实施的一组严格检查,以验证操作系统各个部分的完整性,请在此处阅读更多内容
  • DM-Verity:Android 验证启动的一个组件,用于检查分区以查看分区是否已被读/写过,请在此处阅读更多内容
  • system_as_root:Android 的新安装设置逻辑,将系统分区安装为“/”,而不是“/system”。这意味着系统文件现在位于“ / system / system”中。这是高通检查“/”是否已在“验证启动”下以读/写方式重新挂载的一种方式。它还引入了新标准,即将 Android ramdisk 存储在系统分区中,而不是存储在 bootimage 中。
  • PBL: Primary BootLoader,是高通主要的 Bootloader
  • aboot/ABl: aboot 则为早期的 android bootloader ,是一种 Linux loader,在高通抛弃 Little Kernel 后,采用 ABL 加载 Linux Kernel.
  • QBL,XBL:又名 Qualcomm’s Secondary/eXtensible Bootloader,是 PBL 之后执行的一段程序

启动顺序

如果标有 Qualcomm Secure Boot 的 QFUSE 保险丝行被烧断(在非中文/ OnePlus 设备上是这样),则将验证 PBL(Qualcomm 的主要 Bootloader)并将其从 SoC 上不可写的存储空间 BootROM 加载到内存中。然后执行 PBL 并启动少量硬件,然后验证链中下一个引导程序的签名,将其加载,然后执行。链中的下一个引导程序是 SBL/XBL 这些早期的引导加载程序启动了诸如 CPU 内核,MMU 等核心硬件。它们还负责启动与 Android 并发的核心进程,例如被称为 TrustZone 的高通 ARM 芯片组安全世界。SBL/XBL 的最后一个目的是验证签名,加载并执行 aboot/ABL。绝大多数人都将 Aboot 称为“引导加载程序”,因为其中包含诸如 fastboot 或 OEM 刷机之类的服务。Aboot 会启动剩下的大多数核心硬件,然后通常会验证 bootimage 的签名,通过 dm-verity 向 Android 验证启动报告真实性状态,然后前两个步骤的成功完成会将内核/ramdisk/DTB 加载到内存中。在许多设备上,可以将 Aboot/ABL 配置为跳过签名检查,并允许引导任何内核/引导 bootimage。在 aboot 将所有内容加载到内存中之后,内核(在我们的示例中为 Linux)然后从 bootimage 或 system_as_root 配置中解压缩 ramdisk,对系统分区进行验证并挂载在“ /”位置,并从那里提取 ramdisk。执行此 init 后不久,
禁用 aboot/ABL 中的加密检查的配置选项通常称为“刷机锁”。当设备被称为“锁定”时,这意味着 aboot 当前正在通过 aboot/ABL 在设备的 bootimage 和较新的设备上强制执行数字签名完整性检查,并强制执行“Green“ Android 验证启动状态。这些“锁定”的设备不允许用户自行更改分区,也无法引导自定义的未签名内核。如果锁定的设备被认为是安全的,Android 验证启动通常会报告“绿色”并允许设备继续启动;如果认为设备不安全,则会报告“红色”状态并阻止设备启动。在“未锁定”的设备上,aboot / ABL 可使设备更改分区,并且某些 OEM 允许从内存引导未签名的 boot image

content_qualcomm_firmware_2-1.png

2015 年后,高通对可以更改的区域进行了压缩,并将第二阶段引导加载程序(SBL)链合并为一个统一的 SBL。随着进一步前进,我们看到 SBL 被高通的新专有解决方案 eXtensible Bootloader(XBL)完全取代,该解决方案缓解了 SBL 的许多安全问题。

从图中不难发现,Aboot 也已经从 LittleKernel(一种开源的 Bootloader)中获得了进步,并在完全独立的解决方案中增加了一些功能,该解决方案现在称为专有的 Android Bootloader(ABL)。这种新的引导加载程序允许使用 UEFI,以及针对开发人员/ OEM 的许多其他安全性和质量增强功能。

system_as_root 配置还显着提高了安全性以及常规体系结构。它将 Android-ramdisk 从存储在启动映像中移动到存储在系统分区中(顾名思义,该分区安装为“ /”)。这样做的部分目的是使它可以通过 dm-verity / Android 验证启动进行验证。

注意:新的无缝更新系统(通常称为“A/B”)与 system_as_root 是分开的,即使它们通常是并行的。OEM 可以选择实施一个而不选择另一个。

本文主要谈及 ABL 分区的 Linux Loader,以及 UEFI 对应 Linux kernel 的关系
三星 ABL 分区的提取

我们对三星 GalaxyS8(SM-G9500)的 ABL 分区进行剖析,采用 binwalk 发现其包装结构是一个 UEFI PI Firmware Volume

DECIMAL       HEXADECIMAL     DESCRIPTION
------------------------------------------------------------------------
0             0x0             ELF, 32-bit LSB executable, ARM, version 1 (SYSV)
4488          0x1188          Certificate in DER format (x509 v3), header length: 4, sequence length: 1323
5815          0x16B7          Certificate in DER format (x509 v3), header length: 4, sequence length: 1169
6988          0x1B4C          Certificate in DER format (x509 v3), header length: 4, sequence length: 1161
12288         0x3000          UEFI PI Firmware Volume, volume size: 2097152, header size: 0, revision: 0, EFI Firmware File System v2, GUID: 8C8CE578-8A3D-4F1C-3599-896185C32DD3
12408         0x3078          LZMA compressed data, properties: 0x5D, dictionary size: 16777216 bytes, uncompressed size: 3166472 bytes

同时,使用 imgtool 将 abl 分区里面的 EFI package 解压出来

$:/imgtool abl extract
UEFI firmware image detected at offset 0x3000
Size: 200000, tag: 4856465f, attr: 3feff, checksum:e4b0, version: 2, blockSize: 0x200, blockCount:0x1000
Warning: additional content at offset 0x203000
Next [email protected]: QCOM package (133ed4 bytes, type Firmware Volume Image, attr 0)
COMPRESSED - EE4E5898-3914-4259-9D6E-DC7BD79403CF  - Magic [email protected]
LZMA! 133ea4 bytes
examining decompressed data (3166472 bytes)
Size: 305100, tag: 4856465f, attr: 3feff, checksum:e3bc, version: 2, blockSize: 0x40, blockCount:0xc144
Warning: additional content at offset 0x305100
Next [email protected]: FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF (2c bytes, type Padding, attr 0)
Next [email protected]: Odin (f502c bytes, type Application, attr 0)
    Section @0x90 Type: UI, Size: 0xe Odin
    Section @0xa0 Type: PE32, Size: 0xf5004 
    Extracting Odin
Next [email protected]: LinuxLoader (210038 bytes, type Application, attr 0)
    Section @0xf50c0 Type: UI, Size: 0x1c LinuxLoader
    Section @0xf50dc Type: PE32, Size: 0x210004 
    Extracting LinuxLoader
Warning: non zeros in header

得到了 EFIPkg

由于这里是采用的三星的 S8,三星的 Bootloader 采用了自己的 Knox 架构的 Bootloader.与 Aosp 的 Bootloader 不同,采用了 Odin,而大多数手机采用的是 Fastboot

对 EFIPkg 进行分析
传统的嵌入式 ARM 平台的启动过程: 系统 reset 后,各个 ARM SOC 的从 ROM 代码开始执行(一般 ARM reset 之后,PC=0,而 ROM 缺省地址就是 0)。根据 SOC 厂商约定的规则,ROM code 会从外部设备(串口、网络、NAND flash、USB 磁盘设备或者其他磁盘设备)加载 linux bootloader,bootloader 会收集硬件信息,之后加载 linux kernel。在 UEFI 规范中定义了 BOOT manager,它会根据保存在 NVRAM 参数来决定如何 load EFI Application(可能是 bootloader 或者其他的 image file)。EFI Application 的格式必须符合 PE(Portable Executable )格式。PE 是一种二进制可执行文件的格式(在 linux 世界中,我们多半熟悉的是 ELF 格式),由微软开发,广泛应用在 Windows 平台上。

在 ARMv8 平台上,firmware 中的 boot manager 可以加载支持 UEFI 的传统的 bootloader(例如 uboot),然后由 uboot 加载 kernel,这样,kernel 其实不必关心什么 UEFI。当然这样有些不直观,本来 OS kernel 关心的那些 firmeare 提供的各种信息都是由 bootloader 进行转接,严重影响了系统整合的效率(bootloader 和 kernel 是由不同的团队开发),因此,linux kernel image 自身也可以包装成一个 EFI image,由 boot manager 直接加载,完成启动过程。

一个标准的 pe 格式的 EFIImage,由两部分组成,一部分是为了兼容 MS-DOS 操作系统而包装的外壳(灰色 block),主要由 64B 的 MZ header 和 MS-DOS stub 代码区组成。在遥远的 MSDOS 时代,其可执行文件就需要这样的一个 header,MSDOS 的 program loader 就会根据这个 header 加载程序运行。在 Windows 时代,微软提出了 PE 这种格式文件,它主要是运行在 windows 系列的操作系统中,但是,还需要考虑 MSDSO 的兼容性(也就是说当 MSDOS 执行 PE 格式的文件也能够提供足够的信息让用户知道如何处理)。MS-DOS stub block 是一段 stub code,这段区域的主要作用是:当 PE 格式的 image 在 MS-DOS 下加载运行的时候,程序会执行这个区域的代码(PE 的代码都是 for windows 的,不可能在 DOS 下实际执行,因此,只能执行这些 stub 程序),当然运行的结果仅仅是打印“This program cannot be run in DOS mode”。

另外一个区域就是实际的 PE 格式的文件了。主要包括 PE header(绿色 block)、各种 Section header(蓝色 block,用于描述各个 section)和各个 section 的实际的 Data。

在三星 S8 上的实践
从 EFI 的结构我们可以知道,通过伪装 FD 格式的 Linux Kernel 然后再 Append Dtb,包裹成一个 Boot.img,这样可以成功的将高通的 UEFI 的 Linuxloader Application 装入 Boot.img 镜像。

具体操作
伪装 LinuxKernel 的 Kernel Header,(在 Pkg.fdf 中定义 Header 格式)

0x00000000|0x00008000
DATA = {
  0x01, 0x00, 0x00, 0x10,                         # code0: adr x1, .
  0xff, 0x1f, 0x00, 0x14,                         # code1: b 0x8000
  0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, # text_offset: 512 KB
  0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, # image_size: 2 MB
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, # flags
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, # res2
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, # res3
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, # res4
  0x41, 0x52, 0x4d, 0x64,                         # magic: "ARM\x64"
  0x00, 0x00, 0x00, 0x00                          # res5
}

0x00008000|0x001f8000

这样在 FD 的前面包裹了一层 Linux Kernel 的 Header.

其次,编译成功后的 FD 文件实际上是 Linux Kernel 的格式,在安卓编译 Boot.img 的时候,其实会将 image 使用 gz 压缩成 gzip 的格式,然后 append dtb 形成 zImage,参考高通源码

GZipPkgCheck (BootParamlist *BootParamlistPtr)
{
  UINT32 OutLen = 0;
  UINT64 OutAvaiLen = 0;
  struct kernel64_hdr *Kptr = NULL;

  if (BootParamlistPtr == NULL) {

    DEBUG ((EFI_D_ERROR, "Invalid input parameters\n"));
    return EFI_INVALID_PARAMETER;
  }

  if (BootParamlistPtr->BootingWithGzipPkgKernel) {
    OutAvaiLen = BootParamlistPtr->DeviceTreeLoadAddr -
                 BootParamlistPtr->KernelLoadAddr;

    if (OutAvaiLen > MAX_UINT32) {
      DEBUG ((EFI_D_ERROR,
              "Integer Overflow: the length of decompressed data = %u\n",
      OutAvaiLen));
      return EFI_BAD_BUFFER_SIZE;
    }

    DEBUG ((EFI_D_INFO, "Decompressing kernel image start: %lu ms\n",
                         GetTimerCountms ()));
    if (decompress (
        (UINT8 *)(BootParamlistPtr->ImageBuffer +
        BootParamlistPtr->PageSize),               // Read blob using BlockIo
        BootParamlistPtr->KernelSize,              // Blob size
        (UINT8 *)BootParamlistPtr->KernelLoadAddr, // Load address, allocated
        (UINT32)OutAvaiLen,                        // Allocated Size
        &BootParamlistPtr->DtbOffset, &OutLen)) {
          DEBUG ((EFI_D_ERROR, "Decompressing kernel image failed!!!\n"));
          return RETURN_OUT_OF_RESOURCES;
    }

    if (OutLen <= sizeof (struct kernel64_hdr *)) {
      DEBUG ((EFI_D_ERROR,
              "Decompress kernel size is smaller than image header size\n"));
      return RETURN_OUT_OF_RESOURCES;
    }
    Kptr = (Kernel64Hdr *) BootParamlistPtr->KernelLoadAddr;
    DEBUG ((EFI_D_INFO, "Decompressing kernel image done: %lu ms\n",
                         GetTimerCountms ()));
  } else {
    Kptr = (struct kernel64_hdr *)(BootParamlistPtr->ImageBuffer
                         + BootParamlistPtr->PageSize);
    /* Patch kernel support only for 64-bit */
    if (BootParamlistPtr->BootingWithPatchedKernel) {
      DEBUG ((EFI_D_VERBOSE, "Patched kernel detected\n"));

      /* The size of the kernel is stored at start of kernel image + 16
       * The dtb would start just after the kernel */
      gBS->CopyMem ((VOID *)&BootParamlistPtr->DtbOffset,
                    (VOID *) (BootParamlistPtr->ImageBuffer +
                               BootParamlistPtr->PageSize +
                               sizeof (PATCHED_KERNEL_MAGIC) - 1),
                               sizeof (BootParamlistPtr->DtbOffset));

      BootParamlistPtr->PatchedKernelHdrSize = PATCHED_KERNEL_HEADER_SIZE;
      Kptr = (struct kernel64_hdr *)((VOID *)Kptr +
                 BootParamlistPtr->PatchedKernelHdrSize);
      gBS->CopyMem ((VOID *)BootParamlistPtr->KernelLoadAddr, (VOID *)Kptr,
                 BootParamlistPtr->KernelSize);
    }

    if (Kptr->magic_64 != KERNEL64_HDR_MAGIC) {
      if (BootParamlistPtr->KernelSize <=
          DTB_OFFSET_LOCATION_IN_ARCH32_KERNEL_HDR) {
          DEBUG ((EFI_D_ERROR, "DTB offset goes beyond kernel size.\n"));
          return EFI_BAD_BUFFER_SIZE;
        }
      gBS->CopyMem ((VOID *)&BootParamlistPtr->DtbOffset,
           ((VOID *)Kptr + DTB_OFFSET_LOCATION_IN_ARCH32_KERNEL_HDR),
           sizeof (BootParamlistPtr->DtbOffset));
    }
  }

  if (Kptr->magic_64 != KERNEL64_HDR_MAGIC) {
    /* For GZipped 32-bit Kernel */
    BootParamlistPtr->BootingWith32BitKernel = TRUE;
  } else {
    if (Kptr->ImageSize >
          (BootParamlistPtr->DeviceTreeLoadAddr -
           BootParamlistPtr->KernelLoadAddr)) {
      DEBUG ((EFI_D_ERROR,
            "DTB header can get corrupted due to runtime kernel size\n"));
      return RETURN_OUT_OF_RESOURCES;
    }
  }
  return EFI_SUCCESS;
}

可以知道,Linuxloader 在加载 LinuxKernel 的时候,有将 boot.img 的 zImage 先进行解压然后再加载。
所以逆向的话只需要将 append DTB 之后的 boot.img-zImage 导入到 ALK-linux.zip 这个打包 boot.img 的工具进行打包就可以了。

接下来遇到的一点点小问题
在可以加载 UEFI loader 之后,首先我想到的就是创建 ESP 分区,但是由于三星 S8 的 TWRP 过于老旧,fdisk 在设备上完全用不了,于是用 gsdisk 将 userdata 分区.我个人是将 Userdata 从 54.5G 分成了三个区,10.0G 的 userdata,300M 的 esp 以及 44.2G 的数据区。

Number  Start (sector)    End (sector)  Size       Code  Name
   1             264             775       2048K   0700  modemst1
   2             776            1287       2048K   0700  modemst2
   3            1288            1288        4096   0700  fsc
   4            1289            1290        8192   0700  ssd
   5            1291            9482       32.0M   0700  persist
   6            9483           14602       20.0M   0700  efs
   7           14603           17162       10.0M   0700  param
   8           17163           17418       1024K   0700  misc
   9           17419           17546        512K   0700  keystore
  10           17547           25226       30.0M   0700  bota
  11           25227           30602       21.0M   0700  fota
  12           30603           30730        512K   0700  persistent
  13           30731           31754       4096K   0700  steady
  14           31755           35850       16.0M   0700  keyrefuge
  15           35851           60170       95.0M   0700  apnhlos
  16           60171           81930       85.0M   0700  modem
  17           81931           98314       64.0M   0700  boot
  18           98315          114698       64.0M   0700  recovery
  19          114699         1228298       4350M   0700  system
  20         1228299         1305098        300M   0700  cache
  21         1305099         1307658       10.0M   0700  omr
  22         1307659         1309194       6144K   0700  nad_refer
  23         1309195         1311754       10.0M   0700  debug
/ 24         1311755        15604702       54.5G   0700  userdata
  24         1311755        3933194        10.0G   0700  userdata
  25         3933195        4009994         300M   0700  esp
  26         4009995        15604702       44.2G   0700  C

在 Recovery 下执行了以下的 adb shell:

sgdisk --delete=24 /dev/block/sda
  sgdisk --new 24:1311755:3933194 /dev/block/sda
  sgdisk --new 25:3933195:4009994 /dev/block/sda
  sgdisk --new 26:4009995:15604702 /dev/block/sda
  sgdisk --change-name 24:userdata /dev/block/sda
  sgdisk --change-name 25:esp /dev/block/sda
  sgdisk --change-name 26:C /dev/block/sda

接下来通过@NTAthority 以及来自法国的@Gustave Monce 提醒,我使用 Linux Kernel 的 Mass storage device 来操作分区。 于是我挂在了 ESP 和数据区作为 Mass storage file gadget 设备来写入 Windows 镜像和 ESP 分区。

在图中,我已经成功的加载了 UEFI 的 loader. 接来下需要做的就是需要初始化内存,不知道为什么,从 Intel 获得的 MemoryInitPeiLib 是未能成功将内存初始化,这直接导致了 UEFI 未能直接退出 BS Mode.

来自@imbushuo 他回我的原话是这样:

A few most common boot failure reasons I’ve seen on Windows on ARM are:

you have a problem with SMBIOS, especially memory addressing region and memory size
you have a problem with your MMU configuration, alignment or write back config
you have a problem with your MADT and GTDT table
certain Windows builds
Last Modified: March 4, 2022