这个博客我也鸽了将近有四个月有余,这几个月作为一个咸鱼,偶然间,网上冲浪看到了这个:点击链接
突然间对几个月前在探讨的如何在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

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
未来我还要做这些
- 尝试将内存初始化
- 尝试重写GTDT,MADT 可能在IOMMU管理上有写一些问题直接导致了keypad还是没有成功驱动。
- 尝试在 highly_confidental 的帮助下 拿到MSM8998的BSP (如果可以的话)
- 肯能要鸽了一段时间了。由于学校考试周,然后需要预习很多东西 (X
未来的一段时间或者很长的一段时间这篇博文或许还会更新,随时欢迎dalao来踩一踩。我还是太菜了(x
