Shattered Tablet and The Basics of Ghidra Scripting

2025 Jan 08

Why I started this.

Recently I was doing one of the reverse engineering challenges on HackTheBox (not a paid promotion(HTB, if you're reading this we can fix that)) and could have solved the challenge by doing some work on a piece of paper but instead decided to turn it into an over engineered learning opportunity on scripting with Ghidra.

The challenge

What started all of this comes from Shattered Tablet, a challenge that states "Deep in an ancient tomb, you've discovered a stone tablet with secret information on the locations of other relics. However, while dodging a poison dart, it slipped from your hands and shattered into hundreds of pieces. Can you reassemble it and read the clues?"

After analyzing the binary, the decompiled main function looks thus:

undefined8 main(void)

{
  char local_48 [64];
  
  local_48[0] = '\0';
  local_48[1] = '\0';
  local_48[2] = '\0';
  local_48[3] = '\0';
  local_48[4] = '\0';
  local_48[5] = '\0';
  local_48[6] = '\0';
  local_48[7] = '\0';
  local_48[8] = '\0';
  local_48[9] = '\0';
  local_48[10] = '\0';
  local_48[0xb] = '\0';
  local_48[0xc] = '\0';
  local_48[0xd] = '\0';
  local_48[0xe] = '\0';
  local_48[0xf] = '\0';
  local_48[0x10] = '\0';
  local_48[0x11] = '\0';
  local_48[0x12] = '\0';
  local_48[0x13] = '\0';
  local_48[0x14] = '\0';
  local_48[0x15] = '\0';
  local_48[0x16] = '\0';
  local_48[0x17] = '\0';
  local_48[0x18] = '\0';
  local_48[0x19] = '\0';
  local_48[0x1a] = '\0';
  local_48[0x1b] = '\0';
  local_48[0x1c] = '\0';
  local_48[0x1d] = '\0';
  local_48[0x1e] = '\0';
  local_48[0x1f] = '\0';
  local_48[0x20] = '\0';
  local_48[0x21] = '\0';
  local_48[0x22] = '\0';
  local_48[0x23] = '\0';
  local_48[0x24] = '\0';
  local_48[0x25] = '\0';
  local_48[0x26] = '\0';
  local_48[0x27] = '\0';
  local_48[0x28] = '\0';
  local_48[0x29] = '\0';
  local_48[0x2a] = '\0';
  local_48[0x2b] = '\0';
  local_48[0x2c] = '\0';
  local_48[0x2d] = '\0';
  local_48[0x2e] = '\0';
  local_48[0x2f] = '\0';
  local_48[0x30] = '\0';
  local_48[0x31] = '\0';
  local_48[0x32] = '\0';
  local_48[0x33] = '\0';
  local_48[0x34] = '\0';
  local_48[0x35] = '\0';
  local_48[0x36] = '\0';
  local_48[0x37] = '\0';
  local_48[0x38] = '\0';
  local_48[0x39] = '\0';
  local_48[0x3a] = '\0';
  local_48[0x3b] = '\0';
  local_48[0x3c] = '\0';
  local_48[0x3d] = '\0';
  local_48[0x3e] = '\0';
  local_48[0x3f] = '\0';
  printf("Hmmmm... I think the tablet says: ");
  fgets(local_48,0x40,stdin);
  if (((((local_48[0x22] == '4') && (local_48[0x14] == '3')) && (local_48[0x24] == 'r')) &&
      ((((local_48[1] == 'T' && (local_48[0x15] == 'v')) &&
        ((local_48[6] == '0' && ((local_48[0x27] == '}' && (local_48[0x26] == 'd')))))) &&
       (local_48[0x1f] == 'r')))) &&
     ((((((local_48[0x1d] == '3' && (local_48[8] == '3')) && (local_48[0x16] == 'e')) &&
        ((local_48[0x23] == '1' && (local_48[5] == 'r')))) &&
       ((local_48[0] == 'H' && ((local_48[0x20] == '3' && (local_48[0x12] == '.')))))) &&
      (((((local_48[0xd] == '4' &&
          (((((local_48[3] == '{' && (local_48[10] == '_')) && (local_48[0x10] == '.')) &&
            ((local_48[4] == 'b' && (local_48[7] == 'k')))) && (local_48[0xf] == 't')))) &&
         (((local_48[0xe] == 'r' && (local_48[0x13] == 'n')) &&
          ((local_48[0x19] == 't' &&
           (((local_48[0x11] == '.' && (local_48[9] == 'n')) && (local_48[0x1e] == '_')))))))) &&
        (((local_48[0x1a] == '0' && (local_48[0x18] == '_')) && (local_48[0xc] == 'p')))) &&
       ((((local_48[0x17] == 'r' && (local_48[0x1c] == 'b')) &&
         ((local_48[0x21] == 'p' &&
          (((local_48[2] == 'B' && (local_48[0x1b] == '_')) && (local_48[0xb] == '4')))))) &&
        (local_48[0x25] == '3')))))))) {
    puts("Yes! That\'s right!");
  }
  else {
    puts("No... not that");
  }
  return 0;
}

As we can see, an array is created with a length of 64 (0x40) bytes. fgets is then called to read 0x40 from stdin and store those bytes into the local_48 array. Then, a complicated if statement compares the input stored in local_48 byte by byte in an unsorted way.

One could solve this by hand by finding the first index within the if local_48[0] == 'H' and jotting that down on paper, then going to the next index local_48[1] == 'B' and repeating this processes until we have the flag.

But that's boring and doesn't make you better. So let's do something unnecessary and infinitely harder.

Java vs. Python

Ghidra has two language options for interacting with the API. Java, which is the language Ghidra was created in, and Python (kinda.) Selecting Java as my scripting language for this project really isn't about "why Java," but "why not Python." Python holds a special place in many programmers, hackers, and reverse engineer's hearts. It's approachable, often time it's the first language people learn, and there's a gorillian libraries out there that usually just take care of the thing you want to do. That being said, readers should know that I'm the biggest hater of Python. If Python has no haters then I am dead. There's many reasons for this but I'll keep them in scope of Ghidra to limit the amount of ire invoked by Python fanatics.

  • The official API doesn't support Python3. Python2 support ended in 2020.
  • It's slower.
  • The API doesn't make sense in a pythonic way. Consider the code:
from ghidra.app.decompiler.flatapi import FlatDecompilerAPI
from ghidra.program.flatapi import FlatProgramAPI

fpapi = FlatProgramAPI(getState().getCurrentProgram())
fdapi = FlatDecompilerAPI(fpapi)

for x in dir(fdapi): print(x)

main_decomp = fdapi.decompile(fpapi.getFunction('main'))
print(main_decomp)

Instantiating an object and using methods to accomplish all the work is 90% of Java.

Essentially you're taking a performance hit, taking a risk on something going wrong that would be impossible to fix during the conversion of python2 -> Jython -> Java workflow just to almost write Java anyway.

Setup

First, we'll setup the Ghidra/Eclipse integration so we can use the LSP features. Be warned if you're using Arch Linux or a distro that uses the AUR repos that you're missing some necessary files and will need to install Ghidra from the (official repository)[https://github.com/NationalSecurityAgency/ghidra].

First, ensure that you're using the appropriate Java version for GhidraDev. You can find that in <path_to_ghidra>/Extensions/Eclipse/GhidraDev/GhidraDev_README.html. as of 4.0.0 GhidraDev requires JDK 21. If you don't do this and are running something like JDK 17, there won't be any obvious errors. Just things silently breaking on the back end.

Next, in Eclipse, install the GhidraDev plugin. In the help menu, click on "Install New Software" image

Then, click "Add..." and "Archive..." image And browse to the GhidraDev-*.zip file.

After a few checks on trust and a prompted restart of Eclipse, we should be ready to get to work.

In Ghidra, click "Window" and open "Script Manager" image

As you can see here, Ghidra comes with a few premade scripts but none that fit our current predicament. In the top right of the window, click "Create Script" and select "Java" image

Next it'll ask you where you want to save the Scripts and what filename to save it as. I'll name it SortVar48. image

Finally, to see if the intergration as worked, we right click our new script and select "Edit with Eclipse."

image

If everything works, we should see that the LSP can poll Ghidra specific information to give us nice IDE features. As a sidenote, I'm using Hyprland, which both Ghidra and Eclipse seems to hate, so my colors or fonts might look very different from yours.

image

Do it.

With everything set up, we can get to work. There's a few tags that help improve our script management and quality of life via tags.

  • @author Pretty self explanatory. This will set the name of the author for the script manager to display.
  • @category Determines where the script appears within the category tree.
  • @keybinding Sets a keyboard shortcut for the codebrowser window to run the script. Something like //@keybinding K You must be in the Listing window for this to work (as far as I can tell)
  • @toolbar Places a button on the toolbar that executes the script. Can be used with a custom image too. //@toolbar /path/to/image

First, we'll try to decompile the "main" function and print the result into the scripting console.

//@author numonce
//@category CTF
//@keybinding K
import ghidra.app.decompiler.DecompInterface;
import ghidra.app.script.GhidraScript;
public class SortVar48 extends GhidraScript {
    public void run() throws Exception {
    	DecompInterface ifc = new DecompInterface(); //init the decompiler interface
    	Program program = getCurrentProgram();
    	ifc.openProgram(program);
    	Function main = getGlobalFunctions("main").getFirst(); //Get the first Function with "main" in the namespace.
    	String decomp = ifc.decompileFunction(main, 0, monitor).getCCodeMarkup().toString(); //Decompile the main function and convert the result into a String type.
    	println(decomp);
    }
}

As shown above, we're creating a DecompInterface and hooking it into to our current program. We then get the main function by getting a list of all functions that have "main" within their name space and grabbing the first (and only in our case) one. Lastly we decompile the function and store it into a variable to be printed.

Going back the Ghidra and hitting 'k' we see the following in out scripting console.

image

Now that we have the decompiled function into a variable, we can just use regular ol' regex to grab the local_48's within the if's, sort them by the hex within the brackets, and grab the assosiated character. I'll be the first to admit that I'm awful at regex, and Java has it's own flavor, so this part took a while. I ended up with the pattern: local_48\\[([0-9a-fA-FxX]+)\\]\\s*==\\s*'(.)' which matches on the entire string local_48[0x22] == '4', the hex within [0x22], and the character at the end '4' in different groups.

<-- SNIP -->
    	Pattern pattern = Pattern.compile("local_48\\[([0-9a-fA-FxX]+)\\]\\s*==\\s*'(.)'");
    	List<Match> matches = new ArrayList<>();
    	Matcher matcher = pattern.matcher(decomp);
    	while (matcher.find()) {
    		String match = matcher.group();
    		println(match);
    		String hexIndex = matcher.group(1);
    		String character = matcher.group(2);
    		matches.add(new Match(hexIndex, match,character));
    		}
    	for (int i = 0; i < matches.size(); i++) {
    		println(matches.get(i).getCharacter());
    			}
    		}
    	}

