Zip:手动生成仅存储的zip文件(以百度系iOS动态图.livp为例)
Zip:手动生成仅存储的zip文件(以百度系iOS动态图.livp为例)
背景
本系列上篇文章逆向(iOS):压缩和解压live图(.livp)——百度一刻相册+百度网盘识别支持分析了第三方厂商对Apple动态图的封装规则。由于压缩强度为0(仅存储),故本篇以此为例探究如何手搓仅归档的zip压缩文件。
如无特殊说明,本篇默认以打包生成.livp格式动态live图为例。
简单回顾
Apple系列支持通过系统接口像相册中写入live图,但未曾公开过导出方式。第三方厂商为了适配live图几乎都选择将静态图和动态视频打包成一个.livp文件便于备份、传输、分享,其本质上是一个带有一定格式约束的.zip压缩包。
百度系列的动态图压缩包需要满足以下条件:
- 不压缩(仅存储)
- 静态图在前动态图在后
- 56字节长度的注释(注释构成见上篇文章)
既然zip的格式是已知且公开的,并且生成zip的过程不涉及原始文件压缩,因此我们完全可以不借助zip工具,直接手搓一个.zip文件出来,还能被百度系产品识别为live图。
样本介绍
样本:byiOSLivpMakerB1785140239500.livp
该文件即为百度系产品支持识别的live图,其本质是一个压缩包,包含两个文件:
- 文件一(静态图):IMB_TItMiu.HEIC.heic,大小38179字节
- 文件二(动态视频):IMB_TItMiu.HEIC.mov,大小218269 字节
包含一个注释:
1 | |
压缩方式为仅存储。
zipinfo -v byiOSLivpMakerB1785140239500.livp
1 | |
Zip格式
总格式
zip格式如下:
1 | |
Local File Header、Central Directory File Header、End Of Central Directory长啥样?咱们结合样本进行分析。
分析方式
1 | |
可以得到如:
1 | |
一共有三部分,最左边的:
1 | |
代表起始地址。
中间的:
1 | |
代表该起始地址的16字节的数据。一个数字代表半个字节,如前4个数字504b,其中的50是一个字节。
例如第一行的00000000: 504b 0304 1400 0000 0000 2982 fb5c 9571就代表起始地址是0的16个字节内容为504b 0304 1400 0000 0000 2982 fb5c 9571。
右边部分的:
1 | |
代表左边字节对应的ASCII码,如第一行的前两个字节504b对应的ASCII码为PK:
1 | |
继续回到总格式章节的zip格式:
1 | |
zip首先就是一个一个的文件。咱们先来分析第一个文件。
第一个文件:[Local File Header #1][文件名1][文件数据1]
| 字段 | 偏移 | 长度 | 样本对应值 | 含义 |
|---|---|---|---|---|
| 签名 | 0 | 4字节 | 504B 0304 | 固定魔数,PK\x03\x04 |
| 版本 | 4 | 2字节 | 1400 | 提取所需最低版本 一般设为 0x0014(2.0,即 decimal 20) |
| 通用位标志 | 6 | 2字节 | 0000 | General Purpose Bit Flag(GPBF)0x0000代表CRC、压缩大小、原始大小已经知道,直接写在 Local File Header 中0x0008代表CRC、大小暂时未知,稍后通过 Data Descriptor 写入 |
| 压缩方法 | 8 | 2字节 | 0000 | 0x0000 = 存储(store,无压缩)。 |
| 修改时间 | 10 | 2字节 | 2982 |
|
| 修改日期 | 12 | 2字节 | fb5c |
|
| CRC32 | 14 | 4字节 | 9571 d173 | 0x73d17195 |
| 压缩大小 | 18 | 4字节 | 2395 0000 | 0x9523得38179(字节),hex(38179)=0x9523 |
| 未压缩大小 | 22 | 4字节 | 2395 0000 | 同上 |
| 文件名长度 | 26 | 2字节 | 1400 | 0x14 = len(‘IMB_TItMiu.HEIC.heic’) |
| 额外字段长度 | 28 | 2字节 | 0000 | 本样本中无此字段,暂不研究 |
| 文件名 | 30 | 文件名长度 字节 | 494d 425f 5449 744d 6975 2e48 4549 432e 6865 6963 | 运行可得IMB_TItMiu.HEIC.heic |
| 文件数据 | 30+文件名长度 | 字节 | 0000 0024 6674 7970 6865 6963 … | …$ftypheic.. |
文件1数据起始偏移地址 16x3+2=50
文件1大小:38179(字节)
文件1结束后第二个文件起始偏移地址:50 + 38179 = 0x9555
第二个文件:[Local File Header #2][文件名2][文件数据2]
执行xxd -s 0x9555 byiOSLivpMakerB1785140239500.livp | less得到
1 | |
由最右边部分可以看到文件名IMB_TItMiu.HEIC.mov的最后一个字符v对应着起始地址0x00009585的字节76。
文件起始地址 0x9585 + 1,文件大小 218269 字节 ,可得ZIP下一部分Central Directory File Header #1 起始地址 0x9585 + 1 + 218269 = ‘0x3ea23’。
中央目录1:[Central Directory File Header #1]
执行xxd -s 0x3ea23 byiOSLivpMakerB1785140239500.zip得到:
1 | |
Central Directory File Header #1 格式如下:
| 字段 | 偏移 | 长度 | 样本对应值 | 含义 |
|---|---|---|---|---|
| 签名Signature | 0 | 4字节 | 504b 0102 | 固定魔数 |
| 版本VerMade | 4 | 2字节 | 0000 | 创建此文件所用版本,一般也用2.0(14 00) |
| 版本VerNeeded | 6 | 2字节 | 1400 | 提取所需最低版本,同 Local Header |
| 通用位标志GPFlag | 8 | 2字节 | 0000 | GPBF |
| 压缩方法CompMethod | 10 | 2字节 | 0000 | 0x0000 = 存储(store,无压缩) |
| 修改时间ModTime | 12 | 2字节 | 2982 | 同Local File Header |
| 修改日期ModDate | 14 | 2字节 | fb5c | 同Local File Header |
| CRC32 | 16 | 4字节 | 9571 d173 | 同Local File Header |
| 压缩大小CompSize | 20 | 4字节 | 2395 0000 | 同Local File Header |
| 未压缩大小UncompSize | 24 | 4字节 | 2395 0000 | 同Local File Header |
| 文件名长度NameLen | 28 | 2字节 | 1400 | 同Local File Header |
| 额外字段长度ExtraLen | 30 | 2字节 | 0000 | 同Local File Header |
| 文件注释长度CommentLen | 32 | 2字节 | 0000 | 代表无注释 |
| 磁盘号DiskStart | 34 | 2字节 | 0000 | 此文件起始于第几号磁盘(一般0) |
| 内部属性IntAttr | 36 | 2字节 | 0000 | 内部文件属性,一般 0 |
| 外部属性ExtAttr | 38 | 4字节 | 0000 0000 | 外部文件属性,一般 0 |
| 相对偏移RelOffset | 42 | 4字节 | 0000 0000 | 从 ZIP 文件起始到对应 Local Header 的偏移(字节数)。示例:第一个文件偏移 0。第二个文件按此计算。 |
| 文件名 | 46 | 文件名长度 字节 | 494d 425f 5449 744d 6975 2e48 4549 432e 6865 6963 | 同Local File Header |
| (可选)额外字段/注释 | 46+文件名长度 |
中央目录2:[Central Directory File Header #2]
大多都与中央目录#1相同
- 相同:504b 0102 0000 1400 0000 0000 2982 fb5c
- CRC32:dbcf 6a4e
- 未压缩大小:9d54 0300
- 压缩大小:9d54 0300
- 文件名长度:1300
- 相同:0000 0000 0000 0000 0000 0000
- 相对偏移RelOffset 5595 0000 ,符合前面计算的0x9555(也就是0x00009555)
- 文件名494d 425f 5449 744d 6975 2e48 4549 432e 6d6f 76:运行
print(bytes.fromhex("494d 425f 5449 744d 6975 2e48 4549 432e 6d6f 76").decode("ascii"))可得IMB_TItMiu.HEIC.mov
中央目录2以文件名结束,结束位置快速检索:0003eaa3: 6d6f 76,可得ZIP文件下一部分(EOCD)的起始地址为 0x3eaa3 + 3 = 0x3eaa6。
EOCD [End Of Central Directory]
执行xxd -s 0x3eaa6 byiOSLivpMakerB1785140239500.livp可得:
1 | |
End Of Central Directory格式如下:
| 字段 | 偏移 | 长度 | 样本对应值 | 含义 |
|---|---|---|---|---|
| 签名Signature | 0 | 4字节 | 504b 0506 | 固定魔数,标志 EOCD 开始 |
| 当前磁盘号DiskNum | 4 | 2字节 | 0000 | 本磁盘号,一般0 |
| CD起始磁盘号DiskStart | 6 | 2字节 | 0000 | 中央目录所在磁盘号(一般0) |
| 磁盘内条目数EntriesOnDisk | 8 | 2字节 | 0200 | 本磁盘上的目录条目数。示例0x0002=2(图片+视频)。 |
| 总条目数TotalEntries | 10 | 2字节 | 0200 | ZIP 文件中目录总条目数。示例2。 |
| CD大小CDSize | 12 | 4字节 | 8300 0000 | (CD#1 46+20,CD#2 46+19,共计46+20+46+19=131=0x83)中央目录总大小(字节)。等于所有 Central 头长度之和(不含 EOCD 自身和注释)。其中CD是EOCD前面的 中央目录 Central Directory的缩写 |
| CD偏移CDOffset | 16 | 4字节 | 23ea 0300 | 0x3eaa6-0x83=0x03ea23中央目录起始位置相对 ZIP 开始的偏移(字节数)。 |
| 注释长度CommentLen | 20 | 2字节 | 3800 | 0x38=56 |
Comment [ZIP 注释]
剩下部分全部是注释内容:
1 | |
对应的ASCII码可由以下python脚本求出:
1 | |
运行结果:
1 | |
关于注释含义在上篇文章逆向(iOS):压缩和解压live图(.livp)——百度一刻相册+百度网盘识别支持中已经分析。
file内容分析
逐字节分析了上面的内容,还剩下最后一个跳过的部分:文件。
运行以下命令,没有任何输出,说明zip中文件部分逐字节和原始文件相同。
1 | |
整个分析下来,和zipinfo命令得到的结果完全相同。至此,我们已经具备手搓这种仅存储二进制zip的全部必备知识了。
为什么这样设计?
先文件再中央文件目录的设计,是为了zip可以边压缩边写,支持流式写入,永不回头。
前面有过Local File Header了,后面还有一个专门的Directory File Header,并且二者还有大量重复内容。这样设计是为了存储完成后便于快速从文件尾检索定位。
至于为什么comment要放在EOCD后,我也不是很清楚。大概是因为zip设计之初Comment最大支持长度为65535,所以从文件尾向前检索也很快。
手搓zip
至此,我们已经获得了所有构造.livp文件时需要的数据格式了。花了三个半小时编写了一个脚本,测试通过:
1 | |
其中的 struct.pack("<IHHHHHIIIHH", ...):
<代表小端序(little-endian)I代表unsigned int(4字节)H代表unsigned short(2字节)Q代表unsigned long long(8字节)B代表unsigned char(1字节)
此脚本也同步更新到了GitHub仓库:Github@LetMeFly666/2livp/livp_maker.py
The End
Github仓库:LetMeFly666/2livp
zip格式调研报告:byChatGPT(不要看其生成的脚本,不可用)
样本逐字节分析验证(+撰文制表):All by Hand with Heart❤️.
Phillip Katz的Zip故事:看ChatGPT的第二段对话
同步发文于CSDN和我的个人博客,原创不易,转载经作者同意后请附上原文链接哦~
千篇源码题解已开源
特辑此文以飨读者。