前言
这篇文章大约鸽了一个多月吧,一直懒得写
嗯,它是工具FastbootEnhance的理论基础 我在写这个工具的时候总结而来的
说起Payload,第一感觉就是神秘,因为在引入Payload之前安卓一直采用的是 一个zip里直接装入分区镜像的形式(后来不再使用镜像,而是使用.new.dat
,但是至少还是看得出它是啥的),但是,Payload则是用一种神秘的格式把各个分区的数据全部打包进了一个文件,还需要使用各种奇形怪状的dumper才能解开
Payload好像是伴随着A/B分区和update engine
引入的,因为一直没有接触A/B分区的设备,也就一直没有了解过这个东西。不过,都2021年了,随着动态分区设备开始大规模普及,Payload.bin更是以format version 2
的形式大规模走进了玩机用户的视线
整体结构
其实谷歌已经把它的结构很清楚的写在这里了
这个update_metadata.proto
是谷歌的Protobuf
源文件,而Protobuf
则是谷歌推出的一款类的序列化工具。简而言之,就是可以把一个类对象转换成字节流(然后就可以保存起来),并且可以通过这字节流把类对象还原出来。而这个.proto
的文件就是这个类对象的描述,使用Protobuf
工具编译这个文件,可以得到所需编程语言的源文件(.py / .java / .cs etc.),这些源文件包含了类的定义(又不是js这种怪胎,可以凭空造对象)以及相关的序列化和反序列化方法。因此,只要拿到Protobuf
序列化出来的数据以及对应的.proto
文件,就可以在任意它支持的语言中还原这个类对象
而Payload.bin本身并不是一个序列化的类,序列化的数据仅仅只是它的一部分(header中的manifest以及signature之类的)
所以 update_metadata.proto
这个文件并不是用来描述Payload.bin的结构的,而只是描述了Payload.bin中的某些部分的数据结构,使得使用反序列化方法能够顺利将这部分数据还原为类对象,然后再通过读取这些类对象的属性来获得所需的值
怎么有种脱裤子放屁的感觉,为什么要序列化而不直接往header里放数据嗯,确实,因为header里还真的存了一些数据
update_metadata.proto
在注释中写到了Payload.bin这个文件的整体结构
// Update file format: An update file contains all the operations needed
// to update a system to a specific version. It can be a full payload which
// can update from any version, or a delta payload which can only update
// from a specific version.
// The update format is represented by this struct pseudocode:
// struct delta_update_file {
// char magic[4] = "CrAU";
// uint64 file_format_version; // payload major version
// uint64 manifest_size; // Size of protobuf DeltaArchiveManifest
//
// // Only present if format_version >= 2:
// uint32 metadata_signature_size;
//
// // The DeltaArchiveManifest protobuf serialized, not compressed.
// char manifest[manifest_size];
//
// // The signature of the metadata (from the beginning of the payload up to
// // this location, not including the signature itself). This is a serialized
// // Signatures message.
// char metadata_signature_message[metadata_signature_size];
//
// // Data blobs for files, no specific format. The specific offset
// // and length of each data blob is recorded in the DeltaArchiveManifest.
// struct {
// char data[];
// } blobs[];
//
// // The signature of the entire payload, everything up to this location,
// // except that metadata_signature_message is skipped to simplify signing
// // process. These two are not signed:
// uint64 payload_signatures_message_size;
// // This is a serialized Signatures message.
// char payload_signatures_message[payload_signatures_message_size];
//
// };
可以看到,除了序列化的数据,header中还独立存储了例如file_format_version
和各种size
之类的东西
嗯,这些东西也是必要的,否则怎么知道要读多少东西交给Protobuf
做反序列化啊,那为什么还要序列化,manifest也这样直接存储不香么...难道是Protobuf自带压缩效果?
但是,上面注释中描述的结构并不完全正确,上方描述的payload_signatures_message_size
在data blobs
的后面,但是实际操作的时候却发现data blobs
的大小并没有被明确告知,即payload_signatures_message_size
这个东西的偏移量并不是可以直接得到的。于是我将各分区的数据块大小加起来得到一个可能的偏移量,却读到了一个明显错误的数据...
经过一番探索之后,我发现了payload_signatures_message_size
实际上并不在上方注释所描述的位置,而是在manifest
中
message DeltaArchiveManifest {
......
optional uint64 signatures_offset = 4;
optional uint64 signatures_size = 5;
......
}
把里面的signatures_offset读出来发现居然和上面猜的偏移量是一样的。。。
所以,Payload.bin的实际结构大概是这样的
Manifest 结构
可以看到,这个Payload啊,最重要的应该就是Manifest了
没有了Manifest,data blobs该怎么用都不知道
那接下来看看Manifest是长什么样的
这个Manifest啊,是个Protobuf
对于现在主流的Version 2,它大概是长这样的
(翻译成类似于cpp的形式)
struct Manifest {
uint32 block_size [default = 4096];
uint64 signatures_offset;
uint64 signatures_size;
struct ImageInfo {
//以下内容我实测一个都没读出来。。全是空的
string board;
string key;
string channel;
string version;
string build_channel;
string build_version;
} old_image_info(仅ota更新,非完整包), new_image_info;
// 表示OTA支持的操作,完整包应为0
uint32 minor_version [default = 0];
//一个分区对应一个
struct PartitionUpdate {
//分区名
string partition_name;
//以下几个用于更新前预处理
//似乎都没怎么看到过
bool run_postinstall;
string postinstall_path;
string filesystem_type;
//应该是下方new_partition_info的签名?
struct Signature {
uint32 version [deprecated];
bytes data;
fixed32 unpadded_signature_size;
} new_partition_signature[];
//可以在此处得到新分区的大小和校验值
struct PartitionInfo {
uint64 size;
bytes hash;
} old_partition_info, new_partition_info;
//新分区的安装操作,是个数组,一个分区有多个安装操作,多次安装
struct InstallOperation {
//本次安装操作的类型
enum Type {
REPLACE, //直接使用data blobs中的数据替换
REPLACE_BZ, //bzip解压后再替换
MOVE [deprecated], //OTA的操作,移动数据
BSDIFF [deprecated], //OTA的操作,一种拆分算法
//下面两个也是OTA增量时的操作
SOURCE_COPY,
SOURCE_BSDIFF,
REPLACE_XZ = 8; //xz解压缩后替换数据
ZERO = 6; //写0
DISCARD = 7; //OTA操作,忽略?
BROTLI_BSDIFF = 10; //OTA操作?压缩过的bsdiff?
PUFFDIFF = 9; //OTA操作,另一种差量算法?
} type;
//当前操作的数据
//相较于data blobs头部的偏移量
uint64 data_offset;
//当前操作的数据长度
uint64 data_length;
//一些OTA操作在读取数据时的范围
//指的是设备里的已有分区内容
struct Extent {
uint64 start_block;
uint64 num_blocks;
} src_extents[];
//以及长度
uint64 src_length;
//安装的目的地
struct Extent {
uint64 start_block;
uint64 num_blocks;
} dst_extents[];
//安装时的数据长度(解压后总长)
uint64 dst_length;
//data blobs数据的校验值
bytes data_sha256_hash;
//OTA操作时,分区里已有源数据的校验值
bytes src_sha256_hash;
} operations[];
//分区预处理是否可以跳过
bool postinstall_optional;
//剩下的大概就是一些校验相关的吧
struct Extent {
uint64 start_block;
uint64 num_blocks;
} hash_tree_data_extent, hash_tree_extent;
string hash_tree_algorithm;
bytes hash_tree_salt;
struct Extent {
uint64 start_block;
uint64 num_blocks;
} fec_data_extent, fec_extent;
uint32 fec_roots [default = 2];
} partitions[];
//当前系统的timestamp大于这个值时,视为降级安装
int64 max_timestamp;
//动态分区元数据
struct DynamicPartitionMetadata {
struct DynamicPartitionGroup {
//组的名字
string name;
//组的最大大小
uint64 size;
//属于这个组的分区名
string partition_names[];
} groups[];
//是否启动分区快照
bool snapshot_enabled = 2;
} dynamic_partition_metadata;
}
可以看到,结构还是相当复杂的,而且有许多属性是可选且分Manifest类型出现的(完整包/OTA包)也许这就是为什么不写死在header里吧
对于完整包来说,安装更新只需要
foreach paritions
foreach operations
install()
然后install()
的时候,根据data_offset
和data_length
从data blobs里的指定位置读取数据
根据InstallOperation
的type
来确定如何处理数据(比如xz解压缩)
再把数据丢到dst
指定的区块即可
至于拆分包,就更加复杂了,但是,也没法做实验来测试。。。
还有那个动态分区元数据
对应到device tree中是这个东西
https://github.com/LineageOS/android_device_oneplus_sm8250-common/blob/51943f7a173545b6f9d832ca804fcfefd619e23d/BoardConfigCommon.mk#L151
https://github.com/LineageOS/android_device_oneplus_instantnoodle/blob/7d42a8d7915a8c42da87885ae83c5be11ef5cea3/BoardConfig.mk#L45
从lpdump的结果来看,它真的只是一点点元数据,既不会影响分区的内容,也不会影响分区的排布,似乎仅仅只是用来限制几个分区的整体大小的(或者方便在删除分区时绑在一起?)。。。即使没有它们,默认的default分区组也会把一切安排的好好的。。
总结
这个Payload啊,看起来很神秘,但其实也没有那么神秘
不过往里面塞了个Protobuf,导致无论是打包还是解包,都需要调用谷歌的Protobuf库,实在是有亿点点麻烦
谁知道他到底是为了压缩数据,还是推广自家技术呢?
于是我将各分区的数据块大小加起来得到一个可能的偏移量,却读到了一个明显错误的数据...
博主这个地方 的意思是 你有额外的工具 能支持查看payload.bin的二进制流程数据 请问下是什么工具 ?
payload_dumper 我用过这个工具 但是解压出来的直接是img或系统文件这些 。
另外你说明显错误的数据 -- 怎么判断的 ?按我的理解读到的是二进制数据 怎么知道是错误的呢?
我的意思是我并不知道 payload_signatures_message_size 的具体位置,但是按照它给的注释,payload_signatures_message_size 应该就在 data blobs 的后面,于是我根据 manifest 里各个分区的大小猜测了 data blobs 的总大小,尝试读取其后的 payload_signatures_message_size 。我记得我当时读出了一个非常离谱的值,好像是一个巨大的数,很显然这并不会是 signature size ,所以我说它是明显错误的。
并没有用什么工具,直接写代码读取的。
了解 三克油
好文,学习了,请问一下博主,如果我用payload_dumper解包了payload.bin后得到了一堆img镜像文件,我修改了system.img之后,如果重新在打包回payload.bin呢?不胜感激!(车机,没有fastboot线刷,只能通过OTA升级包卡刷,所以需要重新打包payload.bin,Android 9系统)
直接打包回去是没有用的,因为 payload 中包含了签名校验( signature 部分 ),修改后签名校验肯定会过不了,由于没有厂家的签名,rec 往往会拒绝刷入此类包,这便是为什么大多数工具没有提供打包的操作。
如果厂家的 rec 不会校验签名,可以试试用 AOSP 的打包工具。
https://android.googlesource.com/platform/build/+/refs/tags/android-14.0.0_r21/tools/releasetools/ota_from_target_files.py
感谢博主的答复,我的车机系统很奇葩,有可能是可以的,我先尝试一下。
23款红旗H5对吧,我也卡在这了,打包打不回去了。。。。。
请问你成功打包刷进去了吗
本人也是车机问题,从车机端无法进入ADB,只能从车辆全量刷机包入手,目前也是用dumper解压开,得到了boot/dbt/dtbo/product/system/vbmeta/vendor/这些的img文件,将有三个多G的system改zip解压,发现有很多软件,但是提取出来并不能安装到手机这一类设备
没想到来到这儿都是车机的,我也是,但是目前看起来解包后打包刷入还没有成功案例