从启动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
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

同时,使用imgtoolabl分区里面的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 GUID@0x3048: QCOM package (133ed4 bytes, type Firmware Volume Image, attr 0)
COMPRESSED - EE4E5898-3914-4259-9D6E-DC7BD79403CF  - Magic 0x5d@0x3078
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 GUID@0x48: FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF (2c bytes, type Padding, attr 0)
Next GUID@0x78: Odin (f502c bytes, type Application, attr 0)
    Section @0x90 Type: UI, Size: 0xe Odin
    Section @0xa0 Type: PE32, Size: 0xf5004 
    Extracting Odin
Next GUID@0xf50a8: 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

截屏2019-11-12上午12.51.17.png
截屏2019-11-12上午12.51.17.png

由于这里是采用的三星的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.gif
pe.gif

上图是一个标准的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分区。

IMG_20191118_064434.jpg
IMG_20191118_064434.jpg

在图中,我已经成功的加载了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

未来我还要做这些

  1. 尝试将内存初始化
  2. 尝试重写GTDT,MADT 可能在IOMMU管理上有写一些问题直接导致了keypad还是没有成功驱动。
  3. 尝试在 highly_confidental 的帮助下 拿到MSM8998的BSP (如果可以的话)
  4. 肯能要鸽了一段时间了。由于学校考试周,然后需要预习很多东西 (X

未来的一段时间或者很长的一段时间这篇博文或许还会更新,随时欢迎dalao来踩一踩。我还是太菜了(x
顺便悄悄问一句 有1吗?哈哈哈哈

IMG_20191118_045428.jpg
IMG_20191118_045428.jpg

添加新评论

已有 8 条评论

My brother recommended I might like this website. He was totally
right. This submit truly made my day. You can not believe
simply how so much time I had spent for this information! Thanks!

tantra titan gel tantra titan gel
0 0

I enjoy the knowledge on your website. Thnx.

Seriously this is a advantageous web page.

extension capelli uomo extension capelli uomo
0 0

Great website! It looks very professional! Sustain the great job!

if you need any M$ build I can support you

有 1 啊! 我就是嘻嘻嘻,臭哥哥。

Howdy! Someone in my Facebook group shared this site with us so I
came to take a look. I'm definitely loving the information. I'm bookmarking and will be tweeting this to my followers!

欢迎评论~~~