1. 从一次刷机翻车说起:为什么AVB 2.0值得每个Android底层从业者吃透
去年帮一个做车机方案的朋友排查问题,设备刷完自定义镜像后卡在开机第一屏,串口日志里反复打印dm-verity校验失败,最后定位到根因是vbmeta.img里的哈希描述符和实际boot分区内容对不上。这个坑让我意识到,很多人对 Android Verified Boot(也就是 AVB 2.0)的理解停留在"刷机时多刷一个 vbmeta 分区"的层面,真出问题时完全不知道从哪下手。
AVB 2.0 是 Android 8.0 之后全面铺开的镜像校验与启动验证机制,核心产物就是vbmeta.img这个看起来只有几 KB 的小文件。它做的事情说起来简单:给boot、system、vendor、dtbo等关键分区建立一条从硬件信任根到系统分区的完整信任链,任何一环被篡改,启动就会中断或降级。但真正落地时,涉及avbtool的参数怎么配、描述符怎么生成、fstab里的avb标志怎么加、dm-verity和dm-linear怎么协同、--flags里那些位到底控制什么行为,每一步都有细节。
这篇内容适合三类人:一是做 Android 系统定制、ROM 移植的工程师;二是做车机、IoT 设备、平板方案,需要自己签名和打包镜像的开发者;三是想搞懂"为什么改了 system 分区设备就起不来"这个经典问题的进阶玩家。我会从vbmeta.img的生成讲起,一路拆到启动验证的完整链路,把avbtool的实操参数、描述符结构、常见报错和排查思路都摊开讲。文中所有命令和参数都基于 AOSP 里自带的avbtool,你可以直接对照自己的环境复现。
需要先说明一点:AVB 的完整信任链最终依赖硬件的信任根(比如熔断的 eFuse 或 TEE 里的公钥),这部分因芯片平台而异,本文聚焦在通用工具链和镜像生成这一层,硬件相关的部分只做原理性说明。
2. vbmeta.img 到底是什么:拆开这个几 KB 的小文件看结构
2.1 vbmeta 分区的物理布局与头部字段
vbmeta.img本质上是一个遵循 AVB 规范的元数据容器,它的头部结构定义在 AOSP 的avbtool源码里,字段是固定偏移的。理解这个布局,你才能在avbtool报错时知道它在抱怨哪一段。
| 偏移 | 字段 | 长度 | 说明 |
|---|---|---|---|
| 0 | magic | 4 字节 | 固定为AVB0,用于识别这是 AVB 元数据 |
| 4 | required_libavb_version_major | 4 字节 | 主版本号,AVB 2.0 为 1 |
| 8 | required_libavb_version_minor | 4 字节 | 次版本号 |
| 12 | authentication_data_block_size | 8 字节 | 认证数据块大小,含签名和公钥 |
| 20 | auxiliary_data_block_size | 8 字节 | 辅助数据块大小,含描述符 |
| 28 | algorithm_type | 4 字节 | 签名算法类型,如 SHA256_RSA2048 |
| 32 | hash_offset | 8 字节 | 哈希在认证块中的偏移 |
| 40 | hash_size | 8 字节 | 哈希长度 |
| 48 | signature_offset | 8 字节 | 签名偏移 |
| 56 | signature_size | 8 字节 | 签名长度 |
| 64 | public_key_offset | 8 字节 | 公钥偏移 |
| 72 | public_key_size | 8 字节 | 公钥长度 |
| 80 | public_key_metadata_offset | 8 字节 | 公钥元数据偏移 |
| 88 | public_key_metadata_size | 8 字节 | 公钥元数据长度 |
| 96 | descriptors_offset | 8 字节 | 描述符起始偏移 |
| 104 | descriptors_size | 8 字节 | 描述符总大小 |
| 112 | rollback_index | 8 字节 | 防回滚索引 |
| 120 | flags | 4 字节 | 全局标志位 |
| 124 | rollback_index_location | 4 字节 | 防回滚索引存储位置 |
| 128 | release_string | 48 字节 | 版本字符串,如avbtool 1.2.0 |
| 176 | reserved | 80 字节 | 保留字段 |
这个表你不需要背,但要知道两件事:第一,magic必须是AVB0,如果avbtool报 "Invalid magic" 说明文件根本不是 vbmeta 格式;第二,flags和rollback_index是控制启动行为的关键,后面会专门讲。
2.2 描述符:vbmeta 真正干活的单元
头部只是外壳,真正描述"哪个分区用什么方式校验"的是描述符(descriptor)。每个描述符有一个统一的头部,包含 tag、长度和跟随的 tag 特定数据。AVB 2.0 里常见的描述符类型有四种:
- Hash Descriptor(tag=1):对某个分区做整体哈希校验,适合
boot、dtbo这类小分区。它记录分区名、哈希算法、盐值和 digest。 - Hashtree Descriptor(tag=2):对某个分区建立哈希树,配合
dm-verity做块级校验,适合system、vendor这类大分区。它记录分区名、哈希算法、块大小、树高度、盐值、root digest 等。 - Kernel Cmdline Descriptor(tag=3):向内核命令行追加参数,比如
androidboot.vbmeta.device_state。 - Chain Partition Descriptor(tag=4):把另一个分区的 vbmeta 链接进来,实现链式验证,常见于
vbmeta_system、vbmeta_vendor这种拆分场景。
一个vbmeta.img里可以同时挂多个描述符,avbtool在生成时会按你传入的参数依次追加。理解这一点很重要:当你看到avbtool info_image输出里有一串 descriptor,每一个都对应一个被校验的分区,缺一个就可能导致那个分区"不受保护"。
2.3 认证块与辅助块:签名和描述符各占一块
vbmeta.img在头部之后分成两个块:认证数据块(authentication data block)和辅助数据块(auxiliary data block)。认证块里放的是公钥、公钥元数据、哈希和签名,这部分内容会被签名保护;辅助块里放的是描述符和可选的公钥元数据,它本身不被签名直接覆盖,但它的哈希会参与认证块的哈希计算。
这个设计的意义在于:描述符可以灵活增删,但任何改动都会导致认证块里的哈希变化,进而导致签名验证失败。所以攻击者没法偷偷往描述符里加东西而不被发现。你在调试时如果手动改过描述符,签名必然失效,必须重新用avbtool生成。
3. 用 avbtool 生成 vbmeta.img:参数逐个拆解
3.1 环境准备与 avbtool 的获取
avbtool是纯 Python 脚本,不依赖编译,直接从 AOSP 源码里拿就行。路径通常在external/avb/avbtool,你也可以从已编译的out/host/linux-x86/bin/avbtool拿到。它依赖 Python 3 和pycryptodome(用于 RSA 签名),装依赖的命令:
pip3 install pycryptodome验证工具可用:
python3 avbtool version # 输出类似:avbtool 1.2.0提示:不同 Android 版本自带的 avbtool 版本不同,生成的 vbmeta 头部
required_libavb_version字段会有差异。用高版本工具生成的镜像刷到低版本 bootloader 上可能被拒绝,建议用和目标平台 AOSP 版本一致的 avbtool。
3.2 生成密钥:RSA 还是 ECDSA,位数怎么选
签名密钥决定了信任链的根。avbtool支持 RSA 和 ECDSA 两类算法,常见组合如下:
| 算法参数 | 密钥长度 | 适用场景 | 签名速度 |
|---|---|---|---|
| SHA256_RSA2048 | 2048 位 | 最通用,兼容性最好 | 中等 |
| SHA256_RSA4096 | 4096 位 | 安全性要求高 | 较慢 |
| SHA256_RSA8192 | 8192 位 | 极少用,部分 bootloader 不支持 | 慢 |
| SHA512_RSA2048 | 2048 位 | 需要 SHA512 摘要 | 中等 |
| SHA256_ECDSA_NIST_P256 | 256 位 | 签名快,体积小 | 快 |
生成 RSA2048 密钥对:
openssl genrsa -out vbmeta_key.pem 2048 openssl rsa -in vbmeta_key.pem -pubout -out vbmeta_key_pub.pem生成 ECDSA P256 密钥对:
openssl ecparam -name prime256v1 -genkey -noout -out vbmeta_ec_key.pem openssl ec -in vbmeta_ec_key.pem -pubout -out vbmeta_ec_key_pub.pem选哪个?我的经验是:如果平台 bootloader 没有特殊要求,优先 RSA2048,兼容性最稳。ECDSA 虽然快,但部分老平台的 bootloader 实现不完整,容易在验证阶段报算法不支持。密钥一定要保管好,私钥泄露等于信任链彻底失效,任何人都能签出"合法"镜像。
3.3 生成带哈希描述符的 vbmeta:以 boot 分区为例
假设你已经有了boot.img,要给它生成一个哈希描述符并打包进 vbmeta:
python3 avbtool make_vbmeta_image \ --output vbmeta.img \ --key vbmeta_key.pem \ --algorithm SHA256_RSA2048 \ --padding_size 4096 \ --hash_algorithm sha256 \ --salt $(xxd -p -l 32 /dev/urandom | tr -d '\n') \ --include_descriptors_from_image boot.img \ --prop com.android.build.boot.fingerprint:my_device_boot \ --set_hashtree_disabled_flag这里几个参数值得展开:
--padding_size 4096:把 vbmeta 填充到 4096 字节对齐,方便分区刷写。不填的话文件可能只有几百字节,某些 flash 工具会报错。--salt:盐值,防止相同内容产生相同哈希,建议每次随机生成。注意--salt是全局盐,如果要对多个分区分别设盐,得用add_hash_descriptor子命令。--include_descriptors_from_image boot.img:从已有的 boot.img 里提取描述符。前提是 boot.img 本身已经用avbtool add_hash_descriptor处理过,否则这个参数会失败。--set_hashtree_disabled_flag:设置全局标志,禁用 hashtree 验证。这个标志要慎用,后面讲 flags 时会细说。
更常见的做法是分两步:先给 boot.img 加描述符,再生成 vbmeta。
# 第一步:给 boot.img 添加哈希描述符 python3 avbtool add_hash_descriptor \ --image boot.img \ --partition_name boot \ --partition_size $(stat -c%s boot.img) \ --hash_algorithm sha256 \ --salt $(xxd -p -l 32 /dev/urandom | tr -d '\n') # 第二步:从 boot.img 提取描述符生成 vbmeta python3 avbtool make_vbmeta_image \ --output vbmeta.img \ --key vbmeta_key.pem \ --algorithm SHA256_RSA2048 \ --padding_size 4096 \ --include_descriptors_from_image boot.img3.4 给 system 分区建哈希树:dm-verity 的根基
system、vendor这类大分区不能用整体哈希,因为校验时要读整个分区,太慢。AVB 用的是哈希树(hashtree),配合内核的dm-verity做块级按需校验。生成命令:
python3 avbtool add_hashtree_footer \ --image system.img \ --partition_name system \ --partition_size 2147483648 \ --hash_algorithm sha256 \ --salt $(xxd -p -l 32 /dev/urandom | tr -d '\n') \ --block_size 4096 \ --do_not_generate_fec关键参数说明:
--partition_size:分区实际大小,必须和分区表里一致,否则哈希树计算会错位。--block_size:块大小,通常 4096,要和文件系统块大小匹配。--do_not_generate_fec:不生成前向纠错(FEC)数据。FEC 能在数据损坏时尝试恢复,但会额外占用空间。量产设备一般开启 FEC,调试阶段可以关掉省空间。
执行完后,system.img末尾会追加哈希树和 AVB 尾部结构,同时镜像里嵌入了描述符。然后把这个 system.img 通过--include_descriptors_from_image挂到 vbmeta 上。
注意:
add_hashtree_footer会修改原镜像,务必在副本上操作,或者提前备份。我第一次操作时直接改了原始 system.img,结果哈希树追加进去后镜像大小变了,分区刷写直接失败。
3.5 链式分区:vbmeta_system 与 vbmeta_vendor 的拆分逻辑
Android 10 之后,Google 引入了动态分区和链式 vbmeta,把system、product、system_ext的验证信息放到vbmeta_system.img,vendor、odm放到vbmeta_vendor.img,主vbmeta.img通过链式描述符引用它们。
生成链式描述符:
python3 avbtool make_vbmeta_image \ --output vbmeta_system.img \ --key vbmeta_key.pem \ --algorithm SHA256_RSA2048 \ --padding_size 4096 \ --include_descriptors_from_image system.img \ --include_descriptors_from_image system_ext.img \ --include_descriptors_from_image product.img python3 avbtool make_vbmeta_image \ --output vbmeta.img \ --key vbmeta_key.pem \ --algorithm SHA256_RSA2048 \ --padding_size 4096 \ --chain_partition vbmeta_system:1:vbmeta_system_pub.pem \ --chain_partition vbmeta_vendor:2:vbmeta_vendor_pub.pem--chain_partition的格式是分区名:rollback_index位置:公钥文件。这里的公钥是子 vbmeta 的签名公钥,主 vbmeta 用它来验证子 vbmeta 的签名。链式结构的好处是各分区可以独立更新,不用每次重签整个 vbmeta。
4. 启动验证链路:从 bootloader 到 dm-verity 的完整走查
4.1 bootloader 阶段:验证 vbmeta 的签名
设备上电后,bootloader(或更底层的 BL1/BL2)首先从vbmeta分区读取元数据,用烧录在设备里的公钥(通常在 eFuse 或 RPMB 里)验证 vbmeta 的签名。这一步验证的是认证块,包括公钥、哈希和签名。
验证通过后,bootloader 解析描述符,对每个被描述的分区做校验:
- 对哈希描述符的分区(如
boot),计算整个分区的哈希,和描述符里的 digest 比对。 - 对哈希树描述符的分区(如
system),读取哈希树的 root digest,和描述符里的比对,具体的块级校验留给内核的dm-verity。
如果任何一步失败,根据flags的设置,设备可能直接停止启动、进入 recovery,或者显示警告后继续(仅限解锁状态)。
4.2 flags 字段:控制验证失败后的行为
vbmeta头部的flags字段和描述符里的 flags 共同决定验证失败时的行为。常见的标志位:
| 标志名 | 值 | 含义 |
|---|---|---|
| HASHTREE_DISABLED | 1 | 禁用 hashtree 验证,仅对哈希描述符生效 |
| VERIFICATION_DISABLED | 2 | 完全禁用验证,设备视为已解锁 |
| VERIFICATION_DISABLED_UNTIL_REBOOT | 4 | 本次启动禁用验证,重启后恢复 |
| VERIFICATION_DISABLED_THIS_BOOT | 8 | 仅本次启动禁用 |
--set_hashtree_disabled_flag对应 HASHTREE_DISABLED,--set_verification_disabled_flag对应 VERIFICATION_DISABLED。量产设备绝对不能设 VERIFICATION_DISABLED,否则等于关掉了整个安全启动。调试阶段如果只是想临时跳过验证,用fastboot --disable-verification flash vbmeta vbmeta.img更合适,它不会改镜像本身。
4.3 内核阶段:dm-verity 如何按需校验 system 分区
bootloader 验证完 vbmeta 后,把哈希树的 root digest、盐值、块大小等信息通过内核命令行传给内核,格式类似:
androidboot.vbmeta.device=PARTUUID=xxx androidboot.vbmeta.avb_version=1.2 androidboot.vbmeta.hash_alg=sha256 androidboot.vbmeta.size=6144 androidboot.vbmeta.digest=xxxxx内核启动时,dm-verity驱动根据这些参数建立虚拟块设备。当系统读取system分区的某个块时,dm-verity会沿着哈希树逐层校验,直到 root digest 匹配。任何一块数据被篡改,读取就会返回 I/O 错误,表现为应用崩溃或系统服务异常。
这里有个容易混淆的点:dm-verity是按需校验,不是启动时全量校验。所以篡改 system 分区后,设备可能能启动,但访问到被篡改的块时才会报错。这也是为什么有些改机行为能"骗过"启动,但一用就崩。
4.4 fstab 里的 avb 标志:别漏了这一行
dm-verity要生效,fstab里对应的分区条目必须带avb标志。以system分区为例:
/dev/block/bootdevice/by-name/system /system ext4 ro,barrier=1 wait,avb=vbmeta,avb_keys=/avb/q-gsi.avbpubkeyavb=vbmeta表示这个分区的验证信息来自vbmeta分区,avb_keys指定额外的公钥路径(用于 GSI 等场景)。如果漏了avb标志,即使 vbmeta 里有描述符,dm-verity也不会挂载,分区等于没保护。
提示:Android 10 之后 fstab 通常在
vendor分区的etc/fstab或first_stage_ramdisk里,动态分区场景下路径会变。排查时用adb shell cat /proc/mounts看实际挂载参数,比翻源码快。
5. 踩坑实录:那些年我在 AVB 上翻过的车
5.1 哈希不匹配:从报错日志反推问题
最常见的报错是avbtool verify_image输出Hash mismatch。排查链路是这样的:
- 先确认镜像有没有被二次修改。任何对
boot.img的改动(哪怕改一个字节)都会导致哈希变化。 - 检查
--salt是否一致。生成描述符和验证时用的盐必须相同,盐不同哈希必然不同。 - 检查
--partition_size。如果实际分区大小和描述符里记录的不一致,哈希树计算会错位。 - 用
avbtool info_image --image vbmeta.img看描述符里的 digest,和avbtool calculate_vbmeta_digest算出来的对比。
我遇到过一次,是因为构建脚本里boot.img被mkbootimg重新打包了一次,时间戳变了,哈希自然对不上。解决办法是确保签名和打包的顺序:先打包,再签名,签完不要再动。
5.2 分区大小对不上:padding 和 partition_size 的坑
--partition_size必须和分区表里的实际大小严格一致。如果分区表里system是 2GB,你传了 2147483648(正好 2GB),但实际镜像加上哈希树后超过了这个值,avbtool会报Image size exceeds partition size。
解决办法有两个:一是调大分区表里的分区大小;二是用--do_not_generate_fec省掉 FEC 空间。FEC 通常占分区大小的 1% 到 2%,大分区上这个空间不小。
另一个坑是--padding_size。vbmeta 本身很小,但某些平台的 bootloader 要求 vbmeta 分区至少 4096 字节,不 padding 会报Invalid vbmeta size。这个参数加上就完事,成本极低。
5.3 链式验证失败:公钥和 rollback_index 的对应关系
链式 vbmeta 报错通常是Chain partition verification failed。排查要点:
--chain_partition里指定的公钥必须是子 vbmeta 的签名公钥,不是私钥,也不是主 vbmeta 的公钥。- rollback_index 位置要唯一。主 vbmeta 里给
vbmeta_system分配了位置 1,给vbmeta_vendor分配了位置 2,子 vbmeta 生成时要用--rollback_index_location指定对应的位置,否则防回滚机制会错乱。 - 子 vbmeta 的
--algorithm要和主 vbmeta 验证时用的算法兼容。混用 RSA 和 ECDSA 在某些平台上会失败。
5.4 解锁状态与验证降级:userdebug 和 user 的差异
userdebug版本默认允许adb root和dm-verity降级,user版本则严格验证。如果你在userdebug上测试通过,切到user后启动失败,大概率是某个分区的描述符缺失或签名不对。
判断当前设备验证状态:
adb shell getprop ro.boot.verifiedbootstate # green:验证通过 # yellow:验证通过但用了自定义密钥 # orange:验证被禁用(解锁状态) # red:验证失败ro.boot.veritymode则反映dm-verity的状态,enforcing表示严格校验,disabled表示关闭。这两个属性是排查 AVB 问题的第一手信息。
6. 几个容易被忽略的实操细节
6.1 公钥的嵌入方式:vbmeta 内嵌 vs 设备烧录
avbtool生成的 vbmeta 里内嵌了公钥,但 bootloader 验证时用的是设备里烧录的公钥,两者必须匹配。设备公钥的烧录方式因平台而异,有的通过fastboot flash写入特定分区,有的在产线用专用工具熔断。调试阶段如果换了密钥,记得同步更新设备里的公钥,否则验证必然失败。
6.2 动态分区与 AVB 的配合
Android 10 引入动态分区后,super分区里包含多个逻辑分区,AVB 的描述符要针对逻辑分区而不是super整体。生成时用--partition_name指定逻辑分区名,--partition_size用逻辑分区大小。super分区本身的元数据由liblp管理,和 AVB 是两套机制,别混在一起。
6.3 验证工具链:verify_image 和 info_image 的日常用法
日常调试最常用的两个命令:
# 查看 vbmeta 的完整信息,包括所有描述符 python3 avbtool info_image --image vbmeta.img # 验证镜像的哈希是否和描述符一致 python3 avbtool verify_image --image boot.img --key vbmeta_key_pub.peminfo_image的输出里,重点看Descriptors段落,每个描述符的Partition Name、Hash Algorithm、Salt、Digest都要和预期一致。verify_image则直接告诉你通过还是失败,失败时会指出是哪一步不匹配。
6.4 一个真实案例:车机项目里的 vbmeta 拆分
回到开头那个车机项目。设备用的是 Android 11,动态分区,system和vendor分别在vbmeta_system和vbmeta_vendor里。问题出在vbmeta_vendor的 rollback_index_location 和主 vbmeta 里分配的位置不一致,导致链式验证时读到的防回滚索引是错的,验证直接失败。
修复方法是在生成vbmeta_vendor.img时显式指定:
python3 avbtool make_vbmeta_image \ --output vbmeta_vendor.img \ --key vbmeta_key.pem \ --algorithm SHA256_RSA2048 \ --rollback_index_location 2 \ --include_descriptors_from_image vendor.img同时在主 vbmeta 里用--chain_partition vbmeta_vendor:2:vbmeta_vendor_pub.pem对应上。改完后设备正常启动,ro.boot.verifiedbootstate显示green。
这个案例说明一个道理:AVB 的每个参数都不是孤立的,rollback_index、公钥、算法、分区名,任何一处对不上都会导致验证失败。排查时不要只盯着报错的那一行,要把整条链路的参数都过一遍。
6.5 关于防回滚:rollback_index 的实际作用
rollback_index是防降级攻击的机制。设备里存储了一个当前的最小允许索引,如果 vbmeta 里的索引低于这个值,验证失败。这样攻击者就没法刷入旧版本有漏洞的镜像。
实际使用中,rollback_index的更新要谨慎。一旦设备里的索引被提升,就再也降不回去(除非解锁并清除)。量产设备升级时,新镜像的rollback_index要大于等于当前值,否则升级会失败。调试阶段可以先用 0,量产前再规划好版本策略。
6.6 签名性能:大分区哈希树生成的时间成本
给 2GB 的system分区生成哈希树,在普通开发机上大概要几十秒到一两分钟,取决于磁盘 I/O 和 CPU。如果构建流程里每次都要重新生成,会显著拖慢迭代速度。我的做法是把哈希树生成和签名拆成独立步骤,只在镜像内容真正变化时才重新生成,日常调试用--do_not_generate_fec进一步提速。
另外,avbtool是单线程的,大分区上 CPU 利用率不高,瓶颈通常在磁盘读写。用 SSD 能明显改善,机械盘上生成 4GB 分区的哈希树可能要几分钟。
6.7 与 GSI 的兼容:avb_keys 的用途
刷 GSI(通用系统镜像)时,GSI 的签名密钥和设备厂商的密钥不同。为了让设备能启动 GSI,fstab里要加avb_keys指向 GSI 的公钥,同时 vbmeta 里要允许验证降级。这也是为什么刷 GSI 通常需要先解锁 bootloader。量产设备一般不支持 GSI,因为解锁会破坏安全启动的信任链。
7. 写在最后:AVB 调试的几条个人经验
搞 AVB 这几年,最大的体会是:报错信息往往只是表象,真正的问题在参数配置的某个角落。Hash mismatch可能是盐值不对,也可能是镜像被改过;Chain partition verification failed可能是公钥不匹配,也可能是 rollback_index 位置错了。排查时不要急着改代码,先把avbtool info_image的输出完整看一遍,把每个描述符的参数和预期对照,问题通常就浮出来了。
另一个经验是:密钥和参数要版本化管理。签名密钥、盐值、rollback_index、分区大小,这些都应该记录在构建配置里,而不是散落在各个脚本中。我见过太多项目因为换了密钥没同步更新设备公钥,导致整批设备启动失败。把 AVB 相关的配置集中管理,能省掉大量返工。
最后,调试阶段多用userdebug版本,它允许adb root和验证降级,排查效率高很多。但切记,量产前一定要在user版本上完整验证一遍,确保所有分区的描述符齐全、签名正确、fstab里的avb标志没漏。这一步偷懒,后面就是批量返修。