HackBlue

Tools · Ghidra · 11 / 22

Ghidra: strings and the bytes they occupy

A string is data with an encoding, a length, and an address. The string search window is an index of runs that look like text. A defined string is one the database has committed to. Where it sits, and who references it, is the reading.

16 min read · practice only with authorization

Where the characters sit

A compiler places a string literal in a data block. On the harbor-check ELF that is .rodata, readable, not executable. On a typical PE the same role is played by .rdata. The instruction that uses the literal does not contain the characters. It contains a reference to their address, often a LEA or a MOV of an immediate address. If you find the words check.conf inside .text, look hard before you call it a literal: you may be looking at an immediate that happens to spell letters, or at data the disassembler consumed as code.

Encoding is part of the location. A C "..." literal on this lab program is a terminated byte string. A Windows program often stores user-visible text as UTF-16LE, so the Listing shows wide characters and the search needs the Unicode option enabled. A length-prefixed string, common in some compilers and languages, has a count byte or dword before the characters and no reliance on a terminator. The data type you apply has to match that layout, or the unit will swallow the next field.

The search window is an index, not a verdict

Search > For Strings scans for runs that meet a minimum length and an encoding you select. The result table is a place to navigate. Some rows are already defined data. Some are undecided bytes. Some are coincidences inside machine code or inside an integer table. Sort and filter to the words you care about. Double-click to look. Defining a hit, from the table or with the type chooser in the Listing, is what promotes a suspicion to a data unit the reference analyzer can attach edges to.

Alignment and minimum length change the noise. A minimum of four or five characters is a sane start. Dropping it to two fills the table with register-sized accidents. Requiring a terminator cuts false hits and also hides length-prefixed text. When the table surprises you, change one setting and run it again, and write the setting down if the result mattered.

A string earns its keep through its users

Once the string is defined, references to its address are the users you can prove. One read from the path-choosing function means that function takes the pointer. It does not, by itself, mean the file is opened, that the open is required, or that the path is the only path. Those are questions about the instructions after the load. Follow the reference, read the branch, and then write the sentence.

Zero references after the string is defined and reference analysis has been rerun means no direct operand names this address. The text might be unused, left over from a build, reached through a computed pointer, or not a string at all. Record “no direct reference” and move on. Deleting bytes from the database to make the table prettier destroys evidence you may want when a later function computes the address.

Lab: three strings, three rows

Produce a three-row table for harbor-check: the default path, the word missing, and the word bad-config. For each, record the address, the block name, the encoding, and the function that references it. The status words should be referenced from the function you will later call status_text, or from main if your optimizer inlined that function. The path should be referenced from the chooser.

Then pick one search hit that is not a real literal, if the table offered you any, and write why you rejected it. A short run inside .text with no reference is enough. The skill is sorting text-shaped bytes from strings the program actually takes the address of.

Snippet
A notebook row per string that matters

Text:        /etc/harbor/check.conf
Address:     <from your database>
Block:       .rodata   (readable, not executable)
Encoding:    ASCII, terminated
References:  one READ from the path-choosing function

Diagram

Three ways a run of characters can show up

  1. Bytes

    Characters occupy a block. Read-only literals usually sit in .rodata or .rdata. UTF-16 is common in Windows programs and occupies two bytes a character.

  2. Define

    A string analyzer, or a define action you take from a search hit, turns the run into a data unit.

  3. Index

    The string window lists address, text, and whether a reference exists. Double-click to move. Follow the reference to move into code.

  4. Sentence

    You write which function reads the text, and you stop short of claiming a behavior the function does not implement.

What you are allowed to conclude

  • Defined

    The Listing shows a string data unit, with a label and a length. References can attach to it. This is the form you want before you trust the xref list.

  • Search hit

    Search > For Strings found a run that meets the minimum length. It might already be defined, or it might still be undefined bytes that merely look like text.

  • No reference

    A defined string with an empty reference list has no direct operand pointing at it. It can still be unused, computed, or a false run inside other data.

A notebook row per string that matters
Text:        /etc/harbor/check.conf
Address:     <from your database>
Block:       .rodata   (readable, not executable)
Encoding:    ASCII, terminated
References:  one READ from the path-choosing function

Check yourself

1. Search > For Strings lists "bad-config", and the reference column is empty. The bytes are still undefined in the Listing. What is the careful next step?

2. A Windows lab sample shows the characters R u n separated by zero bytes, in a block named .rdata. What encoding are you looking at?

3. The default config path is defined in .rodata and has one read reference from the path-choosing function. What do you write?