JPG+Zip文件组合问题与Zip格式

11

希望你听说过一个很棒的技巧neat hack,它可以将JPG和Zip文件合并成一个文件,并且对于两种格式都是有效的(或至少可读的)文件。我意识到,由于JPG允许在结尾处放置任意内容,而ZIP在开头处,因此您可以在其中添加一种格式-在中间。对于本问题,假设中间数据是任意二进制数据,保证不会与JPG或ZIP格式冲突(这意味着它不包含魔术zip头0x04034b50)。图示:

0xFFD8 <- start jpg data end -> 0xFFD9 ... ARBITRARY BINARY DATA ... 0x04034b50 <- start zip file ... EOF

我这样进行猫操作:

cat "mss_1600.jpg" filea fileb filea fileb filea fileb filea fileb filea fileb filea fileb filea fileb filea fileb filea fileb filea fileb filea fileb filea fileb filea fileb "null.bytes" "randomzipfile.zip" > temp.zip

这会生成一个6,318 KB的文件。它在7-Zip中无法打开。但是,当我少拼接了一个'double'(也就是12个filea和b而不是13个)时:

cat "mss_1600.jpg" filea fileb filea fileb filea fileb filea fileb filea fileb filea fileb filea fileb filea fileb filea fileb filea fileb filea fileb filea fileb "null.bytes" "randomzipfile.zip" > temp.zip

它产生了一个5,996 KB的文件,可以在7-Zip中打开。

我知道我的任意二进制数据没有魔术Zip文件头来破坏它。我有工作的jpg+数据+zip不工作的jpg+数据+zip的参考文件(保存为,因为浏览器认为它们是图像,并自行添加zip扩展名)。

我想知道为什么它在13个组合中失败了,而在12个组合中没有失败。如果能解决这个问题,我会给予额外奖励。


1
只是想指出,这可能是7Zip算法的问题,因为File Roller也成功打开了那个无法工作的示例。 - laginimaineb
1
不错的技巧 - 从现在开始,我将使用这种技术在所有我的Java .jar(可执行的jar-peg)中插入我的照片 :) - Seth
4个回答

22

我下载了7-Zip的源代码并找到导致这种情况发生的原因。

在CPP/7zip/UI/Common/OpenArchive.cpp中,你会看到以下内容:

// Static-SFX (for Linux) can be big.
const UInt64 kMaxCheckStartPosition = 1 << 22;

这意味着只会搜索文件的前4194304个字节以查找头信息。如果在那里找不到,7-Zip将认为它是无效文件。您可以通过将1 << 22更改为1 << 23来加倍该限制。我通过重新构建7-Zip测试了该更改,并且它有效。编辑:要解决此问题,您可以下载源代码,进行上述更改并进行构建。我使用VS 2008构建它。打开VS命令提示符,导航到提取的源位置\CPP\7zip\Bundles并键入'nmake'。然后在Alone目录中运行'7za t nonworking.jpg',您应该会看到'一切正常'。

真是太好了,先生。我想知道是否可以在那个字节的第一个span中放置一个正确格式的假文件来欺骗7-Zip... 我要玩一下(并且等待一段时间再接受,不冒犯)。 - Tom Ritter

10

其实这是一个双重答案:)

首先,无论别人怎么说,zip文件在技术上不能直接放在文件的末尾。中央目录记录的结尾有一个值,该值指示从当前磁盘的开头开始的字节偏移量(如果您只有一个.zip文件,则表示当前文件)。现在许多处理器都忽略这一点,虽然Windows的zip文件夹不会,因此您需要更正该值以使其在Windows资源管理器中工作(尽管您可能不在乎 ;P)。有关文件格式的信息,请参见Zip APPNOTE。基本上,您需要在十六进制编辑器(或编写工具)中查找“起始磁盘编号相对于中央目录的偏移量”值。然后找到第一个“中央文件头标记”(十六进制为504b0102),并将该值设置为该偏移量。

但很遗憾,这并不能修复7zip,因为7zip试图猜测文件格式的方式。基本上,它只会在前大约4MiB中搜索二进制序列504b0304,如果没有找到它,就会认为它不是Zip文件,并尝试使用其他存档格式。这显然就是为什么添加一个文件会破坏它的原因,因为它超出了搜索限制。

现在要解决这个问题,您需要在不破坏JPEG文件的情况下将该十六进制字符串添加到其中。一种方法是在FFD8 JPEG SOI标头之后添加以下十六进制数据:FFEF0005504B030400。这将添加一个具有您的序列的自定义块,并且正确的JPEG头应该会忽略它。


