mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-06 03:50:02 +02:00
GP-568: Factored pty interfaces, change terms, implements GDB over SSH
This commit is contained in:
parent
f077adfffb
commit
4d710ce2bc
41 changed files with 1588 additions and 319 deletions
|
@ -20,6 +20,7 @@ import java.util.concurrent.CompletableFuture;
|
||||||
import agent.gdb.gadp.GdbLocalDebuggerModelFactory;
|
import agent.gdb.gadp.GdbLocalDebuggerModelFactory;
|
||||||
import agent.gdb.manager.GdbManager;
|
import agent.gdb.manager.GdbManager;
|
||||||
import agent.gdb.model.impl.GdbModelImpl;
|
import agent.gdb.model.impl.GdbModelImpl;
|
||||||
|
import agent.gdb.pty.linux.LinuxPtyFactory;
|
||||||
import ghidra.dbg.DebuggerObjectModel;
|
import ghidra.dbg.DebuggerObjectModel;
|
||||||
import ghidra.dbg.LocalDebuggerModelFactory;
|
import ghidra.dbg.LocalDebuggerModelFactory;
|
||||||
import ghidra.dbg.util.ConfigurableFactory.FactoryDescription;
|
import ghidra.dbg.util.ConfigurableFactory.FactoryDescription;
|
||||||
|
@ -30,8 +31,8 @@ import ghidra.util.classfinder.ExtensionPointProperties;
|
||||||
* may change if it proves stable, though, no?
|
* may change if it proves stable, though, no?
|
||||||
*/
|
*/
|
||||||
@FactoryDescription( //
|
@FactoryDescription( //
|
||||||
brief = "IN-VM GNU gdb local debugger", //
|
brief = "IN-VM GNU gdb local debugger", //
|
||||||
htmlDetails = "Launch a GDB session in this same JVM" //
|
htmlDetails = "Launch a GDB session in this same JVM" //
|
||||||
)
|
)
|
||||||
@ExtensionPointProperties(priority = 80)
|
@ExtensionPointProperties(priority = 80)
|
||||||
public class GdbInJvmDebuggerModelFactory implements LocalDebuggerModelFactory {
|
public class GdbInJvmDebuggerModelFactory implements LocalDebuggerModelFactory {
|
||||||
|
@ -48,7 +49,8 @@ public class GdbInJvmDebuggerModelFactory implements LocalDebuggerModelFactory {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CompletableFuture<? extends DebuggerObjectModel> build() {
|
public CompletableFuture<? extends DebuggerObjectModel> build() {
|
||||||
GdbModelImpl model = new GdbModelImpl();
|
// TODO: Choose Linux or Windows pty based on host OS
|
||||||
|
GdbModelImpl model = new GdbModelImpl(new LinuxPtyFactory());
|
||||||
return model.startGDB(gdbCmd, new String[] {}).thenApply(__ -> model);
|
return model.startGDB(gdbCmd, new String[] {}).thenApply(__ -> model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,130 @@
|
||||||
|
/* ###
|
||||||
|
* 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.gdb;
|
||||||
|
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
import agent.gdb.model.impl.GdbModelImpl;
|
||||||
|
import agent.gdb.pty.ssh.GhidraSshPtyFactory;
|
||||||
|
import ghidra.dbg.DebuggerObjectModel;
|
||||||
|
import ghidra.dbg.LocalDebuggerModelFactory;
|
||||||
|
import ghidra.dbg.util.ConfigurableFactory.FactoryDescription;
|
||||||
|
import ghidra.util.classfinder.ExtensionPointProperties;
|
||||||
|
|
||||||
|
@FactoryDescription(
|
||||||
|
brief = "GNU gdb via SSH",
|
||||||
|
htmlDetails = "Launch a GDB session over an SSH connection")
|
||||||
|
@ExtensionPointProperties(priority = 60)
|
||||||
|
public class GdbOverSshDebuggerModelFactory implements LocalDebuggerModelFactory {
|
||||||
|
|
||||||
|
private String gdbCmd = "gdb";
|
||||||
|
@FactoryOption("GDB launch command")
|
||||||
|
public final Property<String> gdbCommandOption =
|
||||||
|
Property.fromAccessors(String.class, this::getGdbCommand, this::setGdbCommand);
|
||||||
|
|
||||||
|
private boolean existing = false;
|
||||||
|
@FactoryOption("Use existing session via new-ui")
|
||||||
|
public final Property<Boolean> useExistingOption =
|
||||||
|
Property.fromAccessors(boolean.class, this::isUseExisting, this::setUseExisting);
|
||||||
|
|
||||||
|
private String hostname = "localhost";
|
||||||
|
@FactoryOption("SSH hostname")
|
||||||
|
public final Property<String> hostnameOption =
|
||||||
|
Property.fromAccessors(String.class, this::getHostname, this::setHostname);
|
||||||
|
|
||||||
|
private int port = 22;
|
||||||
|
@FactoryOption("SSH TCP port")
|
||||||
|
public final Property<Integer> portOption =
|
||||||
|
Property.fromAccessors(Integer.class, this::getPort, this::setPort);
|
||||||
|
|
||||||
|
private String username = "user";
|
||||||
|
@FactoryOption("SSH username")
|
||||||
|
public final Property<String> usernameOption =
|
||||||
|
Property.fromAccessors(String.class, this::getUsername, this::setUsername);
|
||||||
|
|
||||||
|
private String keyFile = "";
|
||||||
|
@FactoryOption("SSH identity (blank for password auth)")
|
||||||
|
public final Property<String> keyFileOption =
|
||||||
|
Property.fromAccessors(String.class, this::getKeyFile, this::setKeyFile);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<? extends DebuggerObjectModel> build() {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
GhidraSshPtyFactory factory = new GhidraSshPtyFactory();
|
||||||
|
factory.setHostname(hostname);
|
||||||
|
factory.setPort(port);
|
||||||
|
factory.setKeyFile(keyFile);
|
||||||
|
factory.setUsername(username);
|
||||||
|
return new GdbModelImpl(factory);
|
||||||
|
}).thenCompose(model -> {
|
||||||
|
return model.startGDB(gdbCmd, new String[] {}).thenApply(__ -> model);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isCompatible() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getGdbCommand() {
|
||||||
|
return gdbCmd;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setGdbCommand(String gdbCmd) {
|
||||||
|
this.gdbCmd = gdbCmd;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isUseExisting() {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUseExisting(boolean existing) {
|
||||||
|
this.existing = existing;
|
||||||
|
gdbCommandOption.setEnabled(!existing);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getHostname() {
|
||||||
|
return hostname;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHostname(String hostname) {
|
||||||
|
this.hostname = hostname;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPort() {
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPort(int port) {
|
||||||
|
this.port = port;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUsername(String username) {
|
||||||
|
this.username = username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getKeyFile() {
|
||||||
|
return keyFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setKeyFile(String keyFile) {
|
||||||
|
this.keyFile = keyFile;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,153 +0,0 @@
|
||||||
/* ###
|
|
||||||
* 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.gdb.ffi.linux;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
|
|
||||||
import ghidra.util.Msg;
|
|
||||||
import jnr.ffi.Pointer;
|
|
||||||
import jnr.ffi.byref.IntByReference;
|
|
||||||
import jnr.posix.POSIX;
|
|
||||||
import jnr.posix.POSIXFactory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A pseudo-terminal
|
|
||||||
*
|
|
||||||
* A pseudo-terminal is essentially a two way pipe where one end acts as the master, and the other
|
|
||||||
* acts as the slave. The process opening the pseudo-terminal is given a handle to both ends. The
|
|
||||||
* slave end is generally given to a subprocess, possibly designating the pty as the controlling tty
|
|
||||||
* of a new session. This scheme is how, for example, an SSH daemon starts a new login shell. The
|
|
||||||
* shell is given the slave end, and the master end is presented to the SSH client.
|
|
||||||
*
|
|
||||||
* This is more powerful than controlling a process via standard in and standard out. 1) Some
|
|
||||||
* programs detect whether or not stdin/out/err refer to the controlling tty. For example, a program
|
|
||||||
* should avoid prompting for passwords unless stdin is the controlling tty. Using a pty can provide
|
|
||||||
* a controlling tty that is not necessarily controlled by a user. 2) Terminals have other
|
|
||||||
* properties and can, e.g., send signals to the foreground process group (job) by sending special
|
|
||||||
* characters. Normal characters are passed to the slave, but special characters may be interpreted
|
|
||||||
* by the terminal's <em>line discipline</em>. A rather common case is to send Ctrl-C (character
|
|
||||||
* 003). Using stdin, the subprocess simply reads 003. With a properly-configured pty and session,
|
|
||||||
* the subprocess is interrupted (sent SIGINT) instead.
|
|
||||||
*
|
|
||||||
* This class opens a pseudo-terminal and presents both ends as individual handles. The master end
|
|
||||||
* simply provides an input and output stream. These are typical byte-oriented streams, except that
|
|
||||||
* the data passes through the pty, subject to interpretation by the OS kernel. On Linux, this means
|
|
||||||
* the pty will apply the configured line discipline. Consult the host OS documentation for special
|
|
||||||
* character sequences.
|
|
||||||
*
|
|
||||||
* The slave end also provides the input and output streams, but it is uncommon to use them from the
|
|
||||||
* same process. More likely, subprocess is launched in a new session, configuring the slave as the
|
|
||||||
* controlling terminal. Thus, the slave handle provides methods for obtaining the slave pty file
|
|
||||||
* name and/or spawning a new session. Once spawned, the master end is used to control the session.
|
|
||||||
*
|
|
||||||
* Example:
|
|
||||||
*
|
|
||||||
* <pre>
|
|
||||||
* Pty pty = Pty.openpty();
|
|
||||||
* pty.getSlave().session("bash");
|
|
||||||
*
|
|
||||||
* PrintWriter writer = new PrintWriter(pty.getMaster().getOutputStream());
|
|
||||||
* writer.println("echo test");
|
|
||||||
* BufferedReader reader =
|
|
||||||
* new BufferedReader(new InputStreamReader(pty.getMaster().getInputStream()));
|
|
||||||
* System.out.println(reader.readLine());
|
|
||||||
* System.out.println(reader.readLine());
|
|
||||||
*
|
|
||||||
* pty.close();
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
public class Pty implements AutoCloseable {
|
|
||||||
private static final POSIX LIB_POSIX = POSIXFactory.getNativePOSIX();
|
|
||||||
|
|
||||||
private final int amaster;
|
|
||||||
private final int aslave;
|
|
||||||
private final String name;
|
|
||||||
private boolean closed = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open a new pseudo-terminal
|
|
||||||
*
|
|
||||||
* Implementation note: On Linux, this invokes the native {@code openpty()} function. See the
|
|
||||||
* Linux manual for details.
|
|
||||||
*
|
|
||||||
* @return new new Pty
|
|
||||||
* @throws IOException if openpty fails
|
|
||||||
*/
|
|
||||||
public static Pty openpty() throws IOException {
|
|
||||||
// TODO: Support termp and winp?
|
|
||||||
IntByReference m = new IntByReference();
|
|
||||||
IntByReference s = new IntByReference();
|
|
||||||
Pointer n = Pointer.wrap(jnr.ffi.Runtime.getSystemRuntime(), ByteBuffer.allocate(1024));
|
|
||||||
if (Util.INSTANCE.openpty(m, s, n, null, null) < 0) {
|
|
||||||
int errno = LIB_POSIX.errno();
|
|
||||||
throw new IOException(errno + ": " + LIB_POSIX.strerror(errno));
|
|
||||||
}
|
|
||||||
return new Pty(m.intValue(), s.intValue(), n.getString(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
Pty(int amaster, int aslave, String name) {
|
|
||||||
Msg.debug(this, "New Pty: " + name + " at (" + amaster + "," + aslave + ")");
|
|
||||||
this.amaster = amaster;
|
|
||||||
this.aslave = aslave;
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a handle to the master side of the pty
|
|
||||||
*
|
|
||||||
* @return the master handle
|
|
||||||
*/
|
|
||||||
public PtyMaster getMaster() {
|
|
||||||
return new PtyMaster(amaster);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a handle to the slave side of the pty
|
|
||||||
*
|
|
||||||
* @return the slave handle
|
|
||||||
*/
|
|
||||||
public PtySlave getSlave() {
|
|
||||||
return new PtySlave(aslave, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Closes both ends of the pty
|
|
||||||
*
|
|
||||||
* This only closes this process's handles to the pty. For the master end, this should be the
|
|
||||||
* only process with a handle. The slave end may be opened by any number of other processes.
|
|
||||||
* More than likely, however, those processes will terminate once the master end is closed,
|
|
||||||
* since reads or writes on the slave will produce EOF or an error.
|
|
||||||
*
|
|
||||||
* @throws IOException if an I/O error occurs
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public synchronized void close() throws IOException {
|
|
||||||
if (closed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
int result;
|
|
||||||
result = LIB_POSIX.close(aslave);
|
|
||||||
if (result < 0) {
|
|
||||||
throw new IOException(LIB_POSIX.strerror(LIB_POSIX.errno()));
|
|
||||||
}
|
|
||||||
result = LIB_POSIX.close(amaster);
|
|
||||||
if (result < 0) {
|
|
||||||
throw new IOException(LIB_POSIX.strerror(LIB_POSIX.errno()));
|
|
||||||
}
|
|
||||||
closed = true;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -21,6 +21,7 @@ import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
import agent.gdb.gadp.GdbGadpServer;
|
import agent.gdb.gadp.GdbGadpServer;
|
||||||
import agent.gdb.model.impl.GdbModelImpl;
|
import agent.gdb.model.impl.GdbModelImpl;
|
||||||
|
import agent.gdb.pty.linux.LinuxPtyFactory;
|
||||||
import ghidra.dbg.gadp.server.AbstractGadpServer;
|
import ghidra.dbg.gadp.server.AbstractGadpServer;
|
||||||
|
|
||||||
public class GdbGadpServerImpl implements GdbGadpServer {
|
public class GdbGadpServerImpl implements GdbGadpServer {
|
||||||
|
@ -35,7 +36,8 @@ public class GdbGadpServerImpl implements GdbGadpServer {
|
||||||
|
|
||||||
public GdbGadpServerImpl(SocketAddress addr) throws IOException {
|
public GdbGadpServerImpl(SocketAddress addr) throws IOException {
|
||||||
super();
|
super();
|
||||||
this.model = new GdbModelImpl();
|
// TODO: Select Linux or Windows factory based on host OS
|
||||||
|
this.model = new GdbModelImpl(new LinuxPtyFactory());
|
||||||
this.server = new GadpSide(model, addr);
|
this.server = new GadpSide(model, addr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,10 +21,12 @@ import java.util.Map;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
import agent.gdb.ffi.linux.Pty;
|
|
||||||
import agent.gdb.manager.breakpoint.GdbBreakpointInfo;
|
import agent.gdb.manager.breakpoint.GdbBreakpointInfo;
|
||||||
import agent.gdb.manager.breakpoint.GdbBreakpointInsertions;
|
import agent.gdb.manager.breakpoint.GdbBreakpointInsertions;
|
||||||
import agent.gdb.manager.impl.GdbManagerImpl;
|
import agent.gdb.manager.impl.GdbManagerImpl;
|
||||||
|
import agent.gdb.pty.PtyFactory;
|
||||||
|
import agent.gdb.pty.linux.LinuxPty;
|
||||||
|
import agent.gdb.pty.linux.LinuxPtyFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The controlling side of a GDB session, using GDB/MI, usually via a pseudo-terminal
|
* The controlling side of a GDB session, using GDB/MI, usually via a pseudo-terminal
|
||||||
|
@ -85,7 +87,8 @@ public interface GdbManager extends AutoCloseable, GdbBreakpointInsertions {
|
||||||
*/
|
*/
|
||||||
public static void main(String[] args)
|
public static void main(String[] args)
|
||||||
throws InterruptedException, ExecutionException, IOException {
|
throws InterruptedException, ExecutionException, IOException {
|
||||||
try (GdbManager mgr = newInstance()) {
|
// TODO: Choose factory by host OS
|
||||||
|
try (GdbManager mgr = newInstance(new LinuxPtyFactory())) {
|
||||||
mgr.start(DEFAULT_GDB_CMD, args);
|
mgr.start(DEFAULT_GDB_CMD, args);
|
||||||
mgr.runRC().get();
|
mgr.runRC().get();
|
||||||
mgr.consoleLoop();
|
mgr.consoleLoop();
|
||||||
|
@ -101,8 +104,8 @@ public interface GdbManager extends AutoCloseable, GdbBreakpointInsertions {
|
||||||
*
|
*
|
||||||
* @return the manager
|
* @return the manager
|
||||||
*/
|
*/
|
||||||
public static GdbManager newInstance() {
|
public static GdbManager newInstance(PtyFactory ptyFactory) {
|
||||||
return new GdbManagerImpl();
|
return new GdbManagerImpl(ptyFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -203,7 +206,8 @@ public interface GdbManager extends AutoCloseable, GdbBreakpointInsertions {
|
||||||
* Note: depending on the target, its output may not be communicated via this listener. Local
|
* Note: depending on the target, its output may not be communicated via this listener. Local
|
||||||
* targets, e.g., tend to just print output to GDB's controlling TTY. See
|
* targets, e.g., tend to just print output to GDB's controlling TTY. See
|
||||||
* {@link GdbInferior#setTty(String)} for a means to more reliably interact with a target's
|
* {@link GdbInferior#setTty(String)} for a means to more reliably interact with a target's
|
||||||
* input and output. See also {@link Pty} for a means to easily acquire a new TTY from Java.
|
* input and output. See also {@link LinuxPty} for a means to easily acquire a new TTY from
|
||||||
|
* Java.
|
||||||
*
|
*
|
||||||
* @param listener the listener to add
|
* @param listener the listener to add
|
||||||
*/
|
*/
|
||||||
|
@ -507,6 +511,7 @@ public interface GdbManager extends AutoCloseable, GdbBreakpointInsertions {
|
||||||
* Get the name of the mi2 pty for this GDB session
|
* Get the name of the mi2 pty for this GDB session
|
||||||
*
|
*
|
||||||
* @return the filename
|
* @return the filename
|
||||||
|
* @throws IOException if the filename could not be determined
|
||||||
*/
|
*/
|
||||||
String getMi2PtyName();
|
String getMi2PtyName() throws IOException;
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,6 @@ import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||||
import org.python.core.PyDictionary;
|
import org.python.core.PyDictionary;
|
||||||
import org.python.util.InteractiveConsole;
|
import org.python.util.InteractiveConsole;
|
||||||
|
|
||||||
import agent.gdb.ffi.linux.Pty;
|
|
||||||
import agent.gdb.manager.*;
|
import agent.gdb.manager.*;
|
||||||
import agent.gdb.manager.GdbCause.Causes;
|
import agent.gdb.manager.GdbCause.Causes;
|
||||||
import agent.gdb.manager.breakpoint.GdbBreakpointInfo;
|
import agent.gdb.manager.breakpoint.GdbBreakpointInfo;
|
||||||
|
@ -35,6 +34,7 @@ import agent.gdb.manager.evt.*;
|
||||||
import agent.gdb.manager.impl.cmd.*;
|
import agent.gdb.manager.impl.cmd.*;
|
||||||
import agent.gdb.manager.parsing.GdbMiParser;
|
import agent.gdb.manager.parsing.GdbMiParser;
|
||||||
import agent.gdb.manager.parsing.GdbParsingUtils.GdbParseError;
|
import agent.gdb.manager.parsing.GdbParsingUtils.GdbParseError;
|
||||||
|
import agent.gdb.pty.*;
|
||||||
import ghidra.async.*;
|
import ghidra.async.*;
|
||||||
import ghidra.async.AsyncLock.Hold;
|
import ghidra.async.AsyncLock.Hold;
|
||||||
import ghidra.dbg.error.DebuggerModelTerminatingException;
|
import ghidra.dbg.error.DebuggerModelTerminatingException;
|
||||||
|
@ -104,7 +104,7 @@ public class GdbManagerImpl implements GdbManager {
|
||||||
this.pty = pty;
|
this.pty = pty;
|
||||||
this.channel = channel;
|
this.channel = channel;
|
||||||
this.reader =
|
this.reader =
|
||||||
new BufferedReader(new InputStreamReader(pty.getMaster().getInputStream()));
|
new BufferedReader(new InputStreamReader(pty.getParent().getInputStream()));
|
||||||
this.interpreter = interpreter;
|
this.interpreter = interpreter;
|
||||||
hasWriter = new CompletableFuture<>();
|
hasWriter = new CompletableFuture<>();
|
||||||
}
|
}
|
||||||
|
@ -124,7 +124,7 @@ public class GdbManagerImpl implements GdbManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (writer == null) {
|
if (writer == null) {
|
||||||
writer = new PrintWriter(pty.getMaster().getOutputStream());
|
writer = new PrintWriter(pty.getParent().getOutputStream());
|
||||||
hasWriter.complete(null);
|
hasWriter.complete(null);
|
||||||
}
|
}
|
||||||
//Msg.debug(this, channel + ": " + line);
|
//Msg.debug(this, channel + ": " + line);
|
||||||
|
@ -145,6 +145,8 @@ public class GdbManagerImpl implements GdbManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final PtyFactory ptyFactory;
|
||||||
|
|
||||||
private final AsyncReference<GdbState, GdbCause> state =
|
private final AsyncReference<GdbState, GdbCause> state =
|
||||||
new AsyncReference<>(GdbState.NOT_STARTED);
|
new AsyncReference<>(GdbState.NOT_STARTED);
|
||||||
// A copy of state, which is updated on the eventThread.
|
// A copy of state, which is updated on the eventThread.
|
||||||
|
@ -156,7 +158,7 @@ public class GdbManagerImpl implements GdbManager {
|
||||||
private final HandlerMap<GdbEvent<?>, Void, Void> handlerMap = new HandlerMap<>();
|
private final HandlerMap<GdbEvent<?>, Void, Void> handlerMap = new HandlerMap<>();
|
||||||
private final AtomicBoolean exited = new AtomicBoolean(false);
|
private final AtomicBoolean exited = new AtomicBoolean(false);
|
||||||
|
|
||||||
private Process gdb;
|
private PtySession gdb;
|
||||||
private Thread gdbWaiter;
|
private Thread gdbWaiter;
|
||||||
|
|
||||||
private PtyThread iniThread;
|
private PtyThread iniThread;
|
||||||
|
@ -193,8 +195,12 @@ public class GdbManagerImpl implements GdbManager {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instantiate a new manager
|
* Instantiate a new manager
|
||||||
|
*
|
||||||
|
* @param ptyFactory a factory for creating Pty's for child GDBs
|
||||||
*/
|
*/
|
||||||
public GdbManagerImpl() {
|
public GdbManagerImpl(PtyFactory ptyFactory) {
|
||||||
|
this.ptyFactory = ptyFactory;
|
||||||
|
|
||||||
state.filter(this::stateFilter);
|
state.filter(this::stateFilter);
|
||||||
state.addChangeListener(this::trackRunningInterpreter);
|
state.addChangeListener(this::trackRunningInterpreter);
|
||||||
state.addChangeListener((os, ns, c) -> event(() -> asyncState.set(ns, c), "managerState"));
|
state.addChangeListener((os, ns, c) -> event(() -> asyncState.set(ns, c), "managerState"));
|
||||||
|
@ -556,9 +562,9 @@ public class GdbManagerImpl implements GdbManager {
|
||||||
executor = Executors.newSingleThreadExecutor();
|
executor = Executors.newSingleThreadExecutor();
|
||||||
|
|
||||||
if (gdbCmd != null) {
|
if (gdbCmd != null) {
|
||||||
iniThread = new PtyThread(Pty.openpty(), Channel.STDOUT, null);
|
iniThread = new PtyThread(ptyFactory.openpty(), Channel.STDOUT, null);
|
||||||
|
|
||||||
gdb = iniThread.pty.getSlave().session(fullargs.toArray(new String[] {}), null);
|
gdb = iniThread.pty.getChild().session(fullargs.toArray(new String[] {}), null);
|
||||||
gdbWaiter = new Thread(this::waitGdbExit, "GDB WaitExit");
|
gdbWaiter = new Thread(this::waitGdbExit, "GDB WaitExit");
|
||||||
gdbWaiter.start();
|
gdbWaiter.start();
|
||||||
|
|
||||||
|
@ -575,14 +581,16 @@ public class GdbManagerImpl implements GdbManager {
|
||||||
}
|
}
|
||||||
switch (iniThread.interpreter) {
|
switch (iniThread.interpreter) {
|
||||||
case CLI:
|
case CLI:
|
||||||
|
Pty mi2Pty = ptyFactory.openpty();
|
||||||
|
|
||||||
cliThread = iniThread;
|
cliThread = iniThread;
|
||||||
cliThread.setName("GDB Read CLI");
|
cliThread.setName("GDB Read CLI");
|
||||||
|
cliThread.writer.println("new-ui mi2 " + mi2Pty.getChild().nullSession());
|
||||||
|
cliThread.writer.flush();
|
||||||
|
|
||||||
mi2Thread = new PtyThread(Pty.openpty(), Channel.STDOUT, Interpreter.MI2);
|
mi2Thread = new PtyThread(mi2Pty, Channel.STDOUT, Interpreter.MI2);
|
||||||
mi2Thread.setName("GDB Read MI2");
|
mi2Thread.setName("GDB Read MI2");
|
||||||
mi2Thread.start();
|
mi2Thread.start();
|
||||||
cliThread.writer.println("new-ui mi2 " + mi2Thread.pty.getSlave().getFile());
|
|
||||||
cliThread.writer.flush();
|
|
||||||
try {
|
try {
|
||||||
mi2Thread.hasWriter.get(2, TimeUnit.SECONDS);
|
mi2Thread.hasWriter.get(2, TimeUnit.SECONDS);
|
||||||
}
|
}
|
||||||
|
@ -598,10 +606,12 @@ public class GdbManagerImpl implements GdbManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
mi2Thread = new PtyThread(Pty.openpty(), Channel.STDOUT, Interpreter.MI2);
|
Pty mi2Pty = ptyFactory.openpty();
|
||||||
mi2Thread.setName("GDB Read MI2");
|
|
||||||
Msg.info(this, "Agent is waiting for GDB/MI v2 interpreter at " +
|
Msg.info(this, "Agent is waiting for GDB/MI v2 interpreter at " +
|
||||||
mi2Thread.pty.getSlave().getFile());
|
mi2Pty.getChild().nullSession());
|
||||||
|
mi2Thread = new PtyThread(mi2Pty, Channel.STDOUT, Interpreter.MI2);
|
||||||
|
mi2Thread.setName("GDB Read MI2");
|
||||||
|
|
||||||
mi2Thread.start();
|
mi2Thread.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -622,7 +632,7 @@ public class GdbManagerImpl implements GdbManager {
|
||||||
|
|
||||||
private void waitGdbExit() {
|
private void waitGdbExit() {
|
||||||
try {
|
try {
|
||||||
int exitcode = gdb.waitFor();
|
int exitcode = gdb.waitExited();
|
||||||
state.set(GdbState.EXIT, Causes.UNCLAIMED);
|
state.set(GdbState.EXIT, Causes.UNCLAIMED);
|
||||||
exited.set(true);
|
exited.set(true);
|
||||||
if (!executor.isShutdown()) {
|
if (!executor.isShutdown()) {
|
||||||
|
@ -1466,12 +1476,12 @@ public class GdbManagerImpl implements GdbManager {
|
||||||
checkStarted();
|
checkStarted();
|
||||||
Msg.info(this, "Interrupting");
|
Msg.info(this, "Interrupting");
|
||||||
if (cliThread != null) {
|
if (cliThread != null) {
|
||||||
OutputStream os = cliThread.pty.getMaster().getOutputStream();
|
OutputStream os = cliThread.pty.getParent().getOutputStream();
|
||||||
os.write(3);
|
os.write(3);
|
||||||
os.flush();
|
os.flush();
|
||||||
}
|
}
|
||||||
if (mi2Thread != null) {
|
if (mi2Thread != null) {
|
||||||
OutputStream os = mi2Thread.pty.getMaster().getOutputStream();
|
OutputStream os = mi2Thread.pty.getParent().getOutputStream();
|
||||||
os.write(3);
|
os.write(3);
|
||||||
os.flush();
|
os.flush();
|
||||||
}
|
}
|
||||||
|
@ -1589,8 +1599,8 @@ public class GdbManagerImpl implements GdbManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getMi2PtyName() {
|
public String getMi2PtyName() throws IOException {
|
||||||
return mi2Thread.pty.getSlave().getFile().getAbsolutePath();
|
return mi2Thread.pty.getChild().nullSession();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasCli() {
|
public boolean hasCli() {
|
||||||
|
|
|
@ -24,6 +24,7 @@ import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||||
|
|
||||||
import agent.gdb.manager.*;
|
import agent.gdb.manager.*;
|
||||||
import agent.gdb.manager.impl.cmd.GdbCommandError;
|
import agent.gdb.manager.impl.cmd.GdbCommandError;
|
||||||
|
import agent.gdb.pty.PtyFactory;
|
||||||
import ghidra.async.AsyncUtils;
|
import ghidra.async.AsyncUtils;
|
||||||
import ghidra.dbg.DebuggerModelClosedReason;
|
import ghidra.dbg.DebuggerModelClosedReason;
|
||||||
import ghidra.dbg.agent.AbstractDebuggerObjectModel;
|
import ghidra.dbg.agent.AbstractDebuggerObjectModel;
|
||||||
|
@ -67,8 +68,8 @@ public class GdbModelImpl extends AbstractDebuggerObjectModel {
|
||||||
|
|
||||||
protected Map<Object, TargetObject> objectMap = new HashMap<>();
|
protected Map<Object, TargetObject> objectMap = new HashMap<>();
|
||||||
|
|
||||||
public GdbModelImpl() {
|
public GdbModelImpl(PtyFactory ptyFactory) {
|
||||||
this.gdb = GdbManager.newInstance();
|
this.gdb = GdbManager.newInstance(ptyFactory);
|
||||||
this.session = new GdbModelTargetSession(this, ROOT_SCHEMA);
|
this.session = new GdbModelTargetSession(this, ROOT_SCHEMA);
|
||||||
|
|
||||||
this.completedSession = CompletableFuture.completedFuture(session);
|
this.completedSession = CompletableFuture.completedFuture(session);
|
||||||
|
|
|
@ -0,0 +1,100 @@
|
||||||
|
/* ###
|
||||||
|
* 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.gdb.pty;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A pseudo-terminal
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* A pseudo-terminal is essentially a two way pipe where one end acts as the parent, and the other
|
||||||
|
* acts as the child. The process opening the pseudo-terminal is given a handle to both ends. The
|
||||||
|
* child end is generally given to a subprocess, possibly designating the pty as the controlling tty
|
||||||
|
* of a new session. This scheme is how, for example, an SSH daemon starts a new login shell. The
|
||||||
|
* shell is given the child end, and the parent end is presented to the SSH client.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This is more powerful than controlling a process via standard in and standard out. 1) Some
|
||||||
|
* programs detect whether or not stdin/out/err refer to the controlling tty. For example, a program
|
||||||
|
* should avoid prompting for passwords unless stdin is the controlling tty. Using a pty can provide
|
||||||
|
* a controlling tty that is not necessarily controlled by a user. 2) Terminals have other
|
||||||
|
* properties and can, e.g., send signals to the foreground process group (job) by sending special
|
||||||
|
* characters. Normal characters are passed to the child, but special characters may be interpreted
|
||||||
|
* by the terminal's <em>line discipline</em>. A rather common case is to send Ctrl-C (character
|
||||||
|
* 003). Using stdin, the subprocess simply reads 003. With a properly-configured pty and session,
|
||||||
|
* the subprocess is interrupted (sent SIGINT) instead.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This class opens a pseudo-terminal and presents both ends as individual handles. The parent end
|
||||||
|
* simply provides an input and output stream. These are typical byte-oriented streams, except that
|
||||||
|
* the data passes through the pty, subject to interpretation by the OS kernel. On Linux, this means
|
||||||
|
* the pty will apply the configured line discipline. Consult the host OS documentation for special
|
||||||
|
* character sequences.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The child end also provides the input and output streams, but it is uncommon to use them from the
|
||||||
|
* same process. More likely, subprocess is launched in a new session, configuring the child as the
|
||||||
|
* controlling terminal. Thus, the child handle provides methods for obtaining the child pty file
|
||||||
|
* name and/or spawning a new session. Once spawned, the parent end is used to control the session.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Example:
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* Pty pty = factory.openpty();
|
||||||
|
* pty.getChild().session("bash");
|
||||||
|
*
|
||||||
|
* PrintWriter writer = new PrintWriter(pty.getParent().getOutputStream());
|
||||||
|
* writer.println("echo test");
|
||||||
|
* BufferedReader reader =
|
||||||
|
* new BufferedReader(new InputStreamReader(pty.getParent().getInputStream()));
|
||||||
|
* System.out.println(reader.readLine());
|
||||||
|
* System.out.println(reader.readLine());
|
||||||
|
*
|
||||||
|
* pty.close();
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
public interface Pty extends AutoCloseable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a handle to the parent side of the pty
|
||||||
|
*
|
||||||
|
* @return the parent handle
|
||||||
|
*/
|
||||||
|
PtyParent getParent();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a handle to the child side of the pty
|
||||||
|
*
|
||||||
|
* @return the child handle
|
||||||
|
*/
|
||||||
|
PtyChild getChild();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes both ends of the pty
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This only closes this process's handles to the pty. For the parent end, this should be the
|
||||||
|
* only process with a handle. The child end may be opened by any number of other processes.
|
||||||
|
* More than likely, however, those processes will terminate once the parent end is closed,
|
||||||
|
* since reads or writes on the child will produce EOF or an error.
|
||||||
|
*
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
void close() throws IOException;
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
/* ###
|
||||||
|
* 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.gdb.pty;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The child (UNIX "slave") end of a pseudo-terminal
|
||||||
|
*/
|
||||||
|
public interface PtyChild extends PtyEndpoint {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spawn a subprocess in a new session whose controlling tty is this pseudo-terminal
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This method or {@link #nullSession()} can only be invoked once per pty.
|
||||||
|
*
|
||||||
|
* @param args the image path and arguments
|
||||||
|
* @param env the environment
|
||||||
|
* @return a handle to the subprocess
|
||||||
|
* @throws IOException if the session could not be started
|
||||||
|
*/
|
||||||
|
PtySession session(String[] args, Map<String, String> env) throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a session without a real leader, instead obtaining the pty's name
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This method or {@link #session(String[], Map)} can only be invoked once per pty. It must be
|
||||||
|
* called before anyone reads the parent's output stream, since obtaining the filename may be
|
||||||
|
* implemented by the parent sending commands to its child.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* If the child end of the pty is on a remote system, this should be the file (or other
|
||||||
|
* resource) name as it would be accessed on that remote system.
|
||||||
|
*
|
||||||
|
* @return the file name
|
||||||
|
* @throws IOException if the session could not be started or the pty name could not be
|
||||||
|
* determined
|
||||||
|
*/
|
||||||
|
String nullSession() throws IOException;
|
||||||
|
}
|
|
@ -13,44 +13,37 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.ffi.linux;
|
package agent.gdb.pty;
|
||||||
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A base class for either end of a pseudo-terminal
|
* One end of a pseudo-terminal
|
||||||
*
|
|
||||||
* This provides the input and output streams
|
|
||||||
*/
|
*/
|
||||||
public class PtyEndpoint {
|
public interface PtyEndpoint {
|
||||||
private final int fd;
|
|
||||||
|
|
||||||
PtyEndpoint(int fd) {
|
|
||||||
this.fd = fd;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the output stream for this end of the pty
|
* Get the output stream for this end of the pty
|
||||||
*
|
*
|
||||||
|
* <p>
|
||||||
* Writes to this stream arrive on the input stream for the opposite end, subject to the
|
* Writes to this stream arrive on the input stream for the opposite end, subject to the
|
||||||
* terminal's line discipline.
|
* terminal's line discipline.
|
||||||
*
|
*
|
||||||
* @return the output stream
|
* @return the output stream
|
||||||
|
* @throws UnsupportedOperationException if this end is not local
|
||||||
*/
|
*/
|
||||||
public OutputStream getOutputStream() {
|
OutputStream getOutputStream();
|
||||||
return new FdOutputStream(fd);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the input stream for this end of the pty
|
* Get the input stream for this end of the pty
|
||||||
*
|
*
|
||||||
|
* <p>
|
||||||
* Writes to the output stream of the opposite end arrive here, subject to the terminal's line
|
* Writes to the output stream of the opposite end arrive here, subject to the terminal's line
|
||||||
* discipline.
|
* discipline.
|
||||||
*
|
*
|
||||||
* @return the input stream
|
* @return the input stream
|
||||||
|
* @throws UnsupportedOperationException if this end is not local
|
||||||
*/
|
*/
|
||||||
public InputStream getInputStream() {
|
InputStream getInputStream();
|
||||||
return new FdInputStream(fd);
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
/* ###
|
||||||
|
* 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.gdb.pty;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A mechanism for opening pseudo-terminals
|
||||||
|
*/
|
||||||
|
public interface PtyFactory {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open a new pseudo-terminal
|
||||||
|
*
|
||||||
|
* @return new new Pty
|
||||||
|
* @throws IOException for an I/O error, including cancellation
|
||||||
|
*/
|
||||||
|
Pty openpty() throws IOException;
|
||||||
|
}
|
|
@ -13,13 +13,10 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.ffi.linux;
|
package agent.gdb.pty;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The master end of a pseudo-terminal
|
* The parent (UNIX "master") end of a pseudo-terminal
|
||||||
*/
|
*/
|
||||||
public class PtyMaster extends PtyEndpoint {
|
public interface PtyParent extends PtyEndpoint {
|
||||||
PtyMaster(int fd) {
|
|
||||||
super(fd);
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
/* ###
|
||||||
|
* 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.gdb.pty;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A session led by the child pty
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This is typically a handle to the (local or remote) process designated as the "session leader"
|
||||||
|
*/
|
||||||
|
public interface PtySession {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for the session leader to exit, returning its optional exit status code
|
||||||
|
*
|
||||||
|
* @return the status code, if applicable and implemented
|
||||||
|
* @throws InterruptedException if the wait is interrupted
|
||||||
|
*/
|
||||||
|
Integer waitExited() throws InterruptedException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Take the greatest efforts to terminate the session (leader and descendants)
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* If this represents a remote session, this should strive to release the remote resources
|
||||||
|
* consumed by this session. If that is not possible, this should at the very least release
|
||||||
|
* whatever local resources are used in maintaining and controlling the remote session.
|
||||||
|
*/
|
||||||
|
void destroyForcibly();
|
||||||
|
}
|
|
@ -13,7 +13,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.ffi.linux;
|
package agent.gdb.pty.linux;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
@ -25,8 +25,10 @@ import jnr.posix.POSIXFactory;
|
||||||
/**
|
/**
|
||||||
* An input stream that wraps a native POSIX file descriptor
|
* An input stream that wraps a native POSIX file descriptor
|
||||||
*
|
*
|
||||||
* WARNING: This class makes use of jnr-ffi to invoke native functions. An invalid file descriptor
|
* <p>
|
||||||
* is generally detected, but an incorrect, but valid file descriptor may cause undefined behavior.
|
* <b>WARNING:</b> This class makes use of jnr-ffi to invoke native functions. An invalid file
|
||||||
|
* descriptor is generally detected, but an incorrect, but valid file descriptor may cause undefined
|
||||||
|
* behavior.
|
||||||
*/
|
*/
|
||||||
public class FdInputStream extends InputStream {
|
public class FdInputStream extends InputStream {
|
||||||
private static final POSIX LIB_POSIX = POSIXFactory.getNativePOSIX();
|
private static final POSIX LIB_POSIX = POSIXFactory.getNativePOSIX();
|
|
@ -13,7 +13,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.ffi.linux;
|
package agent.gdb.pty.linux;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
@ -25,8 +25,10 @@ import jnr.posix.POSIXFactory;
|
||||||
/**
|
/**
|
||||||
* An output stream that wraps a native POSIX file descriptor
|
* An output stream that wraps a native POSIX file descriptor
|
||||||
*
|
*
|
||||||
* WARNING: This class makes use of jnr-ffi to invoke native functions. An invalid file descriptor
|
* <p>
|
||||||
* is generally detected, but an incorrect, but valid file descriptor may cause undefined behavior.
|
* <b>WARNING:</b> This class makes use of jnr-ffi to invoke native functions. An invalid file
|
||||||
|
* descriptor is generally detected, but an incorrect, but valid file descriptor may cause undefined
|
||||||
|
* behavior.
|
||||||
*/
|
*/
|
||||||
public class FdOutputStream extends OutputStream {
|
public class FdOutputStream extends OutputStream {
|
||||||
private static final POSIX LIB_POSIX = POSIXFactory.getNativePOSIX();
|
private static final POSIX LIB_POSIX = POSIXFactory.getNativePOSIX();
|
|
@ -0,0 +1,87 @@
|
||||||
|
/* ###
|
||||||
|
* 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.gdb.pty.linux;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
import agent.gdb.pty.Pty;
|
||||||
|
import ghidra.util.Msg;
|
||||||
|
import jnr.ffi.Pointer;
|
||||||
|
import jnr.ffi.byref.IntByReference;
|
||||||
|
import jnr.posix.POSIX;
|
||||||
|
import jnr.posix.POSIXFactory;
|
||||||
|
|
||||||
|
public class LinuxPty implements Pty {
|
||||||
|
static final POSIX LIB_POSIX = POSIXFactory.getNativePOSIX();
|
||||||
|
|
||||||
|
private final int aparent;
|
||||||
|
private final int achild;
|
||||||
|
//private final String name;
|
||||||
|
private boolean closed = false;
|
||||||
|
|
||||||
|
private final LinuxPtyParent parent;
|
||||||
|
private final LinuxPtyChild child;
|
||||||
|
|
||||||
|
public static LinuxPty openpty() throws IOException {
|
||||||
|
// TODO: Support termp and winp?
|
||||||
|
IntByReference p = new IntByReference();
|
||||||
|
IntByReference c = new IntByReference();
|
||||||
|
Pointer n = Pointer.wrap(jnr.ffi.Runtime.getSystemRuntime(), ByteBuffer.allocate(1024));
|
||||||
|
if (Util.INSTANCE.openpty(p, c, n, null, null) < 0) {
|
||||||
|
int errno = LIB_POSIX.errno();
|
||||||
|
throw new IOException(errno + ": " + LIB_POSIX.strerror(errno));
|
||||||
|
}
|
||||||
|
return new LinuxPty(p.intValue(), c.intValue(), n.getString(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
LinuxPty(int aparent, int achild, String name) {
|
||||||
|
Msg.debug(this, "New Pty: " + name + " at (" + aparent + "," + achild + ")");
|
||||||
|
this.aparent = aparent;
|
||||||
|
this.achild = achild;
|
||||||
|
//this.name = name;
|
||||||
|
|
||||||
|
this.parent = new LinuxPtyParent(aparent);
|
||||||
|
this.child = new LinuxPtyChild(achild, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LinuxPtyParent getParent() {
|
||||||
|
return parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LinuxPtyChild getChild() {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void close() throws IOException {
|
||||||
|
if (closed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int result;
|
||||||
|
result = LIB_POSIX.close(achild);
|
||||||
|
if (result < 0) {
|
||||||
|
throw new IOException(LIB_POSIX.strerror(LIB_POSIX.errno()));
|
||||||
|
}
|
||||||
|
result = LIB_POSIX.close(aparent);
|
||||||
|
if (result < 0) {
|
||||||
|
throw new IOException(LIB_POSIX.strerror(LIB_POSIX.errno()));
|
||||||
|
}
|
||||||
|
closed = true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,7 +13,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.ffi.linux;
|
package agent.gdb.pty.linux;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
@ -21,60 +21,57 @@ import java.net.URLDecoder;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
/**
|
import agent.gdb.pty.PtyChild;
|
||||||
* The slave end of a pseudo-terminal
|
import agent.gdb.pty.PtySession;
|
||||||
*/
|
import agent.gdb.pty.local.LocalProcessPtySession;
|
||||||
public class PtySlave extends PtyEndpoint {
|
|
||||||
private final File file;
|
|
||||||
|
|
||||||
PtySlave(int fd, String name) {
|
public class LinuxPtyChild extends LinuxPtyEndpoint implements PtyChild {
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
LinuxPtyChild(int fd, String name) {
|
||||||
super(fd);
|
super(fd);
|
||||||
this.file = new File(name);
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String nullSession() {
|
||||||
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the file referring to this pseudo-terminal
|
* {@inheritDoc}
|
||||||
*
|
*
|
||||||
* @return the file
|
* @implNote This uses {@link ProcessBuilder} to launch the subprocess. See its documentation
|
||||||
*/
|
* for more details of the parameters of this method.
|
||||||
public File getFile() {
|
* @implNote This actually launches a special "leader" subprocess, which sets up the session and
|
||||||
return file;
|
* then executes the requested program. The requested program image replaces the
|
||||||
}
|
* leader so that the returned process is indeed a handle to the requested program.
|
||||||
|
* Ordinarily, this does not matter, but it may be useful to know when debugging.
|
||||||
/**
|
* Furthermore, if special characters are sent on the parent before the image is
|
||||||
* Spawn a subprocess in a new session whose controlling tty is this pseudo-terminal
|
* replaced, they may be received by the leader instead. For example, Ctrl-C might be
|
||||||
*
|
* received by the leader by mistake if sent immediately upon spawning a new session.
|
||||||
* Implementation note: This uses {@link ProcessBuilder} to launch the subprocess. See its
|
* Users should send a simple command, e.g., "echo", to confirm that the requested
|
||||||
* documentation for more details of the parameters of this method.
|
* program is active before sending special characters.
|
||||||
*
|
|
||||||
* Deep implementation note: This actually launches a Python script, which sets up the session
|
|
||||||
* and then executes the requested program. The requested program image replaces the Python
|
|
||||||
* interpreter so that the returned process is indeed a handle to the requested program, not a
|
|
||||||
* Python interpreter. Ordinarily, this does not matter, but it may be useful to know when
|
|
||||||
* debugging. Furthermore, if special characters are sent on the master before Python has
|
|
||||||
* executed the requested program, they may be received by the Python interpreter. For example,
|
|
||||||
* Ctrl-C might be received by Python by mistake if sent immediately upon spawning a new
|
|
||||||
* session. Users should send a simple command, e.g., "echo", to confirm that the requested
|
|
||||||
* program is active before sending special characters.
|
|
||||||
*
|
*
|
||||||
* @param args the image path and arguments
|
* @param args the image path and arguments
|
||||||
* @param env the environment
|
* @param env the environment
|
||||||
* @return a handle to the subprocess
|
* @return a handle to the subprocess
|
||||||
* @throws IOException
|
* @throws IOException
|
||||||
*/
|
*/
|
||||||
public Process session(String[] args, Map<String, String> env) throws IOException {
|
@Override
|
||||||
|
public PtySession session(String[] args, Map<String, String> env) throws IOException {
|
||||||
return sessionUsingJavaLeader(args, env);
|
return sessionUsingJavaLeader(args, env);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Process sessionUsingJavaLeader(String[] args, Map<String, String> env)
|
protected PtySession sessionUsingJavaLeader(String[] args, Map<String, String> env)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
final List<String> argsList = new ArrayList<>();
|
final List<String> argsList = new ArrayList<>();
|
||||||
argsList.add("java");
|
argsList.add("java");
|
||||||
argsList.add("-cp");
|
argsList.add("-cp");
|
||||||
argsList.add(System.getProperty("java.class.path"));
|
argsList.add(System.getProperty("java.class.path"));
|
||||||
argsList.add(PtySessionLeader.class.getCanonicalName());
|
argsList.add(LinuxPtySessionLeader.class.getCanonicalName());
|
||||||
|
|
||||||
argsList.add(file.getAbsolutePath());
|
argsList.add(name);
|
||||||
argsList.addAll(Arrays.asList(args));
|
argsList.addAll(Arrays.asList(args));
|
||||||
ProcessBuilder builder = new ProcessBuilder(argsList);
|
ProcessBuilder builder = new ProcessBuilder(argsList);
|
||||||
if (env != null) {
|
if (env != null) {
|
||||||
|
@ -82,17 +79,17 @@ public class PtySlave extends PtyEndpoint {
|
||||||
}
|
}
|
||||||
builder.inheritIO();
|
builder.inheritIO();
|
||||||
|
|
||||||
return builder.start();
|
return new LocalProcessPtySession(builder.start());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Process sessionUsingPythonLeader(String[] args, Map<String, String> env)
|
protected PtySession sessionUsingPythonLeader(String[] args, Map<String, String> env)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
final List<String> argsList = new ArrayList<>();
|
final List<String> argsList = new ArrayList<>();
|
||||||
argsList.add("python");
|
argsList.add("python");
|
||||||
argsList.add("-m");
|
argsList.add("-m");
|
||||||
argsList.add("session");
|
argsList.add("session");
|
||||||
|
|
||||||
argsList.add(file.getAbsolutePath());
|
argsList.add(name);
|
||||||
argsList.addAll(Arrays.asList(args));
|
argsList.addAll(Arrays.asList(args));
|
||||||
ProcessBuilder builder = new ProcessBuilder(argsList);
|
ProcessBuilder builder = new ProcessBuilder(argsList);
|
||||||
if (env != null) {
|
if (env != null) {
|
||||||
|
@ -103,12 +100,12 @@ public class PtySlave extends PtyEndpoint {
|
||||||
builder.environment().put("PYTHONPATH", sourceLoc);
|
builder.environment().put("PYTHONPATH", sourceLoc);
|
||||||
builder.inheritIO();
|
builder.inheritIO();
|
||||||
|
|
||||||
return builder.start();
|
return new LocalProcessPtySession(builder.start());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static File getSourceLocationForResource(String name) {
|
public static File getSourceLocationForResource(String name) {
|
||||||
// TODO: Refactor this with SystemUtilities.getSourceLocationForClass()
|
// TODO: Refactor this with SystemUtilities.getSourceLocationForClass()
|
||||||
URL url = PtySlave.class.getClassLoader().getResource(name);
|
URL url = LinuxPtyChild.class.getClassLoader().getResource(name);
|
||||||
String urlFile = url.getFile();
|
String urlFile = url.getFile();
|
||||||
try {
|
try {
|
||||||
urlFile = URLDecoder.decode(urlFile, "UTF-8");
|
urlFile = URLDecoder.decode(urlFile, "UTF-8");
|
|
@ -0,0 +1,43 @@
|
||||||
|
/* ###
|
||||||
|
* 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.gdb.pty.linux;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
import agent.gdb.pty.PtyEndpoint;
|
||||||
|
|
||||||
|
public class LinuxPtyEndpoint implements PtyEndpoint {
|
||||||
|
//private final int fd;
|
||||||
|
private final FdOutputStream outputStream;
|
||||||
|
private final FdInputStream inputStream;
|
||||||
|
|
||||||
|
LinuxPtyEndpoint(int fd) {
|
||||||
|
//this.fd = fd;
|
||||||
|
this.outputStream = new FdOutputStream(fd);
|
||||||
|
this.inputStream = new FdInputStream(fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OutputStream getOutputStream() {
|
||||||
|
return outputStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream getInputStream() {
|
||||||
|
return inputStream;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
/* ###
|
||||||
|
* 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.gdb.pty.linux;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import agent.gdb.pty.Pty;
|
||||||
|
import agent.gdb.pty.PtyFactory;
|
||||||
|
|
||||||
|
public class LinuxPtyFactory implements PtyFactory {
|
||||||
|
@Override
|
||||||
|
public Pty openpty() throws IOException {
|
||||||
|
return LinuxPty.openpty();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
/* ###
|
||||||
|
* 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.gdb.pty.linux;
|
||||||
|
|
||||||
|
import agent.gdb.pty.PtyParent;
|
||||||
|
|
||||||
|
public class LinuxPtyParent extends LinuxPtyEndpoint implements PtyParent {
|
||||||
|
LinuxPtyParent(int fd) {
|
||||||
|
super(fd);
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,7 +13,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.ffi.linux;
|
package agent.gdb.pty.linux;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.Callable;
|
import java.util.concurrent.Callable;
|
||||||
|
@ -21,14 +21,14 @@ import java.util.concurrent.Callable;
|
||||||
import jnr.posix.POSIX;
|
import jnr.posix.POSIX;
|
||||||
import jnr.posix.POSIXFactory;
|
import jnr.posix.POSIXFactory;
|
||||||
|
|
||||||
public class PtySessionLeader {
|
public class LinuxPtySessionLeader {
|
||||||
private static final POSIX LIB_POSIX = POSIXFactory.getNativePOSIX();
|
private static final POSIX LIB_POSIX = POSIXFactory.getNativePOSIX();
|
||||||
private static final int O_RDWR = 2; // TODO: Find this in libs
|
private static final int O_RDWR = 2; // TODO: Find this in libs
|
||||||
|
|
||||||
public static void main(String[] args) throws Exception {
|
public static void main(String[] args) throws Exception {
|
||||||
PtySessionLeader master = new PtySessionLeader();
|
LinuxPtySessionLeader leader = new LinuxPtySessionLeader();
|
||||||
master.parseArgs(args);
|
leader.parseArgs(args);
|
||||||
master.run();
|
leader.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected String ptyPath;
|
protected String ptyPath;
|
|
@ -13,7 +13,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.ffi.linux;
|
package agent.gdb.pty.linux;
|
||||||
|
|
||||||
import jnr.ffi.LibraryLoader;
|
import jnr.ffi.LibraryLoader;
|
||||||
import jnr.ffi.Pointer;
|
import jnr.ffi.Pointer;
|
|
@ -0,0 +1,39 @@
|
||||||
|
/* ###
|
||||||
|
* 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.gdb.pty.local;
|
||||||
|
|
||||||
|
import agent.gdb.pty.PtySession;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A pty session consisting of a local process and its descendants
|
||||||
|
*/
|
||||||
|
public class LocalProcessPtySession implements PtySession {
|
||||||
|
private final Process process;
|
||||||
|
|
||||||
|
public LocalProcessPtySession(Process process) {
|
||||||
|
this.process = process;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer waitExited() throws InterruptedException {
|
||||||
|
return process.waitFor();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void destroyForcibly() {
|
||||||
|
process.destroyForcibly();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
/* ###
|
||||||
|
* 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.gdb.pty.ssh;
|
||||||
|
|
||||||
|
import ch.ethz.ssh2.KnownHosts;
|
||||||
|
import ch.ethz.ssh2.ServerHostKeyVerifier;
|
||||||
|
import docking.widgets.OptionDialog;
|
||||||
|
import ghidra.util.Msg;
|
||||||
|
|
||||||
|
public class GhidraSshHostKeyVerifier implements ServerHostKeyVerifier {
|
||||||
|
|
||||||
|
private final KnownHosts database;
|
||||||
|
|
||||||
|
public GhidraSshHostKeyVerifier(KnownHosts database) {
|
||||||
|
this.database = database;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean verifyServerHostKey(String hostname, int port, String serverHostKeyAlgorithm,
|
||||||
|
byte[] serverHostKey) throws Exception {
|
||||||
|
switch (database.verifyHostkey(hostname, serverHostKeyAlgorithm, serverHostKey)) {
|
||||||
|
case KnownHosts.HOSTKEY_IS_OK:
|
||||||
|
return true;
|
||||||
|
case KnownHosts.HOSTKEY_IS_NEW:
|
||||||
|
int response = OptionDialog.showYesNoDialogWithNoAsDefaultButton(null,
|
||||||
|
"Unknown SSH Server Host Key",
|
||||||
|
"<html><b>The server " + hostname + " is not known.</b> " +
|
||||||
|
"It is highly recommended you log in to the server using a standard " +
|
||||||
|
"SSH client to confirm the host key first.<br><br>" +
|
||||||
|
"Do you want to continue?</html>");
|
||||||
|
return response == OptionDialog.YES_OPTION;
|
||||||
|
case KnownHosts.HOSTKEY_HAS_CHANGED:
|
||||||
|
Msg.showError(this, null, "SSH Server Host Key Changed",
|
||||||
|
"<html><b>The server " + hostname + " has a different key than before!</b>" +
|
||||||
|
"Use a standard SSH client to resolve the issue.</html>");
|
||||||
|
return false;
|
||||||
|
default:
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,134 @@
|
||||||
|
/* ###
|
||||||
|
* 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.gdb.pty.ssh;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import agent.gdb.pty.PtyFactory;
|
||||||
|
import ch.ethz.ssh2.Connection;
|
||||||
|
import ch.ethz.ssh2.KnownHosts;
|
||||||
|
import docking.DockingWindowManager;
|
||||||
|
import docking.widgets.PasswordDialog;
|
||||||
|
import ghidra.util.exception.CancelledException;
|
||||||
|
|
||||||
|
public class GhidraSshPtyFactory implements PtyFactory {
|
||||||
|
private String hostname = "localhost";
|
||||||
|
private int port = 22;
|
||||||
|
private String username = "user";
|
||||||
|
private String keyFile = "~/.ssh/id_rsa";
|
||||||
|
|
||||||
|
private Connection sshConn;
|
||||||
|
|
||||||
|
public String getHostname() {
|
||||||
|
return hostname;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHostname(String hostname) {
|
||||||
|
this.hostname = Objects.requireNonNull(hostname);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPort() {
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPort(int port) {
|
||||||
|
this.port = port;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUsername(String username) {
|
||||||
|
this.username = Objects.requireNonNull(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getKeyFile() {
|
||||||
|
return keyFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the keyfile path, or empty for password authentication only
|
||||||
|
*
|
||||||
|
* @param keyFile the path
|
||||||
|
*/
|
||||||
|
public void setKeyFile(String keyFile) {
|
||||||
|
this.keyFile = Objects.requireNonNull(keyFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static char[] promptPassword(String hostname, String prompt) throws CancelledException {
|
||||||
|
PasswordDialog dialog =
|
||||||
|
new PasswordDialog("GDB via SSH", "SSH", hostname, prompt, null,
|
||||||
|
"");
|
||||||
|
DockingWindowManager.showDialog(dialog);
|
||||||
|
if (dialog.okWasPressed()) {
|
||||||
|
return dialog.getPassword();
|
||||||
|
}
|
||||||
|
throw new CancelledException();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Connection connectAndAuthenticate() throws IOException {
|
||||||
|
boolean success = false;
|
||||||
|
File knownHostsFile = new File(System.getProperty("user.home") + "/.ssh/known_hosts");
|
||||||
|
KnownHosts knownHosts = new KnownHosts();
|
||||||
|
if (knownHostsFile.exists()) {
|
||||||
|
knownHosts.addHostkeys(knownHostsFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
Connection sshConn = new Connection(hostname, port);
|
||||||
|
try {
|
||||||
|
sshConn.connect(new GhidraSshHostKeyVerifier(knownHosts));
|
||||||
|
if ("".equals(keyFile.trim())) {
|
||||||
|
// TODO: Find an API that uses char[] so I can clear it!
|
||||||
|
String password = new String(promptPassword(hostname, "Password for " + username));
|
||||||
|
if (!sshConn.authenticateWithPassword(username, password)) {
|
||||||
|
throw new IOException("Authentication failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
File pemFile = new File(keyFile);
|
||||||
|
if (!pemFile.canRead()) {
|
||||||
|
throw new IOException("Key file " + keyFile +
|
||||||
|
" cannot be read. Does it exist? Do you have permission?");
|
||||||
|
}
|
||||||
|
String password = new String(promptPassword(hostname, "Password for " + pemFile));
|
||||||
|
if (!sshConn.authenticateWithPublicKey(username, pemFile, password)) {
|
||||||
|
throw new IOException("Authentication failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
success = true;
|
||||||
|
return sshConn;
|
||||||
|
}
|
||||||
|
catch (CancelledException e) {
|
||||||
|
throw new IOException("User cancelled", e);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if (!success) {
|
||||||
|
sshConn.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SshPty openpty() throws IOException {
|
||||||
|
if (sshConn == null || !sshConn.isAuthenticationComplete()) {
|
||||||
|
sshConn = connectAndAuthenticate();
|
||||||
|
}
|
||||||
|
return new SshPty(sshConn.openSession());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
/* ###
|
||||||
|
* 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.gdb.pty.ssh;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import agent.gdb.pty.*;
|
||||||
|
import ch.ethz.ssh2.Session;
|
||||||
|
|
||||||
|
public class SshPty implements Pty {
|
||||||
|
private final Session session;
|
||||||
|
|
||||||
|
public SshPty(Session session) throws IOException {
|
||||||
|
this.session = session;
|
||||||
|
session.requestDumbPTY();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PtyParent getParent() {
|
||||||
|
// TODO: Need I worry about stderr? I thought both pointed to the same tty....
|
||||||
|
return new SshPtyParent(session.getStdin(), session.getStdout());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PtyChild getChild() {
|
||||||
|
return new SshPtyChild(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
session.close();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
/* ###
|
||||||
|
* 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.gdb.pty.ssh;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import javax.help.UnsupportedOperationException;
|
||||||
|
|
||||||
|
import agent.gdb.pty.PtyChild;
|
||||||
|
import ch.ethz.ssh2.Session;
|
||||||
|
import ghidra.util.Msg;
|
||||||
|
|
||||||
|
public class SshPtyChild extends SshPtyEndpoint implements PtyChild {
|
||||||
|
private String name;
|
||||||
|
private final Session session;
|
||||||
|
|
||||||
|
public SshPtyChild(Session session) {
|
||||||
|
super(null, null);
|
||||||
|
this.session = session;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SshPtySession session(String[] args, Map<String, String> env) throws IOException {
|
||||||
|
/**
|
||||||
|
* TODO: This syntax assumes a UNIX-style shell, and even among them, this may not be
|
||||||
|
* universal. This certainly works for my version of bash :)
|
||||||
|
*/
|
||||||
|
String envStr = env == null
|
||||||
|
? ""
|
||||||
|
: env.entrySet()
|
||||||
|
.stream()
|
||||||
|
.map(e -> e.getKey() + "=" + e.getValue())
|
||||||
|
.collect(Collectors.joining(" ")) +
|
||||||
|
" ";
|
||||||
|
String cmdStr = Stream.of(args).collect(Collectors.joining(" "));
|
||||||
|
session.execCommand(envStr + cmdStr);
|
||||||
|
return new SshPtySession(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getTtyNameAndStartNullSession() throws IOException {
|
||||||
|
// NB. Using [InputStream/Buffered]Reader will close my stream. Cannot do that.
|
||||||
|
InputStream stdout = session.getStdout();
|
||||||
|
// NB. UNIX sleep is only required to support integer durations
|
||||||
|
session.execCommand(
|
||||||
|
"sh -c 'tty && cltrc() { echo; } && trap ctrlc INT && while true; do sleep " +
|
||||||
|
Integer.MAX_VALUE + "; done'",
|
||||||
|
"UTF-8");
|
||||||
|
byte[] buf = new byte[1024]; // Should be plenty
|
||||||
|
for (int i = 0; i < 1024; i++) {
|
||||||
|
int chr = stdout.read();
|
||||||
|
if (chr == '\n' || chr == -1) {
|
||||||
|
return new String(buf, 0, i + 1).trim();
|
||||||
|
}
|
||||||
|
buf[i] = (byte) chr;
|
||||||
|
}
|
||||||
|
throw new IOException("Remote tty name exceeds 1024 bytes?");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String nullSession() throws IOException {
|
||||||
|
if (name == null) {
|
||||||
|
this.name = getTtyNameAndStartNullSession();
|
||||||
|
if ("".equals(name)) {
|
||||||
|
throw new IOException("Could not determine child remote tty name");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Msg.debug(this, "Remote SSH pty: " + name);
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream getInputStream() {
|
||||||
|
throw new UnsupportedOperationException("The child is not local");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OutputStream getOutputStream() {
|
||||||
|
throw new UnsupportedOperationException("The child is not local");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
/* ###
|
||||||
|
* 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.gdb.pty.ssh;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
import agent.gdb.pty.PtyEndpoint;
|
||||||
|
|
||||||
|
public class SshPtyEndpoint implements PtyEndpoint {
|
||||||
|
private final OutputStream outputStream;
|
||||||
|
private final InputStream inputStream;
|
||||||
|
|
||||||
|
public SshPtyEndpoint(OutputStream outputStream, InputStream inputStream) {
|
||||||
|
this.outputStream = outputStream;
|
||||||
|
this.inputStream = inputStream;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OutputStream getOutputStream() {
|
||||||
|
return outputStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream getInputStream() {
|
||||||
|
return inputStream;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
/* ###
|
||||||
|
* 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.gdb.pty.ssh;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
import agent.gdb.pty.PtyParent;
|
||||||
|
|
||||||
|
public class SshPtyParent extends SshPtyEndpoint implements PtyParent {
|
||||||
|
public SshPtyParent(OutputStream outputStream, InputStream inputStream) {
|
||||||
|
super(outputStream, inputStream);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
/* ###
|
||||||
|
* 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.gdb.pty.ssh;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InterruptedIOException;
|
||||||
|
|
||||||
|
import agent.gdb.pty.PtySession;
|
||||||
|
import ch.ethz.ssh2.ChannelCondition;
|
||||||
|
import ch.ethz.ssh2.Session;
|
||||||
|
|
||||||
|
public class SshPtySession implements PtySession {
|
||||||
|
|
||||||
|
private final Session session;
|
||||||
|
|
||||||
|
public SshPtySession(Session session) {
|
||||||
|
this.session = session;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer waitExited() throws InterruptedException {
|
||||||
|
try {
|
||||||
|
session.waitForCondition(ChannelCondition.EOF, 0);
|
||||||
|
// NB. May not be available
|
||||||
|
return session.getExitStatus();
|
||||||
|
}
|
||||||
|
catch (InterruptedIOException e) {
|
||||||
|
throw new InterruptedException();
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void destroyForcibly() {
|
||||||
|
/**
|
||||||
|
* TODO: This is imperfect, since it terminates the whole SSH session, not just the pty
|
||||||
|
* session. I don't think that's terribly critical for our use case, but we should adjust
|
||||||
|
* the spec to account for this, or devise a better implementation.
|
||||||
|
*/
|
||||||
|
session.close();
|
||||||
|
}
|
||||||
|
}
|
|
@ -35,6 +35,7 @@ import com.google.common.collect.*;
|
||||||
import agent.gdb.manager.*;
|
import agent.gdb.manager.*;
|
||||||
import agent.gdb.manager.GdbManager.ExecSuffix;
|
import agent.gdb.manager.GdbManager.ExecSuffix;
|
||||||
import agent.gdb.manager.breakpoint.GdbBreakpointInfo;
|
import agent.gdb.manager.breakpoint.GdbBreakpointInfo;
|
||||||
|
import agent.gdb.pty.PtyFactory;
|
||||||
import ghidra.async.AsyncReference;
|
import ghidra.async.AsyncReference;
|
||||||
import ghidra.dbg.testutil.DummyProc;
|
import ghidra.dbg.testutil.DummyProc;
|
||||||
import ghidra.test.AbstractGhidraHeadlessIntegrationTest;
|
import ghidra.test.AbstractGhidraHeadlessIntegrationTest;
|
||||||
|
@ -45,6 +46,8 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
|
||||||
protected static final long TIMEOUT_MILLISECONDS =
|
protected static final long TIMEOUT_MILLISECONDS =
|
||||||
SystemUtilities.isInTestingBatchMode() ? 5000 : Long.MAX_VALUE;
|
SystemUtilities.isInTestingBatchMode() ? 5000 : Long.MAX_VALUE;
|
||||||
|
|
||||||
|
protected abstract PtyFactory getPtyFactory();
|
||||||
|
|
||||||
protected abstract CompletableFuture<Void> startManager(GdbManager manager);
|
protected abstract CompletableFuture<Void> startManager(GdbManager manager);
|
||||||
|
|
||||||
protected void stopManager() throws IOException {
|
protected void stopManager() throws IOException {
|
||||||
|
@ -67,7 +70,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testAddInferior() throws Throwable {
|
public void testAddInferior() throws Throwable {
|
||||||
try (GdbManager mgr = GdbManager.newInstance()) {
|
try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
|
||||||
waitOn(startManager(mgr));
|
waitOn(startManager(mgr));
|
||||||
GdbInferior inferior = waitOn(mgr.addInferior());
|
GdbInferior inferior = waitOn(mgr.addInferior());
|
||||||
assertEquals(2, inferior.getId());
|
assertEquals(2, inferior.getId());
|
||||||
|
@ -77,7 +80,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRemoveInferior() throws Throwable {
|
public void testRemoveInferior() throws Throwable {
|
||||||
try (GdbManager mgr = GdbManager.newInstance()) {
|
try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
|
||||||
waitOn(startManager(mgr));
|
waitOn(startManager(mgr));
|
||||||
GdbInferior inf = waitOn(mgr.addInferior());
|
GdbInferior inf = waitOn(mgr.addInferior());
|
||||||
assertEquals(2, mgr.getKnownInferiors().size());
|
assertEquals(2, mgr.getKnownInferiors().size());
|
||||||
|
@ -90,7 +93,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRemoveCurrentInferior() throws Throwable {
|
public void testRemoveCurrentInferior() throws Throwable {
|
||||||
try (GdbManager mgr = GdbManager.newInstance()) {
|
try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
|
||||||
List<Integer> selEvtIdsTemp = new ArrayList<>();
|
List<Integer> selEvtIdsTemp = new ArrayList<>();
|
||||||
AsyncReference<List<Integer>, Void> selEvtIds = new AsyncReference<>(List.of());
|
AsyncReference<List<Integer>, Void> selEvtIds = new AsyncReference<>(List.of());
|
||||||
mgr.addEventsListener(new GdbEventsListenerAdapter() {
|
mgr.addEventsListener(new GdbEventsListenerAdapter() {
|
||||||
|
@ -114,7 +117,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testConsoleCapture() throws Throwable {
|
public void testConsoleCapture() throws Throwable {
|
||||||
try (GdbManager mgr = GdbManager.newInstance()) {
|
try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
|
||||||
waitOn(startManager(mgr));
|
waitOn(startManager(mgr));
|
||||||
String out = waitOn(mgr.consoleCapture("echo test"));
|
String out = waitOn(mgr.consoleCapture("echo test"));
|
||||||
assertEquals("test", out.trim());
|
assertEquals("test", out.trim());
|
||||||
|
@ -123,7 +126,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testListInferiors() throws Throwable {
|
public void testListInferiors() throws Throwable {
|
||||||
try (GdbManager mgr = GdbManager.newInstance()) {
|
try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
|
||||||
waitOn(startManager(mgr));
|
waitOn(startManager(mgr));
|
||||||
Map<Integer, GdbInferior> inferiors = waitOn(mgr.listInferiors());
|
Map<Integer, GdbInferior> inferiors = waitOn(mgr.listInferiors());
|
||||||
assertEquals(new HashSet<>(Arrays.asList(new Integer[] { 1 })), inferiors.keySet());
|
assertEquals(new HashSet<>(Arrays.asList(new Integer[] { 1 })), inferiors.keySet());
|
||||||
|
@ -132,7 +135,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testListAvailableProcesses() throws Throwable {
|
public void testListAvailableProcesses() throws Throwable {
|
||||||
try (GdbManager mgr = GdbManager.newInstance()) {
|
try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
|
||||||
waitOn(startManager(mgr));
|
waitOn(startManager(mgr));
|
||||||
List<GdbProcessThreadGroup> procs = waitOn(mgr.listAvailableProcesses());
|
List<GdbProcessThreadGroup> procs = waitOn(mgr.listAvailableProcesses());
|
||||||
List<Integer> pids = procs.stream().map(p -> p.getPid()).collect(Collectors.toList());
|
List<Integer> pids = procs.stream().map(p -> p.getPid()).collect(Collectors.toList());
|
||||||
|
@ -142,7 +145,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testInfoOs() throws Throwable {
|
public void testInfoOs() throws Throwable {
|
||||||
try (GdbManager mgr = GdbManager.newInstance()) {
|
try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
|
||||||
waitOn(startManager(mgr));
|
waitOn(startManager(mgr));
|
||||||
GdbTable infoThreads = waitOn(mgr.infoOs("threads"));
|
GdbTable infoThreads = waitOn(mgr.infoOs("threads"));
|
||||||
assertEquals(new LinkedHashSet<>(Arrays.asList("pid", "command", "tid", "core")),
|
assertEquals(new LinkedHashSet<>(Arrays.asList("pid", "command", "tid", "core")),
|
||||||
|
@ -153,7 +156,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testStart() throws Throwable {
|
public void testStart() throws Throwable {
|
||||||
try (GdbManager mgr = GdbManager.newInstance()) {
|
try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
|
||||||
waitOn(startManager(mgr));
|
waitOn(startManager(mgr));
|
||||||
waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo"));
|
waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo"));
|
||||||
waitOn(mgr.console("break main"));
|
waitOn(mgr.console("break main"));
|
||||||
|
@ -164,7 +167,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testAttachDetach() throws Throwable {
|
public void testAttachDetach() throws Throwable {
|
||||||
try (DummyProc echo = run("dd"); GdbManager mgr = GdbManager.newInstance()) {
|
try (DummyProc echo = run("dd"); GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
|
||||||
waitOn(startManager(mgr));
|
waitOn(startManager(mgr));
|
||||||
Set<GdbThread> threads = waitOn(mgr.currentInferior().attach(echo.pid));
|
Set<GdbThread> threads = waitOn(mgr.currentInferior().attach(echo.pid));
|
||||||
// Attach stops the process, so no need to wait for STOPPED or prompt
|
// Attach stops the process, so no need to wait for STOPPED or prompt
|
||||||
|
@ -212,7 +215,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
|
||||||
public void testStartInterrupt() throws Throwable {
|
public void testStartInterrupt() throws Throwable {
|
||||||
assumeFalse("I know no way to get this to pass with these conditions",
|
assumeFalse("I know no way to get this to pass with these conditions",
|
||||||
this instanceof JoinedGdbManagerTest);
|
this instanceof JoinedGdbManagerTest);
|
||||||
try (GdbManager mgr = GdbManager.newInstance()) {
|
try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
|
||||||
/*
|
/*
|
||||||
* Not sure the details here, but it seems GDB will give ^running as soon as the process
|
* Not sure the details here, but it seems GDB will give ^running as soon as the process
|
||||||
* has started. I suspect there are some nuances between the time the process is started
|
* has started. I suspect there are some nuances between the time the process is started
|
||||||
|
@ -239,7 +242,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
|
||||||
assumeFalse("I know no way to get this to pass with these conditions",
|
assumeFalse("I know no way to get this to pass with these conditions",
|
||||||
this instanceof JoinedGdbManagerTest);
|
this instanceof JoinedGdbManagerTest);
|
||||||
// Repeat the start-interrupt sequence, then verify we're preparing to step a syscall
|
// Repeat the start-interrupt sequence, then verify we're preparing to step a syscall
|
||||||
try (GdbManager mgr = GdbManager.newInstance()) {
|
try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
|
||||||
LibraryWaiter libcLoaded = new LibraryWaiter(name -> name.contains("libc"));
|
LibraryWaiter libcLoaded = new LibraryWaiter(name -> name.contains("libc"));
|
||||||
mgr.addEventsListener(libcLoaded);
|
mgr.addEventsListener(libcLoaded);
|
||||||
waitOn(startManager(mgr));
|
waitOn(startManager(mgr));
|
||||||
|
@ -268,7 +271,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSetVarEvaluate() throws Throwable {
|
public void testSetVarEvaluate() throws Throwable {
|
||||||
try (GdbManager mgr = GdbManager.newInstance()) {
|
try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
|
||||||
waitOn(startManager(mgr));
|
waitOn(startManager(mgr));
|
||||||
waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo"));
|
waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo"));
|
||||||
waitOn(mgr.insertBreakpoint("main"));
|
waitOn(mgr.insertBreakpoint("main"));
|
||||||
|
@ -283,7 +286,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSetVarGetVar() throws Throwable {
|
public void testSetVarGetVar() throws Throwable {
|
||||||
try (GdbManager mgr = GdbManager.newInstance()) {
|
try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
|
||||||
waitOn(startManager(mgr));
|
waitOn(startManager(mgr));
|
||||||
String val = waitOn(mgr.currentInferior().getVar("args"));
|
String val = waitOn(mgr.currentInferior().getVar("args"));
|
||||||
assertEquals(null, val);
|
assertEquals(null, val);
|
||||||
|
@ -295,7 +298,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testInsertListDeleteBreakpoint() throws Throwable {
|
public void testInsertListDeleteBreakpoint() throws Throwable {
|
||||||
try (GdbManager mgr = GdbManager.newInstance()) {
|
try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
|
||||||
waitOn(startManager(mgr));
|
waitOn(startManager(mgr));
|
||||||
waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo"));
|
waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo"));
|
||||||
GdbBreakpointInfo breakpoint = waitOn(mgr.insertBreakpoint("main"));
|
GdbBreakpointInfo breakpoint = waitOn(mgr.insertBreakpoint("main"));
|
||||||
|
@ -309,7 +312,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testListReadWriteReadRegisters() throws Throwable {
|
public void testListReadWriteReadRegisters() throws Throwable {
|
||||||
try (GdbManager mgr = GdbManager.newInstance()) {
|
try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
|
||||||
waitOn(startManager(mgr));
|
waitOn(startManager(mgr));
|
||||||
waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo"));
|
waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo"));
|
||||||
waitOn(mgr.insertBreakpoint("main"));
|
waitOn(mgr.insertBreakpoint("main"));
|
||||||
|
@ -345,7 +348,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
|
||||||
@Test
|
@Test
|
||||||
public void testWriteReadMemory() throws Throwable {
|
public void testWriteReadMemory() throws Throwable {
|
||||||
ByteBuffer rBuf = ByteBuffer.allocate(1024);
|
ByteBuffer rBuf = ByteBuffer.allocate(1024);
|
||||||
try (GdbManager mgr = GdbManager.newInstance()) {
|
try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
|
||||||
waitOn(startManager(mgr));
|
waitOn(startManager(mgr));
|
||||||
waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo"));
|
waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo"));
|
||||||
waitOn(mgr.insertBreakpoint("main"));
|
waitOn(mgr.insertBreakpoint("main"));
|
||||||
|
@ -375,7 +378,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testContinue() throws Throwable {
|
public void testContinue() throws Throwable {
|
||||||
try (GdbManager mgr = GdbManager.newInstance()) {
|
try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
|
||||||
waitOn(startManager(mgr));
|
waitOn(startManager(mgr));
|
||||||
waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo"));
|
waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo"));
|
||||||
waitOn(mgr.insertBreakpoint("main"));
|
waitOn(mgr.insertBreakpoint("main"));
|
||||||
|
@ -390,7 +393,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testStep() throws Throwable {
|
public void testStep() throws Throwable {
|
||||||
try (GdbManager mgr = GdbManager.newInstance()) {
|
try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
|
||||||
waitOn(startManager(mgr));
|
waitOn(startManager(mgr));
|
||||||
waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo"));
|
waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo"));
|
||||||
waitOn(mgr.insertBreakpoint("main"));
|
waitOn(mgr.insertBreakpoint("main"));
|
||||||
|
@ -405,7 +408,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testThreadSelect() throws Throwable {
|
public void testThreadSelect() throws Throwable {
|
||||||
try (GdbManager mgr = GdbManager.newInstance()) {
|
try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
|
||||||
waitOn(startManager(mgr));
|
waitOn(startManager(mgr));
|
||||||
waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo"));
|
waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo"));
|
||||||
waitOn(mgr.insertBreakpoint("main"));
|
waitOn(mgr.insertBreakpoint("main"));
|
||||||
|
@ -418,7 +421,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testListFrames() throws Throwable {
|
public void testListFrames() throws Throwable {
|
||||||
try (GdbManager mgr = GdbManager.newInstance()) {
|
try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
|
||||||
waitOn(startManager(mgr));
|
waitOn(startManager(mgr));
|
||||||
waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo"));
|
waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo"));
|
||||||
waitOn(mgr.insertBreakpoint("main"));
|
waitOn(mgr.insertBreakpoint("main"));
|
||||||
|
|
|
@ -21,8 +21,11 @@ import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
import org.junit.Ignore;
|
import org.junit.Ignore;
|
||||||
|
|
||||||
import agent.gdb.ffi.linux.Pty;
|
|
||||||
import agent.gdb.manager.GdbManager;
|
import agent.gdb.manager.GdbManager;
|
||||||
|
import agent.gdb.pty.PtyFactory;
|
||||||
|
import agent.gdb.pty.PtySession;
|
||||||
|
import agent.gdb.pty.linux.LinuxPty;
|
||||||
|
import agent.gdb.pty.linux.LinuxPtyFactory;
|
||||||
import ghidra.util.Msg;
|
import ghidra.util.Msg;
|
||||||
|
|
||||||
@Ignore("Need compatible GDB version for CI")
|
@Ignore("Need compatible GDB version for CI")
|
||||||
|
@ -31,7 +34,7 @@ public class JoinedGdbManagerTest extends AbstractGdbManagerTest {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
BufferedReader reader =
|
BufferedReader reader =
|
||||||
new BufferedReader(new InputStreamReader(ptyUserGdb.getMaster().getInputStream()));
|
new BufferedReader(new InputStreamReader(ptyUserGdb.getParent().getInputStream()));
|
||||||
String line;
|
String line;
|
||||||
try {
|
try {
|
||||||
while (gdb != null && null != (line = reader.readLine())) {
|
while (gdb != null && null != (line = reader.readLine())) {
|
||||||
|
@ -44,20 +47,26 @@ public class JoinedGdbManagerTest extends AbstractGdbManagerTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Pty ptyUserGdb;
|
protected LinuxPty ptyUserGdb;
|
||||||
protected Process gdb;
|
protected PtySession gdb;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PtyFactory getPtyFactory() {
|
||||||
|
// TODO: Choose by host OS
|
||||||
|
return new LinuxPtyFactory();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected CompletableFuture<Void> startManager(GdbManager manager) {
|
protected CompletableFuture<Void> startManager(GdbManager manager) {
|
||||||
try {
|
try {
|
||||||
ptyUserGdb = Pty.openpty();
|
ptyUserGdb = LinuxPty.openpty();
|
||||||
manager.start(null);
|
manager.start(null);
|
||||||
Msg.debug(this, "Starting GDB and invoking new-ui mi2 " + manager.getMi2PtyName());
|
Msg.debug(this, "Starting GDB and invoking new-ui mi2 " + manager.getMi2PtyName());
|
||||||
|
|
||||||
gdb = ptyUserGdb.getSlave()
|
gdb = ptyUserGdb.getChild()
|
||||||
.session(new String[] { GdbManager.DEFAULT_GDB_CMD }, Map.of());
|
.session(new String[] { GdbManager.DEFAULT_GDB_CMD }, Map.of());
|
||||||
new ReaderThread().start();
|
new ReaderThread().start();
|
||||||
PrintWriter gdbCmd = new PrintWriter(ptyUserGdb.getMaster().getOutputStream());
|
PrintWriter gdbCmd = new PrintWriter(ptyUserGdb.getParent().getOutputStream());
|
||||||
gdbCmd.println("new-ui mi2 " + manager.getMi2PtyName());
|
gdbCmd.println("new-ui mi2 " + manager.getMi2PtyName());
|
||||||
gdbCmd.flush();
|
gdbCmd.flush();
|
||||||
return manager.runRC();
|
return manager.runRC();
|
||||||
|
|
|
@ -21,6 +21,8 @@ import java.util.concurrent.CompletableFuture;
|
||||||
import org.junit.Ignore;
|
import org.junit.Ignore;
|
||||||
|
|
||||||
import agent.gdb.manager.GdbManager;
|
import agent.gdb.manager.GdbManager;
|
||||||
|
import agent.gdb.pty.PtyFactory;
|
||||||
|
import agent.gdb.pty.linux.LinuxPtyFactory;
|
||||||
|
|
||||||
@Ignore("Need compatible GDB version for CI")
|
@Ignore("Need compatible GDB version for CI")
|
||||||
public class SpawnedCliGdbManagerTest extends AbstractGdbManagerTest {
|
public class SpawnedCliGdbManagerTest extends AbstractGdbManagerTest {
|
||||||
|
@ -34,4 +36,10 @@ public class SpawnedCliGdbManagerTest extends AbstractGdbManagerTest {
|
||||||
throw new AssertionError(e);
|
throw new AssertionError(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PtyFactory getPtyFactory() {
|
||||||
|
// TODO: Choose by host OS
|
||||||
|
return new LinuxPtyFactory();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,8 @@ import java.util.concurrent.CompletableFuture;
|
||||||
import org.junit.Ignore;
|
import org.junit.Ignore;
|
||||||
|
|
||||||
import agent.gdb.manager.GdbManager;
|
import agent.gdb.manager.GdbManager;
|
||||||
|
import agent.gdb.pty.PtyFactory;
|
||||||
|
import agent.gdb.pty.linux.LinuxPtyFactory;
|
||||||
|
|
||||||
@Ignore("Need to install GDB 7.6.1 to the expected directory on CI")
|
@Ignore("Need to install GDB 7.6.1 to the expected directory on CI")
|
||||||
public class SpawnedMi2Gdb7Dot6Dot1ManagerTest extends AbstractGdbManagerTest {
|
public class SpawnedMi2Gdb7Dot6Dot1ManagerTest extends AbstractGdbManagerTest {
|
||||||
|
@ -34,4 +36,10 @@ public class SpawnedMi2Gdb7Dot6Dot1ManagerTest extends AbstractGdbManagerTest {
|
||||||
throw new AssertionError(e);
|
throw new AssertionError(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PtyFactory getPtyFactory() {
|
||||||
|
// TODO: Choose by host OS
|
||||||
|
return new LinuxPtyFactory();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,8 @@ import java.util.concurrent.CompletableFuture;
|
||||||
import org.junit.Ignore;
|
import org.junit.Ignore;
|
||||||
|
|
||||||
import agent.gdb.manager.GdbManager;
|
import agent.gdb.manager.GdbManager;
|
||||||
|
import agent.gdb.pty.PtyFactory;
|
||||||
|
import agent.gdb.pty.linux.LinuxPtyFactory;
|
||||||
|
|
||||||
@Ignore("Need compatible GDB version for CI")
|
@Ignore("Need compatible GDB version for CI")
|
||||||
public class SpawnedMi2GdbManagerTest2 extends AbstractGdbManagerTest {
|
public class SpawnedMi2GdbManagerTest2 extends AbstractGdbManagerTest {
|
||||||
|
@ -34,4 +36,10 @@ public class SpawnedMi2GdbManagerTest2 extends AbstractGdbManagerTest {
|
||||||
throw new AssertionError(e);
|
throw new AssertionError(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PtyFactory getPtyFactory() {
|
||||||
|
// TODO: Choose by host OS
|
||||||
|
return new LinuxPtyFactory();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
/* ###
|
||||||
|
* 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.gdb.model.ssh;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import agent.gdb.GdbOverSshDebuggerModelFactory;
|
||||||
|
import agent.gdb.pty.ssh.SshPtyTest;
|
||||||
|
import ghidra.dbg.DebuggerModelFactory;
|
||||||
|
import ghidra.dbg.test.AbstractModelHost;
|
||||||
|
import ghidra.util.exception.CancelledException;
|
||||||
|
|
||||||
|
public class SshGdbModelHost extends AbstractModelHost {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DebuggerModelFactory getModelFactory() {
|
||||||
|
return new GdbOverSshDebuggerModelFactory();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> getFactoryOptions() {
|
||||||
|
try {
|
||||||
|
return Map.ofEntries(Map.entry("SSH username", SshPtyTest.promptUser()));
|
||||||
|
}
|
||||||
|
catch (CancelledException e) {
|
||||||
|
throw new AssertionError("Cancelled", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
/* ###
|
||||||
|
* 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.gdb.model.ssh;
|
||||||
|
|
||||||
|
import static org.junit.Assume.assumeFalse;
|
||||||
|
|
||||||
|
import org.junit.Before;
|
||||||
|
|
||||||
|
import agent.gdb.model.AbstractModelForGdbFactoryTest;
|
||||||
|
import ghidra.util.SystemUtilities;
|
||||||
|
|
||||||
|
public class SshModelForGdbFactoryTest extends AbstractModelForGdbFactoryTest {
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void checkInteractive() {
|
||||||
|
assumeFalse(SystemUtilities.isInTestingBatchMode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ModelHost modelHost() throws Throwable {
|
||||||
|
return new SshGdbModelHost();
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,7 +13,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.ffi.linux;
|
package agent.gdb.pty.linux;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
@ -23,21 +23,22 @@ import java.util.*;
|
||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import agent.gdb.pty.PtySession;
|
||||||
import ghidra.dbg.testutil.DummyProc;
|
import ghidra.dbg.testutil.DummyProc;
|
||||||
|
|
||||||
public class PtyTest {
|
public class LinuxPtyTest {
|
||||||
@Test
|
@Test
|
||||||
public void testOpenClosePty() throws IOException {
|
public void testOpenClosePty() throws IOException {
|
||||||
Pty pty = Pty.openpty();
|
LinuxPty pty = LinuxPty.openpty();
|
||||||
pty.close();
|
pty.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testMasterToSlave() throws IOException {
|
public void testParentToChild() throws IOException {
|
||||||
try (Pty pty = Pty.openpty()) {
|
try (LinuxPty pty = LinuxPty.openpty()) {
|
||||||
PrintWriter writer = new PrintWriter(pty.getMaster().getOutputStream());
|
PrintWriter writer = new PrintWriter(pty.getParent().getOutputStream());
|
||||||
BufferedReader reader =
|
BufferedReader reader =
|
||||||
new BufferedReader(new InputStreamReader(pty.getSlave().getInputStream()));
|
new BufferedReader(new InputStreamReader(pty.getChild().getInputStream()));
|
||||||
|
|
||||||
writer.println("Hello, World!");
|
writer.println("Hello, World!");
|
||||||
writer.flush();
|
writer.flush();
|
||||||
|
@ -46,11 +47,11 @@ public class PtyTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSlaveToMaster() throws IOException {
|
public void testChildToParent() throws IOException {
|
||||||
try (Pty pty = Pty.openpty()) {
|
try (LinuxPty pty = LinuxPty.openpty()) {
|
||||||
PrintWriter writer = new PrintWriter(pty.getSlave().getOutputStream());
|
PrintWriter writer = new PrintWriter(pty.getChild().getOutputStream());
|
||||||
BufferedReader reader =
|
BufferedReader reader =
|
||||||
new BufferedReader(new InputStreamReader(pty.getMaster().getInputStream()));
|
new BufferedReader(new InputStreamReader(pty.getParent().getInputStream()));
|
||||||
|
|
||||||
writer.println("Hello, World!");
|
writer.println("Hello, World!");
|
||||||
writer.flush();
|
writer.flush();
|
||||||
|
@ -60,22 +61,24 @@ public class PtyTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSessionBash() throws IOException, InterruptedException {
|
public void testSessionBash() throws IOException, InterruptedException {
|
||||||
try (Pty pty = Pty.openpty()) {
|
try (LinuxPty pty = LinuxPty.openpty()) {
|
||||||
Process bash = pty.getSlave().session(new String[] { DummyProc.which("bash") }, null);
|
PtySession bash =
|
||||||
pty.getMaster().getOutputStream().write("exit\n".getBytes());
|
pty.getChild().session(new String[] { DummyProc.which("bash") }, null);
|
||||||
assertEquals(0, bash.waitFor());
|
pty.getParent().getOutputStream().write("exit\n".getBytes());
|
||||||
|
assertEquals(0, bash.waitExited().intValue());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testForkIntoNonExistent() throws IOException, InterruptedException {
|
public void testForkIntoNonExistent() throws IOException, InterruptedException {
|
||||||
try (Pty pty = Pty.openpty()) {
|
try (LinuxPty pty = LinuxPty.openpty()) {
|
||||||
Process dies = pty.getSlave().session(new String[] { "thisHadBetterNotExist" }, null);
|
PtySession dies =
|
||||||
|
pty.getChild().session(new String[] { "thisHadBetterNotExist" }, null);
|
||||||
/**
|
/**
|
||||||
* NOTE: Java subprocess dies with code 1 on unhandled exception. TODO: Is there a nice
|
* NOTE: Java subprocess dies with code 1 on unhandled exception. TODO: Is there a nice
|
||||||
* way to distinguish whether the code is from java or the execed image?
|
* way to distinguish whether the code is from java or the execed image?
|
||||||
*/
|
*/
|
||||||
assertEquals(1, dies.waitFor());
|
assertEquals(1, dies.waitExited().intValue());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,11 +112,12 @@ public class PtyTest {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public Thread runExitCheck(int expected, Process proc) {
|
public Thread runExitCheck(int expected, PtySession session) {
|
||||||
Thread exitCheck = new Thread(() -> {
|
Thread exitCheck = new Thread(() -> {
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
assertEquals("Early exit with wrong code", expected, proc.waitFor());
|
assertEquals("Early exit with wrong code", expected,
|
||||||
|
session.waitExited().intValue());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
catch (InterruptedException e) {
|
catch (InterruptedException e) {
|
||||||
|
@ -132,12 +136,12 @@ public class PtyTest {
|
||||||
env.put("PS1", "BASH:");
|
env.put("PS1", "BASH:");
|
||||||
env.put("PROMPT_COMMAND", "");
|
env.put("PROMPT_COMMAND", "");
|
||||||
env.put("TERM", "");
|
env.put("TERM", "");
|
||||||
try (Pty pty = Pty.openpty()) {
|
try (LinuxPty pty = LinuxPty.openpty()) {
|
||||||
PtyMaster master = pty.getMaster();
|
LinuxPtyParent parent = pty.getParent();
|
||||||
PrintWriter writer = new PrintWriter(master.getOutputStream());
|
PrintWriter writer = new PrintWriter(parent.getOutputStream());
|
||||||
BufferedReader reader = loggingReader(master.getInputStream());
|
BufferedReader reader = loggingReader(parent.getInputStream());
|
||||||
Process bash =
|
PtySession bash =
|
||||||
pty.getSlave().session(new String[] { DummyProc.which("bash"), "--norc" }, env);
|
pty.getChild().session(new String[] { DummyProc.which("bash"), "--norc" }, env);
|
||||||
runExitCheck(3, bash);
|
runExitCheck(3, bash);
|
||||||
|
|
||||||
writer.println("echo test");
|
writer.println("echo test");
|
||||||
|
@ -155,7 +159,7 @@ public class PtyTest {
|
||||||
assertTrue("Not 'exit 3' or 'BASH:exit 3': '" + line + "'",
|
assertTrue("Not 'exit 3' or 'BASH:exit 3': '" + line + "'",
|
||||||
Set.of("BASH:exit 3", "exit 3").contains(line));
|
Set.of("BASH:exit 3", "exit 3").contains(line));
|
||||||
|
|
||||||
assertEquals(3, bash.waitFor());
|
assertEquals(3, bash.waitExited().intValue());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,12 +169,12 @@ public class PtyTest {
|
||||||
env.put("PS1", "BASH:");
|
env.put("PS1", "BASH:");
|
||||||
env.put("PROMPT_COMMAND", "");
|
env.put("PROMPT_COMMAND", "");
|
||||||
env.put("TERM", "");
|
env.put("TERM", "");
|
||||||
try (Pty pty = Pty.openpty()) {
|
try (LinuxPty pty = LinuxPty.openpty()) {
|
||||||
PtyMaster master = pty.getMaster();
|
LinuxPtyParent parent = pty.getParent();
|
||||||
PrintWriter writer = new PrintWriter(master.getOutputStream());
|
PrintWriter writer = new PrintWriter(parent.getOutputStream());
|
||||||
BufferedReader reader = loggingReader(master.getInputStream());
|
BufferedReader reader = loggingReader(parent.getInputStream());
|
||||||
Process bash =
|
PtySession bash =
|
||||||
pty.getSlave().session(new String[] { DummyProc.which("bash"), "--norc" }, env);
|
pty.getChild().session(new String[] { DummyProc.which("bash"), "--norc" }, env);
|
||||||
runExitCheck(3, bash);
|
runExitCheck(3, bash);
|
||||||
|
|
||||||
writer.println("echo test");
|
writer.println("echo test");
|
||||||
|
@ -210,7 +214,7 @@ public class PtyTest {
|
||||||
writer.flush();
|
writer.flush();
|
||||||
assertTrue(Set.of("BASH:exit 3", "exit 3").contains(reader.readLine()));
|
assertTrue(Set.of("BASH:exit 3", "exit 3").contains(reader.readLine()));
|
||||||
|
|
||||||
assertEquals(3, bash.waitFor());
|
assertEquals(3, bash.waitExited().intValue());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,195 @@
|
||||||
|
/* ###
|
||||||
|
* 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.gdb.pty.ssh;
|
||||||
|
|
||||||
|
import static org.junit.Assume.assumeFalse;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import ch.ethz.ssh2.*;
|
||||||
|
import ghidra.app.script.AskDialog;
|
||||||
|
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
|
||||||
|
import ghidra.util.SystemUtilities;
|
||||||
|
import ghidra.util.exception.CancelledException;
|
||||||
|
|
||||||
|
public class SshExperimentsTest extends AbstractGhidraHeadedIntegrationTest {
|
||||||
|
@Before
|
||||||
|
public void checkInteractive() {
|
||||||
|
assumeFalse(SystemUtilities.isInTestingBatchMode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testExpExecCommandIsAsync()
|
||||||
|
throws IOException, CancelledException, InterruptedException {
|
||||||
|
Connection conn = new Connection("localhost");
|
||||||
|
|
||||||
|
conn.addConnectionMonitor(new ConnectionMonitor() {
|
||||||
|
@Override
|
||||||
|
public void connectionLost(Throwable reason) {
|
||||||
|
System.err.println("Lost connection: " + reason);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
conn.connect();
|
||||||
|
|
||||||
|
String user = SshPtyTest.promptUser();
|
||||||
|
while (true) {
|
||||||
|
char[] password =
|
||||||
|
GhidraSshPtyFactory.promptPassword("localhost", "Password for " + user);
|
||||||
|
boolean auth = conn.authenticateWithPassword(user, new String(password));
|
||||||
|
if (auth) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
System.err.println("Authentication Failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
Session session = conn.openSession();
|
||||||
|
System.err.println("PRE: signal=" + session.getExitSignal());
|
||||||
|
|
||||||
|
Thread thread = new Thread("reader") {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
InputStream stdout = session.getStdout();
|
||||||
|
try {
|
||||||
|
stdout.transferTo(System.out);
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
thread.setDaemon(true);
|
||||||
|
thread.start();
|
||||||
|
|
||||||
|
// Demonstrates that execCommand returns before the remote command exits
|
||||||
|
System.err.println("Invoking sleep remotely");
|
||||||
|
session.execCommand("sleep 10");
|
||||||
|
System.err.println("Returned from execCommand");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testExpEOFImpliesCommandExited()
|
||||||
|
throws IOException, CancelledException, InterruptedException {
|
||||||
|
Connection conn = new Connection("localhost");
|
||||||
|
|
||||||
|
conn.addConnectionMonitor(new ConnectionMonitor() {
|
||||||
|
@Override
|
||||||
|
public void connectionLost(Throwable reason) {
|
||||||
|
System.err.println("Lost connection: " + reason);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
conn.connect();
|
||||||
|
|
||||||
|
AskDialog<String> dialog = new AskDialog<>("SSH", "Username:", AskDialog.STRING, "");
|
||||||
|
if (dialog.isCanceled()) {
|
||||||
|
throw new CancelledException();
|
||||||
|
}
|
||||||
|
String user = dialog.getValueAsString();
|
||||||
|
while (true) {
|
||||||
|
char[] password =
|
||||||
|
GhidraSshPtyFactory.promptPassword("localhost", "Password for " + user);
|
||||||
|
boolean auth = conn.authenticateWithPassword(user, new String(password));
|
||||||
|
if (auth) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
System.err.println("Authentication Failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
Session session = conn.openSession();
|
||||||
|
System.err.println("PRE: signal=" + session.getExitSignal());
|
||||||
|
|
||||||
|
Thread thread = new Thread("reader") {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
InputStream stdout = session.getStdout();
|
||||||
|
try {
|
||||||
|
stdout.transferTo(System.out);
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
thread.setDaemon(true);
|
||||||
|
thread.start();
|
||||||
|
|
||||||
|
// Demonstrates the ability to wait for the specific command
|
||||||
|
System.err.println("Invoking sleep remotely");
|
||||||
|
session.execCommand("sleep 3");
|
||||||
|
session.waitForCondition(ChannelCondition.EOF, 0);
|
||||||
|
System.err.println("Returned from waitForCondition");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testExpEnvWorks()
|
||||||
|
throws IOException, CancelledException, InterruptedException {
|
||||||
|
Connection conn = new Connection("localhost");
|
||||||
|
|
||||||
|
conn.addConnectionMonitor(new ConnectionMonitor() {
|
||||||
|
@Override
|
||||||
|
public void connectionLost(Throwable reason) {
|
||||||
|
System.err.println("Lost connection: " + reason);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
conn.connect();
|
||||||
|
|
||||||
|
AskDialog<String> dialog = new AskDialog<>("SSH", "Username:", AskDialog.STRING, "");
|
||||||
|
if (dialog.isCanceled()) {
|
||||||
|
throw new CancelledException();
|
||||||
|
}
|
||||||
|
String user = dialog.getValueAsString();
|
||||||
|
while (true) {
|
||||||
|
char[] password =
|
||||||
|
GhidraSshPtyFactory.promptPassword("localhost", "Password for " + user);
|
||||||
|
boolean auth = conn.authenticateWithPassword(user, new String(password));
|
||||||
|
if (auth) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
System.err.println("Authentication Failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
Session session = conn.openSession();
|
||||||
|
System.err.println("PRE: signal=" + session.getExitSignal());
|
||||||
|
|
||||||
|
Thread thread = new Thread("reader") {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
InputStream stdout = session.getStdout();
|
||||||
|
try {
|
||||||
|
stdout.transferTo(System.out);
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
thread.setDaemon(true);
|
||||||
|
thread.start();
|
||||||
|
|
||||||
|
// Demonstrates a syntax for specifying env.
|
||||||
|
// I suspect this depends on the remote shell.
|
||||||
|
System.err.println("Echoing...");
|
||||||
|
session.execCommand("MY_DATA=test bash -c 'echo data:$MY_DATA:end'");
|
||||||
|
session.waitForCondition(ChannelCondition.EOF, 0);
|
||||||
|
System.err.println("Done");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
/* ###
|
||||||
|
* 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.gdb.pty.ssh;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assume.assumeFalse;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import agent.gdb.pty.PtySession;
|
||||||
|
import ghidra.app.script.AskDialog;
|
||||||
|
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
|
||||||
|
import ghidra.util.SystemUtilities;
|
||||||
|
import ghidra.util.exception.CancelledException;
|
||||||
|
|
||||||
|
public class SshPtyTest extends AbstractGhidraHeadedIntegrationTest {
|
||||||
|
protected GhidraSshPtyFactory factory;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setupSshPtyTest() throws CancelledException {
|
||||||
|
assumeFalse(SystemUtilities.isInTestingBatchMode());
|
||||||
|
factory = new GhidraSshPtyFactory();
|
||||||
|
factory.setHostname("localhost");
|
||||||
|
factory.setUsername(promptUser());
|
||||||
|
factory.setKeyFile("");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String promptUser() throws CancelledException {
|
||||||
|
AskDialog<String> dialog = new AskDialog<>("SSH", "Username:", AskDialog.STRING, "");
|
||||||
|
if (dialog.isCanceled()) {
|
||||||
|
throw new CancelledException();
|
||||||
|
}
|
||||||
|
return dialog.getValueAsString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSessionBash() throws IOException, InterruptedException {
|
||||||
|
try (SshPty pty = factory.openpty()) {
|
||||||
|
PtySession bash = pty.getChild().session(new String[] { "bash" }, null);
|
||||||
|
pty.getParent().getOutputStream().write("exit\n".getBytes());
|
||||||
|
assertEquals(0, bash.waitExited().intValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,7 +31,7 @@ import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind;
|
||||||
import ghidra.dbg.target.TargetEventScope.TargetEventType;
|
import ghidra.dbg.target.TargetEventScope.TargetEventType;
|
||||||
import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState;
|
import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState;
|
||||||
import ghidra.dbg.testutil.*;
|
import ghidra.dbg.testutil.*;
|
||||||
import ghidra.test.AbstractGhidraHeadlessIntegrationTest;
|
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
|
||||||
import ghidra.util.Msg;
|
import ghidra.util.Msg;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -41,7 +41,7 @@ import ghidra.util.Msg;
|
||||||
* <li>TODO: ensure registersUpdated(RegisterBank) immediately upon created(RegisterBank) ?</li>
|
* <li>TODO: ensure registersUpdated(RegisterBank) immediately upon created(RegisterBank) ?</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
*/
|
*/
|
||||||
public abstract class AbstractDebuggerModelTest extends AbstractGhidraHeadlessIntegrationTest
|
public abstract class AbstractDebuggerModelTest extends AbstractGhidraHeadedIntegrationTest
|
||||||
implements TestDebuggerModelProvider, DebuggerModelTestUtils {
|
implements TestDebuggerModelProvider, DebuggerModelTestUtils {
|
||||||
|
|
||||||
protected DummyProc dummy;
|
protected DummyProc dummy;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue