GZF¶
Level: 201 · for anyone with a hex editor open
One line: A .gzf is the same packed-file container as a .gdt with Program in its content-type field — measured: a 14-byte program packs a 409,600-byte database into 5,312 bytes — and its item name is written by Java's writeUTF, which counts modified-UTF-8 bytes in a 16-bit field, so the longest program name depends on which letters it has.
What the loader reads¶
A Ghidra Zip File is an exported program: File > Export Program with the Ghidra Zip File format, or a .gzf attached to a bug report. It is produced by DomainFile.packFile, which calls the same ItemSerializer.outputItem that writes a GDT, so the container is that page's: a Java serialization header, one block of primitive data holding a magic, a version, two strings, a type and a length, then a deflated ZIP entry named FOLDER_ITEM and no central directory. The differences are the two strings — the item name is the program's, the content type is Program — and what the inflated payload is: a Ghidra database buffer file, which begins with its own eight-byte signature.
In Python¶
Verified output of gzf_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. THE CONTAINER, WITH 'Program' IN THE TYPE FIELD
------------------------------------------------------------------------
ac ed 00 05 77 2c 2e 30 21 26 34 e9 2c 20 00 00
00 01 00 09 68 65 6c 6c 6f 2e 62 69 6e 00 07 50
72 6f 67 72 61 6d 00 00 00 00 00 00 00 00 00 00 10 00
block length 44 item b'hello.bin' content type b'Program'
Byte for byte the GDT layout: the same magic at 6, and the type
string is what says whether the ZIP holds a program, a data type
archive or a trace. The GZF loader reads the extension and the
magic, and finds out which it has when it unpacks.
2. writeUTF COUNTS BYTES OF MODIFIED UTF-8
------------------------------------------------------------------------
'hello.bin' 9 chars count 00 09 = 9 bytes 68 65 6c 6c 6f 2e 62 69 6e
'café.bin' 8 chars count 00 09 = 9 bytes 63 61 66 c3 a9 2e 62 69 6e
'a\x00b' 3 chars count 00 04 = 4 bytes 61 c0 80 62
'😀.bin' 5 chars count 00 0a = 10 bytes ed a0 bd ed b8 80 2e 62 69 6e
The item name is a Java String written by DataOutput.writeUTF: two
bytes of count, then modified UTF-8, the encoding of the Java class
file and of DEX. A NUL costs two bytes and an emoji six, and the
count is of those bytes -- the third format here that carries it.
3. THE COUNT IS 16 BITS, AND SO IS THE NAME'S CEILING
------------------------------------------------------------------------
65535 ASCII characters writes
65536 ASCII characters refused: UTFDataFormatException: encoded string too long
32768 é's refused: 65536 encoded bytes
Java throws UTFDataFormatException past 65,535 encoded bytes, so
the longest program name a GZF can carry depends on its letters.
4. A LONG NAME MOVES THE MAGIC
------------------------------------------------------------------------
name of 9 chars block 44 bytes header byte 0x77 TC_BLOCKDATA magic at offset 6
name of 200 chars block 235 bytes header byte 0x77 TC_BLOCKDATA magic at offset 6
name of 230 chars block 265 bytes header byte 0x7a TC_BLOCKDATALONG magic at offset 9
ObjectOutputStream writes a block's length in one byte up to 255
and in four above that, with a different tag. Ghidra's isPackedFile
reads eight bytes at offset 6 unconditionally, so -- computed here,
not tried on Ghidra -- a name long enough to push the block past
255 bytes leaves the magic at offset 9, where the check never looks.
Measured: a real one¶
Ghidra 12.1.3 was asked, headless, to import a 14-byte file as raw x86-64 bytes and pack the result:
hello.gzf 5,312 bytes
00000000: aced 0005 772c 2e30 2126 34e9 2c20 0000 ....w,.0!&4., ..
00000010: 0001 0009 6865 6c6c 6f2e 6269 6e00 0750 ....hello.bin..P
00000020: 726f 6772 616d 0000 0000 0000 0000 0006 rogram..........
00000030: 4000 504b 0304 1400 0808 0800 ac5b 2d5d @.PK.........[-]
block length 44 itemName 'hello.bin' contentType 'Program' fileType 0 length 409,600
ZIP entry 'FOLDER_ITEM', raw inflate: 409,600 bytes; descriptor crc matches; no central directory
the inflated payload begins 2f 30 31 2c 34 29 2c 2a '/01,4),*'
A program of fourteen bytes is a database of 409,600, and the database deflates to about five kilobytes because it is nearly all zeros. 00 06 40 00 is the length field, big-endian: 0x64000. The first eight bytes of the payload are the same in every packed file this chapter opened — the GDT's too — and are the buffer file's own magic, one container inside another.
The name is a Java string¶
Section 2 is what a GZF adds to the GDT page. The item name is the program's name as Ghidra knows it, and writeUTF writes it as a 16-bit byte count followed by modified UTF-8 — café.bin is 8 characters and 9 bytes, a NUL is c0 80, an emoji six bytes. The count is of encoded bytes, so section 3's ceiling of 65,535 is reached by 65,535 ASCII letters or 32,767 és, and Java refuses the string with UTFDataFormatException rather than truncating it. The Java class file has the same field with the same limit, for the same reason: writeUTF and the class file's CONSTANT_Utf8_info are the same format, and ObjectOutputStream is where a Java program meets it without meaning to.
Where the magic moves¶
Section 4 is a consequence nobody measured on Ghidra, and the page says so. ObjectOutputStream writes a block of up to 255 bytes with a one-byte length after TC_BLOCKDATA, and a longer block with a four-byte length after TC_BLOCKDATALONG. The 44-byte block has room for about 200 characters of name; past that the tag changes, the length grows by three bytes, and the magic that isPackedFile reads at offset 6 is at offset 9. Computed, not tried: a program named with 230 ASCII characters would pack into a file the loader does not recognise as its own. Whether Ghidra lets a name get that long is a different question, and not one a byte layout answers.
What Ghidra checks¶
GzfLoader ↗ accepts a file whose name ends in .gzf and for which ItemSerializer.isPackedFile is true, copies it to a temporary file, and restores it into the project as a PackedDatabase; if what comes out is not a Program it throws File imported is not a Program. Its name in 12.1.3 is GZF Input Format, where the list has Ghidra Zip File (GZF). Nothing in the import dialog's opinion depends on the content-type string; only the unpacking does.
If you are coming from Python or ABAP¶
Python. s.encode('utf-8') is the wrong encoder for the name field on two characters and right on every other, which is the dangerous kind of wrong: the twelve-line mutf8() in the program is the one to keep. struct.pack('>H', len(b)) is the count, and the ValueError the program raises past 65,535 is standing in for Java's exception. To open a real GZF, the GDT page's recipe applies unchanged — the type string is the only field that differs.
ABAP. (Not machine-checked — CI cannot run ABAP.) A length prefix counted in encoded bytes rather than characters is the shape of every xstring-carrying interface: the count you store is xstrlen of the converted bytes, not strlen of the source, and for a non-ASCII name the two differ. Convert first, count second, and store the count of what you stored.
Try it¶
- Export any program from Ghidra as a
.gzfandxxd -l 64it. Find the program's name after byte 18 andProgramafter it. - Read the eight-byte length field before
PKand compare it with whatzlib.decompressobj(-15)gives back. - Rename the program to something with an accent, export again, and compare the count bytes at 18–19 with the character count.
- Rename the
.gzfto.gdtand offer it to the importer. Which loader claims it, and what does it say when it unpacks? filethe export. It says Java serialization data, which is true and not the interesting half.
See also¶
- GDT — the container taken apart byte by byte, and the ZIP with no end
- GZT — the two-question check, and what it never reads
- Java class file — where
writeUTF's encoding and 16-bit count come from - DEX — the same encoding with a different length rule
- Raw binary — the loader the measured file above was imported with
GzfExporter.java↗ — the export side:saveToPackedFile, and nothing else