<rss version="2.0">
  <channel>
    <title>Zip on LLBBL Blog</title>
    <link>https://llbbl.blog/categories/zip/</link>
    <description></description>
    
    <language>en</language>
    
    <lastBuildDate>Mon, 17 Aug 2026 10:00:00 -0500</lastBuildDate>
    
    <item>
      <title>ZIP Files Are Read Backwards</title>
      <link>https://llbbl.blog/2026/08/17/zip-files-are-read-backwards.html</link>
      <pubDate>Mon, 17 Aug 2026 10:00:00 -0500</pubDate>
      
      <guid>http://llbbl.micro.blog/2026/08/17/zip-files-are-read-backwards.html</guid>
      <description>&lt;p&gt;Every format in this series so far reads front to back. PNG starts with a signature and you walk chunks in order. A text file is bytes from the beginning. Markdown parsers scan line by line, top to bottom.&lt;/p&gt;
&lt;p&gt;ZIP reads backwards. The index is at the end of the file, and a reader is expected to seek to the end first and work its way back.&lt;/p&gt;
&lt;p&gt;That one decision explains almost everything strange about ZIP, including a few things that look like bugs and one thing that is definitely a bug.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;the-index-lives-at-the-end&#34;&gt;The Index Lives at the End&lt;/h2&gt;
&lt;p&gt;Here&amp;rsquo;s a real ZIP containing two small text files. 241 bytes total. Scanning it for the four-byte record signatures gives the whole layout:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;offset  bytes         ascii   record
     0  50 4b 03 04   P K . . Local File Header      &amp;lt;- hello.txt
    53  50 4b 03 04   P K . . Local File Header      &amp;lt;- second.txt
   108  50 4b 01 02   P K . . Central Directory Header
   163  50 4b 01 02   P K . . Central Directory Header
   219  50 4b 05 06   P K . . End of Central Directory
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Every one of those starts with the same two bytes. &lt;code&gt;0x50&lt;/code&gt; is decimal 80, which is &lt;code&gt;P&lt;/code&gt; in ASCII. &lt;code&gt;0x4b&lt;/code&gt; is decimal 75, which is &lt;code&gt;K&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;PK&lt;/code&gt;. Phil Katz, who wrote PKZIP in 1989, put his initials in the first two bytes of every structure in the format, and they are still there in every &lt;code&gt;.docx&lt;/code&gt;, &lt;code&gt;.jar&lt;/code&gt;, and &lt;code&gt;.epub&lt;/code&gt; on your machine.&lt;/p&gt;
&lt;p&gt;The two bytes after &lt;code&gt;PK&lt;/code&gt; are the record type: &lt;code&gt;03 04&lt;/code&gt; for a local file header, &lt;code&gt;01 02&lt;/code&gt; for a central directory entry, &lt;code&gt;05 06&lt;/code&gt; for the end-of-central-directory record. Those aren&amp;rsquo;t printable characters, which is deliberate. A four-byte constant made of two readable letters and two control bytes is unlikely to appear by accident in text, and easy to spot by eye in a hex dump.&lt;/p&gt;
&lt;p&gt;The last 22 bytes are the &lt;strong&gt;End of Central Directory&lt;/strong&gt; record, and it&amp;rsquo;s the entry point:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;50 4b 05 06 00 00 00 00 02 00 02 00 6f 00 00 00 6c 00 00 00 00 00

