Skip to content

GZT

Level: 201 · for anyone with a hex editor open

One line: The .gzt loader asks two questions — the extension, then eight bytes at offset 6 — so the Java stream header and the block length in front of the magic are never checked at all: a file with byte 5 changed still passes, a file cut off at byte 14 still passes, and a file with the right bytes and the wrong name is never looked at.

What the loader reads

A Ghidra Zip Debugger Trace File is a debugger trace — the recording the Debugger makes of a target's memory and registers over time — packed with the same ItemSerializer as a GDT and a GZF. Its content-type string is Trace, from DBTraceContentHandler.TRACE_CONTENT_TYPE; everything else about the container is the GDT page's. No trace was recorded for this chapter, so nothing here is measured on a real .gzt; the program builds one from the source's rules and the page is about the check, which is the same three lines for all three formats.

In Python

Verified output of gzt_py.py — regenerated by tools/run_examples.py, never hand-typed.

1. TWO QUESTIONS: THE NAME, THEN EIGHT BYTES AT OFFSET 6
------------------------------------------------------------------------
   ac ed 00 05 77 2a 2e 30 21 26 34 e9 2c 20 00 00
   offset 6..14   2e 30 21 26 34 e9 2c 20   == MAGIC_NUMBER 0x2e30212634e92c20
   content type   'Trace'

   session-1.gzt    accepted by the GZT loader: True
   session-1.GZT    accepted by the GZT loader: True
   session-1.gzf    accepted by the GZT loader: False
   session-1        accepted by the GZT loader: False

   The extension is compared case-insensitively and it is compared
   first: the bytes are not read at all for a file not named .gzt.
   The same bytes named .gzf go to the GZF loader, which asks the same
   two questions with its own extension and would then find 'Trace'
   where it expected a Program, and stop.

2. WHAT THE EIGHT-BYTE CHECK DOES NOT LOOK AT
------------------------------------------------------------------------
   the real file                              isPackedFile True
   byte 5, the block length, set to 0xff      isPackedFile True
   bytes 0..4, the stream header, zeroed      isPackedFile True
   byte 4, the block tag, set to 0x7a         isPackedFile True
   one byte of the magic changed              isPackedFile False
   everything after byte 14 removed           isPackedFile True
   the ZIP part removed                       isPackedFile True

   Six of seven pass. The check reads exactly eight bytes and nothing
   else, so the serialization header, the block length, the strings
   and the ZIP are all beyond it: a recognised file and a loadable
   file are different things, and the loader finds out which it has
   only when it hands the bytes to the database layer.

3. THE MAGIC IS A NUMBER, AND ALSO EIGHT BYTES
------------------------------------------------------------------------
   big-endian bytes   2e 30 21 26 34 e9 2c 20
   as text            '.0!&4é, '
   read little-endian 0x202ce9342621302e

   0x2e30212634e92c20 is compared as a long through a big-endian
   converter, so the byte order is fixed in the reader, not the file.
   Seven of the eight bytes are printable ASCII and one, e9, is not:
   a magic chosen so that no text file starts this way at offset 6.

What a signature test reads, and what it does not

ItemSerializer.isPackedFile is short enough to quote in full: skip six bytes, read eight, convert them as a big-endian long, compare with MAGIC_NUMBER. Section 2 damages a file six ways and the check passes five of them, including a file that has nothing after the magic. That is not a defect in the check — it answers the question it was asked, is this one of ours — but it is the difference between a recognised file and a loadable one, and every loader on this list has some version of it: ELF accepts four bytes and finds out about the rest later, COFF accepts a machine number, Raw binary accepts anything. What the GZT check leaves outside is unusually concrete, because the file's first six bytes are somebody else's format — Java serialization's — and the check was written to step over them.

Section 1 is the other half: the extension is compared first, case-insensitively, and a file not named .gzt has its bytes read by nobody in this loader. The same bytes named .gzf go to the GZF loader, whose check is identical with its own extension, and which then finds Trace in the type field where it wanted Program. Three loaders, one container, and the filename is what routes a file to the loader that will refuse it.

The magic as bytes

Section 3 prints the magic three ways. As a number it is 0x2e30212634e92c20; as bytes, big-endian, 2e 30 21 26 34 e9 2c 20; and seven of those eight are printable ASCII — .0!&4, then e9, then , — which is to say the constant was chosen, or fell out, as something a person can half-read in a dump and a text file will not begin with. Ghidra compares through BigEndianDataConverter regardless of the machine, so the byte order of the check is the reader's decision and the file carries no say — the same arrangement as a Java class file, which is unsurprising in a format Java's own stream writer produced.

What Ghidra checks

GztLoader, in the Debugger module rather than with the other loaders, accepts a file whose lower-cased name ends in .gzt and for which isPackedFile is true; it then restores the packed database and throws File imported is not a Trace if the content is anything else. Its name in 12.1.3 is GZT Input Format, where the list has Ghidra Zip Debugger Trace File (GZT); it takes no options, and it cannot add a trace to an existing program.

If you are coming from Python or ABAP

Python. The check is one line, struct.unpack_from('>Q', data, 6)[0] == MAGIC, and the program wraps it with the extension test to make Ghidra's whole opinion. Writing the same test yourself, note what unpack_from does on a file shorter than 14 bytes — it raises struct.error — which is why the program's version checks the length first; Java's version reads what it can into a zeroed buffer and compares that, so a 10-byte file is not packed in both, by different routes.

ABAP. (Not machine-checked — CI cannot run ABAP.) A file type decided by its extension and then by one field at a fixed offset is the shape of cl_gui_frontend_services=>gui_upload with filetype declared by the caller: the routing is by a name somebody typed, and the content check comes after. The transferable rule is section 2's — write down what a signature test does not cover before trusting a file it passed, because a passed check says only that the bytes it read were right.

Try it

  1. Record any trace in Ghidra's Debugger, save it as .gzt, and xxd -l 64 it. Trace is after the name.
  2. cp session.gzt session.gzf and import the copy. Read the error message and find the word it uses for what it found.
  3. head -c 14 session.gzt > stub.gzt and import that. The opinion passes; the load does not.
  4. Change byte 5 with a hex editor and import. Then change byte 6.
  5. Take the eight magic bytes and printf them into a file at offset 6 with six zero bytes in front, name it .gzt, and see which of the two questions it answers.

See also

  • GDT — the container, and the ZIP with no central directory it ends in
  • GZF — a real packed file measured, and the name field's encoding
  • Raw binary — the loader whose check reads nothing at all
  • The first two bytes — the kernel's own two-byte check, and what it too does not read
  • ItemSerializer.isPackedFile — the eleven lines section 2 is about