mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2024-10-23 13:41:04 +00:00
GP-568: Factored pty interfaces, change terms, implements GDB over SSH
This commit is contained in:
parent
f077adfffb
commit
4d710ce2bc
|
@ -20,6 +20,7 @@ import java.util.concurrent.CompletableFuture;
|
|||
import agent.gdb.gadp.GdbLocalDebuggerModelFactory;
|
||||
import agent.gdb.manager.GdbManager;
|
||||
import agent.gdb.model.impl.GdbModelImpl;
|
||||
import agent.gdb.pty.linux.LinuxPtyFactory;
|
||||
import ghidra.dbg.DebuggerObjectModel;
|
||||
import ghidra.dbg.LocalDebuggerModelFactory;
|
||||
import ghidra.dbg.util.ConfigurableFactory.FactoryDescription;
|
||||
|
@ -48,7 +49,8 @@ public class GdbInJvmDebuggerModelFactory implements LocalDebuggerModelFactory {
|
|||
|
||||
@Override
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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.model.impl.GdbModelImpl;
|
||||
import agent.gdb.pty.linux.LinuxPtyFactory;
|
||||
import ghidra.dbg.gadp.server.AbstractGadpServer;
|
||||
|
||||
public class GdbGadpServerImpl implements GdbGadpServer {
|
||||
|
@ -35,7 +36,8 @@ public class GdbGadpServerImpl implements GdbGadpServer {
|
|||
|
||||
public GdbGadpServerImpl(SocketAddress addr) throws IOException {
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -21,10 +21,12 @@ import java.util.Map;
|
|||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import agent.gdb.ffi.linux.Pty;
|
||||
import agent.gdb.manager.breakpoint.GdbBreakpointInfo;
|
||||
import agent.gdb.manager.breakpoint.GdbBreakpointInsertions;
|
||||
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
|
||||
|
@ -85,7 +87,8 @@ public interface GdbManager extends AutoCloseable, GdbBreakpointInsertions {
|
|||
*/
|
||||
public static void main(String[] args)
|
||||
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.runRC().get();
|
||||
mgr.consoleLoop();
|
||||
|
@ -101,8 +104,8 @@ public interface GdbManager extends AutoCloseable, GdbBreakpointInsertions {
|
|||
*
|
||||
* @return the manager
|
||||
*/
|
||||
public static GdbManager newInstance() {
|
||||
return new GdbManagerImpl();
|
||||
public static GdbManager newInstance(PtyFactory ptyFactory) {
|
||||
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
|
||||
* 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
|
||||
* 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
|
||||
*/
|
||||
|
@ -507,6 +511,7 @@ public interface GdbManager extends AutoCloseable, GdbBreakpointInsertions {
|
|||
* Get the name of the mi2 pty for this GDB session
|
||||
*
|
||||
* @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.util.InteractiveConsole;
|
||||
|
||||
import agent.gdb.ffi.linux.Pty;
|
||||
import agent.gdb.manager.*;
|
||||
import agent.gdb.manager.GdbCause.Causes;
|
||||
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.parsing.GdbMiParser;
|
||||
import agent.gdb.manager.parsing.GdbParsingUtils.GdbParseError;
|
||||
import agent.gdb.pty.*;
|
||||
import ghidra.async.*;
|
||||
import ghidra.async.AsyncLock.Hold;
|
||||
import ghidra.dbg.error.DebuggerModelTerminatingException;
|
||||
|
@ -104,7 +104,7 @@ public class GdbManagerImpl implements GdbManager {
|
|||
this.pty = pty;
|
||||
this.channel = channel;
|
||||
this.reader =
|
||||
new BufferedReader(new InputStreamReader(pty.getMaster().getInputStream()));
|
||||
new BufferedReader(new InputStreamReader(pty.getParent().getInputStream()));
|
||||
this.interpreter = interpreter;
|
||||
hasWriter = new CompletableFuture<>();
|
||||
}
|
||||
|
@ -124,7 +124,7 @@ public class GdbManagerImpl implements GdbManager {
|
|||
}
|
||||
}
|
||||
if (writer == null) {
|
||||
writer = new PrintWriter(pty.getMaster().getOutputStream());
|
||||
writer = new PrintWriter(pty.getParent().getOutputStream());
|
||||
hasWriter.complete(null);
|
||||
}
|
||||
//Msg.debug(this, channel + ": " + line);
|
||||
|
@ -145,6 +145,8 @@ public class GdbManagerImpl implements GdbManager {
|
|||
}
|
||||
}
|
||||
|
||||
private final PtyFactory ptyFactory;
|
||||
|
||||
private final AsyncReference<GdbState, GdbCause> state =
|
||||
new AsyncReference<>(GdbState.NOT_STARTED);
|
||||
// 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 AtomicBoolean exited = new AtomicBoolean(false);
|
||||
|
||||
private Process gdb;
|
||||
private PtySession gdb;
|
||||
private Thread gdbWaiter;
|
||||
|
||||
private PtyThread iniThread;
|
||||
|
@ -193,8 +195,12 @@ public class GdbManagerImpl implements GdbManager {
|
|||
|
||||
/**
|
||||
* 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.addChangeListener(this::trackRunningInterpreter);
|
||||
state.addChangeListener((os, ns, c) -> event(() -> asyncState.set(ns, c), "managerState"));
|
||||
|
@ -556,9 +562,9 @@ public class GdbManagerImpl implements GdbManager {
|
|||
executor = Executors.newSingleThreadExecutor();
|
||||
|
||||
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.start();
|
||||
|
||||
|
@ -575,14 +581,16 @@ public class GdbManagerImpl implements GdbManager {
|
|||
}
|
||||
switch (iniThread.interpreter) {
|
||||
case CLI:
|
||||
Pty mi2Pty = ptyFactory.openpty();
|
||||
|
||||
cliThread = iniThread;
|
||||
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.start();
|
||||
cliThread.writer.println("new-ui mi2 " + mi2Thread.pty.getSlave().getFile());
|
||||
cliThread.writer.flush();
|
||||
try {
|
||||
mi2Thread.hasWriter.get(2, TimeUnit.SECONDS);
|
||||
}
|
||||
|
@ -598,10 +606,12 @@ public class GdbManagerImpl implements GdbManager {
|
|||
}
|
||||
}
|
||||
else {
|
||||
mi2Thread = new PtyThread(Pty.openpty(), Channel.STDOUT, Interpreter.MI2);
|
||||
mi2Thread.setName("GDB Read MI2");
|
||||
Pty mi2Pty = ptyFactory.openpty();
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
@ -622,7 +632,7 @@ public class GdbManagerImpl implements GdbManager {
|
|||
|
||||
private void waitGdbExit() {
|
||||
try {
|
||||
int exitcode = gdb.waitFor();
|
||||
int exitcode = gdb.waitExited();
|
||||
state.set(GdbState.EXIT, Causes.UNCLAIMED);
|
||||
exited.set(true);
|
||||
if (!executor.isShutdown()) {
|
||||
|
@ -1466,12 +1476,12 @@ public class GdbManagerImpl implements GdbManager {
|
|||
checkStarted();
|
||||
Msg.info(this, "Interrupting");
|
||||
if (cliThread != null) {
|
||||
OutputStream os = cliThread.pty.getMaster().getOutputStream();
|
||||
OutputStream os = cliThread.pty.getParent().getOutputStream();
|
||||
os.write(3);
|
||||
os.flush();
|
||||
}
|
||||
if (mi2Thread != null) {
|
||||
OutputStream os = mi2Thread.pty.getMaster().getOutputStream();
|
||||
OutputStream os = mi2Thread.pty.getParent().getOutputStream();
|
||||
os.write(3);
|
||||
os.flush();
|
||||
}
|
||||
|
@ -1589,8 +1599,8 @@ public class GdbManagerImpl implements GdbManager {
|
|||
}
|
||||
|
||||
@Override
|
||||
public String getMi2PtyName() {
|
||||
return mi2Thread.pty.getSlave().getFile().getAbsolutePath();
|
||||
public String getMi2PtyName() throws IOException {
|
||||
return mi2Thread.pty.getChild().nullSession();
|
||||
}
|
||||
|
||||
public boolean hasCli() {
|
||||
|
|
|
@ -24,6 +24,7 @@ import org.apache.commons.lang3.exception.ExceptionUtils;
|
|||
|
||||
import agent.gdb.manager.*;
|
||||
import agent.gdb.manager.impl.cmd.GdbCommandError;
|
||||
import agent.gdb.pty.PtyFactory;
|
||||
import ghidra.async.AsyncUtils;
|
||||
import ghidra.dbg.DebuggerModelClosedReason;
|
||||
import ghidra.dbg.agent.AbstractDebuggerObjectModel;
|
||||
|
@ -67,8 +68,8 @@ public class GdbModelImpl extends AbstractDebuggerObjectModel {
|
|||
|
||||
protected Map<Object, TargetObject> objectMap = new HashMap<>();
|
||||
|
||||
public GdbModelImpl() {
|
||||
this.gdb = GdbManager.newInstance();
|
||||
public GdbModelImpl(PtyFactory ptyFactory) {
|
||||
this.gdb = GdbManager.newInstance(ptyFactory);
|
||||
this.session = new GdbModelTargetSession(this, ROOT_SCHEMA);
|
||||
|
||||
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
|
||||
* limitations under the License.
|
||||
*/
|
||||
package agent.gdb.ffi.linux;
|
||||
package agent.gdb.pty;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
/**
|
||||
* A base class for either end of a pseudo-terminal
|
||||
*
|
||||
* This provides the input and output streams
|
||||
* One end of a pseudo-terminal
|
||||
*/
|
||||
public class PtyEndpoint {
|
||||
private final int fd;
|
||||
|
||||
PtyEndpoint(int fd) {
|
||||
this.fd = fd;
|
||||
}
|
||||
public interface PtyEndpoint {
|
||||
|
||||
/**
|
||||
* 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
|
||||
* terminal's line discipline.
|
||||
*
|
||||
* @return the output stream
|
||||
* @throws UnsupportedOperationException if this end is not local
|
||||
*/
|
||||
public OutputStream getOutputStream() {
|
||||
return new FdOutputStream(fd);
|
||||
}
|
||||
OutputStream getOutputStream();
|
||||
|
||||
/**
|
||||
* 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
|
||||
* discipline.
|
||||
*
|
||||
* @return the input stream
|
||||
* @throws UnsupportedOperationException if this end is not local
|
||||
*/
|
||||
public InputStream getInputStream() {
|
||||
return new FdInputStream(fd);
|
||||
}
|
||||
InputStream getInputStream();
|
||||
}
|
|
@ -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
|
||||
* 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 {
|
||||
PtyMaster(int fd) {
|
||||
super(fd);
|
||||
}
|
||||
public interface PtyParent extends PtyEndpoint {
|
||||
}
|
|
@ -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
|
||||
* limitations under the License.
|
||||
*/
|
||||
package agent.gdb.ffi.linux;
|
||||
package agent.gdb.pty.linux;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
@ -25,8 +25,10 @@ import jnr.posix.POSIXFactory;
|
|||
/**
|
||||
* 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
|
||||
* is generally detected, but an incorrect, but valid file descriptor may cause undefined behavior.
|
||||
* <p>
|
||||
* <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 {
|
||||
private static final POSIX LIB_POSIX = POSIXFactory.getNativePOSIX();
|
|
@ -13,7 +13,7 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package agent.gdb.ffi.linux;
|
||||
package agent.gdb.pty.linux;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
@ -25,8 +25,10 @@ import jnr.posix.POSIXFactory;
|
|||
/**
|
||||
* 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
|
||||
* is generally detected, but an incorrect, but valid file descriptor may cause undefined behavior.
|
||||
* <p>
|
||||
* <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 {
|
||||
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
|
||||
* limitations under the License.
|
||||
*/
|
||||
package agent.gdb.ffi.linux;
|
||||
package agent.gdb.pty.linux;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.URL;
|
||||
|
@ -21,40 +21,36 @@ import java.net.URLDecoder;
|
|||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* The slave end of a pseudo-terminal
|
||||
*/
|
||||
public class PtySlave extends PtyEndpoint {
|
||||
private final File file;
|
||||
import agent.gdb.pty.PtyChild;
|
||||
import agent.gdb.pty.PtySession;
|
||||
import agent.gdb.pty.local.LocalProcessPtySession;
|
||||
|
||||
PtySlave(int fd, String name) {
|
||||
public class LinuxPtyChild extends LinuxPtyEndpoint implements PtyChild {
|
||||
private final String name;
|
||||
|
||||
LinuxPtyChild(int fd, String name) {
|
||||
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
|
||||
*/
|
||||
public File getFile() {
|
||||
return file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn a subprocess in a new session whose controlling tty is this pseudo-terminal
|
||||
*
|
||||
* Implementation note: This uses {@link ProcessBuilder} to launch the subprocess. See its
|
||||
* documentation for more details of the parameters of this method.
|
||||
*
|
||||
* 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
|
||||
* @implNote This uses {@link ProcessBuilder} to launch the subprocess. See its documentation
|
||||
* for more details of the parameters of this method.
|
||||
* @implNote This actually launches a special "leader" subprocess, which sets up the session and
|
||||
* 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
|
||||
* 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.
|
||||
* 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
|
||||
|
@ -62,19 +58,20 @@ public class PtySlave extends PtyEndpoint {
|
|||
* @return a handle to the subprocess
|
||||
* @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);
|
||||
}
|
||||
|
||||
protected Process sessionUsingJavaLeader(String[] args, Map<String, String> env)
|
||||
protected PtySession sessionUsingJavaLeader(String[] args, Map<String, String> env)
|
||||
throws IOException {
|
||||
final List<String> argsList = new ArrayList<>();
|
||||
argsList.add("java");
|
||||
argsList.add("-cp");
|
||||
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));
|
||||
ProcessBuilder builder = new ProcessBuilder(argsList);
|
||||
if (env != null) {
|
||||
|
@ -82,17 +79,17 @@ public class PtySlave extends PtyEndpoint {
|
|||
}
|
||||
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 {
|
||||
final List<String> argsList = new ArrayList<>();
|
||||
argsList.add("python");
|
||||
argsList.add("-m");
|
||||
argsList.add("session");
|
||||
|
||||
argsList.add(file.getAbsolutePath());
|
||||
argsList.add(name);
|
||||
argsList.addAll(Arrays.asList(args));
|
||||
ProcessBuilder builder = new ProcessBuilder(argsList);
|
||||
if (env != null) {
|
||||
|
@ -103,12 +100,12 @@ public class PtySlave extends PtyEndpoint {
|
|||
builder.environment().put("PYTHONPATH", sourceLoc);
|
||||
builder.inheritIO();
|
||||
|
||||
return builder.start();
|
||||
return new LocalProcessPtySession(builder.start());
|
||||
}
|
||||
|
||||
public static File getSourceLocationForResource(String name) {
|
||||
// TODO: Refactor this with SystemUtilities.getSourceLocationForClass()
|
||||
URL url = PtySlave.class.getClassLoader().getResource(name);
|
||||
URL url = LinuxPtyChild.class.getClassLoader().getResource(name);
|
||||
String urlFile = url.getFile();
|
||||
try {
|
||||
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
|
||||
* limitations under the License.
|
||||
*/
|
||||
package agent.gdb.ffi.linux;
|
||||
package agent.gdb.pty.linux;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Callable;
|
||||
|
@ -21,14 +21,14 @@ import java.util.concurrent.Callable;
|
|||
import jnr.posix.POSIX;
|
||||
import jnr.posix.POSIXFactory;
|
||||
|
||||
public class PtySessionLeader {
|
||||
public class LinuxPtySessionLeader {
|
||||
private static final POSIX LIB_POSIX = POSIXFactory.getNativePOSIX();
|
||||
private static final int O_RDWR = 2; // TODO: Find this in libs
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
PtySessionLeader master = new PtySessionLeader();
|
||||
master.parseArgs(args);
|
||||
master.run();
|
||||
LinuxPtySessionLeader leader = new LinuxPtySessionLeader();
|
||||
leader.parseArgs(args);
|
||||
leader.run();
|
||||
}
|
||||
|
||||
protected String ptyPath;
|
|
@ -13,7 +13,7 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package agent.gdb.ffi.linux;
|
||||
package agent.gdb.pty.linux;
|
||||
|
||||
import jnr.ffi.LibraryLoader;
|
||||
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.GdbManager.ExecSuffix;
|
||||
import agent.gdb.manager.breakpoint.GdbBreakpointInfo;
|
||||
import agent.gdb.pty.PtyFactory;
|
||||
import ghidra.async.AsyncReference;
|
||||
import ghidra.dbg.testutil.DummyProc;
|
||||
import ghidra.test.AbstractGhidraHeadlessIntegrationTest;
|
||||
|
@ -45,6 +46,8 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
|
|||
protected static final long TIMEOUT_MILLISECONDS =
|
||||
SystemUtilities.isInTestingBatchMode() ? 5000 : Long.MAX_VALUE;
|
||||
|
||||
protected abstract PtyFactory getPtyFactory();
|
||||
|
||||
protected abstract CompletableFuture<Void> startManager(GdbManager manager);
|
||||
|
||||
protected void stopManager() throws IOException {
|
||||
|
@ -67,7 +70,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
|
|||
|
||||
@Test
|
||||
public void testAddInferior() throws Throwable {
|
||||
try (GdbManager mgr = GdbManager.newInstance()) {
|
||||
try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
|
||||
waitOn(startManager(mgr));
|
||||
GdbInferior inferior = waitOn(mgr.addInferior());
|
||||
assertEquals(2, inferior.getId());
|
||||
|
@ -77,7 +80,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
|
|||
|
||||
@Test
|
||||
public void testRemoveInferior() throws Throwable {
|
||||
try (GdbManager mgr = GdbManager.newInstance()) {
|
||||
try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
|
||||
waitOn(startManager(mgr));
|
||||
GdbInferior inf = waitOn(mgr.addInferior());
|
||||
assertEquals(2, mgr.getKnownInferiors().size());
|
||||
|
@ -90,7 +93,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
|
|||
|
||||
@Test
|
||||
public void testRemoveCurrentInferior() throws Throwable {
|
||||
try (GdbManager mgr = GdbManager.newInstance()) {
|
||||
try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
|
||||
List<Integer> selEvtIdsTemp = new ArrayList<>();
|
||||
AsyncReference<List<Integer>, Void> selEvtIds = new AsyncReference<>(List.of());
|
||||
mgr.addEventsListener(new GdbEventsListenerAdapter() {
|
||||
|
@ -114,7 +117,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
|
|||
|
||||
@Test
|
||||
public void testConsoleCapture() throws Throwable {
|
||||
try (GdbManager mgr = GdbManager.newInstance()) {
|
||||
try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
|
||||
waitOn(startManager(mgr));
|
||||
String out = waitOn(mgr.consoleCapture("echo test"));
|
||||
assertEquals("test", out.trim());
|
||||
|
@ -123,7 +126,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
|
|||
|
||||
@Test
|
||||
public void testListInferiors() throws Throwable {
|
||||
try (GdbManager mgr = GdbManager.newInstance()) {
|
||||
try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
|
||||
waitOn(startManager(mgr));
|
||||
Map<Integer, GdbInferior> inferiors = waitOn(mgr.listInferiors());
|
||||
assertEquals(new HashSet<>(Arrays.asList(new Integer[] { 1 })), inferiors.keySet());
|
||||
|
@ -132,7 +135,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
|
|||
|
||||
@Test
|
||||
public void testListAvailableProcesses() throws Throwable {
|
||||
try (GdbManager mgr = GdbManager.newInstance()) {
|
||||
try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
|
||||
waitOn(startManager(mgr));
|
||||
List<GdbProcessThreadGroup> procs = waitOn(mgr.listAvailableProcesses());
|
||||
List<Integer> pids = procs.stream().map(p -> p.getPid()).collect(Collectors.toList());
|
||||
|
@ -142,7 +145,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
|
|||
|
||||
@Test
|
||||
public void testInfoOs() throws Throwable {
|
||||
try (GdbManager mgr = GdbManager.newInstance()) {
|
||||
try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
|
||||
waitOn(startManager(mgr));
|
||||
GdbTable infoThreads = waitOn(mgr.infoOs("threads"));
|
||||
assertEquals(new LinkedHashSet<>(Arrays.asList("pid", "command", "tid", "core")),
|
||||
|
@ -153,7 +156,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
|
|||
|
||||
@Test
|
||||
public void testStart() throws Throwable {
|
||||
try (GdbManager mgr = GdbManager.newInstance()) {
|
||||
try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
|
||||
waitOn(startManager(mgr));
|
||||
waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo"));
|
||||
waitOn(mgr.console("break main"));
|
||||
|
@ -164,7 +167,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
|
|||
|
||||
@Test
|
||||
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));
|
||||
Set<GdbThread> threads = waitOn(mgr.currentInferior().attach(echo.pid));
|
||||
// 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 {
|
||||
assumeFalse("I know no way to get this to pass with these conditions",
|
||||
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
|
||||
* 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",
|
||||
this instanceof JoinedGdbManagerTest);
|
||||
// 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"));
|
||||
mgr.addEventsListener(libcLoaded);
|
||||
waitOn(startManager(mgr));
|
||||
|
@ -268,7 +271,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
|
|||
|
||||
@Test
|
||||
public void testSetVarEvaluate() throws Throwable {
|
||||
try (GdbManager mgr = GdbManager.newInstance()) {
|
||||
try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
|
||||
waitOn(startManager(mgr));
|
||||
waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo"));
|
||||
waitOn(mgr.insertBreakpoint("main"));
|
||||
|
@ -283,7 +286,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
|
|||
|
||||
@Test
|
||||
public void testSetVarGetVar() throws Throwable {
|
||||
try (GdbManager mgr = GdbManager.newInstance()) {
|
||||
try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
|
||||
waitOn(startManager(mgr));
|
||||
String val = waitOn(mgr.currentInferior().getVar("args"));
|
||||
assertEquals(null, val);
|
||||
|
@ -295,7 +298,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
|
|||
|
||||
@Test
|
||||
public void testInsertListDeleteBreakpoint() throws Throwable {
|
||||
try (GdbManager mgr = GdbManager.newInstance()) {
|
||||
try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
|
||||
waitOn(startManager(mgr));
|
||||
waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo"));
|
||||
GdbBreakpointInfo breakpoint = waitOn(mgr.insertBreakpoint("main"));
|
||||
|
@ -309,7 +312,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
|
|||
|
||||
@Test
|
||||
public void testListReadWriteReadRegisters() throws Throwable {
|
||||
try (GdbManager mgr = GdbManager.newInstance()) {
|
||||
try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
|
||||
waitOn(startManager(mgr));
|
||||
waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo"));
|
||||
waitOn(mgr.insertBreakpoint("main"));
|
||||
|
@ -345,7 +348,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
|
|||
@Test
|
||||
public void testWriteReadMemory() throws Throwable {
|
||||
ByteBuffer rBuf = ByteBuffer.allocate(1024);
|
||||
try (GdbManager mgr = GdbManager.newInstance()) {
|
||||
try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
|
||||
waitOn(startManager(mgr));
|
||||
waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo"));
|
||||
waitOn(mgr.insertBreakpoint("main"));
|
||||
|
@ -375,7 +378,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
|
|||
|
||||
@Test
|
||||
public void testContinue() throws Throwable {
|
||||
try (GdbManager mgr = GdbManager.newInstance()) {
|
||||
try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
|
||||
waitOn(startManager(mgr));
|
||||
waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo"));
|
||||
waitOn(mgr.insertBreakpoint("main"));
|
||||
|
@ -390,7 +393,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
|
|||
|
||||
@Test
|
||||
public void testStep() throws Throwable {
|
||||
try (GdbManager mgr = GdbManager.newInstance()) {
|
||||
try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
|
||||
waitOn(startManager(mgr));
|
||||
waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo"));
|
||||
waitOn(mgr.insertBreakpoint("main"));
|
||||
|
@ -405,7 +408,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
|
|||
|
||||
@Test
|
||||
public void testThreadSelect() throws Throwable {
|
||||
try (GdbManager mgr = GdbManager.newInstance()) {
|
||||
try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
|
||||
waitOn(startManager(mgr));
|
||||
waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo"));
|
||||
waitOn(mgr.insertBreakpoint("main"));
|
||||
|
@ -418,7 +421,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
|
|||
|
||||
@Test
|
||||
public void testListFrames() throws Throwable {
|
||||
try (GdbManager mgr = GdbManager.newInstance()) {
|
||||
try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
|
||||
waitOn(startManager(mgr));
|
||||
waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo"));
|
||||
waitOn(mgr.insertBreakpoint("main"));
|
||||
|
|
|
@ -21,8 +21,11 @@ import java.util.concurrent.CompletableFuture;
|
|||
|
||||
import org.junit.Ignore;
|
||||
|
||||
import agent.gdb.ffi.linux.Pty;
|
||||
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;
|
||||
|
||||
@Ignore("Need compatible GDB version for CI")
|
||||
|
@ -31,7 +34,7 @@ public class JoinedGdbManagerTest extends AbstractGdbManagerTest {
|
|||
@Override
|
||||
public void run() {
|
||||
BufferedReader reader =
|
||||
new BufferedReader(new InputStreamReader(ptyUserGdb.getMaster().getInputStream()));
|
||||
new BufferedReader(new InputStreamReader(ptyUserGdb.getParent().getInputStream()));
|
||||
String line;
|
||||
try {
|
||||
while (gdb != null && null != (line = reader.readLine())) {
|
||||
|
@ -44,20 +47,26 @@ public class JoinedGdbManagerTest extends AbstractGdbManagerTest {
|
|||
}
|
||||
}
|
||||
|
||||
protected Pty ptyUserGdb;
|
||||
protected Process gdb;
|
||||
protected LinuxPty ptyUserGdb;
|
||||
protected PtySession gdb;
|
||||
|
||||
@Override
|
||||
protected PtyFactory getPtyFactory() {
|
||||
// TODO: Choose by host OS
|
||||
return new LinuxPtyFactory();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CompletableFuture<Void> startManager(GdbManager manager) {
|
||||
try {
|
||||
ptyUserGdb = Pty.openpty();
|
||||
ptyUserGdb = LinuxPty.openpty();
|
||||
manager.start(null);
|
||||
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());
|
||||
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.flush();
|
||||
return manager.runRC();
|
||||
|
|
|
@ -21,6 +21,8 @@ import java.util.concurrent.CompletableFuture;
|
|||
import org.junit.Ignore;
|
||||
|
||||
import agent.gdb.manager.GdbManager;
|
||||
import agent.gdb.pty.PtyFactory;
|
||||
import agent.gdb.pty.linux.LinuxPtyFactory;
|
||||
|
||||
@Ignore("Need compatible GDB version for CI")
|
||||
public class SpawnedCliGdbManagerTest extends AbstractGdbManagerTest {
|
||||
|
@ -34,4 +36,10 @@ public class SpawnedCliGdbManagerTest extends AbstractGdbManagerTest {
|
|||
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 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")
|
||||
public class SpawnedMi2Gdb7Dot6Dot1ManagerTest extends AbstractGdbManagerTest {
|
||||
|
@ -34,4 +36,10 @@ public class SpawnedMi2Gdb7Dot6Dot1ManagerTest extends AbstractGdbManagerTest {
|
|||
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 agent.gdb.manager.GdbManager;
|
||||
import agent.gdb.pty.PtyFactory;
|
||||
import agent.gdb.pty.linux.LinuxPtyFactory;
|
||||
|
||||
@Ignore("Need compatible GDB version for CI")
|
||||
public class SpawnedMi2GdbManagerTest2 extends AbstractGdbManagerTest {
|
||||
|
@ -34,4 +36,10 @@ public class SpawnedMi2GdbManagerTest2 extends AbstractGdbManagerTest {
|
|||
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
|
||||
* 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.assertTrue;
|
||||
|
@ -23,21 +23,22 @@ import java.util.*;
|
|||
|
||||
import org.junit.Test;
|
||||
|
||||
import agent.gdb.pty.PtySession;
|
||||
import ghidra.dbg.testutil.DummyProc;
|
||||
|
||||
public class PtyTest {
|
||||
public class LinuxPtyTest {
|
||||
@Test
|
||||
public void testOpenClosePty() throws IOException {
|
||||
Pty pty = Pty.openpty();
|
||||
LinuxPty pty = LinuxPty.openpty();
|
||||
pty.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMasterToSlave() throws IOException {
|
||||
try (Pty pty = Pty.openpty()) {
|
||||
PrintWriter writer = new PrintWriter(pty.getMaster().getOutputStream());
|
||||
public void testParentToChild() throws IOException {
|
||||
try (LinuxPty pty = LinuxPty.openpty()) {
|
||||
PrintWriter writer = new PrintWriter(pty.getParent().getOutputStream());
|
||||
BufferedReader reader =
|
||||
new BufferedReader(new InputStreamReader(pty.getSlave().getInputStream()));
|
||||
new BufferedReader(new InputStreamReader(pty.getChild().getInputStream()));
|
||||
|
||||
writer.println("Hello, World!");
|
||||
writer.flush();
|
||||
|
@ -46,11 +47,11 @@ public class PtyTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testSlaveToMaster() throws IOException {
|
||||
try (Pty pty = Pty.openpty()) {
|
||||
PrintWriter writer = new PrintWriter(pty.getSlave().getOutputStream());
|
||||
public void testChildToParent() throws IOException {
|
||||
try (LinuxPty pty = LinuxPty.openpty()) {
|
||||
PrintWriter writer = new PrintWriter(pty.getChild().getOutputStream());
|
||||
BufferedReader reader =
|
||||
new BufferedReader(new InputStreamReader(pty.getMaster().getInputStream()));
|
||||
new BufferedReader(new InputStreamReader(pty.getParent().getInputStream()));
|
||||
|
||||
writer.println("Hello, World!");
|
||||
writer.flush();
|
||||
|
@ -60,22 +61,24 @@ public class PtyTest {
|
|||
|
||||
@Test
|
||||
public void testSessionBash() throws IOException, InterruptedException {
|
||||
try (Pty pty = Pty.openpty()) {
|
||||
Process bash = pty.getSlave().session(new String[] { DummyProc.which("bash") }, null);
|
||||
pty.getMaster().getOutputStream().write("exit\n".getBytes());
|
||||
assertEquals(0, bash.waitFor());
|
||||
try (LinuxPty pty = LinuxPty.openpty()) {
|
||||
PtySession bash =
|
||||
pty.getChild().session(new String[] { DummyProc.which("bash") }, null);
|
||||
pty.getParent().getOutputStream().write("exit\n".getBytes());
|
||||
assertEquals(0, bash.waitExited().intValue());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testForkIntoNonExistent() throws IOException, InterruptedException {
|
||||
try (Pty pty = Pty.openpty()) {
|
||||
Process dies = pty.getSlave().session(new String[] { "thisHadBetterNotExist" }, null);
|
||||
try (LinuxPty pty = LinuxPty.openpty()) {
|
||||
PtySession dies =
|
||||
pty.getChild().session(new String[] { "thisHadBetterNotExist" }, null);
|
||||
/**
|
||||
* 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?
|
||||
*/
|
||||
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(() -> {
|
||||
while (true) {
|
||||
try {
|
||||
assertEquals("Early exit with wrong code", expected, proc.waitFor());
|
||||
assertEquals("Early exit with wrong code", expected,
|
||||
session.waitExited().intValue());
|
||||
return;
|
||||
}
|
||||
catch (InterruptedException e) {
|
||||
|
@ -132,12 +136,12 @@ public class PtyTest {
|
|||
env.put("PS1", "BASH:");
|
||||
env.put("PROMPT_COMMAND", "");
|
||||
env.put("TERM", "");
|
||||
try (Pty pty = Pty.openpty()) {
|
||||
PtyMaster master = pty.getMaster();
|
||||
PrintWriter writer = new PrintWriter(master.getOutputStream());
|
||||
BufferedReader reader = loggingReader(master.getInputStream());
|
||||
Process bash =
|
||||
pty.getSlave().session(new String[] { DummyProc.which("bash"), "--norc" }, env);
|
||||
try (LinuxPty pty = LinuxPty.openpty()) {
|
||||
LinuxPtyParent parent = pty.getParent();
|
||||
PrintWriter writer = new PrintWriter(parent.getOutputStream());
|
||||
BufferedReader reader = loggingReader(parent.getInputStream());
|
||||
PtySession bash =
|
||||
pty.getChild().session(new String[] { DummyProc.which("bash"), "--norc" }, env);
|
||||
runExitCheck(3, bash);
|
||||
|
||||
writer.println("echo test");
|
||||
|
@ -155,7 +159,7 @@ public class PtyTest {
|
|||
assertTrue("Not 'exit 3' or 'BASH:exit 3': '" + 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("PROMPT_COMMAND", "");
|
||||
env.put("TERM", "");
|
||||
try (Pty pty = Pty.openpty()) {
|
||||
PtyMaster master = pty.getMaster();
|
||||
PrintWriter writer = new PrintWriter(master.getOutputStream());
|
||||
BufferedReader reader = loggingReader(master.getInputStream());
|
||||
Process bash =
|
||||
pty.getSlave().session(new String[] { DummyProc.which("bash"), "--norc" }, env);
|
||||
try (LinuxPty pty = LinuxPty.openpty()) {
|
||||
LinuxPtyParent parent = pty.getParent();
|
||||
PrintWriter writer = new PrintWriter(parent.getOutputStream());
|
||||
BufferedReader reader = loggingReader(parent.getInputStream());
|
||||
PtySession bash =
|
||||
pty.getChild().session(new String[] { DummyProc.which("bash"), "--norc" }, env);
|
||||
runExitCheck(3, bash);
|
||||
|
||||
writer.println("echo test");
|
||||
|
@ -210,7 +214,7 @@ public class PtyTest {
|
|||
writer.flush();
|
||||
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.TargetExecutionStateful.TargetExecutionState;
|
||||
import ghidra.dbg.testutil.*;
|
||||
import ghidra.test.AbstractGhidraHeadlessIntegrationTest;
|
||||
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
|
||||
import ghidra.util.Msg;
|
||||
|
||||
/**
|
||||
|
@ -41,7 +41,7 @@ import ghidra.util.Msg;
|
|||
* <li>TODO: ensure registersUpdated(RegisterBank) immediately upon created(RegisterBank) ?</li>
|
||||
* </ul>
|
||||
*/
|
||||
public abstract class AbstractDebuggerModelTest extends AbstractGhidraHeadlessIntegrationTest
|
||||
public abstract class AbstractDebuggerModelTest extends AbstractGhidraHeadedIntegrationTest
|
||||
implements TestDebuggerModelProvider, DebuggerModelTestUtils {
|
||||
|
||||
protected DummyProc dummy;
|
||||
|
|
Loading…
Reference in New Issue
Block a user