HackBlue

Tools · Ghidra · 10 / 22

Ghidra: data types and why the decompiler changes

A type is a promise about how bytes are grouped: a pointer, an integer, a structure, an enum. The decompiler believes the promise immediately and reprints the function. A careful type makes the reprint calmer. A wrong type makes it lie in complete sentences.

17 min read · practice only with authorization

A type is a promise about bytes

Until you say otherwise, Ghidra labels values by width. undefined4 is four bytes with no agreed meaning. A pointer, an int, and a float can all occupy four or eight bytes depending on the platform, so the width is the only fact the bytes forced. A type you apply is an additional promise: these bytes are a char *, this return value is the enum of status codes, this address is the first field of a structure. The decompiler applies the promise at once and rewrites every use.

That rewrite is the reason types feel powerful and the reason they need a check. Apply char * to the path and fopen’s call should read as a path. If the decompiler instead starts indexing the pointer as an array of structures, the promise was too specific or simply wrong. Go back to the Listing. Look at the instruction that produced the value. A LEA of a string address supports a pointer. A compare of a small immediate supports an integer status. Let the instruction pick the type.

Where you apply it

In the Listing, T opens the data type chooser for the unit under the cursor. That is how you turn undefined bytes into a string, a pointer, or a dword. In the decompiler, right-click a variable and choose Retype Variable. The function signature editor is the right place when the change is a parameter or the return type, because those belong to the function, not to one use. After a retype, read the whole function from the top. A type that fixes the call and breaks the loop is not finished.

Commit Locals and Commit Params/Return write the decompiler’s current picture back into the database. Use them when the picture is calm. If you commit in the middle of a wrong type, you have frozen the lie as a set of names. Undo exists inside the session. Save exists so the good version outlives the session. Use both.

Archives, structures, and enums

The Data Type Manager is the tree of types available to this program. Built-in types are always there. Ghidra ships archives for common platforms and for the C library. Opening an archive lets you apply size_t, FILE, or a Windows type without inventing the layout. You can also parse C headers you are allowed to use, via the parse-C-source action, when the program really was built against those headers. Parsed types that do not match this binary will make the decompiler confident and wrong. Prefer a small enum or a small structure you can justify from the instructions.

A structure is justified when several offsets from the same base pointer are used consistently. If you keep seeing reads at offset 0 and offset 8 from one parameter, those are two fields, and a structure with those members will make every use readable. Auto Create Structure, from the decompiler’s menu on a pointer, drafts that structure from the accesses it can see. Read the draft. Delete members that came from a stray cast. The same standard applies to the status enum in the lab: three names, the three values the branches actually return.

Lab: type the path and the status

In the function that opens the file, retype the path parameter as char *. Confirm the decompiler now passes that pointer to fopen as the path, and confirm the Listing still shows the pointer arriving in the first argument register. In the function that returns the words ok, missing, and bad-config, create an enum with values 0, 1, and 2 and apply it to the argument that is compared. The decompiler’s switch or if-chain should become readable. If your compiler emitted a jump table, typing the argument should not destroy the table. If the view suddenly fills with casts, undo and look at the compares again.

Write a before-and-after pair in the notebook for one line only. The before line contains undefined or a raw integer. The after line contains the type you applied. That pair is the evidence that the type earned its place.

Snippet
A status enum you can create because you wrote the program

enum harbor_status {
  HARBOR_OK = 0,
  HARBOR_MISSING = 1,
  HARBOR_BAD = 2
};

Apply it to the status variable only after the branches
return those values. The enum is your note, stored as a type.

Diagram

The same parameter, before and after a type

  1. Width

    undefined1, undefined4, and undefined8 already say how many bytes are involved.

  2. Chooser

    The T key in the Listing, or Retype Variable in the decompiler, opens the type chooser.

  3. Reprint

    The decompiler reprints from the new promise. Scroll the body before you trust a single line.

  4. Archive

    The Data Type Manager holds built-in types, types parsed from headers you are allowed to use, and types you create, such as an enum of status codes.

What a type changes in the reading

undefined8

Promise. Eight bytes, no interpretation.

Decompiler. Pointer math shows up as integer arithmetic, or as a cast on every use.

Risk. You describe a bug that is only the missing type.

char *

Promise. These eight bytes are a pointer to a character.

Decompiler. The parameter is passed to fopen as the path, and indexing looks like a string.

Risk. The promise is still yours. The Listing’s LEA or MOV has to agree.

A status enum you can create because you wrote the program
enum harbor_status {
  HARBOR_OK = 0,
  HARBOR_MISSING = 1,
  HARBOR_BAD = 2
};

Apply it to the status variable only after the branches
return those values. The enum is your note, stored as a type.

Check yourself

1. The path parameter is undefined8, and the Listing shows it being passed to fopen in the first argument register. What type matches that evidence?

2. You retype a parameter and the decompiler turns a simple load into a nest of casts and shifted fields. What should you suspect first?

3. You want the status codes 0, 1, and 2 to print as names in the decompiler. Where does that name set live?