各种Android设备都只能刷专门为相应型号的设备定制的镜像。
厂商会提供一套系统镜像把它作为“出厂默认”的 Android 系统刷在设备上。
一个完整的系统镜像是由以下几个文件组成的,刷机时它们会被写入各自对应的分区中。
/init.rc
及相关文件规定了系统余下的其他部分该如何被加载。boot 镜像会被刷到“boot”分区里去。Android 设备上的 Boot Loader 一般都会被加锁。也就是说,如果数字签名验证没有通过,手机/平板电脑会拒绝刷机或用更新的镜像启动。厂商会在 ROM 中提供他们的公钥,该密钥被用来建立一条贯穿启动过程始终的信任链。这样,所有的启动组件(从rpm到 sbl再到Android Boot Loader)就都可以被验证 (是否来自厂商或有无遭到修改)了。对这些组件的逆向分析表明,其中都有一个X.509v3 的证书,以及验证密钥时所必需的 OpenSSL 相关代码。
被解锁之后(如果可能的话), Boot Loader 就能完全控制/data 分区中的信息。所以解锁 BootLoader 会从根本上危及手机的安全性,因为攻击者拿到手机之后,就能给手机刷一个能绕过用户的开机密码或者锁屏图案的恶意更新包,或者把/data 分区中的数据复制出来,把里面的所有个人信息全部偷走。
如果 BootLoader 不能解锁,那么至少从理论上说,只要不被 root,手机就是安全的。不过在实践中,Android 也并不是不存在能够 root 它的漏洞。事实上,任何一个 3.13 及以后版本的Linux内核漏洞利用代码都有可能被用来 root Android 系统。著名的“TowelRoot”(这个漏洞利用代码是由 GeoHot 公开的)会影响到目前市场上销售的所有 Android 设备,而这还只是好几个般被称为“一键 root”(相当于 iOS 中的越狱程序)的漏洞利用程序中的一个。
Android 的 Boot 镜像中存储的是操作系统的核心组件,包括内核和 RAM disk。此外,Boot镜像(它是由Android 源码树中的 mkbootimg 创建的)的组成部分还有一个很小的头部、内核命令行、一个 Hash以及一个可选的(在实践中并不使用的)二级启动加载器。
所有这些组件都是闪存页边界(通常为2KB)对齐的。Boot 镜像可以用它的文件签名“ANDROID!”予以识别,这和上文中我们可以用Boot Loader 的文件签名“BOOTLDR!”识别 Boot Loader 的原理是一样的。
与大多数操作系统的内核不同,Linux 的内核大多是经过压缩的。内核镜像文件格式(也被称为 zImage 格式)规定,其中必须含有用来把内核镜像中其他经过压缩的部分解压到内存中去的自解压代码。由于不同压缩算法各有所长,所以可选的压缩算法也有好几个,具体使用哪种算法是在 build 内核的过程时 (make config)决定的,如图:
内核总是会先运行自解压代码,这也就意味着,我们得先搜索一下文件,找到压缩算法的签名。传统上,大多数ARM 内核使用的都是 zmage,尽管严格意义上说并不一定要这样做。
内核是 Android 系统中与体系结构最为紧密相关的部分。尽管其他的一些组件也需要关心处理器的类型 (系统中使用的到底是 ARM、Intel 还是 MIPS 处理器),但是内核还要关心主板类型和芯片组的型号。因为事实上,移动设备的处理器是个单晶片系统(SoC,System-on-Chip,它由一组芯片构成),其中还含有其他芯片,内核也必须为这些芯片提供专用的驱动。这些驱动是源码树的一部分,谷歌为不同的芯片组提供了不同的内核设备树 (device tree)。
Boot或recovery 镜像的另一个组件就是“initialRAM disk”,它通常也被简称为initrd。RAMdisk 提供了最初的文件系统,在操作系统启动时,它被用作根文件系统(/)。它会和内核一起被 BootLoader 预加载到 RAM (也就是内存,所以得名 RAM disk)中去,所以是可以直接访问而不需要经过任何特殊的驱动的。
这并不是一个 Linux 特性,我们知道,在其他一些 UNIX 系统中也使用它,其中最著名的当属iOS 了,尽管 iOS 中是把它和
kernel cache 并排放在ipsw 系统镜像里的。
传统上,initramfs 是用来提供内核操作时所需的相关设备的驱动程序的。在它的帮助下,Linux 发行版本才能提供一个通用的、相对紧凑的内核,并在最开始安装的过程中,把所需的驱动打包放进一个单独的文件中。
为了解开“先有鸡还是先有蛋”,即加载驱动先要能访问存储器,而要能访问存储器又先要有存储器的驱动这个死结,一些关键的必备驱动会被打包放在 intramfs 中。这样,内核就能直接在 RAM中访问到它们了。initramfs 中还含有一个启动程序/init,内核会把它作为系统中的第一个进程(PID=1)运行起来,用于完成一些需要在用户模式 (user mode) 下执行的初始启动操作,比如加载模块。
当RAM disk 的操作完成之后,Linux 通常都会丢弃掉它,以便使用磁盘上的文件系统,用一个通常被称为“pivoting root”的进程。
但在Android 中,initramfs 却还是会被保留在内存中用来提供根文件系统(/)。由于其中的文件会被经常访问,而且 RAM disk 本身所占的内存空间也非常的小,所以这样做还是很有用的。此外,这么做也使得修改根文件系统的难度变得更大了一些,因为 Boot 镜像是有数字签名保护的。
Linux支持两种文件格式的RAM disk–initrd (ext4文件系统镜像)和initramfs (CPIO文件)。后者更为常用,不过常常也会被称为 imitrd。CPIO 文档是一种只需占用很少内存空间的简单格式。为了进一步节省空间,该文档还会经过 gzip 压缩(内核是支持 zlib 的,因为内核要用它把自己解压出来)。
根据相关规定,内核在被压缩之后会和 RAM disk 一起放在一个单独的分区中。这背后有一个非常重要的设计思想,就是把两个东西打包在一起,只要用一个数字签名,就能同时保护这两个东西。
即,只要应用一次防篡改措施,就能保护两者都不会被篡改。内核当然是系统的关键组件,但 RAM disk 也是非常重要的,其中的/init 及其对应的/init.rc 文件控制着系统的启动过程。/init 会以 root 权限启动,负责启动所有其他的系统组件,只要修改一下/init.rc 文件就能获取设备的 root 访问权限,但只要搞不定数字签名,这一切就是水中花,井中月。
大多数ARM内核需要靠设备树 (device tree)文件向内核提供硬件设备定义的相关信息这个文件提供了设备相互连接的层次关系,使得内核能够据此启动相应的设备。
设备树文件一般会被附加在内核镜像的尾部,但有时也会为它专门分配一个分区。
设备树是一个二进制的 blob 文件,可以通过它的文件签名 0xd0dfeed 予以识别。
“Blob” 一词通常用来指代二进制大型对象(Binary Large Object),是一种存储二进制数据的数据类型。在计算机领域中,“blob” 可以表示任意类型的二进制数据,如图像、音频、视频等。这个术语通常用于描述数据库中存储的非结构化数据。
另外,在编程和计算机科学中,“blob” 也可能指代内存中的一块连续的二进制数据块,通常用来存储临时数据或需要以原始形式处理的数据。
因为厂商可以提供自己专门开发的分区刷写工具,所以,用哪种镜像文件格式存储它们完全可以由厂商自己说了算。不过由于大多数厂商使用的还都是 fastboot,所以他们很可能也就沿用了谷歌在自己的镜像上使用的 simg(sparseimage)格式。能处理这种文件格式的实用程序可以在AOSP的system/core/libsparse
目录下找到。
《最强Android书:架构大剖析》