class Match {
	private String index;
	private String match;
	private String character;
	
	public Match(String index, String match, String character) {
		this.index = index;
		this.match = match;
		this.character = character; 
		}
	public String getIndex() {
		return index;
		}
	public String getMatch() {
		return match;
		}
	public String getCharacter() {
		return character;
		}

	}
<-- SNIP -->

First, I created a class Match to hold the data index [0x22] the match local_48[0x22] == '4', and the character '4', finally creating some methods to read the individual data for debugging. Then, I create the aformentioned pattern, and empty list to hold the Matches, and execute the regex against the decompiled funciton. In a while loop, I parse out the data and append the Match type to the matches list. To check if all of this is working, I print the match and character members for all the Match types in matches.

image

Finally, I want to take the index member and sort the matches in numerical order. Then, I want to grab all of the characters and concat them into a flag variable and print it.

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.*;
import ghidra.app.decompiler.*;
import ghidra.app.script.GhidraScript;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.Program;
public class SortVar48 extends GhidraScript {
	public void run() throws Exception {
        //Init the decompiler interface and open the program.
        DecompInterface ifc = new DecompInterface();
        Program program = getCurrentProgram();
        ifc.openProgram(program);
        //Create a list of functions that matches the name main, storing the only match into the main variable.
        List<Function> functions = getGlobalFunctions("main");
        Function main = functions.getFirst();
        String decomp = ifc.decompileFunction(main, 0, monitor).getCCodeMarkup().toString();
        //Create a regex pattern to match all the local_48[n] variables in the if statement.
        Pattern pattern = Pattern.compile("local_48\\[([0-9a-fA-FxX]+)\\]\\s*==\\s*'(.)'");
        List<Match> matches = new ArrayList<>();
        Matcher matcher = pattern.matcher(decomp);
       //init a string builder to hold the chars 
         StringBuilder flag = new StringBuilder();
         while (matcher.find()) {
             String match = matcher.group();         // Full match (local_48[...] == 'X')
             String hexIndex = matcher.group(1);     // Hexadecimal index in the brackets
             String character = matcher.group(2);    // Character after ==
             if (hexIndex.startsWith("0x") || hexIndex.startsWith("0X")) {
                 hexIndex = hexIndex.substring(2);  // Remove "0x" or "0X" prefix
             int index = Integer.parseInt(hexIndex, 16);  // Parse the hexadecimal index to decimal
             matches.add(new Match(index, match, character));
             	} else {
             int index = Integer.parseInt(hexIndex);  // If not hex, then parse to decimal.
             matches.add(new Match(index, match, character));
             	}
             // Convert hex index to decimal for sorting
             // Add the match with its index to the list
         	}
         Collections.sort(matches);
         // Print the sorted results
         for (Match m : matches) {
             flag.append(m.character);
         		}
             println(flag.toString());
}

	class Match implements Comparable<Match> {
		private int index;
		private String match;
		private String character;

		public Match(int index, String match, String character) {
			this.index = index;
			this.match = match;
			this.character = character;
		}

		public int getIndex() {
			return index;
		}

		public String getMatch() {
			return match;
		}

		public String getCharacter() {
			return character;
		}

		// Compare based on the index (for sorting)
		@Override
		public int compareTo(Match other) {
			return Integer.compare(this.index, other.index);
		}
	}
}

Running this script results in thse simple:

image