signature            0x06054b50
total CD records     2
central dir size     111 bytes
central dir offset   108
comment length       0
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;A reader opens the file, jumps to the end, finds that record, reads &amp;ldquo;the index is at offset 108,&amp;rdquo; seeks there, and reads the catalog. Listing the contents of a 4 GB archive touches a few hundred bytes.&lt;/p&gt;
&lt;p&gt;Note that each file appears &lt;strong&gt;twice&lt;/strong&gt;: once as a Local File Header immediately before its compressed data, and once as an entry in the Central Directory at the end. Hold that thought.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;why-would-you-do-this&#34;&gt;Why Would You Do This?&lt;/h2&gt;
&lt;p&gt;Because in 1989 you were writing to a floppy disk, and often to a floppy disk that wasn&amp;rsquo;t big enough.&lt;/p&gt;
&lt;p&gt;If the index goes at the front, you have to know everything about every file before you write the first byte: how many files, how big each one compresses to, where each one lands. That means compressing everything to a temporary location, then writing the header, then copying it all back. On a machine with 640K of RAM and two floppy drives, that&amp;rsquo;s brutal.&lt;/p&gt;
&lt;p&gt;Put the index at the end and you can stream. Compress a file, write it, remember where it went. Compress the next one. When you run out of files, write down everything you remembered. One pass, no temporary copy, and you never needed to know the total size in advance.&lt;/p&gt;
&lt;p&gt;TAR solved the same problem by having no index at all, which is why &lt;code&gt;tar&lt;/code&gt; has to read an entire archive to find one file, and why you cannot randomly access a &lt;code&gt;.tar.gz&lt;/code&gt;. ZIP got both streaming writes and random-access reads. That&amp;rsquo;s the trade that made it win.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;the-backwards-scan-is-fuzzier-than-it-sounds&#34;&gt;The Backwards Scan Is Fuzzier Than It Sounds&lt;/h2&gt;
&lt;p&gt;The EOCD record is 22 bytes, so you&amp;rsquo;d think a reader could just read the last 22 bytes and be done.&lt;/p&gt;
&lt;p&gt;It can&amp;rsquo;t, because the record ends with a variable-length archive comment of up to 65,535 bytes. The signature isn&amp;rsquo;t at a fixed offset from the end of the file. So a reader has to seek near the end and &lt;strong&gt;scan backwards looking for the four-byte signature&lt;/strong&gt;, potentially across 65,557 bytes.&lt;/p&gt;
&lt;p&gt;Searching for a magic number is not the same as knowing where a structure is. If those four bytes happen to appear inside the comment, or inside compressed data near the end of the file, a naive parser can lock onto the wrong one. Different implementations pick different candidates when there&amp;rsquo;s more than one. This is a recurring source of &amp;ldquo;this archive opens in one tool and not another.&amp;rdquo;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;you-can-put-anything-in-front-of-a-zip&#34;&gt;You Can Put Anything in Front of a ZIP&lt;/h2&gt;
&lt;p&gt;If a reader finds the archive by scanning backwards from the end, then whatever sits at the &lt;em&gt;front&lt;/em&gt; of the file is not the reader&amp;rsquo;s problem.&lt;/p&gt;
&lt;p&gt;Take a valid 69-byte PNG, take the 241-byte ZIP, and concatenate them with &lt;code&gt;cat&lt;/code&gt;. No special tooling:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;$ file polyglot.png
polyglot.png: PNG image data, 1 x 1, 8-bit/color RGB, non-interlaced

$ unzip -l polyglot.png
  Length      Date    Time    Name
---------  ---------- -----   ----
       12  08-10-2026 16:44   hello.txt
       13  08-10-2026 16:44   second.txt
