This module assumes you have completed the Beginner portion of this course, as well as the Scripting module of the Intermediate course.
As with Ghidra Scripting, the primary use case we consider in this module is automation. It also permits some one-off analysis of a live target or interacting with the dynamic target. There are also some extension points useful for Modeling that are easily accessed in scripts for prototyping.
The script development environment is set up exactly the same as it is for the rest of Ghidra.
To create a Debugger script, do as you normally would then append
implements FlatDebuggerAPI
to the script’s class
declaration, e.g.:
import ghidra.app.script.GhidraScript;
import ghidra.debug.flatapi.FlatDebuggerAPI;
public class DemoDebuggerScript extends GhidraScript implements FlatDebuggerAPI {
@Override
protected void run() throws Exception {
}
}
NOTE: The scripting API has been refactored a little
since the transition from Recorder-based to TraceRmi-based targets.
Parts of the API that are back-end agnostic are accessible from the
FlatDebuggerAPI
interface. Parts of the API that require a
specific back end are in FlatDebuggerRmiAPI
and
FlatDebuggerRecorderAPI
, the latter of which is deprecated.
If a script written for version 11.0.2 or prior is not compiling, it can
most likely be patched up by changing
implements FlatDebuggerAPI
to
implements FlatDebuggerRecorderAPI
, but we recommend
porting it to use implements FlatDebuggerRmiAPI
.
Technically, the Debugger’s “deep” API is accessible to scripts;
however, the flat API is preferred for scripting. Also, the flat API is
usually more stable than the deep API. However, because the dynamic
analysis flat API is newer, it may not be as stable as the static
analysis flat API. It is also worth noting that the
FlatDebuggerAPI
interface adds the flat API to
your script. The static analysis flat API is still available, and it
will manipulate the static portions of the Debugger tool, just as they
would in the CodeBrowser tool. In this tutorial, we will explore reading
machine state, setting breakpoints, waiting for conditions, and
controlling the target.
We will write a script that assumes the current session is for
termmines
and dumps the game board to the console, allowing
you to cheat. You can label your variables however you would like but,
for this tutorial, we will assume you have labeled them
width
, height
, and cells
. If you
have not already located and labeled these variables, do so now.
First, we will do some validation. Check that we have an active session (trace):
Trace trace = getCurrentTrace();
if (trace == null) {
throw new AssertionError("There is no active session");
}
Now, check that the current program is termmines
:
Now, check that termmines
is actually part of the
current trace. There is not a great way to do this directly in the flat
API, but we are going to need to map some symbols from the
termmines
module, anyway. In this step, we will both verify
that the user has placed the required labels, as well as verify that
those symbols can be mapped to the target:
List<Symbol> widthSyms = getSymbols("width", null);
if (widthSyms.isEmpty()) {
throw new AssertionError("Symbol 'width' is required");
}
List<Symbol> heightSyms = getSymbols("height", null);
if (heightSyms.isEmpty()) {
throw new AssertionError("Symbol 'height' is required");
}
List<Symbol> cellsSyms = getSymbols("cells", null);
if (cellsSyms.isEmpty()) {
throw new AssertionError("Symbol 'cells' is required");
}
Address widthDyn = translateStaticToDynamic(widthSyms.get(0).getAddress());
if (widthDyn == null) {
throw new AssertionError("Symbol 'width' is not mapped to target");
}
Address heightDyn = translateStaticToDynamic(heightSyms.get(0).getAddress());
if (heightDyn == null) {
throw new AssertionError("Symbol 'height' is not mapped to target");
}
Address cellsDyn = translateStaticToDynamic(cellsSyms.get(0).getAddress());
if (cellsDyn == null) {
throw new AssertionError("Symbol 'cells' is not mapped to target");
}
The getSymbols()
method is part of the static flat API,
so it returns symbols from the current static listing. The
translateStaticToDynamic()
is part of the dynamic flat API.
This allows us to locate that symbol in the dynamic context.
Now, we want to read the dimensions and the whole board from the target. You should know from earlier exercises that the board is allocated 32 cells by 32 cells, so we will want to read at least 1024 bytes. Note that this will implicitly capture the board to the trace:
Beyond this, everything is pretty standard Java / Ghidra scripting. We will need to do some quick conversion of the bytes to integers, and then we can iterate over the cells and print the mines’ locations:
int width = ByteBuffer.wrap(widthDat).order(ByteOrder.LITTLE_ENDIAN).getInt();
int height = ByteBuffer.wrap(heightDat).order(ByteOrder.LITTLE_ENDIAN).getInt();
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
if ((cellsData[(y + 1) * 32 + x + 1] & 0x80) == 0x80) {
println("Mine at (%d,%d)".formatted(x, y));
}
}
}
To test, launch termmines
in Ghidra using GDB. You will
need to allow it to set up the first game board before running the
script. The simplest way to do that is to resume and then interrupt the
target while it waits for input. Now, run the script and examine its
output. Resume and play the game. Once you win, check that the script
output describes the actual board.
Write a script that will remove the mines from the board.
NOTE: The writeMemory()
and related
methods are all subject to the current Control Mode. If
the mode is read-only, the script cannot modify the target’s machine
state using those methods.
Most of the Debugger is implemented using asynchronous event-driven
programming. This will become apparent if you browse any deeper beyond
the flat API. Check the return value carefully. A method that might
intuitively return void
may actually return
CompletableFuture<Void>
. Java’s completable futures
allow you to register callbacks and/or chain additional futures onto
them.
However, Ghidra’s scripting system provides a dedicated thread for
each execution of a script, so it is acceptable to use the
.get()
methods instead, essentially converting to a
synchronous style. Most of the methods in the flat API will do this for
you. See also the flat API’s waitOn()
method. The most
common two methods to use when waiting for a condition is
waitForBreak()
and flushAsyncPipelines()
. The
first simply waits for the target to enter the STOPPED state. Once that
happens, the framework and UI will get to work interrogating the
back-end debugger to update the various displays. Unfortunately, if a
script does not wait for this update to complete, it may be subject to
race conditions. Thus, the second method politely waits for everything
else to finish. Sadly, it may slow your script down.
The general template for waiting on a condition is a bit klunky, but conceptually straightforward:
NOTE: The solution to this exercise is given as a
tutorial below, but give it an honest try before peeking. If you are not
already familiar with Eclipse’s searching and discovery features, try
pressing CTRL
-O
twice in the
editor for your script. You should now be able to type patterns,
optionally with wildcards, to help you find applicable methods.
Your task is to write a script that will wait for the player to win then patch the machine state, so that the game always prints a score of 0 seconds. Some gotchas to consider up front:
getExecutionState()
and interrupt()
. You
will not likely be able to place or toggle breakpoints while the target
is running.writeMemory()
are subject to the current
Control Mode. You may want to check and/or correct this
at the top of your script.getSymbols()
.You are successful when you can attach to a running
termmines
and execute your script. Then, assuming you win
the game, the game should award you a score of 0 seconds. It is okay if
you have to re-execute your script after each win.
As in the previous script, we will do some verifications at the top of the script. Your level of pedantry may vary.
Trace trace = getCurrentTrace();
if (trace == null) {
throw new AssertionError("There is no active session");
}
if (!"termmines".equals(currentProgram.getName())) {
throw new AssertionError("The current program must be termmines");
}
if (getExecutionState(trace).isRunning()) {
monitor.setMessage("Interrupting target and waiting for STOPPED");
interrupt();
waitForBreak(3, TimeUnit.SECONDS);
}
flushAsyncPipelines(trace);
if (!getControlService().getCurrentMode(trace).canEdit(getCurrentDebuggerCoordinates())) {
throw new AssertionError("Current control mode is read-only");
}
The first two blocks check that there is an active target with
termmines
as the current program. As before, the
association of the current program to the current target will be
implicitly verified when we map symbols. The second block will interrupt
the target if it is running. We then allow everything to sync up before
checking the control mode. We could instead change the control mode to
Control Target (with edits), but I prefer to keep the
user aware that the script needs to modify target machine state.
Next, we retrieve and map our symbols. This works pretty much the
same as in the previous script, but with attention to the containing
function namespace. The way termmines
computes the score is
to record the start time of the game. Then, when the player wins, it
subtracts the recorded time from the current time. This script requires
the user to label the start time variable timer
, and to
label the instruction that computes the score reset_timer
.
The function that prints the score must be named
print_win
.
List<Symbol> timerSyms = getSymbols("timer", null);
if (timerSyms.isEmpty()) {
throw new AssertionError("Symbol 'timer' is required");
}
List<Function> winFuncs = getGlobalFunctions("print_win");
if (winFuncs.isEmpty()) {
throw new AssertionError("Function 'print_win' is required");
}
List<Symbol> resetSyms = getSymbols("reset_timer", winFuncs.get(0));
if (resetSyms.isEmpty()) {
throw new AssertionError("Symbol 'reset_timer' is required");
}
Address timerDyn = translateStaticToDynamic(timerSyms.get(0).getAddress());
if (timerDyn == null) {
throw new AssertionError("Symbol 'timer' is not mapped to target");
}
Address resetDyn = translateStaticToDynamic(resetSyms.get(0).getAddress());
if (resetDyn == null) {
throw new AssertionError("Symbol 'reset_timer' is not mapped to target");
}
The first actual operation we perform on the debug session is to
toggle or place a breakpoint on the reset_timer
label. The
API prefers to specify breakpoints in the static context, but you can do
either. To establish that context, you must use a
ProgramLocation
. For static context, use the current
(static) program as the program. For dynamic context, use the current
(dynamic) trace view as the program — see
getCurrentView()
.
To avoid creating a pile of breakpoints, we will first attempt to
enable an existing breakpoint at the desired location. Technically, the
existing breakpoints may not be EXECUTE breakpoints, but we will blindly
assume they are. Again, your level of pedantry may vary. The
breakpointsEnable
method will return the existing
breakpoints, so we can check that and create a new breakpoint, if
necessary:
This next loop is quite extensive, but it follows the template given earlier for waiting on conditions. It is an indefinite loop, so we should check the monitor for cancellation somewhat frequently. This implies we should use relatively short timeouts in our API calls. In our case, we just want to confirm that the cause of breaking was hitting our breakpoint. We do not need to be precise in this check; it suffices to check the program counter:
while (true) {
monitor.checkCancelled();
TargetExecutionState execState = getExecutionState(trace);
switch (execState) {
case STOPPED:
resume();
break;
case TERMINATED:
case INACTIVE:
throw new AssertionError("Target terminated");
case ALIVE:
println(
"I don't know whether or not the target is running. Please make it RUNNING.");
break;
case RUNNING:
/**
* Probably timed out waiting for break. That's fine. Give the player time to
* win.
*/
break;
default:
throw new AssertionError("Unrecognized state: " + execState);
}
try {
monitor.setMessage("Waiting for player to win");
waitForBreak(1, TimeUnit.SECONDS);
}
catch (TimeoutException e) {
// Give the player time to win.
continue;
}
flushAsyncPipelines(trace);
Address pc = getProgramCounter();
println("STOPPED at pc = " + pc);
if (resetDyn.equals(pc)) {
break;
}
}
The “center” of this loop is a call to waitForBreak()
on
line 27. This is the simplest primitive for waiting on the target to
meet any condition. Because we expect the user to take more than a
second to win the game, we should expect a timeout exception and just
keep waiting. Using a timeout of 1 second ensures we can terminate
promptly should the user cancel the script.
Before waiting, we need to make sure the target is running. Because
we could repeat the loop while the target is already running, we should
only call resume()
if the target is stopped. There are
utility methods on TargetExecutionState
like
isRunning()
, which you might prefer to use. Here, we
exhaustively handle every kind of state using a switch statement, which
does make the code a bit verbose.
When the target does break, we first allow the UI to finish
interrogating the target. We can then reliably retrieve and check the
program counter. If the PC matches the dynamic location of
reset_timer
, then the player has won, and we need to reset
the start time.
When the player has won, this particular compilation of
termmines
first calls time
to get the current
time and moves it into ECX
. It then subtracts, using a
memory operand, the recorded start time. There are certainly other
strategies, but this script expects the user to label that
SUB
instruction reset_timer
. We would like the
result of that computation to be 0, so we will simply copy the value of
ECX
over the recorded start time:
int time = readRegister("ECX").getUnsignedValue().intValue();
if (!writeMemory(timerDyn,
ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(time).array())) {
throw new AssertionError("Could not write over timer. Does control mode allow edits?");
}
resume();
The final resume()
simply allows the target to finish
printing the score, which ought to be 0 now!
For another demonstration of the flat API, see DemoDebuggerScript,
or just ask Eclipse for all the implementations of
FlatDebuggerAPI
. If you want a list of methods with
explanations, you should refer to the documentation in the
FlatDebuggerAPI
interface.