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的第二段对话
这个是原探究过程,TL;DR:
[Local File Header #1][文件名1][文件数据1][Local File Header #2][文件名2][文件数据2]
[Central Directory File Header #1][Central Directory File Header #2]
[End Of Central Directory (EOCD)][ZIP 注释(百度自定义)]
xxd byiOSLivpMakerB1785140239500.zip | less
一个数字代表半个字节,如504b,50是一个字节
Local File Header
0000 0024 6674 7970 6865 6963 0000
签名 504B 0304 固定魔数
版本 1400 提取所需最低版本,一般设为 0x0014(2.0,即 decimal 20)
通用位标志 0000 GPFlag
压缩方法 0000 0x0000 = 存储(store,无压缩)。
修改时间 2982 bit 15-11 小时(5 bit)
bit 10-5 分钟(6 bit)
bit 4-0 秒/2(5 bit)
0x8229 = 1000 0010 0010 1001
小时10000 = 16
分钟010001 = 17
秒/2 01001 = 9
16:17:18
修改日期 fb5c bit 15-9 年份偏移(从1980开始)
bit 8-5 月
bit 4-0 日
0x5cfb = 0101 1100 1111 1011
年diff 年0101110=46
月0111=7
日11011=27
2026年7月27日
CRC32 9571 d173 0x73d17195 见python脚本livp3.crc.py
压缩大小 2395 0000 0x9523,stat -f "%z" IMB_TItMiu.HEIC.heic得38179(字节),hex(38179)=0x9523
未压缩大小 2395 0000 同上
文件名长度 1400 0x14 = len(‘IMB_TItMiu.HEIC.heic’)
额外字段长度 0000
文件名 494d 425f 5449 744d 6975 2e48 4549 432e 6865 6963 运行print(bytes.fromhex("494d 425f 5449 744d 6975 2e48 4549 432e 6865 6963").decode("ascii"))可得IMB_TItMiu.HEIC.heic
文件数据 0000 0024 6674 7970 6865 6963 … …$ftypheic..
文件数据起始偏移地址 16x3+2=50
文件结束后第二个文件起始偏移地址:50 + 38179 = 0x9555
执行xxd -s 0x9555 byiOSLivpMakerB1785140239500.zip | less得到
00009555: 504b 0304 1400 0000 0000 2982 fb5c dbcf PK……..)....
00009565: 6a4e 9d54 0300 9d54 0300 1300 0000 494d jN.T…T……IM
00009575: 425f 5449 744d 6975 2e48 4549 432e 6d6f B_TItMiu.HEIC.mo
00009585: 7600 0000 1466 7479 7071 7420 2000 0000 v….ftypqt …
文件起始地址 0x9585 + 1,文件大小 218269 字节 ,ZIP下一部分Central Directory File Header #1 起始地址 0x9585 + 1 + 218269 = ‘0x3ea23’
执行xxd -s 0x3ea23 byiOSLivpMakerB1785140239500.zip得到
0003ea23: 504b 0102 0000 1400 0000 0000 2982 fb5c PK……….)..
0003ea33: 9571 d173 2395 0000 2395 0000 1400 0000 .q.s#…#…….
0003ea43: 0000 0000 0000 0000 0000 0000 0000 494d …………..IM
0003ea53: 425f 5449 744d 6975 2e48 4549 432e 6865 B_TItMiu.HEIC.he
0003ea63: 6963 504b 0102 0000 1400 0000 0000 2982 icPK……….).
0003ea73: fb5c dbcf 6a4e 9d54 0300 9d54 0300 1300 ...jN.T…T….
0003ea83: 0000 0000 0000 0000 0000 0000 5595 0000 …………U…
0003ea93: 494d 425f 5449 744d 6975 2e48 4549 432e IMB_TItMiu.HEIC.
0003eaa3: 6d6f 7650 4b05 0600 0000 0002 0002 0083 movPK………..
0003eab3: 0000 0023 ea03 0038 0030 3030 3230 3030 …#…8.0002000
0003eac3: 3030 3033 3230 3030 3039 3532 3330 3030 0003200009523000
0003ead3: 3330 3030 3039 3538 3630 3030 3335 3439 3000095860003549
0003eae3: 4433 3133 3033 3033 3034 4334 3935 3635 D313030304C49565
0003eaf3: 30 0
Central Directory File Header #1
签名Signature 504b 0102 固定魔数
版本VerMade 0000 创建此文件所用版本,一般也用2.0(14 00)
版本VerNeeded 1400 提取所需最低版本,同 Local Header
通用位标志GPFlag 0000 GPFlag
压缩方法CompMethod 0000 0x0000 = 存储(store,无压缩)
修改时间ModTime 2982 同Local File Header
修改日期ModDate fb5c 同Local File Header
CRC32 9571 d173 同Local File Header
压缩大小CompSize 2395 0000 同Local File Header
未压缩大小UncompSize 2395 0000 同Local File Header
文件名长度NameLen 1400 同Local File Header
额外字段长度ExtraLen 0000 同Local File Header
文件注释长度CommentLen 0000 代表无注释
磁盘号DiskStart 0000 此文件起始于第几号磁盘(一般0)
内部属性IntAttr 0000 内部文件属性,一般 0
外部属性ExtAttr 0000 0000 外部文件属性,一般 0
相对偏移RelOffset 0000 0000 从 ZIP 文件起始到对应 Local Header 的偏移(字节数)。示例:第一个文件偏移 0。第二个文件按此计算。
文件名 494d 425f 5449 744d 6975 2e48 4549 432e 6865 6963
(可选)额外字段/注释
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
End of Central Directory (EOCD):
xxd -s 0x3eaa6 byiOSLivpMakerB1785140239500.zip
0003eaa6: 504b 0506 0000 0000 0200 0200 8300 0000 PK…………..
0003eab6: 23ea 0300 3800 3030 3032 3030 3030 3030 #…8.0002000000
0003eac6: 3332 3030 3030 3935 3233 3030 3033 3030 3200009523000300
0003ead6: 3030 3935 3836 3030 3033 3534 3944 3331 0095860003549D31
0003eae6: 3330 3330 3330 3443 3439 3536 3530 3030304C495650
签名Signature 504b 0506 固定魔数,标志 EOCD 开始
当前磁盘号DiskNum 0000 本磁盘号,一般0
CD起始磁盘号DiskStart 0000 中央目录所在磁盘号(一般0)
磁盘内条目数EntriesOnDisk 0200 本磁盘上的目录条目数。示例0x0002=2(图片+视频)。
总条目数TotalEntries 0200 ZIP 文件中目录总条目数。示例2。
CD大小CDSize 8300 0000 (CD#1 46+20,CD#2 46+19,共计46+20+46+19=131=0x83)中央目录总大小(字节)。等于所有 Central 头长度之和(不含 EOCD 自身和注释)。
CD偏移CDOffset 23ea 0300 0x3eaa6-0x83=0x03ea23中央目录起始位置相对 ZIP 开始的偏移(字节数)。
注释长度CommentLen 3800 0x38=56
注释内容 3030 3032 3030 3030 3030 3332 3030 3030 3935 3233 3030 3033 3030 3030 3935 3836 3030 3033 3534 3944 3331 3330 3330 3330 3443 3439 3536 3530
print(bytes.fromhex(“3030 3032 3030 3030 3030 3332 3030 3030 3935 3233 3030 3033 3030 3030 3935 3836 3030 3033 3534 3944 3331 3330 3330 3330 3443 3439 3536 3530”).decode(“ascii”))得到:000200000032000095230003000095860003549D313030304C495650
同步发文于CSDN和我的个人博客,原创不易,转载经作者同意后请附上原文链接哦~
千篇源码题解已开源
特辑此文以飨读者。