GDT¶
Level: 201 · for anyone with a hex editor open
One line: A .gdt is a Java serialization stream carrying a 44-byte block — a magic, a version, two writeUTF strings, a type and a length — followed by a ZIP with one entry and no central directory, which unzip and Python's zipfile both refuse and raw zlib inflates without complaint.
What the loader reads¶
A Ghidra Data Type Archive is Ghidra's own file: a database of C types — windows_vs12_64.gdt, mac_osx.gdt, generic_clib_64.gdt — packed into one file by ItemSerializer.outputItem, the same routine that writes a GZF and a GZT. There is no specification but the source, and the source is short: an ObjectOutputStream writes a long, an int, two strings, an int and a long; then a ZipOutputStream writes one deflated entry named FOLDER_ITEM; then the file is closed. What that produces, byte by byte:
| offset | bytes | what wrote it |
|---|---|---|
| 0 | ac ed 00 05 |
ObjectOutputStream's stream header: STREAM_MAGIC, STREAM_VERSION 5 |
| 4 | 77 |
TC_BLOCKDATA: a block of primitive data follows |
| 5 | one byte | the block's length — 44 for an item named DTArchive, 42 for one named Archive |
| 6 | 8 bytes | MAGIC_NUMBER, 0x2e30212634e92c20, big-endian |
| 14 | 4 bytes | FORMAT_VERSION, 1 |
| 18 | 2 + n | writeUTF(itemName): a 16-bit byte count, then modified UTF-8 |
| 2 + n | writeUTF(contentType): Archive for a GDT, Program for a GZF, Trace for a GZT |
|
| 4 | fileType |
|
| 8 | length: the size of the database before compression |
|
| 6 + block | 50 4b 03 04 |
a ZIP local file header, flags 0x0808, method 8, sizes zero |
the deflated database, then 50 4b 07 08 and a data descriptor — and then the file ends |
In Python¶
Verified output of gdt_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. A JAVA SERIALIZATION STREAM, THEN ONE BLOCK OF DATA
------------------------------------------------------------------------
ac ed 00 05 77 2c 2e 30 21 26 34 e9 2c 20 00 00
00 01 00 09 44 54 41 72 63 68 69 76 65 00 07 41
72 63 68 69 76 65 00 00 00 00 00 00 00 00 00 00 10 00
bytes 0..4 ac ed 00 05 STREAM_MAGIC ac ed, STREAM_VERSION 5: ObjectOutputStream's header
byte 4 0x77 TC_BLOCKDATA: a block of primitive data follows
byte 5 0x2c = 44 its length in bytes
bytes 6..14 2e 30 21 26 34 e9 2c 20 MAGIC_NUMBER 0x2e30212634e92c20, big-endian
then FORMAT_VERSION 1
writeUTF item name b'DTArchive' (9 bytes, after a 2-byte count)
writeUTF content type b'Archive'
fileType 0 length 4096 = the payload's size before compression
block ends at 50 = 6 + 44
writeLong, writeInt, writeUTF: Java's DataOutput, big-endian, with
writeUTF counting bytes of modified UTF-8 in a 16-bit field. The
whole 44 bytes travel as one serialization block.
2. THEN A ZIP WITH ONE ENTRY AND NO CENTRAL DIRECTORY
------------------------------------------------------------------------
at 50: local header 0x04034b50 flags 0x0808 method 8 (deflate) entry b'FOLDER_ITEM'
crc 0x0 sizes 0/0: all zero, because flag bit 3 says a data descriptor follows the data
PK\1\2 (central directory) present: False PK\5\6 (end record) present: False
last 16 bytes 50 4b 07 08 18 ba fd 13 1e 00 00 00 00 10 00 00 the descriptor: PK\7\8, crc, compressed size, size
ItemSerializer calls closeEntry() and flush() on a ZipOutputStream
and never finish(): the central directory is never written. A
stream reader that walks local headers does not need one; an
archive reader that starts from the end cannot find anything.
3. WHO CAN OPEN IT
------------------------------------------------------------------------
isPackedFile (8 bytes at offset 6 == magic) True
zipfile.ZipFile on the ZIP part refused: BadZipFile
zlib, raw deflate from byte 91 4096 bytes out; descriptor crc matches: True; size matches: True
the payload begins b'/01,4),*'
Same bytes, three verdicts. The Java stream reader Ghidra uses
reads entries in order and is satisfied; unzip and zipfile look
for the end record first and give up; zlib does not know what a
ZIP is and inflates what it is pointed at.
4. WHAT A REAL ONE HOLDS
------------------------------------------------------------------------
item 'DTArchive' content type 'Archive' block length 44 length field 0xcac000 = 13,287,424 bytes unpacked
item 'Archive' content type 'Archive' block length 42 length field 0x74000 = 475,136 bytes unpacked
The sixteen .gdt files Ghidra 12.1.3 ships all open ac ed 00 05 77
and then 2c or 2a -- 44 or 42 -- because 'DTArchive' is two bytes
longer than 'Archive'. The block length is the only byte that
varies before the magic, and the magic is what the loader reads.
Measured: the sixteen archives Ghidra ships¶
all 16 files ac ed 00 05 77 2c|2a 2e 30 21 26 34 e9 2c 20 00 00 00 01 ...
mac_osx.gdt 3,272,587 bytes on disk
block length 44 magic 0x2e30212634e92c20 version 1
itemName 'DTArchive' contentType 'Archive' fileType 0 length 13,287,424
ZIP local header at 50: flags 0x0808, method 8, entry 'FOLDER_ITEM', crc/sizes 0
raw inflate from byte 91: 13,287,424 bytes; descriptor crc b94c5f5e matches; length matches
PK\1\2 present: False PK\5\6 present: False
unzip: End-of-central-directory signature not found
zipfile: BadZipFile
file(1): Java serialization data, version 5 (file-5.41 and file-5.45 alike)
the inflated payload begins 2f 30 31 2c 34 29 2c 2a '/01,4),*'
Nine of the sixteen are named DTArchive inside and seven Archive, which is the whole of why the block-length byte is 2c in some and 2a in others. file(1) reads the first four bytes and stops, correctly: it is Java serialization data, and everything Ghidra-specific begins at byte 6.
A ZIP with no end¶
Section 2 is the finding. ItemSerializer calls closeEntry() and flush() on its ZipOutputStream and never finish() or close(), so the central directory and the end-of-central-directory record are never written — the file stops after the data descriptor. Java's own ZipInputStream does not mind, because it reads local headers in order and never looks for the end. Every reader that starts from the end, which is every archive tool (Android APK is the page on why), finds nothing: unzip says the signature is missing, zipfile raises. Section 3 opens the same bytes three ways, and zlib — which knows nothing of ZIP and inflates from wherever it is pointed — gets the whole database back with the CRC intact.
So the file is a ZIP by the letter of the local-header format and not a ZIP to any ZIP program, which is a version of binary is a verdict: whether these bytes are an archive is a fact about the reader.
The strings are Java's¶
writeUTF is DataOutput's: a 16-bit count of bytes, then modified UTF-8 — the Java class file encoding, with NUL as c0 80 and an emoji as six bytes. For a GDT the two strings are ASCII and the distinction is invisible; the GZF page, where the item name is the program's, works it through.
What Ghidra checks¶
GdtLoader ↗ accepts a file when its name ends in .gdt and ItemSerializer.isPackedFile says yes — which skips six bytes, reads eight as a big-endian long, and compares with MAGIC_NUMBER. Nothing before offset 6 and nothing after 14 is examined; the GZT page measures what that leaves out. The loader's name in 12.1.3 is Ghidra Data Type Archive Format, and importing a .gdt produces a data type archive in the project, not a program.
If you are coming from Python or ABAP¶
Python. There is no ObjectInputStream in the standard library and none is needed: the block is struct.unpack_from('>QI', data, 6), two '>H'-prefixed strings, and '>iq'. For the ZIP part do not reach for zipfile — it will raise — but for zlib.decompressobj(-15), which is raw deflate with no header; its unused_data afterwards is the data descriptor, and zlib.crc32 over the output should equal the descriptor's second word. zlib.compress(b)[2:-4] is the matching writer: strip zlib's two-byte header and four-byte trailer and what is left is what a ZIP stores.
ABAP. (Not machine-checked — CI cannot run ABAP.) cl_abap_zip expects a central directory and will not open this file; cl_abap_gzip speaks gzip and zlib, not raw deflate, so neither standard class reaches the payload. The shape to recognise is a stream format that a streaming reader handles and a random-access reader does not — an IDoc file without a control-record index has the same property — and the fix is to read it in order, not to look for an index that was never written.
Try it¶
xxd -l 64any.gdtunder your Ghidra'sFeatures/Base/data/typeinfo/. Read the block length at byte 5 and the item name after byte 18.fileit. Thenunzip -lit. Then, in Python, inflate it withzlib.decompressobj(-15)from the byte afterFOLDER_ITEM.- Compare the
lengthfield in the block with the number of bytes the inflate returns. strings -n 8 payload | headon the inflated bytes. The first eight are the buffer file's own magic.- Rename a
.gdtto.gzfand offer it to Ghidra's importer. The loader that accepts it will say what it found inside.
See also¶
- GZF — the same container with
Programin the type field, and a name that is not ASCII - GZT — the two-question check, and everything it does not look at
- Android APK — why a ZIP is read from the end, and so why this one cannot be
- Java class file — the encoding
writeUTFwrites - Binary is a verdict, not a property — whether these bytes are a ZIP depends on who is asked
ItemSerializer.java↗ — the sixty lines that define the format