在 IoT 中保证设备安全性的重要一环就是保证代码的完整性,不让恶意代码影响业务的正常逻辑。一般而言是及时修复现有攻击面所面临的漏洞,比如浏览器、蓝牙、调试接口;另一方面需要确保的是即便恶意代码获取了执行权限,也无法修改系统镜像进行持久化。针对这点所构造的安全方案通常称为 Secure Boot,对于不同的厂商,实现上可能会引入不同的名字,比如 Verified Boot、High Assurance Boot 等等,但本质上都是类似的。
Secure Boot 顾名思义就是安全启动,确保设备启动之后所加载执行的代码都是可信的。其中涉及的主要概念有两个:信任链和信任根。前者保障执行流程的可靠交接,后者则保障初始信任代码的可信。
可信启动的一个核心思路就是在当前启动代码加载下一级代码之前,对所加载的代码进行完整性校验,并且使用PKI公钥基础设施进行核实。这些启动代码通常可以分为若干个阶段(stage),例如在ARM中有:
实际上每个启动阶段还会进行细分,但这里的重点是需要清楚信任链的作用是在每一阶段代码加载执行下一阶段代码时都会进行验证。
信任链的作用是对下一阶段要执行的代码进行校验,那么就会回归到一个问题:最初的代码由谁来校验?其实上面有提到,最初的代码即BL1的代码,是保存在BootROM中,出厂烧写后不可修改的。因此BootROM代码需要尽可能简单,只需要进行必要的初始化操作。
这样一来,信任根就变成了可以烧写BootROM代码的芯片厂商。信任是可以传递的,芯片厂商作为信任根将代码执行权限交给下一级之后,比如OEM厂商,下级代码就拥有了信任链所有权,也就是说下级代码就变成了新的信任根。但是ROM的空间有限,所以通常还使用OTP(One-Time-Programmable)来保存不同阶段的签名信息。OTP是支持一次性编程的硬件,如多晶硅熔断器(poly-silicon fuses),烧毁之后无法恢复,从而保证写入后无法被篡改。
Android作为一个应用广泛的平台,同样支持Verified Boot。以硬件提供保护作为信任根,实现从bootloader到boot分区以及其他分区(system、vendor、oem等)完整信任链路,在每一步转交执行权限之前都会对数据和代码的完整性(integrity)和真实性(authenticity)进行验证。几个关键结点如下:
Verified Boot 2.0
,对分区尾部的数据格式进行格式化,并增加版本回滚保护的功能。这只是Android官方的feature时间结点,事实上许多OEM厂商也会针对启动功能进行自定义的配置,比如使用AOSP中的宏来设置或者在自己的私有bootloader中使用不同的校验方式。
参考:
A/B系统是在Android N中推出的一个新特性,主要目的是优化OTA升级的过程,实现无缝升级(seamless update)。
在使用A/B之前,系统的OTA升级过程如下:
下载更新包到cache
或者data
分区。系统校验更新包的证书/system/etc/security/otacerts.zip
,校验通过后提示用户可以进行升级。
用户点击升级后系统重启到recovery,并根据/cache/recovery/command
中的内容找到OTA包
recovery再次使用公钥/res/keys
校验签名
根据OTA包中的指令进行更新
值得一提的是,对开启了dm-verity校验的文件系统进行文件修改会导致校验失败,也就说在OTA之后设备将无法正常启动;为了解决这个问题需要将file-based OTA改为block-based OTA。
参考:
在非AB系统的升级过程中,如果升级失败,则系统无法继续正常启动,解决的唯一办法只有重新升级(线刷或卡刷)。AB系统正是为此而生的。在AB系统中,系统分为两套分区(slot
),通常是slot A
和slot B
。这样的好处是在OTA中无需修改当前slot,以便在升级失败后可以回退到正常可启动的分区。
每个slot都有下面三个属性:
升级过程可以分为以下几步:
update_verifier
执行dm-verity验证(在zygote之前),验证成功后将当前slot标记为successful,完成升级A/B系统带来的好处不言而喻,可直觉上对每个分区都分成两份似乎造成了很大的空间浪费。由于实施了A/B分区,也减少了一部分磁盘的开销,比如:
A/B中的boot分区也就相当于以前的recovery分区。以Pixel手机为例,Google给出的实际A/B额外开销只有300多MB,如下所示:
Pixel partition sizes | A/B | Non-A/B |
---|---|---|
Bootloader | 50*2 | 50 |
Boot | 32*2 | 32 |
Recovery | 0 | 32 |
Cache | 0 | 100 |
Radio | 70*2 | 70 |
Vendor | 300*2 | 300 |
System | 2048*2 | 4096 |
Total | 5000 | 4680 |
显然,A/B系统需要bootloader支持。通常bootloader对于各个Vendor而言是不同的,而且通常是闭源的。Google需要厂商实现HAL接口boot_control
以支持A/B系统,接口文件为:
同时也需要实现bootloader中AB升级相关的状态机:
值得一提的是,system-as-root并不是只有AB系统能用,非AB系统通过更新分区实现,见 https://source.android.com/devices/bootloader/system-as-root#about-system-as-root
参考:
在介绍信任链的时候我们说到,每次加载新的代码或数据之前都需要对其进行验证。对于比较小的分区,如boot或者dtbo,可以直接加载到内存并计算他们的hash,然后将其与预置的hash进行比对。预置的hash通常存放在对应分区文件的头部或者尾部,或者存放在独立的分区中。不论他们的位置在哪,都是会使用信任根进行直接或间接签名的。
但是对于较大的分区,比如system分区,实际上包含了整个文件系统,是无法全部读取到内存里的。这时就需要其他的方法,在Android中使用的是hash tree。当数据加载到内存时,系统就会计算该hash tree的root hash,并与预置的root hash进行比对验证。
在介绍dm-verity
之前有必要先了解其中的dm
,即Device Mapper
的作用。Device Mapper是Linux内核中提供的一个映射框架,可以方便用户程序通过ioctl自行创建和管理设备之间的映射。其中涉及到3个核心元素:
其中Mapped Device是创建的虚拟设备,通过Target Driver描述的映射关系,创建到Target Device的映射。Target Device可以是最终的物理设备,也可以是其他的Mapped Device,也就是说映射关系是可以级联的。
from: https://en.wikipedia.org/wiki/File:IO_stack_of_the_Linux_kernel.svg
BIO是对于块设备的基本IO操作单位,Device Mapper拓展的核心就是提供了BIO的具体映射,包括线性映射dm-linear
,测试映射dm-flakey
、dm-error
、dm-delay
以及加密映射dm-crypt
等等。
基于Device Mapper框架实现的应用有逻辑卷管理器LVM、软件阵列RAID以及Docker(COW)等。当然,我们所讨论的dm-verity
也是其中一个。
dm-verity
的代码在内核中为drivers/md/dm-verity.c
(以Linux4.4为例,在upstream中进行了重构),主要作用是用来验证文件系统中data block的完整性,主要验证的调用链路如下:
当block验证失败后,内核会根据v->mode
选择是打印错误(DM_VERITY_MODE_LOGGING)还是重启系统(DM_VERITY_MODE_RESTART)。
预置的root hash则是在mapped device的构造函数verity_ctr
中传入的,如下所示:
/*
* Target parameters:
* The current format is version 1.
* Vsn 0 is compatible with original Chromium OS releases.
*
*
*
*
*
*
*
*
* Hex string or "-" if no salt.
*/
static int verity_ctr(struct dm_target *ti, unsigned argc, char **argv)
{
// 处理arg[0-7] ...
v->root_digest = kmalloc(v->digest_size, GFP_KERNEL);
if (!v->root_digest) {
ti->error = "Cannot allocate root digest";
r = -ENOMEM;
goto bad;
}
if (strlen(argv[8]) != v->digest_size * 2 ||
hex2bin(v->root_digest, argv[8], v->digest_size)) {
ti->error = "Invalid root digest";
r = -EINVAL;
goto bad;
}
// ...
}
dm-verity将系统镜像切分为4k的块(block)大小,并对每一个块计算hash,这些hash以树状结构组合,称为hash tree,即哈希树。哈希树中的每个结点都是一个哈希,对于叶子结点值是对应块的hash,对于中间结点其值是所有子结点的hash。因此中间结点所包含的子节点(hash)容量也是一个块,和所采用的hash算法以及对应块大小有关。一个含有32768个块,块大小为4096字节且采用sha246 hash算法的哈希树结构示例如下:
alg = sha256, num_blocks = 32768, block_size = 4096
[ root ]
/ . . . \
[entry_0] [entry_1]
/ . . . \ . . . \
[entry_0_0] . . . [entry_0_127] . . . . [entry_1_127]
/ ... \ / . . . \ / \
blk_0 ... blk_127 blk_16256 blk_16383 blk_32640 . . . blk_32767
由此可见,任意一个块结点的修改都会导致其hash变化,从而导致root hash变化,因此验证hash tree的完整性可以通过验证root hash来实现。
在AOSP构建环境中,生成hash tree的工具为build_verity_tree
,代码在system/extras/verity/build_verity_tree.cpp
。
$ build_verity_tree -h
usage: build_verity_tree [ ] -s |
options:
-a,--salt-str= set salt to
-A,--salt-hex= set salt to
-h show this help
-s,--verity-size= print the size of the verity tree
-v, enable verbose logging
-S treat as a sparse file
输出verity哈希树镜像文件verity.img
,并在标准输出打印哈希树的root hash以及使用的salt。
Verity Table也称为dm-verity mapping table
,该映射表包含目标设备的位置、对应hash表的位置、hash tree的root hash值和salt等。其值是一个字符串,在AOSP中通过build_verity_metadata.py
脚本生成。
$ ./system/extras/verity/build_verity_metadata.py build -h
usage: build_verity_metadata.py build [-h] [--signer_args SIGNER_ARGS]
blocks metadata_image root_hash salt
block_device signer_path signing_key
positional arguments:
blocks data image blocks
metadata_image metadata image
root_hash root hash
salt salt
block_device block device
signer_path verity signer path
signing_key verity signing key
optional arguments:
-h, --help show this help message and exit
--signer_args SIGNER_ARGS
verity signer args
一个完整的verity table字符串以及每个字段的含义示例如下:
0 417792 verity 1 /dev/sdb /dev/sdc 4096 4096 52224 1 sha256 2aa4f7b7b6...f4952060e8 762307f4bc8...d2a6b7595d8..
| | | | | | | | | | | | |
start| | | data_dev | data_block | #blocks | hash_alg root_digest salt
size | version hash_dev | hash_offset
target hash_block
可以看到从version
开始后面的内容和传递到内核dm-verity
驱动构造函数中的参数是一致的,在上面有介绍到。每个参数的详细含义除了源码也可以参考内核的文档Documentation/device-mapper/verity.txt
。
Verified Boot中对于不同的磁盘镜像有不同的校验方式。比如对于较小的镜像,如boot、recovery,可以直接加载到内存中进行校验;而对于较大的镜像,比如system、vendor等,则无法一次性载入内存计算hash,因此需要借助dm-verity实现块级别的校验。下面分别介绍两种类型校验的实现。
在Android中基于dm-verity实现可信启动的步骤如下:
哈希树
dm-verity表
(即前面提到的verity table
字符串)这些操作可以用下图来表示:
前3步在上面的章节中已经有介绍了,第4步中对verity表签名使用的是RSA-2048
算法,公钥在最终烧写在目标机器的根目录下/verity_key
。值得一提的是,其私钥在AOSP中的build/target/product/security
目录里,该目录除了包括verity_key,还有testkey、platform、shared、media相关的key。不同之处是后面4个key使用development/tools/make_key
脚本生成,而verity可以用make_key
也可以用out/host/linux-x86/bin/generate_verity_key
生成(需要先编译make generate_verity_key
)。
# 生成verity_key.pub
generate_verity_key -convert verity.x509.pem verity_key
mv verity_key.pub verity_key
其中verity.x509.pem
是公钥的x509格式,转换的内部实现摘要如下:
// system/extras/verity/generate_verity_key.c
static int convert_x509(const char *pem_file, const char *key_file) {
f = fopen(pem_file, "r");
cert = PEM_read_X509(f, &cert, NULL, NULL);
pkey = X509_get_pubkey(cert);
rsa = EVP_PKEY_get1_RSA(pkey);
write_public_keyfile(rsa, key_file)
}
verity_key
包含RSA公钥信息,使用的是Android的特殊编码方法android_pubkey_encode
。
参考:
前面说过对于较大的磁盘镜像如system.img需要通过dm-verity在运行时访问磁盘block的过程中进行校验,但是对于较小的镜像,则可以直接加载到内存中进行校验。比如boot.img
和recovery.img
。对于二者的签名和校验可以通过boot_signer
工具完成:
$ boot_signer -verify out/target/product/angler/boot.img
Signature is VALID
# 相当于使用verity key进行校验
$ boot_signer -verify out/target/product/angler/boot.img -certificate build/target/product/security/verity.x509.pem
NOTE: verifying using public key from build/target/product/security/verity.x509.pem
Signature is VALID
boot_signer
是一个使用Java写的小工具,其实现代码为system/extras/verity/BootSignature.java
,从代码中可以看到boot.img的签名是保存在原镜像末尾的。
签名数据的格式使用ASN.1定义如下:
AndroidVerifiedBootSignature DEFINITIONS ::=
BEGIN
formatVersion ::= INTEGER
certificate ::= Certificate
algorithmIdentifier ::= SEQUENCE {
algorithm OBJECT IDENTIFIER,
parameters ANY DEFINED BY algorithm OPTIONAL
}
authenticatedAttributes ::= SEQUENCE {
target CHARACTER STRING,
length INTEGER
}
signature ::= OCTET STRING
END
在verified boot 1.0中,boot.img
的校验通过其头部的证书和签名信息进行验证。boot镜像中包括ramdisk文件系统,其中在根目录下就包含了校验其他镜像所用到的/verity_key
。boot.img由内核进行校验和挂载,内核负责启动其中的init进程。
系统启动后通过fs_mgr对system.img中的hashtree进行校验(root hash),获取verity table,使用其中的信息初始化dm-verity驱动并挂载镜像,从而实现运行时block级别的校验。
如下图所示:
对于AB系统则略有不同,因为boot.img中不再包含根文件系统(而是在system.img),因此使用内核中的keyring保存verity的公钥信息。而dm-verity驱动的参数则通过DTB进行保存,DTB可以保存在内核镜像之后,也可以作为独立的镜像文件保存。后续对于system分区的校验则是类似的。
注意这里是内核负责获取system镜像的metadata,提取dm-verity参数并初始化dm-verity驱动
AVB是安卓8.0之后对于Verified Boot的一个参考实现,也称为Verified Boot2.0。在AVB中一个重要的数据结构就是VBMeta,其中包括了一系列描述符和元信息,并且其中所有的信息都是签名的。描述符包括hash描述符(boot.img、dtbo.img等小镜像)以及hashtree描述符(system.img、vendor.img等大镜像)。除了签名,VBMeta中还包含版本信息以及Rollback Index,从涉及上就考虑了降级攻击的威胁。
在大部分AVB实现中,都有一个独立的vbmeta.im
镜像文件,这个文件格式的定义在 external/avb/libavb/avb_vbmeta_image.h 中,截取部分代码如下:
typedef struct AvbVBMetaImageHeader {
/* 0: Four bytes equal to "AVB0" (AVB_MAGIC). */
uint8_t magic[AVB_MAGIC_LEN];
/* 4: The major version of libavb required for this header. */
uint32_t required_libavb_version_major;
/* 8: The minor version of libavb required for this header. */
uint32_t required_libavb_version_minor;
/* 12: The size of the signature block. */
uint64_t authentication_data_block_size;
/* 20: The size of the auxiliary data block. */
uint64_t auxiliary_data_block_size;
/* 28: The verification algorithm used, see |AvbAlgorithmType| enum. */
uint32_t algorithm_type;
...
}
以Pixel 4XL最新的原厂固件为例进行分析,其中的vbmeta.img
文件如下:
$ head -c 200 vbmeta.img | xxd
00000000: 4156 4230 0000 0001 0000 0000 0000 0000 AVB0............
00000010: 0000 0240 0000 0000 0000 1000 0000 0002 ...@............
00000020: 0000 0000 0000 0000 0000 0000 0000 0020 ...............
00000030: 0000 0000 0000 0020 0000 0000 0000 0200 ....... ........
00000040: 0000 0000 0000 0be8 0000 0000 0000 0408 ................
00000050: 0000 0000 0000 0ff0 0000 0000 0000 0000 ................
00000060: 0000 0000 0000 0000 0000 0000 0000 0be8 ................
00000070: 0000 0000 5eb0 ac80 0000 0000 0000 0000 ....^...........
00000080: 6176 6274 6f6f 6c20 312e 312e 3000 0000 avbtool 1.1.0...
00000090: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000000a0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000000b0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000000c0: 0000 0000 0000 0000 ........
使用avbtool
工具可以查看镜像的详细信息:
$ avbtool info_image --image vbmeta.img
Minimum libavb version: 1.0
Header Block: 256 bytes
Authentication Block: 576 bytes
Auxiliary Block: 4096 bytes
Algorithm: SHA256_RSA4096
Rollback Index: 1588636800
Flags: 0
Release String: 'avbtool 1.1.0'
Descriptors:
Chain Partition descriptor:
Partition Name: vbmeta_system
Rollback Index Location: 1
Public key (sha1): 8c44014b96f0f41f3daa3825d4af410233372b65
Prop: com.android.build.product.fingerprint -> 'google/coral/coral:R/RPP4.200409.015/6455311:user/dev-keys'
Prop: com.android.build.product.os_version -> '10'
Prop: com.android.build.product.security_patch -> '2020-05-05'
Prop: com.android.build.vendor.fingerprint -> 'google/coral/coral:R/RPP4.200409.015/6455311:user/release-keys'
Prop: com.android.build.vendor.os_version -> '10'
Prop: com.android.build.vendor.security_patch -> '2020-05-05'
Prop: com.android.build.boot.fingerprint -> 'google/coral/coral:R/RPP4.200409.015/6455311:user/release-keys'
Prop: com.android.build.boot.os_version -> '10'
Prop: com.android.build.boot.security_patch -> '2020-05-05'
Prop: com.android.build.dtbo.fingerprint -> 'google/coral/coral:R/RPP4.200409.015/6455311:user/release-keys'
Hash descriptor:
Image Size: 34037760 bytes
Hash Algorithm: sha256
Partition Name: boot
Salt: a5d378a2cf0b56ad731bc531760791a68eda4c11903388d92a872c0610c011fb
Digest: f9968f0e26ede13f1e8769163e22a0cb53e747af8ea67915fc6a3ccc4b239744
Hash descriptor:
Image Size: 3330722 bytes
Hash Algorithm: sha256
Partition Name: dtbo
Salt: 7cfa6cab9ee5db15f872b86afc8767b4a440e8d3411096fe5effd3e9ccf3c35b
Digest: 215c5440b139eac525ece71f2decfca1c6b553906b188fad12adb2b3048edf3d
Hashtree descriptor:
Version of dm-verity: 1
Image Size: 1976545280 bytes
Tree Offset: 1976545280
Tree Size: 15568896 bytes
Data Block Size: 4096 bytes
Hash Block Size: 4096 bytes
FEC num roots: 2
FEC offset: 1992114176
FEC size: 15753216 bytes
Hash Algorithm: sha1
Partition Name: product
Salt: 9cc26ca7a5b14b40af3dc0afffd29748a5f56fec335ec3974572ce665d2cb22b
Root Digest: 7511feaf0fbc611576daac502cbfc328d931604f
Hashtree descriptor:
Version of dm-verity: 1
Image Size: 764170240 bytes
Tree Offset: 764170240
Tree Size: 6025216 bytes
Data Block Size: 4096 bytes
Hash Block Size: 4096 bytes
FEC num roots: 2
FEC offset: 770195456
FEC size: 6094848 bytes
Hash Algorithm: sha1
Partition Name: vendor
Salt: 9cc26ca7a5b14b40af3dc0afffd29748a5f56fec335ec3974572ce665d2cb22b
Root Digest: dfca98b6e4014f2fe6b3d12402368225c47783b1
从中可以看出vbmeta中包含了boot、dtbo镜像的签名,以及product、vendor镜像的hashtree签名。而system和system_ext的签名信息则保存在vbmeta_system.img
中。
在AVB2.0中,bootloader必须集成libavb
,负责处理hashtree描述符,并转换为dm-verity参数。通过将参数写入内核cmdline启动内核时初始化dm-verity驱动。/system
的hashtree描述符可以在/system
中,也可以在/vbmeta
中。
bootloader中集成的OEM公钥负责用来校验vbmeta和内核(即boot.img),随后vbmeta中的其他公钥用于校验对应的分区。
除了使用预置的公钥,新版的AOSP也支持设置用户信任根(user-settable root of trust):
avbtool extract_public_key --key key.pem --output pkmd.bin
fastboot flash avb_custom_key pkmd.bin
fastboot erase avb_custom_key
启动校验流程如下:
from: https://source.android.com/devices/tech/ota/images/ab-updates-state-machine.png
Secure Boot 是保障系统完整性和内部软件安全的一个重要屏障,本文主要针对Android智能设备的Secure Boot实现进行梳理和分析。Android Secure Boot实现主要有两个版本,一个是Verified Boot 1.0,另一个是Verified Boot 2.0,也称为AVB。前者主要通过在镜像末尾添加证书和签名等元信息,而后者将这些信息统一成VBMeta,并根据实现可置于独立分区中。由于两种不同的分区策略(AB和non-AB),两种Secure Boot的具体校验流程也略有不同,但整体大同小异。
虽然设计在理论上比较完善,但设备厂商的具体实现也可能存在缺陷,比如使用了错误的秘钥、eFuse不完全、或者bootloader中添加了隐藏的功能等等,这都将导致系统的完整性遭到破坏,从而影响产品的整体安全性。因此,设备厂商也应该遵循合理的安全开发流程,在发版之前由安全工程师进行审计或者使用自动化工具进行测试验证,使系统的信任根和信任链路得以充分安全实现。