mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2026-09-28 17:11:11 -09:00
GP-5972: post-review
GP-5792: minimize calls while running GP-5972: better tx refs GP-5972: fixes for typing errors GP-5972: temp mods GP-5972: vscode mods GP-5972: post-review GP-5972: post-review GP-5972: updates docs GP-5972: support for 32-bit GP-5972: mods post-PR GP-5972: better error handling GP-5972: ss-only GP-5972: help GP-5972: help GP-5972: mostly functional tests GP-5972: lame hook tests GP-5972: methods pass GP-5972: first pass - works a little GP5972: some functionalityGP-5972: mem+GP-5921: basicsGP-5921: pc, spGP-5972: regsGP-5972: regs/mem/modsGP-5972: ghidraxdbgGP-5972: _base -> clientGP-5972: mods/memGP-5972: availableGP-5972: bptsGP-5972: -pybagGP-5972: initial stateGP-5972: misc fixesGP-5972: bptsGP-5972: various methods/slightly better stateGP-5972: del bptsGP-5972: more bptsGP-5972: better attachGP-5972: better launchGP-5972: tests round 0GP-5972: cmd tests - setsGP-5972: cmd tests passGP-5972: methodsGP-5972: methods exc 3 GP-5972: x64dbg init
This commit is contained in:
@@ -0,0 +1,512 @@
|
||||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package agent.x64dbg.rmi;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.junit.Assume.assumeTrue;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
|
||||
import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest;
|
||||
import ghidra.app.plugin.core.debug.service.tracermi.TraceRmiPlugin;
|
||||
import ghidra.app.plugin.core.debug.utils.ManagedDomainObject;
|
||||
import ghidra.app.services.TraceRmiService;
|
||||
import ghidra.debug.api.tracermi.*;
|
||||
import ghidra.framework.*;
|
||||
import ghidra.framework.main.ApplicationLevelOnlyPlugin;
|
||||
import ghidra.framework.model.DomainFile;
|
||||
import ghidra.framework.plugintool.Plugin;
|
||||
import ghidra.framework.plugintool.PluginsConfiguration;
|
||||
import ghidra.framework.plugintool.util.*;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.address.AddressRangeImpl;
|
||||
import ghidra.pty.testutil.DummyProc;
|
||||
import ghidra.trace.model.Lifespan;
|
||||
import ghidra.trace.model.breakpoint.TraceBreakpointKind;
|
||||
import ghidra.trace.model.breakpoint.TraceBreakpointKind.TraceBreakpointKindSet;
|
||||
import ghidra.trace.model.target.TraceObject;
|
||||
import ghidra.trace.model.target.TraceObjectValue;
|
||||
import ghidra.util.*;
|
||||
|
||||
public abstract class AbstractX64dbgTraceRmiTest extends AbstractGhidraHeadedDebuggerTest {
|
||||
/**
|
||||
* Some features have to be disabled to avoid permissions issues in the test container. Namely,
|
||||
* don't try to disable ASLR.
|
||||
*/
|
||||
public static final String PREAMBLE = """
|
||||
from ghidraxdbg.commands import *
|
||||
from ghidratrace.client import Schedule
|
||||
from x64dbg_automate.models import *
|
||||
""";
|
||||
// Connecting should be the first thing the script does, so use a tight timeout.
|
||||
protected static final int CONNECT_TIMEOUT_MS = 3000;
|
||||
protected static final int TIMEOUT_SECONDS = 300;
|
||||
protected static final int QUIT_TIMEOUT_MS = 1000;
|
||||
|
||||
/** Some snapshot likely to exceed the latest */
|
||||
protected static final long SNAP = 100;
|
||||
|
||||
protected static boolean didSetupPython = false;
|
||||
|
||||
public static final String NOTEPAD = "C:\\\\Windows\\\\notepad.exe";
|
||||
public static final String INSTRUMENT_STATE = """
|
||||
import sys
|
||||
from ghidraxdbg import commands
|
||||
from x64dbg_automate.events import *
|
||||
print("Instrumenting")
|
||||
def on_state_changed(*args):
|
||||
print("State changed")
|
||||
sys.stdout.flush()
|
||||
proc = util.selected_process()
|
||||
trace = commands.STATE.trace
|
||||
with commands.STATE.client.batch():
|
||||
with trace.open_tx("State changed proc {}".format(proc)):
|
||||
commands.put_state(proc)
|
||||
return
|
||||
|
||||
def install_hooks():
|
||||
print("Installing")
|
||||
util.dbg.client.watch_debug_event(EventType.EVENT_DEBUG, lambda x: on_state_changed(x))
|
||||
|
||||
install_hooks()
|
||||
""";
|
||||
|
||||
protected TraceRmiService traceRmi;
|
||||
private Path pythonPath;
|
||||
private Path outFile;
|
||||
private Path errFile;
|
||||
|
||||
@BeforeClass
|
||||
public static void setupPython() throws Throwable {
|
||||
if (didSetupPython) {
|
||||
// Only do this once when running the full suite.
|
||||
return;
|
||||
}
|
||||
if (SystemUtilities.isInTestingBatchMode()) {
|
||||
// Don't run gradle in gradle. It already did this task.
|
||||
return;
|
||||
}
|
||||
String gradle = DummyProc.which("gradle.bat");
|
||||
new ProcessBuilder(gradle, "assemblePyPackage")
|
||||
.directory(TestApplicationUtils.getInstallationDirectory())
|
||||
.inheritIO()
|
||||
.start()
|
||||
.waitFor();
|
||||
didSetupPython = true;
|
||||
}
|
||||
|
||||
protected void setPythonPath(ProcessBuilder pb) throws IOException {
|
||||
String sep =
|
||||
OperatingSystem.CURRENT_OPERATING_SYSTEM == OperatingSystem.WINDOWS ? ";" : ":";
|
||||
String rmiPyPkg = Application.getModuleSubDirectory("Debugger-rmi-trace",
|
||||
"build/pypkg/src").getAbsolutePath();
|
||||
String gdbPyPkg = Application.getModuleSubDirectory("Debugger-agent-x64dbg",
|
||||
"build/pypkg/src").getAbsolutePath();
|
||||
String add = rmiPyPkg + sep + gdbPyPkg;
|
||||
pb.environment().compute("PYTHONPATH", (k, v) -> v == null ? add : (v + sep + add));
|
||||
}
|
||||
|
||||
protected void setX64dbgPath(ProcessBuilder pb) throws IOException {
|
||||
pb.environment().put("OPT_X64DBG_EXE", "C:\\Software\\snapshot_2025-08-19_19-40\\release\\x64\\x64dbg.exe");
|
||||
}
|
||||
|
||||
@BeforeClass
|
||||
public static void assertOS() {
|
||||
assumeTrue("Not on Windows",
|
||||
OperatingSystem.CURRENT_OPERATING_SYSTEM == OperatingSystem.WINDOWS);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setupTraceRmi() throws Throwable {
|
||||
traceRmi = addPlugin(tool, TraceRmiPlugin.class);
|
||||
|
||||
try {
|
||||
pythonPath = Paths.get(DummyProc.which("python3"));
|
||||
}
|
||||
catch (RuntimeException e) {
|
||||
pythonPath = Paths.get(DummyProc.which("python"));
|
||||
}
|
||||
|
||||
pythonPath = new File("/C:/Python313/python.exe").toPath();
|
||||
assertTrue(pythonPath.toFile().exists());
|
||||
outFile = Files.createTempFile("pydbgout", null);
|
||||
errFile = Files.createTempFile("pydbgerr", null);
|
||||
}
|
||||
|
||||
protected void addAllDebuggerPlugins() throws PluginException {
|
||||
PluginsConfiguration plugConf = new PluginsConfiguration() {
|
||||
@Override
|
||||
protected boolean accepts(Class<? extends Plugin> pluginClass) {
|
||||
return !ApplicationLevelOnlyPlugin.class.isAssignableFrom(pluginClass);
|
||||
}
|
||||
};
|
||||
|
||||
for (PluginDescription pd : plugConf
|
||||
.getPluginDescriptions(PluginPackage.getPluginPackage("Debugger"))) {
|
||||
addPlugin(tool, pd.getPluginClass());
|
||||
}
|
||||
}
|
||||
|
||||
protected static String addrToStringForPython(InetAddress address) {
|
||||
if (address.isAnyLocalAddress()) {
|
||||
return "127.0.0.1"; // Can't connect to 0.0.0.0 as such. Choose localhost.
|
||||
}
|
||||
return address.getHostAddress();
|
||||
}
|
||||
|
||||
protected static String sockToStringForPython(SocketAddress address) {
|
||||
if (address instanceof InetSocketAddress tcp) {
|
||||
return addrToStringForPython(tcp.getAddress()) + ":" + tcp.getPort();
|
||||
}
|
||||
throw new AssertionError("Unhandled address type " + address);
|
||||
}
|
||||
|
||||
protected record PythonResult(boolean timedOut, int exitCode, String stdout, String stderr) {
|
||||
protected String handle() {
|
||||
if (stderr.contains("Error") || (0 != exitCode && 1 != exitCode && 143 != exitCode)) {
|
||||
throw new PythonError(exitCode, stdout, stderr);
|
||||
}
|
||||
System.out.println("--stdout--");
|
||||
System.out.println(stdout);
|
||||
System.out.println("--stderr--");
|
||||
System.out.println(stderr);
|
||||
return stdout;
|
||||
}
|
||||
}
|
||||
|
||||
protected record ExecInPython(Process python, CompletableFuture<PythonResult> future) {}
|
||||
|
||||
protected void pump(InputStream streamIn, OutputStream streamOut) {
|
||||
Thread t = new Thread(() -> {
|
||||
try (PrintStream printOut = new PrintStream(streamOut);
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(streamIn))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
printOut.println(line);
|
||||
printOut.flush();
|
||||
}
|
||||
}
|
||||
catch (IOException e) {
|
||||
Msg.info(this, "Terminating stdin pump, because " + e);
|
||||
}
|
||||
});
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
}
|
||||
|
||||
protected void pumpTee(InputStream streamIn, File fileOut, PrintStream streamOut) {
|
||||
Thread t = new Thread(() -> {
|
||||
try (PrintStream fileStream = new PrintStream(fileOut);
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(streamIn))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
streamOut.println(line);
|
||||
streamOut.flush();
|
||||
fileStream.println(line);
|
||||
fileStream.flush();
|
||||
}
|
||||
}
|
||||
catch (IOException e) {
|
||||
Msg.info(this, "Terminating tee: " + fileOut + ", because " + e);
|
||||
}
|
||||
});
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
}
|
||||
|
||||
@SuppressWarnings("resource") // Do not close stdin
|
||||
protected ExecInPython execInPython(String script) throws IOException {
|
||||
ProcessBuilder pb = new ProcessBuilder(pythonPath.toString(), "-i");
|
||||
setPythonPath(pb);
|
||||
setX64dbgPath(pb);
|
||||
|
||||
// If commands come from file, Python will quit after EOF.
|
||||
Msg.info(this, "outFile: " + outFile);
|
||||
Msg.info(this, "errFile: " + errFile);
|
||||
|
||||
//pb.inheritIO();
|
||||
pb.redirectInput(ProcessBuilder.Redirect.PIPE);
|
||||
if (SystemUtilities.isInTestingBatchMode()) {
|
||||
pb.redirectOutput(outFile.toFile());
|
||||
pb.redirectError(errFile.toFile());
|
||||
}
|
||||
else {
|
||||
pb.redirectOutput(ProcessBuilder.Redirect.PIPE);
|
||||
pb.redirectError(ProcessBuilder.Redirect.PIPE);
|
||||
}
|
||||
Process pyproc = pb.start();
|
||||
|
||||
if (!SystemUtilities.isInTestingBatchMode()) {
|
||||
pumpTee(pyproc.getInputStream(), outFile.toFile(), System.out);
|
||||
pumpTee(pyproc.getErrorStream(), errFile.toFile(), System.err);
|
||||
}
|
||||
|
||||
OutputStream stdin = pyproc.getOutputStream();
|
||||
stdin.write(script.getBytes());
|
||||
stdin.flush();
|
||||
|
||||
if (!SystemUtilities.isInTestingBatchMode()) {
|
||||
pump(System.in, stdin);
|
||||
}
|
||||
|
||||
return new ExecInPython(pyproc, CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
if (!pyproc.waitFor(TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
|
||||
Msg.error(this, "Timed out waiting for Python");
|
||||
pyproc.destroyForcibly();
|
||||
pyproc.waitFor(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
return new PythonResult(true, -1, Files.readString(outFile),
|
||||
Files.readString(errFile));
|
||||
}
|
||||
Msg.info(this, "Python exited with code " + pyproc.exitValue());
|
||||
return new PythonResult(false, pyproc.exitValue(), Files.readString(outFile),
|
||||
Files.readString(errFile));
|
||||
}
|
||||
catch (Exception e) {
|
||||
return ExceptionUtils.rethrow(e);
|
||||
}
|
||||
finally {
|
||||
pyproc.destroyForcibly();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public static class PythonError extends RuntimeException {
|
||||
public final int exitCode;
|
||||
public final String stdout;
|
||||
public final String stderr;
|
||||
|
||||
public PythonError(int exitCode, String stdout, String stderr) {
|
||||
super("""
|
||||
exitCode=%d:
|
||||
----stdout----
|
||||
%s
|
||||
----stderr----
|
||||
%s
|
||||
""".formatted(exitCode, stdout, stderr));
|
||||
this.exitCode = exitCode;
|
||||
this.stdout = stdout;
|
||||
this.stderr = stderr;
|
||||
}
|
||||
}
|
||||
|
||||
protected String runThrowError(String script) throws Exception {
|
||||
CompletableFuture<PythonResult> result = execInPython(script).future;
|
||||
return result.get(TIMEOUT_SECONDS, TimeUnit.SECONDS).handle();
|
||||
}
|
||||
|
||||
protected record PythonAndConnection(ExecInPython exec, TraceRmiConnection connection)
|
||||
implements AutoCloseable {
|
||||
protected RemoteMethod getMethod(String name) {
|
||||
return Objects.requireNonNull(connection.getMethods().get(name));
|
||||
}
|
||||
|
||||
public void execute(String cmd) {
|
||||
RemoteMethod execute = getMethod("execute");
|
||||
try {
|
||||
execute.invoke(Map.of("cmd", cmd));
|
||||
} catch (Exception e) {
|
||||
Msg.warn(this, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public RemoteAsyncResult executeAsync(String cmd) {
|
||||
RemoteMethod execute = getMethod("execute");
|
||||
return execute.invokeAsync(Map.of("cmd", cmd));
|
||||
}
|
||||
|
||||
public String executeCapture(String cmd) {
|
||||
RemoteMethod execute = getMethod("execute");
|
||||
return (String) execute.invoke(Map.of("cmd", cmd, "to_string", true));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
Msg.info(this, "Cleaning up python");
|
||||
exec.python().destroy();
|
||||
try {
|
||||
PythonResult r = exec.future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
r.handle();
|
||||
waitForPass(this, () -> assertTrue(connection.isClosed()),
|
||||
TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
}
|
||||
finally {
|
||||
exec.python.destroyForcibly();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected PythonAndConnection startAndConnectPython(Function<String, String> scriptSupplier)
|
||||
throws Exception {
|
||||
TraceRmiAcceptor acceptor = traceRmi.acceptOne(null);
|
||||
ExecInPython exec =
|
||||
execInPython(scriptSupplier.apply(sockToStringForPython(acceptor.getAddress())));
|
||||
acceptor.setTimeout(CONNECT_TIMEOUT_MS);
|
||||
try {
|
||||
TraceRmiConnection connection = acceptor.accept();
|
||||
return new PythonAndConnection(exec, connection);
|
||||
}
|
||||
catch (SocketTimeoutException e) {
|
||||
exec.python.destroyForcibly();
|
||||
exec.future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS).handle();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
protected PythonAndConnection startAndConnectPython() throws Exception {
|
||||
return startAndConnectPython(addr -> """
|
||||
%s
|
||||
ghidra_trace_connect('%s')
|
||||
""".formatted(PREAMBLE, addr));
|
||||
}
|
||||
|
||||
@SuppressWarnings("resource")
|
||||
protected String runThrowError(Function<String, String> scriptSupplier)
|
||||
throws Exception {
|
||||
PythonAndConnection conn = startAndConnectPython(scriptSupplier);
|
||||
PythonResult r = conn.exec.future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
String stdout = r.handle();
|
||||
waitForPass(this, () -> assertTrue(conn.connection.isClosed()),
|
||||
TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
return stdout;
|
||||
}
|
||||
|
||||
protected void waitStopped(String message) {
|
||||
TraceObject proc =
|
||||
Objects.requireNonNull(tb.objAny("Sessions[].Processes[]", Lifespan.at(0)));
|
||||
waitForPass(() -> assertEquals(message, "STOPPED", tb.objValue(proc, 0, "_state")));
|
||||
waitTxDone();
|
||||
}
|
||||
|
||||
protected void waitRunning(String message) {
|
||||
TraceObject proc =
|
||||
Objects.requireNonNull(tb.objAny("Sessions[].Processes[]", Lifespan.at(0)));
|
||||
waitForPass(() -> assertEquals(message, "RUNNING", tb.objValue(proc, 0, "_state")));
|
||||
waitTxDone();
|
||||
}
|
||||
|
||||
protected String extractOutSection(String out, String head) {
|
||||
String[] split = out.split("\n");
|
||||
String xout = "";
|
||||
for (String s : split) {
|
||||
if (!s.startsWith("(python)") && !s.equals("")) {
|
||||
xout += s + "\n";
|
||||
}
|
||||
}
|
||||
return xout.split(head)[1].split("---")[0].replace("(python)", "").trim();
|
||||
}
|
||||
|
||||
record MemDump(long address, byte[] data) {}
|
||||
|
||||
protected MemDump parseHexDump(String dump) throws IOException {
|
||||
// First, get the address. Assume contiguous, so only need top line.
|
||||
List<String> lines = List.of(dump.split("\n"));
|
||||
List<String> toksLine0 = List.of(lines.get(0).split("\\s+"));
|
||||
String addrstr = toksLine0.get(0);
|
||||
if (addrstr.contains(":")) {
|
||||
addrstr = addrstr.substring(0, addrstr.indexOf(":"));
|
||||
}
|
||||
long address = Long.parseLong(addrstr, 16);
|
||||
|
||||
ByteArrayOutputStream buf = new ByteArrayOutputStream();
|
||||
for (String l : lines) {
|
||||
List<String> parts = List.of(l.split(":"));
|
||||
assertEquals(2, parts.size());
|
||||
String hex = parts.get(1).substring(0, 48);
|
||||
byte[] lineData = NumericUtilities.convertStringToBytes(hex);
|
||||
assertNotNull("Converted to null: " + hex, parts.get(1));
|
||||
buf.write(lineData);
|
||||
}
|
||||
return new MemDump(address, buf.toByteArray());
|
||||
}
|
||||
|
||||
record RegDump() {}
|
||||
|
||||
protected RegDump parseRegDump(String dump) {
|
||||
return new RegDump();
|
||||
}
|
||||
|
||||
protected ManagedDomainObject openDomainObject(String path) throws Exception {
|
||||
DomainFile df = env.getProject().getProjectData().getFile(path);
|
||||
assertNotNull(df);
|
||||
return new ManagedDomainObject(df, false, false, monitor);
|
||||
}
|
||||
|
||||
protected ManagedDomainObject waitDomainObject(String path) throws Exception {
|
||||
DomainFile df;
|
||||
long start = System.currentTimeMillis();
|
||||
while (true) {
|
||||
df = env.getProject().getProjectData().getFile(path);
|
||||
if (df != null) {
|
||||
return new ManagedDomainObject(df, false, false, monitor);
|
||||
}
|
||||
Thread.sleep(1000);
|
||||
if (System.currentTimeMillis() - start > 30000) {
|
||||
throw new TimeoutException("30 seconds expired waiting for domain file");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void assertBreakLoc(TraceObjectValue locVal, Address addr, int len, String type) throws Exception {
|
||||
TraceObject loc = locVal.getChild();
|
||||
TraceObject spec = loc;
|
||||
assertEquals(new AddressRangeImpl(addr, len), loc.getValue(0, "_range").getValue());
|
||||
assertEquals(type, spec.getValue(0, "Type").getValue());
|
||||
}
|
||||
|
||||
protected void assertWatchLoc(TraceObjectValue locVal, Address addr, int len, String type) throws Exception {
|
||||
TraceObject loc = locVal.getChild();
|
||||
assertEquals(new AddressRangeImpl(addr, len), loc.getValue(0, "_range").getValue());
|
||||
assertEquals(type, loc.getValue(0, "TypeEx").getValue());
|
||||
}
|
||||
|
||||
protected void waitTxDone() {
|
||||
waitFor(() -> tb.trace.getCurrentTransactionInfo() == null);
|
||||
}
|
||||
|
||||
public static void waitForPass(Runnable runnable, long timeoutMs, long retryDelayMs) {
|
||||
long start = System.currentTimeMillis();
|
||||
AssertionError lastError = null;
|
||||
while (System.currentTimeMillis() - start < timeoutMs) {
|
||||
try {
|
||||
runnable.run();
|
||||
return;
|
||||
}
|
||||
catch (AssertionError e) {
|
||||
lastError = e;
|
||||
}
|
||||
try {
|
||||
Thread.sleep(retryDelayMs);
|
||||
}
|
||||
catch (InterruptedException e) {
|
||||
// Retry sooner, I guess.
|
||||
}
|
||||
}
|
||||
if (lastError == null) {
|
||||
throw new AssertionError("Timed out before first try?");
|
||||
}
|
||||
throw lastError;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,399 @@
|
||||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package agent.x64dbg.rmi;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import ghidra.app.plugin.core.debug.utils.ManagedDomainObject;
|
||||
import ghidra.debug.api.tracermi.RemoteMethod;
|
||||
import ghidra.program.model.address.AddressSpace;
|
||||
import ghidra.trace.database.ToyDBTraceBuilder;
|
||||
import ghidra.trace.model.Lifespan;
|
||||
import ghidra.trace.model.Trace;
|
||||
import ghidra.trace.model.memory.TraceMemorySpace;
|
||||
import ghidra.trace.model.target.TraceObject;
|
||||
import ghidra.trace.model.target.path.*;
|
||||
import ghidra.trace.model.time.TraceSnapshot;
|
||||
|
||||
public class X64dbgHooksTest extends AbstractX64dbgTraceRmiTest {
|
||||
private static final long RUN_TIMEOUT_MS = 5000;
|
||||
private static final long RETRY_MS = 500;
|
||||
|
||||
record PythonAndTrace(PythonAndConnection conn, ManagedDomainObject mdo)
|
||||
implements AutoCloseable {
|
||||
public void execute(String cmd) {
|
||||
conn.execute(cmd);
|
||||
}
|
||||
|
||||
public String executeCapture(String cmd) {
|
||||
return conn.executeCapture(cmd);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
try {
|
||||
conn.execute("util.terminate_session()");
|
||||
conn.close();
|
||||
} catch (Exception e) {
|
||||
//IGNORE
|
||||
}
|
||||
try {
|
||||
mdo.close();
|
||||
} catch (Exception e) {
|
||||
//IGNORE
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("resource")
|
||||
protected PythonAndTrace startAndSyncPython(String exec) throws Exception {
|
||||
PythonAndConnection conn = startAndConnectPython();
|
||||
try {
|
||||
ManagedDomainObject mdo;
|
||||
conn.execute("from ghidraxdbg.commands import *");
|
||||
conn.execute(
|
||||
"util.set_convenience_variable('ghidra-language', 'x86:LE:64:default')");
|
||||
if (exec != null) {
|
||||
start(conn, exec);
|
||||
mdo = waitDomainObject("/New Traces/x64dbg/" + exec.substring(exec.lastIndexOf("\\")+1));
|
||||
}
|
||||
else {
|
||||
conn.execute("ghidra_trace_start()");
|
||||
mdo = waitDomainObject("/New Traces/x64dbg/noname");
|
||||
}
|
||||
clearBreakpoints(conn);
|
||||
tb = new ToyDBTraceBuilder((Trace) mdo.get());
|
||||
return new PythonAndTrace(conn, mdo);
|
||||
}
|
||||
catch (Exception e) {
|
||||
clearBreakpoints(conn);
|
||||
conn.execute("util.terminate_session()");
|
||||
conn.close();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
protected long lastSnap(PythonAndTrace conn) {
|
||||
return conn.conn.connection().getLastSnapshot(tb.trace);
|
||||
}
|
||||
|
||||
static final int INIT_NOTEPAD_THREAD_COUNT = 4; // This could be fragile
|
||||
|
||||
//@Test - doesn't generate more than the initial 4
|
||||
public void testOnNewThread() throws Exception {
|
||||
try (PythonAndTrace conn = startAndSyncPython(NOTEPAD)) {
|
||||
conn.execute("from ghidraxdbg.commands import *");
|
||||
txPut(conn, "processes");
|
||||
|
||||
waitForPass(() -> {
|
||||
TraceObject proc = tb.objAny0("Sessions[].Processes[]");
|
||||
assertNotNull(proc);
|
||||
assertEquals("STOPPED", tb.objValue(proc, lastSnap(conn), "_state"));
|
||||
}, RUN_TIMEOUT_MS, RETRY_MS);
|
||||
|
||||
txPut(conn, "threads");
|
||||
waitForPass(() -> assertEquals(INIT_NOTEPAD_THREAD_COUNT,
|
||||
tb.objValues(lastSnap(conn), "Sessions[].Processes[].Threads[]").size()),
|
||||
RUN_TIMEOUT_MS, RETRY_MS);
|
||||
|
||||
// Via method, go is asynchronous
|
||||
RemoteMethod go = conn.conn.getMethod("go");
|
||||
TraceObject proc = tb.objAny0("Sessions[].Processes[]");
|
||||
go.invoke(Map.of("process", proc)); // Initial breakpoint
|
||||
go.invoke(Map.of("process", proc));
|
||||
|
||||
waitForPass(() -> assertThat(
|
||||
tb.objValues(lastSnap(conn), "Sessions[].Processes[].Threads[]").size(),
|
||||
greaterThan(INIT_NOTEPAD_THREAD_COUNT)),
|
||||
RUN_TIMEOUT_MS, RETRY_MS);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnNewModule() throws Exception {
|
||||
try (PythonAndTrace conn = startAndSyncPython(NOTEPAD)) {
|
||||
conn.execute("from ghidraxdbg.commands import *");
|
||||
txPut(conn, "processes");
|
||||
|
||||
TraceObject proc = tb.objAny0("Sessions[].Processes[]");
|
||||
waitForPass(() -> {
|
||||
assertNotNull(proc);
|
||||
assertEquals("STOPPED", tb.objValue(proc, lastSnap(conn), "_state"));
|
||||
}, RUN_TIMEOUT_MS, RETRY_MS);
|
||||
|
||||
txPut(conn, "modules");
|
||||
waitForPass(() -> assertThat(
|
||||
tb.objValues(lastSnap(conn), "Sessions[].Processes[].Modules[]").size(),
|
||||
greaterThan(0)),
|
||||
RUN_TIMEOUT_MS, RETRY_MS);
|
||||
|
||||
int size = tb.objValues(lastSnap(conn), "Sessions[].Processes[].Modules[]").size();
|
||||
// Via method, go is asynchronous
|
||||
RemoteMethod go = conn.conn.getMethod("go");
|
||||
go.invoke(Map.of("process", proc)); // Initial breakpoint
|
||||
go.invoke(Map.of("process", proc));
|
||||
|
||||
waitForPass(() -> assertThat(
|
||||
tb.objValues(lastSnap(conn), "Sessions[].Processes[].Modules[]").size(),
|
||||
greaterThan(size)),
|
||||
RUN_TIMEOUT_MS, RETRY_MS);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnThreadSelected() throws Exception {
|
||||
try (PythonAndTrace conn = startAndSyncPython(NOTEPAD)) {
|
||||
txPut(conn, "processes");
|
||||
conn.execute("util.dbg.client.stepi()"); // no initial event
|
||||
|
||||
waitForPass(() -> {
|
||||
TraceObject proc = tb.objAny0("Sessions[0].Processes[]");
|
||||
assertNotNull(proc);
|
||||
assertEquals("STOPPED", tb.objValue(proc, lastSnap(conn), "_state"));
|
||||
}, RUN_TIMEOUT_MS, RETRY_MS);
|
||||
|
||||
txPut(conn, "threads");
|
||||
waitForPass(() -> {
|
||||
List<Object> values = tb.objValues(lastSnap(conn), "Sessions[0].Processes[].Threads[]");
|
||||
assertEquals(INIT_NOTEPAD_THREAD_COUNT, values.size());
|
||||
}, RUN_TIMEOUT_MS, RETRY_MS);
|
||||
|
||||
// Now the real test
|
||||
List<Object> values = tb.objValues(lastSnap(conn), "Sessions[0].Processes[].Threads[]");
|
||||
TraceObject thread = (TraceObject) values.get(0);
|
||||
Object tid0 = tb.objValue(thread, lastSnap(conn), "TID");
|
||||
conn.execute("util.select_thread("+tid0.toString()+")");
|
||||
waitForPass(() -> {
|
||||
String tnum = conn.executeCapture("print(util.selected_thread())").strip();
|
||||
assertEquals(tid0.toString(), tnum);
|
||||
}, RUN_TIMEOUT_MS, RETRY_MS);
|
||||
|
||||
thread = (TraceObject) values.get(1);
|
||||
Object tid1 = tb.objValue(thread, lastSnap(conn), "TID");
|
||||
conn.execute("util.select_thread("+tid1.toString()+")");
|
||||
waitForPass(() -> {
|
||||
String tnum = conn.executeCapture("print(util.selected_thread())").strip();
|
||||
assertEquals(tid1.toString(), tnum);
|
||||
}, RUN_TIMEOUT_MS, RETRY_MS);
|
||||
|
||||
thread = (TraceObject) values.get(2);
|
||||
Object tid2 = tb.objValue(thread, lastSnap(conn), "TID");
|
||||
conn.execute("util.select_thread("+tid2.toString()+")");
|
||||
waitForPass(() -> {
|
||||
String tnum = conn.executeCapture("print(util.selected_thread())").strip();
|
||||
assertEquals(tid2.toString(), tnum);
|
||||
}, RUN_TIMEOUT_MS, RETRY_MS);
|
||||
}
|
||||
}
|
||||
|
||||
protected String getIndex(TraceObject object, String pattern, int n) {
|
||||
if (object == null) {
|
||||
return null;
|
||||
}
|
||||
PathPattern pat = PathFilter.parse(pattern).getSingletonPattern();
|
||||
KeyPath path = object.getCanonicalPath();
|
||||
if (path.size() < pat.asPath().size()) {
|
||||
return null;
|
||||
}
|
||||
List<String> matched = pat.matchKeys(path, false);
|
||||
if (matched == null) {
|
||||
return null;
|
||||
}
|
||||
if (matched.size() <= n) {
|
||||
return null;
|
||||
}
|
||||
return matched.get(n);
|
||||
}
|
||||
|
||||
protected String threadIndex(TraceObject object) {
|
||||
return getIndex(object, "Sessions[].Processes[].Threads[]", 2);
|
||||
}
|
||||
|
||||
protected String frameIndex(TraceObject object) {
|
||||
return getIndex(object, "Sessions[].Processes[].Threads[].Stack.Frames[]", 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnRegisterChanged() throws Exception {
|
||||
try (PythonAndTrace conn = startAndSyncPython(NOTEPAD)) {
|
||||
|
||||
conn.execute("ghidra_trace_txstart('Tx')");
|
||||
conn.execute("ghidra_trace_putreg()");
|
||||
conn.execute("ghidra_trace_txcommit()");
|
||||
conn.execute("util.dbg.cmd('rax=0x1234')");
|
||||
conn.execute("util.dbg.client.stepi()"); // no real event for register changes
|
||||
|
||||
String path = "Sessions[].Processes[].Threads[].Registers";
|
||||
TraceObject registers = Objects.requireNonNull(tb.objAny(path, Lifespan.at(0)));
|
||||
AddressSpace space = tb.trace.getBaseAddressFactory()
|
||||
.getAddressSpace(registers.getCanonicalPath().toString());
|
||||
TraceMemorySpace regs = tb.trace.getMemoryManager().getMemorySpace(space, false);
|
||||
waitForPass(() -> assertEquals("1234",
|
||||
regs.getValue(lastSnap(conn), tb.reg("RAX")).getUnsignedValue().toString(16)));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnCont() throws Exception {
|
||||
try (PythonAndTrace conn = startAndSyncPython(NOTEPAD)) {
|
||||
txPut(conn, "processes");
|
||||
|
||||
conn.execute("util.dbg.client.go()");
|
||||
conn.execute("util.dbg.client.go()");
|
||||
|
||||
TraceObject proc = waitForValue(() -> tb.objAny0("Sessions[].Processes[]"));
|
||||
waitForPass(() -> {
|
||||
assertEquals("RUNNING", tb.objValue(proc, lastSnap(conn), "_state"));
|
||||
}, RUN_TIMEOUT_MS, RETRY_MS);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnStop() throws Exception {
|
||||
try (PythonAndTrace conn = startAndSyncPython(NOTEPAD)) {
|
||||
txPut(conn, "processes");
|
||||
|
||||
TraceObject proc = waitForValue(() -> tb.objAny0("Sessions[].Processes[]"));
|
||||
waitForPass(() -> {
|
||||
conn.execute("util.terminate_session()");
|
||||
assertEquals("STOPPED", tb.objValue(proc, lastSnap(conn), "_state"));
|
||||
}, RUN_TIMEOUT_MS, RETRY_MS);
|
||||
}
|
||||
}
|
||||
|
||||
//@Test - TODO: currently missing relevant events
|
||||
public void testOnExited() throws Exception {
|
||||
try (PythonAndTrace conn = startAndSyncPython("netstat.exe")) {
|
||||
txPut(conn, "processes");
|
||||
|
||||
// Do the synchronous wait here, since netstat should terminate
|
||||
conn.execute("util.dbg.client.go()");
|
||||
|
||||
waitForPass(() -> {
|
||||
TraceSnapshot snapshot =
|
||||
tb.trace.getTimeManager().getSnapshot(lastSnap(conn), false);
|
||||
assertNotNull(snapshot);
|
||||
assertEquals("Exited with code 0", snapshot.getDescription());
|
||||
|
||||
TraceObject proc = tb.objAny0("Sessions[].Processes[]");
|
||||
assertNotNull(proc);
|
||||
Object val = tb.objValue(proc, lastSnap(conn), "_exit_code");
|
||||
assertThat(val, instanceOf(Number.class));
|
||||
assertEquals(0, ((Number) val).longValue());
|
||||
conn.execute("util.terminate_session()");
|
||||
}, RUN_TIMEOUT_MS, RETRY_MS);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnBreakpointCreated() throws Exception {
|
||||
try (PythonAndTrace conn = startAndSyncPython(NOTEPAD)) {
|
||||
txPut(conn, "breakpoints");
|
||||
assertEquals(0,
|
||||
tb.objValues(lastSnap(conn), "Sessions[].Processes[].Debug.Software Breakpoints[]").size());
|
||||
|
||||
conn.execute("pc = util.get_pc()");
|
||||
conn.execute("util.dbg.client.set_breakpoint(address_or_symbol=pc)");
|
||||
conn.execute("util.dbg.client.stepi()"); // no real event for bpt changes
|
||||
|
||||
waitForPass(() -> {
|
||||
List<Object> brks =
|
||||
tb.objValues(lastSnap(conn), "Sessions[].Processes[].Debug.Software Breakpoints[]");
|
||||
assertEquals(1, brks.size());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//@Test - works but has timing issues
|
||||
public void testOnBreakpointModified() throws Exception {
|
||||
try (PythonAndTrace conn = startAndSyncPython(NOTEPAD)) {
|
||||
txPut(conn, "breakpoints");
|
||||
assertEquals(0,
|
||||
tb.objValues(lastSnap(conn), "Sessions[].Processes[].Debug.Software Breakpoints[]").size());
|
||||
|
||||
conn.execute("pc = util.get_pc()");
|
||||
conn.execute("util.dbg.client.set_breakpoint(address_or_symbol=pc)");
|
||||
conn.execute("util.dbg.client.stepi()"); // no real event for bpt changes
|
||||
|
||||
TraceObject brk = waitForPass(() -> {
|
||||
List<Object> brks =
|
||||
tb.objValues(lastSnap(conn), "Sessions[].Processes[].Debug.Software Breakpoints[]");
|
||||
assertEquals(1, brks.size());
|
||||
return (TraceObject) brks.get(0);
|
||||
});
|
||||
|
||||
assertEquals(true, tb.objValue(brk, lastSnap(conn), "Enabled"));
|
||||
conn.execute("util.dbg.client.toggle_breakpoint(address_name_symbol_or_none=pc, on=False)");
|
||||
conn.execute("util.dbg.client.stepi()");
|
||||
conn.execute("util.dbg.client.wait_until_stopped()");
|
||||
conn.execute("util.dbg.client.stepi()");
|
||||
assertEquals(false, tb.objValue(brk, lastSnap(conn), "Enabled"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnBreakpointDeleted() throws Exception {
|
||||
try (PythonAndTrace conn = startAndSyncPython(NOTEPAD)) {
|
||||
txPut(conn, "breakpoints");
|
||||
assertEquals(0,
|
||||
tb.objValues(lastSnap(conn), "Sessions[].Processes[].Debug.Software Breakpoints[]").size());
|
||||
|
||||
conn.execute("pc = util.get_pc()");
|
||||
conn.execute("util.dbg.client.set_breakpoint(address_or_symbol=pc)");
|
||||
conn.execute("util.dbg.client.stepi()"); // no real event for bpt changes
|
||||
|
||||
TraceObject brk = waitForPass(() -> {
|
||||
List<Object> brks =
|
||||
tb.objValues(lastSnap(conn), "Sessions[].Processes[].Debug.Software Breakpoints[]");
|
||||
assertEquals(1, brks.size());
|
||||
return (TraceObject) brks.get(0);
|
||||
});
|
||||
|
||||
conn.execute("util.dbg.client.clear_breakpoint(address_name_symbol_or_none=pc)");
|
||||
conn.execute("util.dbg.client.stepi()");
|
||||
|
||||
waitForPass(() -> assertEquals(0,
|
||||
tb.objValues(lastSnap(conn), "Sessions[].Processes[].Debug.Software Breakpoints[]").size()));
|
||||
}
|
||||
}
|
||||
|
||||
private void start(PythonAndConnection conn, String obj) {
|
||||
conn.execute("from ghidraxdbg.commands import *");
|
||||
if (obj != null)
|
||||
conn.execute("ghidra_trace_create('" + obj + "', wait=True)");
|
||||
else
|
||||
conn.execute("ghidra_trace_create()");
|
||||
conn.execute("ghidra_trace_sync_enable()");
|
||||
}
|
||||
|
||||
private void txPut(PythonAndTrace conn, String obj) {
|
||||
conn.execute("ghidra_trace_txstart('Tx" + obj + "')");
|
||||
conn.execute("ghidra_trace_put_" + obj + "()");
|
||||
conn.execute("ghidra_trace_txcommit()");
|
||||
}
|
||||
|
||||
private void clearBreakpoints(PythonAndConnection conn) {
|
||||
conn.execute("util.dbg.client.clear_breakpoint(None)");
|
||||
conn.execute("util.dbg.client.clear_hardware_breakpoint(None)");
|
||||
conn.execute("util.dbg.client.clear_memory_breakpoint(None)");
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user