这让我完成了60%。我还必须修改504b0102条目以更改它们的偏移量,否则它会打开但不允许你提取文件。在Windows Explorer和7-Zip中,我认为我有一个可以工作的jpg / zip,但我明天需要进行更多测试。 - Tom Ritter

4

对于其他遇到这个问题的人,这里是故事:

Andy关于7-Zip无法打开文件的原因是完全正确的,但这并没有解决我的问题,因为我不能让别人使用我的版本的7-Zip。

然而,tyranid给了我解决方案。

  • 首先,按照他建议的将一个小字节串添加到JPG中将允许7-Zip打开它。 但是,它与有效的JPG片段略有偏差,需要为FFEF00 07 504B030400 - 长度偏移了2个字节。
  • 这样可以让7-Zip打开它,但无法提取文件,会默默地失败。 这是因为中央目录中的条目具有指向文件条目的内部指针/偏移量。 由于您在该文件之前放了一堆东西,因此需要更正所有这些指针!
  • 要使zip文件在Windows内置的zip支持下打开,您需要像tyranid所说的那样,更正“与起始磁盘编号相对应的中央目录开始偏移量”。 这是一个Python脚本来执行最后两步,尽管它只是一个片段,不是立即可用的复制粘贴代码。

#Now we need to read the file and rewrite all the zip headers.  Fun!
torewrite = open(magicfilename, 'rb')
magicdata = torewrite.read()
torewrite.close()

#Change the Central Repository's Offset
offsetOfCentralRepro = magicdata.find('\x50\x4B\x01\x02') #this is the beginning of the central repo
start = len(magicdata) - 6 #it so happens, that on my files, the point is stored 2 bytes from the end.  so datadatadatdaata OF FS ET !! 00 00 EOF where OFFSET!! is the 4 bytes 00 00 are the last two bytes, then EOF
magicdata = magicdata[:start] + pack('I', offsetOfCentralRepro) + magicdata[start+4:]

#Now change the individual offsets in the central directory files
startOfCentralDirectoryEntry = magicdata.find('\x50\x4B\x01\x02', 0) #find the first central directory entry
startOfFileDirectoryEntry = magicdata.find('\x50\x4B\x03\x04', 10) #find the first file entry (we start at 10 because we have to skip past the first fake entry in the jpg)
while startOfCentralDirectoryEntry > 0:
    #Now I move a magic number of bytes past the entry (really! It's 42!)
    startOfCentralDirectoryEntry = startOfCentralDirectoryEntry + 42

    #get the current offset just to output something to the terminal
    (oldoffset,) = unpack('I', magicdata[startOfCentralDirectoryEntry : startOfCentralDirectoryEntry+4])
    print "Old Offset: ", oldoffset, " New Offset: ", startOfFileDirectoryEntry , " at ", startOfCentralDirectoryEntry
    #now replace it
    magicdata = magicdata[:startOfCentralDirectoryEntry] + pack('I', startOfFileDirectoryEntry) + magicdata[startOfCentralDirectoryEntry+4:]

    #now I move to the next central directory entry, and the next file entry
    startOfCentralDirectoryEntry = magicdata.find('\x50\x4B\x01\x02', startOfCentralDirectoryEntry)
    startOfFileDirectoryEntry = magicdata.find('\x50\x4B\x03\x04', startOfFileDirectoryEntry+1)

#Finally write the rewritten headers' data
towrite = open(magicfilename, 'wb')
towrite.write(magicdata)
towrite.close()

感谢分享你的代码(并揭示了意义是42 ;))。无需解释 - 我学到了很多,而且很有趣。 - Andy West
抱歉如果我有些地方说错了。不过还是谢谢你 :) - tyranid

2
您可以使用DotNetZip生成混合的JPG+ZIP文件。DotNetZip可以保存到流中,并且足够智能,以识别先前现有流的原始偏移量,然后在其中开始编写zip内容。因此,在伪代码中,您可以通过以下方式获得JPG+ZIP文件:
 open stream on an existing JPG file for update
 seek to the end of that stream
 open or create a zip file
 call ZipFile.Save to write zip content to the JPG stream
 close

所有的偏移量都计算正确。同样的技术也被用来生成自解压缩存档。您可以打开EXE上的流,然后寻找到结尾,并将ZIP内容写入该流。如果您以这种方式操作,则所有的偏移量都会被正确计算。
另外一件事情是,在另一篇帖子的评论中提到的... ZIP文件可以在文件的开头和结尾包含任意数据。据我所知,并没有要求zip中央目录必须在文件末尾,尽管这是典型的。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接