2026/11/2 5:41:50

Android OTA payload.bin 解包与分区镜像提取实战

Android OTA payload.bin 解包与分区镜像提取实战 做 Android 固件分析这行手里拿到一个 OTA 全量包之后最想干的事就是把它拆成一个个单独的 img 文件system 里看预装应用vendor 里翻驱动配置boot 里查内核版本顺带确认某个安全补丁有没有真的打进去。可真把 zip 解开来往往只剩一个几百兆到两三个 G 的 payload.bin外加两个记着哈希的文本文件——没有传统意义上的 META-INF 脚本也没有按分区切好的镜像目录。payload-dumper-go 就是用来解决这一步的它读取 payload.bin 里的 protobuf 元数据按记录解压出各个分区镜像输出成可以直接挂载或继续解析的 img 文件。相比早年那套 Python 脚本它单文件、免依赖、多线程是我目前处理 Android OTA 更新包的主力工具。这篇内容主要写给两类人一类是刚接触 A/B 分区、想搞清楚 payload.bin 究竟是什么结构的入门读者另一类是已经会跑命令、但被内存报警和校验失败折腾过、想弄明白参数怎么调的老手。1. 为什么我会盯上 payload.bin 这个文件1.1 从一次解包失败的深夜说起几年前我第一次拿到一个 A/B 机型的全量 OTA 包习惯性地unzip展开期待看到熟悉的system.img、boot.img目录结构。结果出来的是payload.bin、payload_properties.txt、care_map.pb、metadata这么几样东西最大的那个文件体积几乎等于整个包。当时我以为是压缩包套压缩包又解了一遍当然没戏——payload.bin 根本不是归档文件它是一份带索引的数据流。那次折腾让我意识到一个关键事实Android 从 A 分区方案转向 A/B 无缝更新之后OTA 包的物理组织方式彻底变了。旧的块式 OTA 是把镜像和更新脚本一起塞进 zip设备端在 recovery 里跑脚本新的流式更新则只传“变化的数据块 一份描述怎么摆放的清单”由设备上的更新引擎在后台把数据写进空闲的 B 槽。这份“清单 数据”被打包成单文件就是 payload.bin。因此想拿到可用镜像不能靠解压得靠“解释”——读懂清单按清单把数据块搬到正确位置。payload-dumper-go 干的正是这件事。理解了这层后面所有参数、报错、校验逻辑都能串起来。不知道这个前提的人很容易在“为什么解压出来是乱码”“为什么不能直接 mount payload.bin”这类问题上卡很久。1.2 A/B 分区机制决定了数据长什么样A/B 方案的核心思路是“用空间换可靠性”设备上有两套完整分区A 槽和 B 槽系统运行在 A 槽时后台可以把更新写进 B 槽写完切换启动标志重启即生效出错还能回滚。这个设计直接影响了 OTA 包的数据形态。传统块式 OTA 的更新粒度是“块”但整个流程依赖 recovery 环境里的一堆脚本中间任何一步失败都可能变砖。流式更新的做法更彻底它不传输文件系统只传输分区上的字节块并且这些块可以是不连续的。为什么允许不连续因为一次系统更新真正变化的可能只是 system 分区里几段 SoC 驱动、几个预装应用和若干配置文件其余几十万个块根本没动。把这些真正变化的块单独打包配合一份“目标偏移 长度”的记录包体积能小一大截。这就解释了 payload.bin 里为什么会有 “extent” 这个概念——一条操作记录不是简单说“从偏移 X 复制 N 字节”而是说“把这批数据分散写到 [start_block120, num_blocks8]、[start_block900, num_blocks3] 这些位置”。理解 extent 是理解整个解包过程的关键也是后面判断“为什么提取出来的 img 是稀疏文件”的依据。1.3 payload-dumper-go 相比老方案的实际优势早年的做法是跑一套 Python 脚本依赖 protobuf 运行时、若干解压库还得自己处理多进程。我在不同机器上装过好几轮Python 版本、protobuf 版本、brotli 库编译每换一台机器就要重新踩一遍。payload-dumper-go 用 Go 写成交叉编译后是单个静态二进制丢到任何一台有对应架构的机器上就能跑这点在临时借用的机器上尤其省事。性能上的差距也很直观。Python 脚本的并行通常是按分区切进程单个大分区比如 system内部还是串行解压遇到两三 G 的分区就成了单核跑满、其他核心闲着。payload-dumper-go 的并行粒度更细多个分区、多个数据块可以同时处理在我的四核小机器上同一份 OTA 包的处理时间从二十多分钟压到六七分钟差别主要来自 CPU 利用率和减少了中间落盘。另外它自带分区列举和哈希校验出问题的时候能较快定位到底是包本身有问题还是磁盘写入有问题。对做固件分析的人来说省下的不是命令敲击次数而是“怀疑人生”的时间。2. 拆开 payload.bin 的内部结构2.1 文件头里藏着什么从字节层面看payload.bin 的开头很规整先是 4 字节魔数CrAU接着 8 字节的大端版本号常见是 2代表支持多分区操作然后是 8 字节的大端元数据长度紧跟其后的就是元数据本体最后才是数据区。这个布局挺好记——头、索引、数据三段式。元数据本体是 protobuf 序列化后的DeltaArchiveManifest结构。用十六进制编辑器看会是一片二进制但用工具列出来就很清晰里面记录着块大小通常是 4096、签名偏移与长度、以及一个分区列表。每个分区条目包含分区名、新镜像的总大小与整体哈希、以及一组按顺序执行的操作。数据区则是被这些操作引用的原始字节流每条操作通过data_offset和data_length定位到自己的那一截。我习惯在做任何提取之前先确认两件事魔数对不对版本号是多少。如果魔数不是CrAU说明这个文件要么被截断要么根本不是 payload有些机型在 zip 里还会塞别的格式。如果版本号异常后面的解析大概率也会出问题早点停下比跑到一半报错要好。2.2 分区记录与层次关系把元数据摊开来看层次是这样的层级字段作用清单block_size全局块大小绝大多数是 4096清单partitions分区更新列表分区partition_name分区名如 boot、system、vendor分区new_partition_info目标镜像大小与整镜像哈希分区operations有序操作序列逐条执行操作type操作类型决定数据怎么解操作data_offset / data_length在数据区中的位置操作dst_extents写到目标镜像的哪些块区间操作data_sha256_hash该段原始数据的哈希这套结构的设计意图很清楚让更新引擎可以边读边写、不必把整个分区读进内存也让校验可以按段做而不是等全部写完。对解包工具来说这意味着它同样可以流式处理——读一段、解一段、写一段内存占用和数据量解耦。这也是为什么解一个三 G 的分区峰值内存只有几百兆的原因。2.3 六种常见的操作类型操作类型决定了这一段数据要怎么“还原”我按遇到频率排序REPLACE数据区里躺着的就是目标块的原始内容直接搬过去。boot、dtbo、vendor_boot 这些本身已经是压缩或加密容器的分区基本都是这种因为再压一遍没收益。REPLACE_XZ数据是 xz 压缩流解压后落到目标块。system、product 这类文本和代码密集的分区主力用它压缩比通常在三到五倍。REPLACE_BZbzip2 压缩流现在少见老包里有。ZERO不占数据区目标块全部填零用来表达“这块被清空了”。DISCARD语义上和 ZERO 接近表示这些块可以丢弃工具处理时一般也写成零。BROTLI_BSDIFF / PUFFDIFF / SOURCE_COPY / SOURCE_BSDIFF这些都是增量更新用的需要一个“旧镜像”作为基准才能还原。全量包里不会出现一旦看到说明你手上的是增量包。最后这一条特别值得强调很多人拿到增量包直接喂给工具跑到某个分区报错以为是工具有 bug。其实不是增量包的设计前提就是设备上存在旧版本分区工具手上没有底包自然无法还原。判断方法很简单先列一下分区看操作类型里有没有SOURCE_或BSDIFF字样有就说明方向错了。2.4 全量包与增量包的分水岭全量包和增量包的区别不只是体积。全量包包体积通常在一到三个 G官方渠道下载的完整固件基本都是这种增量包往往只有几百兆甚至更小因为它只装差异。对解包来说这个差别的实际后果是全量包的每条操作都是自包含的给定 payload.bin 一个文件就能还原所有分区增量包的每条操作依赖旧分区对应位置的数据没有底包就是无源之水。有些工具支持传入旧镜像目录来满足依赖但实际使用中成功率不高因为版本必须严格匹配差一个补丁号就可能对不上。我的做法是要做分析就找全量包别在增量包上耗时间。找全量包的方法通常是去官方固件下载渠道翻完整包或者用设备上的更新缓存——有些设备在下载完整更新后会保留一份完整的包。判断手上的包是全量还是增量除了看操作类型还可以看分区记录里有没有old_partition_info字段有就是增量。这一步花十秒能省掉后面半小时的困惑。3. 环境准备与工具获取3.1 平台选择与依赖情况payload-dumper-go 是静态编译的 Go 程序理论上不依赖系统库但这不意味着随便哪个环境都一样顺手。我试过三种环境各有取舍Linux 桌面环境是我最推荐的。原因不是工具本身而是后续处理链条完整——unzip、sha256sum、mount -o loop、simg2img、debugfs这些系统里现成就有提取完直接进入下一步分析不用来回倒腾文件。而且稀疏文件的支持成熟提取出来的镜像不会莫名其妙撑爆磁盘。Windows 环境能跑但会碰上稀疏文件和路径分隔符的小问题需要较新的文件系统支持杀毒软件有时也会对单个几 G 的输出文件做实时扫描拖慢写入速度。真要在 Windows 上做建议把输出目录排除在实时扫描之外能明显快一截。容器或轻量环境适合批处理。把工具和脚本一起打进去一次性处理一批包输出挂到外部卷上干净利落。唯一要注意的是容器默认的内存限制并行度开太高会被 OOM 直接杀掉表现出来就是“进程莫名其妙消失”很容易误判成工具崩溃。依赖方面只需要确认一件事目标机器上有没有payload.bin的读取权限和足够的磁盘空间。除此之外没有运行时依赖这也是它相比 Python 方案最舒服的地方。3.2 获取与验证二进制获取渠道是项目自己的发布页下载对应架构的压缩包解开就是一个可执行文件。步骤本身没什么好说的值得说的是验证环节这一步很多人跳过但我建议保留。下载完成后先看文件大小是否合理再确认它能不能正常运行直接跑./payload-dumper-go -h能打出用法说明就说明架构匹配、动态库没问题。如果报“无法执行二进制文件”九成是架构下错了比如在 ARM 机器上拿了 x86 的包。然后把它放到一个固定的目录并加进环境变量路径省得每次都要敲完整路径。我习惯在~/.local/bin下放一份脚本里直接调名字换机器时复制这个目录就行。注意不要把工具和待处理的数据放在同一个临时目录里尤其是用脚本批量处理时。临时目录被清理脚本扫掉、工具没了的场面我见过不止一次重新配一遍很费神。3.3 磁盘与内存的预估方法这是我觉得最该提前算、又最容易被忽略的一步。磁盘空间的估算逻辑是这样的先看 payload.bin 的实际大小。全量包解压后的总体积通常是它的一点五到二点五倍具体倍数取决于压缩占比。如果包里以 REPLACE_XZ 为主膨胀倍数偏向二以上如果 boot、vendor_boot、dtbo 这些未压缩分区占比高倍数就低。保守一点按二点五倍准备再加上百分之二十的余量。举个实际的例子一个二点三 G 的 payload.bin解出来所有分区镜像加起来大约五点五 G我一般会预留七 G 空间。这个数字看着夸张但真到写盘阶段发现空间不够前面的解压全白做重来一遍的时间成本更高。内存的估算更粗略一些因为它和并行度直接挂钩。工具是流式处理的单个任务的内存占用主要由“一次读入的数据块大小”决定乘以并行任务数就是大致峰值。经验值是每路并行三五十兆开八路大概三五百兆。这个数量级在普通机器上完全不是问题真正会出事的是在容器里跑、内存上限设得很紧的场景。提示先用小包试参数是个好习惯。拿一个体积较小的包跑一次观察峰值内存和耗时再决定大包用什么并行度比直接上大包试错要稳妥。4. 完整实操从 OTA 包到可用镜像4.1 第一步是只读校验不是解压拿到 OTA 包之后我的第一件事永远不是解压而是校验。zip 文件本身可以校验完整性unzip -t会逐条检查 CRC这一步能提前发现下载不完整、传输损坏的问题。如果 CRC 有错后面做什么都是白费。unzip -t update.zip | tail -5确认无误后再解出需要的几个文件。注意不要整包解压到当前目录OTA 包动辄两三个 G解出来一地文件很难收拾。用通配符只取需要的那几样mkdir -p ota_pkg unzip -o update.zip payload.bin payload_properties.txt -d ota_pkgpayload.bin在 zip 里通常是以存储方式不压缩放的所以这一步很快不会真的耗时。解出来后立刻做一次哈希校验对照payload_properties.txt里的FILE_HASH和FILE_SIZE。这里有个坑payload_properties.txt里的哈希是 base64 编码的不是常见的十六进制字符串直接和sha256sum的输出对比是对不上的。需要先转换cat ota_pkg/payload_properties.txt # 取出 FILE_HASH 的值做个 base64 解码转成十六进制 echo FILE_HASH的值 | base64 -d | xxd -p -c 64 sha256sum ota_pkg/payload.bin两串十六进制对上说明文件完好。这一步花的时间以分钟计但能挡掉后面所有“到底是包坏了还是工具有问题”的扯皮。同一个文件里还有METADATA_HASH和METADATA_SIZE它们描述的是元数据部分的哈希工具解析时会自己核对你不需要手工算。4.2 先列清单别急着提取确认包没问题后下一步是列出分区。这个操作几乎是瞬时的但它决定了后面的所有决策./payload-dumper-go -l ota_pkg/payload.bin输出会是一个分区列表包含分区名和大小。我一般会看三件事第一总大小是否和预期相符第二有没有我不认识的分区名新机型常有新增分区第三各分区体积分布心里对磁盘占用有个底。看完列表第二步是挑出真正需要的分区。做应用分析的人要system、product、system_ext看内核和启动参数的人要boot、vendor_boot、dtbo查驱动的人要vendor。没必要每次都全量提取尤其是磁盘紧张的时候。我在一台剩余空间只有十来个 G 的机器上试过全量提取一个大包确实成功了但后面操作空间很局促体验不好。4.3 全量提取与并行度调优全量提取的命令很简单指定输出目录和输入文件即可./payload-dumper-go -o extracted -p 4 ota_pkg/payload.bin-o是输出目录-p是并行线程数。这两个参数里-p值得单独说说因为它直接影响时间和稳定性的平衡。并行度的选择逻辑不是“越大越快”。解压是 CPU 密集任务理论上线程数接近物理核心数时效率最高。超过物理核心数之后收益迅速衰减因为线程调度和内存带宽成了新瓶颈。我在四核八线程的机器上做过对比-p 4用时七分钟左右-p 8六分半-p 16反而回到七分多而且内存峰值翻了一倍。所以我的经验规则是物理核心数以内放心开超线程核心数可以开但别指望明显提速纯粹的逻辑核心数以上基本是浪费甚至倒退。在容器或共享机器上还要考虑别人也在用 CPU这时候保守一点更合适。注意如果你的机器是笔记本并且在用电池长时间满核解压会让频率掉得很快实际耗时可能比预想的长。插电源再跑别问我怎么知道的。4.4 只提取指定分区磁盘紧张或只想看某个分区时用分区清单参数限定范围。不同版本的参数语义可能略有差别先跑一次帮助确认最稳妥./payload-dumper-go -h按常见用法指定分区是这样./payload-dumper-go -o extracted -p 4 -c boot,system ota_pkg/payload.bin这里要提醒一句不同发布版本的参数名和语义有过调整有的版本里这个参数是“只处理列表内的分区”有的版本里它更偏向“校验列表内的分区”。所以第一次用某个版本时别凭空假设先用一个小的分区试跑一次看输出目录里实际生成了什么确认行为之后再处理大分区。这个习惯能避免很多“参数写对了但结果不对”的困惑。只提取单个分区还有个隐性好处校验变快了。因为只处理一部分数据出问题的时候排查范围小容易定位是哪个分区、哪一段数据有问题。4.5 校验输出别跳过这一步提取完成之后很多人直接进入分析环节结果发现挂载失败、镜像损坏还得回头重来。我的习惯是提取完立刻做一轮校验分两层第一层是工具自己的校验。运行时它会把每条操作的data_sha256_hash和实际解出来的数据比对不一致会报错。看到“hash mismatch”之类的提示基本可以确定包损坏或工具读取有问题这时候不要再往下走。第二层是整镜像校验。元数据里记录了每个分区目标镜像的整体哈希可以手工核对sha256sum extracted/system.img注意这里的细节如果提取出来的镜像是稀疏文件用du看会显得很小但ls -lh和sha256sum看到的都是完整逻辑大小。这是因为镜像里有大量未分配的区间对应 ZERO 和 DISCARD 操作文件系统只记录“这里全是零”而不实际占盘。这个现象是正常的不要以为提取不全。想确认实际占用用du -h --apparent-size对比一下。4.6 提取之后镜像怎么用起来拿到 img 之后做什么取决于你的目的。这里几条常见路径我都走过看文件系统内容。现在不少分区用的是只读压缩文件系统不是传统 ext4。判断方法是看魔数ext4 的特征在偏移 0x438 附近有53 EF如果是新的只读文件系统魔数在开头几个字节。ext4 镜像可以mount -o ro,loop也可以在没法挂载的环境里用debugfs -R ls -l /直接读目录后者不需要 root 权限很实用。只读文件系统镜像则要用对应的解包工具把它展开成目录树。看 boot 镜像。boot.img 有自己的头部格式魔数一般是ANDROID!。解析它可以拿到内核、ramdisk 和命令行参数命令行里的启动参数、SELinux 状态、分区表信息都很有价值。有专门的镜像解包工具可以一步展开也可以按格式手工读偏移。处理稀疏镜像。有些老包提取出来的是稀疏格式需要先转成原始镜像再挂载。转换工具就是干这个的转完体积会变大属于正常现象。知道这几种后续路径的价值在于它决定了你提取的时候要挑哪些分区。只要系统镜像就能做完大部分分析但要看启动链路就必须带上 boot 和 vendor_boot要看硬件抽象层就必须带上 vendor。提前规划比全量提取完再挑省事得多。5. 常见问题与排查实录5.1 报错速查表下面这些是我在实际操作中遇到过并记录下来的按发生频率排序现象可能原因处理方式提示磁盘空间不足预留空间不够稀疏区间被当实占清理空间按包体积二点五倍预留进程中途消失、无报错容器内存上限被打满降低并行度比如从 8 降到 4解析元数据失败payload.bin 被截断或下载不全重新校验 zip 完整性重下头部数据找不到旧分区数据手上是增量包换全量包或确认有对应底包分区名不存在参数里的分区名拼错或该包无此分区先用列表参数确认准确名称哈希不一致包损坏、磁盘写入异常换输出目录重试检查磁盘健康提取出的 img 显得很小稀疏文件逻辑大小正常用 apparent-size 方式查看输出目录里文件为 0权限不足或磁盘已满但未报错检查目录权限与剩余空间这张表覆盖了我在实际使用中的八成问题。剩下的两成通常和具体环境相关需要看完整输出日志。5.2 三类瓶颈的识别方法卡住的时候先判断是 CPU、内存还是 IO 在拖后腿这个判断决定了解法方向。CPU 瓶颈表现为某个核心长期跑满、整体耗时和并行度强相关。这种情况下调并行度有用但也别指望线性提速因为解压本身有单线程上限——一个操作只能由一个线程处理你能并行的只是不同操作之间。内存瓶颈的典型特征是进程被杀、日志突然中断、系统开始大量交换。这时候的唯一解法是降并行度顺便看看是不是同时开了别的内存大户。我遇到过一次在内存受限的机器上开十六路跑到 system 分区时直接静默退出一开始以为是包坏了查了半天才发现是内核的 OOM 处理把进程杀了。IO 瓶颈的特征是 CPU 占用不高但进度条走得慢或者写入速率明显低于解压速率。可能原因是机械硬盘随机写、网络挂载盘延迟高、或者实时扫描在拖慢写入。换到本地 SSD 上跑速度差异会非常明显。5.3 增量包、缺失文件与残缺包有三类输入问题最耽误时间值得单独拎出来。第一类是增量包。前面说过特征是操作类型里出现SOURCE_或BSDIFF。有些增量包的元数据结构看起来和全量一样工具会解到某一步才报错这时候容易误判。我的做法是提前用列表参数看一遍如果分区大小加起来远小于包体积说明大部分数据是差异而非完整镜像基本可以判定是增量包。第二类是包不完整。下载中断、传输错误都会导致 payload.bin 头部完好但数据区被截断。这种情况下前几个分区可能正常提取后面的分区报错很有迷惑性。校验方法就是前面说的 base64 哈希对比一次到位。第三类是包本身正常但文件系统层有问题。比如输出目录在一个区分大小写的系统上创建过、后来挂到不区分的系统上文件名冲突导致覆盖或者输出目录所在卷有配额限制。这类问题不会报错只会让结果莫名其妙地不对排查时容易走偏。5.4 结果不对时的回查顺序拿到一个可疑的输出我的回查顺序是固定的从外到内先看包本身zip 完整性、payload 哈希、文件大小三个数据点确认无误再往下。然后看工具版本和参数特别是分区清单参数在这个版本里的实际语义。接着看输出目录文件是否齐全、表观大小是否与元数据记录一致、有没有零字节文件。最后才看镜像内容本身用十六进制看一眼开头几个字节判断文件系统类型和是否有有效数据。这个顺序的好处是每一步都能排除掉一大片可能性。我见过有人一上来就怀疑镜像内容折腾半天挂载参数最后发现是包没下完。从外到内排查永远比从内到外快。6. 几个踩坑换来的经验6.1 并行度不是竞赛稳定比快重要我最初用这个工具时喜欢把并行度拉满觉得核心多就该用满。实际对比下来四核机器上开四路和开十六路的差距远没有想象中大反而在开十六路时遇到过两次进程被系统清理。后来我固定用物理核心数稳定运行一次成功这才是真正的省时间。如果你要批量处理多个包还有另一个策略单个包低并行多个包串行跑。这样总吞吐可能更高因为避免了同时抢内存和磁盘。我在一台老机器上批量处理过十几个包用“每包四路、一次一个”的方式一晚上跑完中间没出过岔子。6.2 原始文件别删那是唯一的真相来源提取出来的镜像方便好用但它们都是衍生物。一旦怀疑某个分区有问题能回去核对的只有原始 payload.bin 和 properties 文件。我吃过一次亏为了省空间删了原始包后来发现某个分区提取结果可疑只能重新下载两三个 G等了一下午。现在我的做法是原始包放在一个专门的目录里配上校验信息一起存着。如果空间实在紧张至少保留 properties 文件和每个分区镜像的哈希记录这样即使包删了也能确认手里的镜像是不是当初提取的那一份。6.3 把反复敲的命令封成脚本处理一个包要敲四五条命令处理十个包就是四五十条中间任何一条打错都可能白跑。我后来写了个很小的脚本把校验、列分区、提取、核对串起来只留一个参数控制提取哪些分区。脚本不长但省下的注意力和出错概率很值。脚本里我加了两处检查提取前检查可用磁盘空间是否达到包里最大分区体积的三倍提取后检查输出目录里非空文件数量是否和预期分区数一致。这两处检查拦下过好几次问题都是那种“跑完了才发现不对”的场景。写脚本时有个细节值得提不要在脚本里假设分区名。不同机型的分区集不一样硬编码分区名会让脚本在换机型时直接失效。我的做法是把分区名做成参数脚本只负责流程控制不关心具体提取什么。6.4 关于后续扩展的一点想法如果这套流程要长期用还有两个方向可以往下走。一个是把提取结果和镜像内容索引结合起来提取完自动输出一份分区清单包含每个分区的文件系统类型、表观大小、关键目录结构摘要下次再处理同系列机型时能快速比对差异不用每次从零开始看。另一个方向是做版本对比。同一个机型两个不同版本的全量包各自提取 system 分区之后做目录级的差异分析能很快看出这个版本改了什么新增了哪些应用、哪些系统组件版本跳了、配置文件有什么变化。这套对比做熟了判断更新内容的速度会比逐个翻文件快一个量级。我目前是在手工做这件事步骤不多但重复度高迟早要把它脚本化。补丁级别和系统版本号的对应能快速确认vendor里看驱动和硬件抽象层的版本能了解底层变化boot里的内核版本和启动参数能确认安全加固项。这些点串起来一份 OTA 包能读出的信息比想象中多而起点就是把 payload.bin 老老实实地解出来。