---------                     -------
       25                     2 files
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;One 310-byte file. An image viewer reads the PNG signature at byte 0 and renders an image. An archive tool scans backwards, finds the EOCD, and extracts two files. Both are correct. Neither is being fooled by a trick; they&amp;rsquo;re each doing exactly what their format says to do.&lt;/p&gt;
&lt;p&gt;This is the mechanism behind self-extracting archives, where the front of the file is a real executable and the back is a real ZIP. The same property is why &amp;ldquo;GIFAR&amp;rdquo; attacks worked: a file that a server accepted as a harmless image was loaded by Java as an archive of classes.&lt;/p&gt;
&lt;p&gt;It also means the offsets inside the Central Directory are relative to the start of the &lt;em&gt;archive&lt;/em&gt;, not the start of the file, and readers have to work out that difference. Prepending data shifts everything, and well-behaved parsers cope by computing the delta between where the EOCD says the directory should be and where it found it.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;two-indexes-one-file&#34;&gt;Two Indexes, One File&lt;/h2&gt;
&lt;p&gt;Back to that detail from earlier: every file&amp;rsquo;s name and metadata are stored twice, in the Local File Header and again in the Central Directory.&lt;/p&gt;
&lt;p&gt;Nothing enforces that they agree.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the same archive with &lt;strong&gt;only the Central Directory copy&lt;/strong&gt; of the first filename patched from &lt;code&gt;hello.txt&lt;/code&gt; to &lt;code&gt;BOGUS.txt&lt;/code&gt;. The local header is untouched:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;$ unzip -l mismatch.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
       12  08-10-2026 16:44   BOGUS.txt
       13  08-10-2026 16:44   second.txt

  local file header at offset 0 still says: hello.txt
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The archive is not corrupt. &lt;code&gt;unzip&lt;/code&gt; lists it happily. It just contains two different answers to &amp;ldquo;what is this file called,&amp;rdquo; and which one you get depends on which structure your parser decided to trust.&lt;/p&gt;
&lt;p&gt;Now imagine two programs reading the same archive, one checking a signature and the other extracting files. That&amp;rsquo;s the Android &amp;ldquo;Master Key&amp;rdquo; bug from 2013, and the detail is better than the summary.&lt;/p&gt;
&lt;p&gt;An APK is a ZIP. The attacker puts two entries in it, both named &lt;code&gt;classes.dex&lt;/code&gt;. Android&amp;rsquo;s Java verifier loaded entries into a map keyed by filename, so a duplicate name overwrote the earlier one and &lt;strong&gt;the last entry was the one whose signature got checked&lt;/strong&gt;. The native installer used a hash table with linear probing that didn&amp;rsquo;t replace on collision, so &lt;strong&gt;the first entry was the one that got loaded and run&lt;/strong&gt;. Plant malicious code first, legitimately signed code second, and the device verifies one file and executes the other.&lt;/p&gt;
&lt;p&gt;A second bug the same year came from the same &amp;ldquo;two readings, one file&amp;rdquo; family, via a signed integer. The extra-field length is a 16-bit value, and the Java code read it &lt;em&gt;signed&lt;/em&gt;. A length of 65,533 (&lt;code&gt;0xFFFD&lt;/code&gt;) sign-extends to −3. Since the offset of the compressed data is computed by &lt;strong&gt;adding&lt;/strong&gt; that length, a negative value moves the read pointer backward into the header region instead of forward past it.&lt;/p&gt;
&lt;p&gt;The lesson generalizes past ZIP. Any format that stores the same fact twice has to decide what happens when the copies disagree, and &amp;ldquo;the spec doesn&amp;rsquo;t say&amp;rdquo; is the same answer as &amp;ldquo;attackers decide.&amp;rdquo;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;offsets-are-just-numbers&#34;&gt;Offsets Are Just Numbers&lt;/h2&gt;
&lt;p&gt;The Central Directory locates each file by offset. Nothing in the format says two entries can&amp;rsquo;t point at the same bytes.&lt;/p&gt;
&lt;p&gt;The classic zip bomb didn&amp;rsquo;t need that. &lt;code&gt;42.zip&lt;/code&gt; is 42 kilobytes of archives nested five layers deep, sixteen at each layer, unpacking to roughly 4.5 petabytes. The defense is obvious once you&amp;rsquo;ve seen it: cap recursion depth, don&amp;rsquo;t auto-extract nested archives.&lt;/p&gt;
&lt;p&gt;David Fifield&amp;rsquo;s 2019 construction doesn&amp;rsquo;t recurse at all. It expands in a &lt;strong&gt;single pass&lt;/strong&gt;, so depth limits are irrelevant. The trick is overlap: many Central Directory entries reference one shared kernel of compressed data, and each entry&amp;rsquo;s compressed stream uses DEFLATE&amp;rsquo;s stored-block mode to quote the &lt;em&gt;next&lt;/em&gt; entry&amp;rsquo;s local file header as literal bytes. Entries nest inside each other, and output grows quadratically against input.&lt;/p&gt;
&lt;p&gt;He published several, and they aren&amp;rsquo;t interchangeable:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;File&lt;/th&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;Compressed&lt;/th&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;Uncompressed&lt;/th&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;Ratio&lt;/th&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;Needs Zip64&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;&lt;code&gt;zbsm.zip&lt;/code&gt;&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;42 KB&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;5.5 GB&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;~130,000:1&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;No&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;&lt;code&gt;zblg.zip&lt;/code&gt;&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;10 MB&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;281.4 TB&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;~28,000,000:1&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;No&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;&lt;code&gt;zbxl.zip&lt;/code&gt;&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;46 MB&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;4.5 PB&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;~98,000,000:1&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;Yes&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The Zip64 requirement on the largest one matters, because not every reader supports Zip64, which makes the merely-enormous version the more portable weapon.&lt;/p&gt;
&lt;p&gt;None of these are malformed files. Every one is a valid archive that a conforming parser is supposed to accept. The format allows two entries to describe the same bytes, and no rule anywhere says the total uncompressed size has to bear any relationship to the file you&amp;rsquo;re holding.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;everything-is-secretly-a-zip&#34;&gt;Everything Is Secretly a ZIP&lt;/h2&gt;
&lt;p&gt;Once you know the structure, you start recognizing it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.docx&lt;/code&gt;, &lt;code&gt;.xlsx&lt;/code&gt;, &lt;code&gt;.pptx&lt;/code&gt; are ZIP archives of XML&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.jar&lt;/code&gt;, &lt;code&gt;.war&lt;/code&gt;, &lt;code&gt;.apk&lt;/code&gt; are ZIP archives of class files and resources&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.epub&lt;/code&gt; is a ZIP of XHTML&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.odt&lt;/code&gt;, &lt;code&gt;.ods&lt;/code&gt; are ZIP of XML again&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That&amp;rsquo;s not a coincidence or a hack. ISO/IEC 21320-1, &amp;ldquo;Document Container File,&amp;rdquo; defines a constrained ZIP profile for exactly this use. It narrows the format so a &lt;code&gt;.docx&lt;/code&gt; reader doesn&amp;rsquo;t have to implement all of ZIP&amp;rsquo;s accumulated history: compression must be stored or deflated and nothing else, and the various encryption and digital-signature mechanisms in the original spec are all forbidden.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s a narrowing, not a rewrite. Zip64 version 1 is still permitted, for instance; only version 2 is ruled out. The profile is best understood as a list of the parts of ZIP that turned out to be a bad idea.&lt;/p&gt;
&lt;p&gt;Which means the next post in this series is mostly about a ZIP file with XML inside it. You already know half of how a Word document works.&lt;/p&gt;
&lt;h2 id=&#34;sources&#34;&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT&#34;&gt;PKWARE APPNOTE.TXT&lt;/a&gt; — the original and still-authoritative ZIP specification, currently version 6.3.10; §4.3.16 defines the end of central directory record&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.iso.org/standard/60101.html&#34;&gt;ISO/IEC 21320-1:2015&lt;/a&gt; — the constrained ZIP profile used by document formats. Fair warning, this one is a paid ISO standard; the catalog page tells you what it covers but you cannot read the text without buying it&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.loc.gov/preservation/digital/formats/fdd/fdd000354.shtml&#34;&gt;Library of Congress format description for ZIP&lt;/a&gt; — history and preservation notes&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.bamsoftware.com/hacks/zipbomb/&#34;&gt;David Fifield: A Better Zip Bomb&lt;/a&gt; — the overlapping-stream construction&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;I&amp;rsquo;d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at &lt;a href=&#34;https://micro.blog/llbbl?remote_follow=1&#34;&gt;@logan@llbbl.blog&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
</description>
    </item>
    
  </channel>
</rss>