HOME ABOUT PAGES

Zip Heck [forensics]

solved by not_really

Writeup by not_really

I zipped up the flag a few times for extra security.

https://files.uiuc.tf/flag.zip

The intended solution runs in under 10 minutes on a typical computer.

Author: kuilin

File mirror: flag.zip

I actually had a computer normally extract this zip and it crashed a few times but eventually it was able to decode the zip successfully. So if someone didn’t know any better they could have done that and gotten the flag without any skill. But surprisingly this problem only has 12 solves.

I originally tried loop extracting the zip the normal way hoping I would get somewhere, but it seemed like it was taking forever.

import zipfile
import os

if not os.path.exists("temp"):
    os.mkdir("temp")

while True:
    with zipfile.ZipFile("flag.zip", "r") as flagZip:
        flagZip.extractall("temp")
    os.remove("flag.zip")
    os.rename("temp/flag.zip", "flag.zip")
    size = os.path.getsize("flag.zip")
    print(f"now at size {size}b")

It all seems to be going fine until:

now at size 47098999b
now at size 47084853b
now at size 47075775b
now at size 47207829b
now at size 47207683b
now at size 47207537b

Wait a second, did the size just get bigger? It seems to decompress faster for a little bit before going slow again. Let’s investigate.

image-20200722182057512

Hmm, isn’t zip supposed to compress this kind of stuff? But no, we see lots of flag.zip and UIUC. Let’s look in 7-zip.

image-20200722182338982

Ah, as I thought. It looks like instead of using the default Deflate method, we’re using Store. That means that there’s no compression here. Most likely we need to get rid of the chain of Store quicker than normally extracting would be.

The top of each uncompressed zip is 0x26 bytes:

image-20200722182913001

And the bottom is 0x6C:

image-20200722183112065

So now we need to know when to stop manually stripping the uncompressed bits off and go back to extractall for the Deflate parts.

image-20200722183444003

Looks like when it goes back to being compressed, this byte here is set to 0x08.

To ignore the Store zips, we’ll look at the first 0x08 bytes of the zip. If it’s a 0x00, we seek 0x26 bytes and check if the next byte is a 0x00 and so on. If it’s a 0x08, we move back to the beginning of the header and read the whole zip file. For every header there’s also a “footer” of 0x6C bytes, so the length of the zip file we’ll read is origSize - (0x26+0x6c) * storeHeadersRead.

Looks like we have everything we need, let’s write up the code.

import zipfile
import os

if not os.path.exists("temp"):
    os.mkdir("temp")

while True:
    size = os.path.getsize("flag.zip")
    print(f"now at size {size}b")
    
    with open("flag.zip", "rb") as flagZipBytes:
        flagZipBytes.seek(0x08)
        if flagZipBytes.read(1) == b'\x00': # store
            print("detected store chain!")
            count = 1
            while True:
                flagZipBytes.seek(0x08 + 0x26 * count)
                if flagZipBytes.read(1) == b'\x08': # deflate
                    print(f"back to deflate (unzipped {count} times)")
                    # move back to beginning of zip header
                    flagZipBytes.seek(0x26 * count)
                    # original size - header - footer
                    sizeOfNewZip = size - (count * 0x26) - (count * 0x6c)
                    newFlagZipBytes = flagZipBytes.read(sizeOfNewZip)
                    with open("temp/flag.zip", "wb") as newFlagZip:
                        newFlagZip.write(newFlagZipBytes)
                    
                    flagZipBytes.close()
                    os.remove("flag.zip")
                    os.rename("temp/flag.zip", "flag.zip")
                    break
                else:
                    count += 1
    
    with zipfile.ZipFile("flag.zip", "r") as flagZip:
        flagZip.extractall("temp")
    os.remove("flag.zip")
    os.rename("temp/flag.zip", "flag.zip")

Once it throws an exception, check the temp folder for the flag.