GP-568: Factored pty interfaces, change terms, implements GDB over SSH

This commit is contained in:
Dan 2021-04-16 15:41:58 -04:00
parent f077adfffb
commit 4d710ce2bc
41 changed files with 1588 additions and 319 deletions

View file

@ -20,6 +20,7 @@ import java.util.concurrent.CompletableFuture;
import agent.gdb.gadp.GdbLocalDebuggerModelFactory; import agent.gdb.gadp.GdbLocalDebuggerModelFactory;
import agent.gdb.manager.GdbManager; import agent.gdb.manager.GdbManager;
import agent.gdb.model.impl.GdbModelImpl; import agent.gdb.model.impl.GdbModelImpl;
import agent.gdb.pty.linux.LinuxPtyFactory;
import ghidra.dbg.DebuggerObjectModel; import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.LocalDebuggerModelFactory; import ghidra.dbg.LocalDebuggerModelFactory;
import ghidra.dbg.util.ConfigurableFactory.FactoryDescription; import ghidra.dbg.util.ConfigurableFactory.FactoryDescription;
@ -30,8 +31,8 @@ import ghidra.util.classfinder.ExtensionPointProperties;
* may change if it proves stable, though, no? * may change if it proves stable, though, no?
*/ */
@FactoryDescription( // @FactoryDescription( //
brief = "IN-VM GNU gdb local debugger", // brief = "IN-VM GNU gdb local debugger", //
htmlDetails = "Launch a GDB session in this same JVM" // htmlDetails = "Launch a GDB session in this same JVM" //
) )
@ExtensionPointProperties(priority = 80) @ExtensionPointProperties(priority = 80)
public class GdbInJvmDebuggerModelFactory implements LocalDebuggerModelFactory { public class GdbInJvmDebuggerModelFactory implements LocalDebuggerModelFactory {
@ -48,7 +49,8 @@ public class GdbInJvmDebuggerModelFactory implements LocalDebuggerModelFactory {
@Override @Override
public CompletableFuture<? extends DebuggerObjectModel> build() { public CompletableFuture<? extends DebuggerObjectModel> build() {
GdbModelImpl model = new GdbModelImpl(); // TODO: Choose Linux or Windows pty based on host OS
GdbModelImpl model = new GdbModelImpl(new LinuxPtyFactory());
return model.startGDB(gdbCmd, new String[] {}).thenApply(__ -> model); return model.startGDB(gdbCmd, new String[] {}).thenApply(__ -> model);
} }

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -21,6 +21,7 @@ import java.util.concurrent.CompletableFuture;
import agent.gdb.gadp.GdbGadpServer; import agent.gdb.gadp.GdbGadpServer;
import agent.gdb.model.impl.GdbModelImpl; import agent.gdb.model.impl.GdbModelImpl;
import agent.gdb.pty.linux.LinuxPtyFactory;
import ghidra.dbg.gadp.server.AbstractGadpServer; import ghidra.dbg.gadp.server.AbstractGadpServer;
public class GdbGadpServerImpl implements GdbGadpServer { public class GdbGadpServerImpl implements GdbGadpServer {
@ -35,7 +36,8 @@ public class GdbGadpServerImpl implements GdbGadpServer {
public GdbGadpServerImpl(SocketAddress addr) throws IOException { public GdbGadpServerImpl(SocketAddress addr) throws IOException {
super(); super();
this.model = new GdbModelImpl(); // TODO: Select Linux or Windows factory based on host OS
this.model = new GdbModelImpl(new LinuxPtyFactory());
this.server = new GadpSide(model, addr); this.server = new GadpSide(model, addr);
} }

View file

@ -21,10 +21,12 @@ import java.util.Map;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import agent.gdb.ffi.linux.Pty;
import agent.gdb.manager.breakpoint.GdbBreakpointInfo; import agent.gdb.manager.breakpoint.GdbBreakpointInfo;
import agent.gdb.manager.breakpoint.GdbBreakpointInsertions; import agent.gdb.manager.breakpoint.GdbBreakpointInsertions;
import agent.gdb.manager.impl.GdbManagerImpl; import agent.gdb.manager.impl.GdbManagerImpl;
import agent.gdb.pty.PtyFactory;
import agent.gdb.pty.linux.LinuxPty;
import agent.gdb.pty.linux.LinuxPtyFactory;
/** /**
* The controlling side of a GDB session, using GDB/MI, usually via a pseudo-terminal * The controlling side of a GDB session, using GDB/MI, usually via a pseudo-terminal
@ -85,7 +87,8 @@ public interface GdbManager extends AutoCloseable, GdbBreakpointInsertions {
*/ */
public static void main(String[] args) public static void main(String[] args)
throws InterruptedException, ExecutionException, IOException { throws InterruptedException, ExecutionException, IOException {
try (GdbManager mgr = newInstance()) { // TODO: Choose factory by host OS
try (GdbManager mgr = newInstance(new LinuxPtyFactory())) {
mgr.start(DEFAULT_GDB_CMD, args); mgr.start(DEFAULT_GDB_CMD, args);
mgr.runRC().get(); mgr.runRC().get();
mgr.consoleLoop(); mgr.consoleLoop();
@ -101,8 +104,8 @@ public interface GdbManager extends AutoCloseable, GdbBreakpointInsertions {
* *
* @return the manager * @return the manager
*/ */
public static GdbManager newInstance() { public static GdbManager newInstance(PtyFactory ptyFactory) {
return new GdbManagerImpl(); return new GdbManagerImpl(ptyFactory);
} }
/** /**
@ -203,7 +206,8 @@ public interface GdbManager extends AutoCloseable, GdbBreakpointInsertions {
* Note: depending on the target, its output may not be communicated via this listener. Local * Note: depending on the target, its output may not be communicated via this listener. Local
* targets, e.g., tend to just print output to GDB's controlling TTY. See * targets, e.g., tend to just print output to GDB's controlling TTY. See
* {@link GdbInferior#setTty(String)} for a means to more reliably interact with a target's * {@link GdbInferior#setTty(String)} for a means to more reliably interact with a target's
* input and output. See also {@link Pty} for a means to easily acquire a new TTY from Java. * input and output. See also {@link LinuxPty} for a means to easily acquire a new TTY from
* Java.
* *
* @param listener the listener to add * @param listener the listener to add
*/ */
@ -507,6 +511,7 @@ public interface GdbManager extends AutoCloseable, GdbBreakpointInsertions {
* Get the name of the mi2 pty for this GDB session * Get the name of the mi2 pty for this GDB session
* *
* @return the filename * @return the filename
* @throws IOException if the filename could not be determined
*/ */
String getMi2PtyName(); String getMi2PtyName() throws IOException;
} }

View file

@ -26,7 +26,6 @@ import org.apache.commons.lang3.exception.ExceptionUtils;
import org.python.core.PyDictionary; import org.python.core.PyDictionary;
import org.python.util.InteractiveConsole; import org.python.util.InteractiveConsole;
import agent.gdb.ffi.linux.Pty;
import agent.gdb.manager.*; import agent.gdb.manager.*;
import agent.gdb.manager.GdbCause.Causes; import agent.gdb.manager.GdbCause.Causes;
import agent.gdb.manager.breakpoint.GdbBreakpointInfo; import agent.gdb.manager.breakpoint.GdbBreakpointInfo;
@ -35,6 +34,7 @@ import agent.gdb.manager.evt.*;
import agent.gdb.manager.impl.cmd.*; import agent.gdb.manager.impl.cmd.*;
import agent.gdb.manager.parsing.GdbMiParser; import agent.gdb.manager.parsing.GdbMiParser;
import agent.gdb.manager.parsing.GdbParsingUtils.GdbParseError; import agent.gdb.manager.parsing.GdbParsingUtils.GdbParseError;
import agent.gdb.pty.*;
import ghidra.async.*; import ghidra.async.*;
import ghidra.async.AsyncLock.Hold; import ghidra.async.AsyncLock.Hold;
import ghidra.dbg.error.DebuggerModelTerminatingException; import ghidra.dbg.error.DebuggerModelTerminatingException;
@ -104,7 +104,7 @@ public class GdbManagerImpl implements GdbManager {
this.pty = pty; this.pty = pty;
this.channel = channel; this.channel = channel;
this.reader = this.reader =
new BufferedReader(new InputStreamReader(pty.getMaster().getInputStream())); new BufferedReader(new InputStreamReader(pty.getParent().getInputStream()));
this.interpreter = interpreter; this.interpreter = interpreter;
hasWriter = new CompletableFuture<>(); hasWriter = new CompletableFuture<>();
} }
@ -124,7 +124,7 @@ public class GdbManagerImpl implements GdbManager {
} }
} }
if (writer == null) { if (writer == null) {
writer = new PrintWriter(pty.getMaster().getOutputStream()); writer = new PrintWriter(pty.getParent().getOutputStream());
hasWriter.complete(null); hasWriter.complete(null);
} }
//Msg.debug(this, channel + ": " + line); //Msg.debug(this, channel + ": " + line);
@ -145,6 +145,8 @@ public class GdbManagerImpl implements GdbManager {
} }
} }
private final PtyFactory ptyFactory;
private final AsyncReference<GdbState, GdbCause> state = private final AsyncReference<GdbState, GdbCause> state =
new AsyncReference<>(GdbState.NOT_STARTED); new AsyncReference<>(GdbState.NOT_STARTED);
// A copy of state, which is updated on the eventThread. // A copy of state, which is updated on the eventThread.
@ -156,7 +158,7 @@ public class GdbManagerImpl implements GdbManager {
private final HandlerMap<GdbEvent<?>, Void, Void> handlerMap = new HandlerMap<>(); private final HandlerMap<GdbEvent<?>, Void, Void> handlerMap = new HandlerMap<>();
private final AtomicBoolean exited = new AtomicBoolean(false); private final AtomicBoolean exited = new AtomicBoolean(false);
private Process gdb; private PtySession gdb;
private Thread gdbWaiter; private Thread gdbWaiter;
private PtyThread iniThread; private PtyThread iniThread;
@ -193,8 +195,12 @@ public class GdbManagerImpl implements GdbManager {
/** /**
* Instantiate a new manager * Instantiate a new manager
*
* @param ptyFactory a factory for creating Pty's for child GDBs
*/ */
public GdbManagerImpl() { public GdbManagerImpl(PtyFactory ptyFactory) {
this.ptyFactory = ptyFactory;
state.filter(this::stateFilter); state.filter(this::stateFilter);
state.addChangeListener(this::trackRunningInterpreter); state.addChangeListener(this::trackRunningInterpreter);
state.addChangeListener((os, ns, c) -> event(() -> asyncState.set(ns, c), "managerState")); state.addChangeListener((os, ns, c) -> event(() -> asyncState.set(ns, c), "managerState"));
@ -556,9 +562,9 @@ public class GdbManagerImpl implements GdbManager {
executor = Executors.newSingleThreadExecutor(); executor = Executors.newSingleThreadExecutor();
if (gdbCmd != null) { if (gdbCmd != null) {
iniThread = new PtyThread(Pty.openpty(), Channel.STDOUT, null); iniThread = new PtyThread(ptyFactory.openpty(), Channel.STDOUT, null);
gdb = iniThread.pty.getSlave().session(fullargs.toArray(new String[] {}), null); gdb = iniThread.pty.getChild().session(fullargs.toArray(new String[] {}), null);
gdbWaiter = new Thread(this::waitGdbExit, "GDB WaitExit"); gdbWaiter = new Thread(this::waitGdbExit, "GDB WaitExit");
gdbWaiter.start(); gdbWaiter.start();
@ -575,14 +581,16 @@ public class GdbManagerImpl implements GdbManager {
} }
switch (iniThread.interpreter) { switch (iniThread.interpreter) {
case CLI: case CLI:
Pty mi2Pty = ptyFactory.openpty();
cliThread = iniThread; cliThread = iniThread;
cliThread.setName("GDB Read CLI"); cliThread.setName("GDB Read CLI");
cliThread.writer.println("new-ui mi2 " + mi2Pty.getChild().nullSession());
cliThread.writer.flush();
mi2Thread = new PtyThread(Pty.openpty(), Channel.STDOUT, Interpreter.MI2); mi2Thread = new PtyThread(mi2Pty, Channel.STDOUT, Interpreter.MI2);
mi2Thread.setName("GDB Read MI2"); mi2Thread.setName("GDB Read MI2");
mi2Thread.start(); mi2Thread.start();
cliThread.writer.println("new-ui mi2 " + mi2Thread.pty.getSlave().getFile());
cliThread.writer.flush();
try { try {
mi2Thread.hasWriter.get(2, TimeUnit.SECONDS); mi2Thread.hasWriter.get(2, TimeUnit.SECONDS);
} }
@ -598,10 +606,12 @@ public class GdbManagerImpl implements GdbManager {
} }
} }
else { else {
mi2Thread = new PtyThread(Pty.openpty(), Channel.STDOUT, Interpreter.MI2); Pty mi2Pty = ptyFactory.openpty();
mi2Thread.setName("GDB Read MI2");
Msg.info(this, "Agent is waiting for GDB/MI v2 interpreter at " + Msg.info(this, "Agent is waiting for GDB/MI v2 interpreter at " +
mi2Thread.pty.getSlave().getFile()); mi2Pty.getChild().nullSession());
mi2Thread = new PtyThread(mi2Pty, Channel.STDOUT, Interpreter.MI2);
mi2Thread.setName("GDB Read MI2");
mi2Thread.start(); mi2Thread.start();
} }
} }
@ -622,7 +632,7 @@ public class GdbManagerImpl implements GdbManager {
private void waitGdbExit() { private void waitGdbExit() {
try { try {
int exitcode = gdb.waitFor(); int exitcode = gdb.waitExited();
state.set(GdbState.EXIT, Causes.UNCLAIMED); state.set(GdbState.EXIT, Causes.UNCLAIMED);
exited.set(true); exited.set(true);
if (!executor.isShutdown()) { if (!executor.isShutdown()) {
@ -1466,12 +1476,12 @@ public class GdbManagerImpl implements GdbManager {
checkStarted(); checkStarted();
Msg.info(this, "Interrupting"); Msg.info(this, "Interrupting");
if (cliThread != null) { if (cliThread != null) {
OutputStream os = cliThread.pty.getMaster().getOutputStream(); OutputStream os = cliThread.pty.getParent().getOutputStream();
os.write(3); os.write(3);
os.flush(); os.flush();
} }
if (mi2Thread != null) { if (mi2Thread != null) {
OutputStream os = mi2Thread.pty.getMaster().getOutputStream(); OutputStream os = mi2Thread.pty.getParent().getOutputStream();
os.write(3); os.write(3);
os.flush(); os.flush();
} }
@ -1589,8 +1599,8 @@ public class GdbManagerImpl implements GdbManager {
} }
@Override @Override
public String getMi2PtyName() { public String getMi2PtyName() throws IOException {
return mi2Thread.pty.getSlave().getFile().getAbsolutePath(); return mi2Thread.pty.getChild().nullSession();
} }
public boolean hasCli() { public boolean hasCli() {

View file

@ -24,6 +24,7 @@ import org.apache.commons.lang3.exception.ExceptionUtils;
import agent.gdb.manager.*; import agent.gdb.manager.*;
import agent.gdb.manager.impl.cmd.GdbCommandError; import agent.gdb.manager.impl.cmd.GdbCommandError;
import agent.gdb.pty.PtyFactory;
import ghidra.async.AsyncUtils; import ghidra.async.AsyncUtils;
import ghidra.dbg.DebuggerModelClosedReason; import ghidra.dbg.DebuggerModelClosedReason;
import ghidra.dbg.agent.AbstractDebuggerObjectModel; import ghidra.dbg.agent.AbstractDebuggerObjectModel;
@ -67,8 +68,8 @@ public class GdbModelImpl extends AbstractDebuggerObjectModel {
protected Map<Object, TargetObject> objectMap = new HashMap<>(); protected Map<Object, TargetObject> objectMap = new HashMap<>();
public GdbModelImpl() { public GdbModelImpl(PtyFactory ptyFactory) {
this.gdb = GdbManager.newInstance(); this.gdb = GdbManager.newInstance(ptyFactory);
this.session = new GdbModelTargetSession(this, ROOT_SCHEMA); this.session = new GdbModelTargetSession(this, ROOT_SCHEMA);
this.completedSession = CompletableFuture.completedFuture(session); this.completedSession = CompletableFuture.completedFuture(session);

View file

@ -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;
}

View file

@ -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;
}

View file

@ -13,44 +13,37 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package agent.gdb.ffi.linux; package agent.gdb.pty;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
/** /**
* A base class for either end of a pseudo-terminal * One end of a pseudo-terminal
*
* This provides the input and output streams
*/ */
public class PtyEndpoint { public interface PtyEndpoint {
private final int fd;
PtyEndpoint(int fd) {
this.fd = fd;
}
/** /**
* Get the output stream for this end of the pty * Get the output stream for this end of the pty
* *
* <p>
* Writes to this stream arrive on the input stream for the opposite end, subject to the * Writes to this stream arrive on the input stream for the opposite end, subject to the
* terminal's line discipline. * terminal's line discipline.
* *
* @return the output stream * @return the output stream
* @throws UnsupportedOperationException if this end is not local
*/ */
public OutputStream getOutputStream() { OutputStream getOutputStream();
return new FdOutputStream(fd);
}
/** /**
* Get the input stream for this end of the pty * Get the input stream for this end of the pty
* *
* <p>
* Writes to the output stream of the opposite end arrive here, subject to the terminal's line * Writes to the output stream of the opposite end arrive here, subject to the terminal's line
* discipline. * discipline.
* *
* @return the input stream * @return the input stream
* @throws UnsupportedOperationException if this end is not local
*/ */
public InputStream getInputStream() { InputStream getInputStream();
return new FdInputStream(fd);
}
} }

View file

@ -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;
}

View file

@ -13,13 +13,10 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package agent.gdb.ffi.linux; package agent.gdb.pty;
/** /**
* The master end of a pseudo-terminal * The parent (UNIX "master") end of a pseudo-terminal
*/ */
public class PtyMaster extends PtyEndpoint { public interface PtyParent extends PtyEndpoint {
PtyMaster(int fd) {
super(fd);
}
} }

View file

@ -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();
}

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package agent.gdb.ffi.linux; package agent.gdb.pty.linux;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -25,8 +25,10 @@ import jnr.posix.POSIXFactory;
/** /**
* An input stream that wraps a native POSIX file descriptor * An input stream that wraps a native POSIX file descriptor
* *
* WARNING: This class makes use of jnr-ffi to invoke native functions. An invalid file descriptor * <p>
* is generally detected, but an incorrect, but valid file descriptor may cause undefined behavior. * <b>WARNING:</b> This class makes use of jnr-ffi to invoke native functions. An invalid file
* descriptor is generally detected, but an incorrect, but valid file descriptor may cause undefined
* behavior.
*/ */
public class FdInputStream extends InputStream { public class FdInputStream extends InputStream {
private static final POSIX LIB_POSIX = POSIXFactory.getNativePOSIX(); private static final POSIX LIB_POSIX = POSIXFactory.getNativePOSIX();

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package agent.gdb.ffi.linux; package agent.gdb.pty.linux;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
@ -25,8 +25,10 @@ import jnr.posix.POSIXFactory;
/** /**
* An output stream that wraps a native POSIX file descriptor * An output stream that wraps a native POSIX file descriptor
* *
* WARNING: This class makes use of jnr-ffi to invoke native functions. An invalid file descriptor * <p>
* is generally detected, but an incorrect, but valid file descriptor may cause undefined behavior. * <b>WARNING:</b> This class makes use of jnr-ffi to invoke native functions. An invalid file
* descriptor is generally detected, but an incorrect, but valid file descriptor may cause undefined
* behavior.
*/ */
public class FdOutputStream extends OutputStream { public class FdOutputStream extends OutputStream {
private static final POSIX LIB_POSIX = POSIXFactory.getNativePOSIX(); private static final POSIX LIB_POSIX = POSIXFactory.getNativePOSIX();

View file

@ -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;
}
}

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package agent.gdb.ffi.linux; package agent.gdb.pty.linux;
import java.io.*; import java.io.*;
import java.net.URL; import java.net.URL;
@ -21,60 +21,57 @@ import java.net.URLDecoder;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.*; import java.util.*;
/** import agent.gdb.pty.PtyChild;
* The slave end of a pseudo-terminal import agent.gdb.pty.PtySession;
*/ import agent.gdb.pty.local.LocalProcessPtySession;
public class PtySlave extends PtyEndpoint {
private final File file;
PtySlave(int fd, String name) { public class LinuxPtyChild extends LinuxPtyEndpoint implements PtyChild {
private final String name;
LinuxPtyChild(int fd, String name) {
super(fd); super(fd);
this.file = new File(name); this.name = name;
}
@Override
public String nullSession() {
return name;
} }
/** /**
* Get the file referring to this pseudo-terminal * {@inheritDoc}
* *
* @return the file * @implNote This uses {@link ProcessBuilder} to launch the subprocess. See its documentation
*/ * for more details of the parameters of this method.
public File getFile() { * @implNote This actually launches a special "leader" subprocess, which sets up the session and
return file; * then executes the requested program. The requested program image replaces the
} * leader so that the returned process is indeed a handle to the requested program.
* Ordinarily, this does not matter, but it may be useful to know when debugging.
/** * Furthermore, if special characters are sent on the parent before the image is
* Spawn a subprocess in a new session whose controlling tty is this pseudo-terminal * replaced, they may be received by the leader instead. For example, Ctrl-C might be
* * received by the leader by mistake if sent immediately upon spawning a new session.
* Implementation note: This uses {@link ProcessBuilder} to launch the subprocess. See its * Users should send a simple command, e.g., "echo", to confirm that the requested
* documentation for more details of the parameters of this method. * program is active before sending special characters.
*
* Deep implementation note: This actually launches a Python script, which sets up the session
* and then executes the requested program. The requested program image replaces the Python
* interpreter so that the returned process is indeed a handle to the requested program, not a
* Python interpreter. Ordinarily, this does not matter, but it may be useful to know when
* debugging. Furthermore, if special characters are sent on the master before Python has
* executed the requested program, they may be received by the Python interpreter. For example,
* Ctrl-C might be received by Python by mistake if sent immediately upon spawning a new
* session. Users should send a simple command, e.g., "echo", to confirm that the requested
* program is active before sending special characters.
* *
* @param args the image path and arguments * @param args the image path and arguments
* @param env the environment * @param env the environment
* @return a handle to the subprocess * @return a handle to the subprocess
* @throws IOException * @throws IOException
*/ */
public Process session(String[] args, Map<String, String> env) throws IOException { @Override
public PtySession session(String[] args, Map<String, String> env) throws IOException {
return sessionUsingJavaLeader(args, env); return sessionUsingJavaLeader(args, env);
} }
protected Process sessionUsingJavaLeader(String[] args, Map<String, String> env) protected PtySession sessionUsingJavaLeader(String[] args, Map<String, String> env)
throws IOException { throws IOException {
final List<String> argsList = new ArrayList<>(); final List<String> argsList = new ArrayList<>();
argsList.add("java"); argsList.add("java");
argsList.add("-cp"); argsList.add("-cp");
argsList.add(System.getProperty("java.class.path")); argsList.add(System.getProperty("java.class.path"));
argsList.add(PtySessionLeader.class.getCanonicalName()); argsList.add(LinuxPtySessionLeader.class.getCanonicalName());
argsList.add(file.getAbsolutePath()); argsList.add(name);
argsList.addAll(Arrays.asList(args)); argsList.addAll(Arrays.asList(args));
ProcessBuilder builder = new ProcessBuilder(argsList); ProcessBuilder builder = new ProcessBuilder(argsList);
if (env != null) { if (env != null) {
@ -82,17 +79,17 @@ public class PtySlave extends PtyEndpoint {
} }
builder.inheritIO(); builder.inheritIO();
return builder.start(); return new LocalProcessPtySession(builder.start());
} }
protected Process sessionUsingPythonLeader(String[] args, Map<String, String> env) protected PtySession sessionUsingPythonLeader(String[] args, Map<String, String> env)
throws IOException { throws IOException {
final List<String> argsList = new ArrayList<>(); final List<String> argsList = new ArrayList<>();
argsList.add("python"); argsList.add("python");
argsList.add("-m"); argsList.add("-m");
argsList.add("session"); argsList.add("session");
argsList.add(file.getAbsolutePath()); argsList.add(name);
argsList.addAll(Arrays.asList(args)); argsList.addAll(Arrays.asList(args));
ProcessBuilder builder = new ProcessBuilder(argsList); ProcessBuilder builder = new ProcessBuilder(argsList);
if (env != null) { if (env != null) {
@ -103,12 +100,12 @@ public class PtySlave extends PtyEndpoint {
builder.environment().put("PYTHONPATH", sourceLoc); builder.environment().put("PYTHONPATH", sourceLoc);
builder.inheritIO(); builder.inheritIO();
return builder.start(); return new LocalProcessPtySession(builder.start());
} }
public static File getSourceLocationForResource(String name) { public static File getSourceLocationForResource(String name) {
// TODO: Refactor this with SystemUtilities.getSourceLocationForClass() // TODO: Refactor this with SystemUtilities.getSourceLocationForClass()
URL url = PtySlave.class.getClassLoader().getResource(name); URL url = LinuxPtyChild.class.getClassLoader().getResource(name);
String urlFile = url.getFile(); String urlFile = url.getFile();
try { try {
urlFile = URLDecoder.decode(urlFile, "UTF-8"); urlFile = URLDecoder.decode(urlFile, "UTF-8");

View file

@ -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;
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package agent.gdb.ffi.linux; package agent.gdb.pty.linux;
import java.util.List; import java.util.List;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
@ -21,14 +21,14 @@ import java.util.concurrent.Callable;
import jnr.posix.POSIX; import jnr.posix.POSIX;
import jnr.posix.POSIXFactory; import jnr.posix.POSIXFactory;
public class PtySessionLeader { public class LinuxPtySessionLeader {
private static final POSIX LIB_POSIX = POSIXFactory.getNativePOSIX(); private static final POSIX LIB_POSIX = POSIXFactory.getNativePOSIX();
private static final int O_RDWR = 2; // TODO: Find this in libs private static final int O_RDWR = 2; // TODO: Find this in libs
public static void main(String[] args) throws Exception { public static void main(String[] args) throws Exception {
PtySessionLeader master = new PtySessionLeader(); LinuxPtySessionLeader leader = new LinuxPtySessionLeader();
master.parseArgs(args); leader.parseArgs(args);
master.run(); leader.run();
} }
protected String ptyPath; protected String ptyPath;

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package agent.gdb.ffi.linux; package agent.gdb.pty.linux;
import jnr.ffi.LibraryLoader; import jnr.ffi.LibraryLoader;
import jnr.ffi.Pointer; import jnr.ffi.Pointer;

View file

@ -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();
}
}

View file

@ -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();
}
}
}

View file

@ -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());
}
}

View file

@ -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();
}
}

View file

@ -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");
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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();
}
}

View file

@ -35,6 +35,7 @@ import com.google.common.collect.*;
import agent.gdb.manager.*; import agent.gdb.manager.*;
import agent.gdb.manager.GdbManager.ExecSuffix; import agent.gdb.manager.GdbManager.ExecSuffix;
import agent.gdb.manager.breakpoint.GdbBreakpointInfo; import agent.gdb.manager.breakpoint.GdbBreakpointInfo;
import agent.gdb.pty.PtyFactory;
import ghidra.async.AsyncReference; import ghidra.async.AsyncReference;
import ghidra.dbg.testutil.DummyProc; import ghidra.dbg.testutil.DummyProc;
import ghidra.test.AbstractGhidraHeadlessIntegrationTest; import ghidra.test.AbstractGhidraHeadlessIntegrationTest;
@ -45,6 +46,8 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
protected static final long TIMEOUT_MILLISECONDS = protected static final long TIMEOUT_MILLISECONDS =
SystemUtilities.isInTestingBatchMode() ? 5000 : Long.MAX_VALUE; SystemUtilities.isInTestingBatchMode() ? 5000 : Long.MAX_VALUE;
protected abstract PtyFactory getPtyFactory();
protected abstract CompletableFuture<Void> startManager(GdbManager manager); protected abstract CompletableFuture<Void> startManager(GdbManager manager);
protected void stopManager() throws IOException { protected void stopManager() throws IOException {
@ -67,7 +70,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
@Test @Test
public void testAddInferior() throws Throwable { public void testAddInferior() throws Throwable {
try (GdbManager mgr = GdbManager.newInstance()) { try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
waitOn(startManager(mgr)); waitOn(startManager(mgr));
GdbInferior inferior = waitOn(mgr.addInferior()); GdbInferior inferior = waitOn(mgr.addInferior());
assertEquals(2, inferior.getId()); assertEquals(2, inferior.getId());
@ -77,7 +80,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
@Test @Test
public void testRemoveInferior() throws Throwable { public void testRemoveInferior() throws Throwable {
try (GdbManager mgr = GdbManager.newInstance()) { try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
waitOn(startManager(mgr)); waitOn(startManager(mgr));
GdbInferior inf = waitOn(mgr.addInferior()); GdbInferior inf = waitOn(mgr.addInferior());
assertEquals(2, mgr.getKnownInferiors().size()); assertEquals(2, mgr.getKnownInferiors().size());
@ -90,7 +93,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
@Test @Test
public void testRemoveCurrentInferior() throws Throwable { public void testRemoveCurrentInferior() throws Throwable {
try (GdbManager mgr = GdbManager.newInstance()) { try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
List<Integer> selEvtIdsTemp = new ArrayList<>(); List<Integer> selEvtIdsTemp = new ArrayList<>();
AsyncReference<List<Integer>, Void> selEvtIds = new AsyncReference<>(List.of()); AsyncReference<List<Integer>, Void> selEvtIds = new AsyncReference<>(List.of());
mgr.addEventsListener(new GdbEventsListenerAdapter() { mgr.addEventsListener(new GdbEventsListenerAdapter() {
@ -114,7 +117,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
@Test @Test
public void testConsoleCapture() throws Throwable { public void testConsoleCapture() throws Throwable {
try (GdbManager mgr = GdbManager.newInstance()) { try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
waitOn(startManager(mgr)); waitOn(startManager(mgr));
String out = waitOn(mgr.consoleCapture("echo test")); String out = waitOn(mgr.consoleCapture("echo test"));
assertEquals("test", out.trim()); assertEquals("test", out.trim());
@ -123,7 +126,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
@Test @Test
public void testListInferiors() throws Throwable { public void testListInferiors() throws Throwable {
try (GdbManager mgr = GdbManager.newInstance()) { try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
waitOn(startManager(mgr)); waitOn(startManager(mgr));
Map<Integer, GdbInferior> inferiors = waitOn(mgr.listInferiors()); Map<Integer, GdbInferior> inferiors = waitOn(mgr.listInferiors());
assertEquals(new HashSet<>(Arrays.asList(new Integer[] { 1 })), inferiors.keySet()); assertEquals(new HashSet<>(Arrays.asList(new Integer[] { 1 })), inferiors.keySet());
@ -132,7 +135,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
@Test @Test
public void testListAvailableProcesses() throws Throwable { public void testListAvailableProcesses() throws Throwable {
try (GdbManager mgr = GdbManager.newInstance()) { try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
waitOn(startManager(mgr)); waitOn(startManager(mgr));
List<GdbProcessThreadGroup> procs = waitOn(mgr.listAvailableProcesses()); List<GdbProcessThreadGroup> procs = waitOn(mgr.listAvailableProcesses());
List<Integer> pids = procs.stream().map(p -> p.getPid()).collect(Collectors.toList()); List<Integer> pids = procs.stream().map(p -> p.getPid()).collect(Collectors.toList());
@ -142,7 +145,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
@Test @Test
public void testInfoOs() throws Throwable { public void testInfoOs() throws Throwable {
try (GdbManager mgr = GdbManager.newInstance()) { try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
waitOn(startManager(mgr)); waitOn(startManager(mgr));
GdbTable infoThreads = waitOn(mgr.infoOs("threads")); GdbTable infoThreads = waitOn(mgr.infoOs("threads"));
assertEquals(new LinkedHashSet<>(Arrays.asList("pid", "command", "tid", "core")), assertEquals(new LinkedHashSet<>(Arrays.asList("pid", "command", "tid", "core")),
@ -153,7 +156,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
@Test @Test
public void testStart() throws Throwable { public void testStart() throws Throwable {
try (GdbManager mgr = GdbManager.newInstance()) { try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
waitOn(startManager(mgr)); waitOn(startManager(mgr));
waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo")); waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo"));
waitOn(mgr.console("break main")); waitOn(mgr.console("break main"));
@ -164,7 +167,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
@Test @Test
public void testAttachDetach() throws Throwable { public void testAttachDetach() throws Throwable {
try (DummyProc echo = run("dd"); GdbManager mgr = GdbManager.newInstance()) { try (DummyProc echo = run("dd"); GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
waitOn(startManager(mgr)); waitOn(startManager(mgr));
Set<GdbThread> threads = waitOn(mgr.currentInferior().attach(echo.pid)); Set<GdbThread> threads = waitOn(mgr.currentInferior().attach(echo.pid));
// Attach stops the process, so no need to wait for STOPPED or prompt // Attach stops the process, so no need to wait for STOPPED or prompt
@ -212,7 +215,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
public void testStartInterrupt() throws Throwable { public void testStartInterrupt() throws Throwable {
assumeFalse("I know no way to get this to pass with these conditions", assumeFalse("I know no way to get this to pass with these conditions",
this instanceof JoinedGdbManagerTest); this instanceof JoinedGdbManagerTest);
try (GdbManager mgr = GdbManager.newInstance()) { try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
/* /*
* Not sure the details here, but it seems GDB will give ^running as soon as the process * Not sure the details here, but it seems GDB will give ^running as soon as the process
* has started. I suspect there are some nuances between the time the process is started * has started. I suspect there are some nuances between the time the process is started
@ -239,7 +242,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
assumeFalse("I know no way to get this to pass with these conditions", assumeFalse("I know no way to get this to pass with these conditions",
this instanceof JoinedGdbManagerTest); this instanceof JoinedGdbManagerTest);
// Repeat the start-interrupt sequence, then verify we're preparing to step a syscall // Repeat the start-interrupt sequence, then verify we're preparing to step a syscall
try (GdbManager mgr = GdbManager.newInstance()) { try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
LibraryWaiter libcLoaded = new LibraryWaiter(name -> name.contains("libc")); LibraryWaiter libcLoaded = new LibraryWaiter(name -> name.contains("libc"));
mgr.addEventsListener(libcLoaded); mgr.addEventsListener(libcLoaded);
waitOn(startManager(mgr)); waitOn(startManager(mgr));
@ -268,7 +271,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
@Test @Test
public void testSetVarEvaluate() throws Throwable { public void testSetVarEvaluate() throws Throwable {
try (GdbManager mgr = GdbManager.newInstance()) { try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
waitOn(startManager(mgr)); waitOn(startManager(mgr));
waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo")); waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo"));
waitOn(mgr.insertBreakpoint("main")); waitOn(mgr.insertBreakpoint("main"));
@ -283,7 +286,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
@Test @Test
public void testSetVarGetVar() throws Throwable { public void testSetVarGetVar() throws Throwable {
try (GdbManager mgr = GdbManager.newInstance()) { try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
waitOn(startManager(mgr)); waitOn(startManager(mgr));
String val = waitOn(mgr.currentInferior().getVar("args")); String val = waitOn(mgr.currentInferior().getVar("args"));
assertEquals(null, val); assertEquals(null, val);
@ -295,7 +298,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
@Test @Test
public void testInsertListDeleteBreakpoint() throws Throwable { public void testInsertListDeleteBreakpoint() throws Throwable {
try (GdbManager mgr = GdbManager.newInstance()) { try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
waitOn(startManager(mgr)); waitOn(startManager(mgr));
waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo")); waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo"));
GdbBreakpointInfo breakpoint = waitOn(mgr.insertBreakpoint("main")); GdbBreakpointInfo breakpoint = waitOn(mgr.insertBreakpoint("main"));
@ -309,7 +312,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
@Test @Test
public void testListReadWriteReadRegisters() throws Throwable { public void testListReadWriteReadRegisters() throws Throwable {
try (GdbManager mgr = GdbManager.newInstance()) { try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
waitOn(startManager(mgr)); waitOn(startManager(mgr));
waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo")); waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo"));
waitOn(mgr.insertBreakpoint("main")); waitOn(mgr.insertBreakpoint("main"));
@ -345,7 +348,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
@Test @Test
public void testWriteReadMemory() throws Throwable { public void testWriteReadMemory() throws Throwable {
ByteBuffer rBuf = ByteBuffer.allocate(1024); ByteBuffer rBuf = ByteBuffer.allocate(1024);
try (GdbManager mgr = GdbManager.newInstance()) { try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
waitOn(startManager(mgr)); waitOn(startManager(mgr));
waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo")); waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo"));
waitOn(mgr.insertBreakpoint("main")); waitOn(mgr.insertBreakpoint("main"));
@ -375,7 +378,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
@Test @Test
public void testContinue() throws Throwable { public void testContinue() throws Throwable {
try (GdbManager mgr = GdbManager.newInstance()) { try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
waitOn(startManager(mgr)); waitOn(startManager(mgr));
waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo")); waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo"));
waitOn(mgr.insertBreakpoint("main")); waitOn(mgr.insertBreakpoint("main"));
@ -390,7 +393,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
@Test @Test
public void testStep() throws Throwable { public void testStep() throws Throwable {
try (GdbManager mgr = GdbManager.newInstance()) { try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
waitOn(startManager(mgr)); waitOn(startManager(mgr));
waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo")); waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo"));
waitOn(mgr.insertBreakpoint("main")); waitOn(mgr.insertBreakpoint("main"));
@ -405,7 +408,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
@Test @Test
public void testThreadSelect() throws Throwable { public void testThreadSelect() throws Throwable {
try (GdbManager mgr = GdbManager.newInstance()) { try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
waitOn(startManager(mgr)); waitOn(startManager(mgr));
waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo")); waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo"));
waitOn(mgr.insertBreakpoint("main")); waitOn(mgr.insertBreakpoint("main"));
@ -418,7 +421,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
@Test @Test
public void testListFrames() throws Throwable { public void testListFrames() throws Throwable {
try (GdbManager mgr = GdbManager.newInstance()) { try (GdbManager mgr = GdbManager.newInstance(getPtyFactory())) {
waitOn(startManager(mgr)); waitOn(startManager(mgr));
waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo")); waitOn(mgr.currentInferior().fileExecAndSymbols("/usr/bin/echo"));
waitOn(mgr.insertBreakpoint("main")); waitOn(mgr.insertBreakpoint("main"));

View file

@ -21,8 +21,11 @@ import java.util.concurrent.CompletableFuture;
import org.junit.Ignore; import org.junit.Ignore;
import agent.gdb.ffi.linux.Pty;
import agent.gdb.manager.GdbManager; import agent.gdb.manager.GdbManager;
import agent.gdb.pty.PtyFactory;
import agent.gdb.pty.PtySession;
import agent.gdb.pty.linux.LinuxPty;
import agent.gdb.pty.linux.LinuxPtyFactory;
import ghidra.util.Msg; import ghidra.util.Msg;
@Ignore("Need compatible GDB version for CI") @Ignore("Need compatible GDB version for CI")
@ -31,7 +34,7 @@ public class JoinedGdbManagerTest extends AbstractGdbManagerTest {
@Override @Override
public void run() { public void run() {
BufferedReader reader = BufferedReader reader =
new BufferedReader(new InputStreamReader(ptyUserGdb.getMaster().getInputStream())); new BufferedReader(new InputStreamReader(ptyUserGdb.getParent().getInputStream()));
String line; String line;
try { try {
while (gdb != null && null != (line = reader.readLine())) { while (gdb != null && null != (line = reader.readLine())) {
@ -44,20 +47,26 @@ public class JoinedGdbManagerTest extends AbstractGdbManagerTest {
} }
} }
protected Pty ptyUserGdb; protected LinuxPty ptyUserGdb;
protected Process gdb; protected PtySession gdb;
@Override
protected PtyFactory getPtyFactory() {
// TODO: Choose by host OS
return new LinuxPtyFactory();
}
@Override @Override
protected CompletableFuture<Void> startManager(GdbManager manager) { protected CompletableFuture<Void> startManager(GdbManager manager) {
try { try {
ptyUserGdb = Pty.openpty(); ptyUserGdb = LinuxPty.openpty();
manager.start(null); manager.start(null);
Msg.debug(this, "Starting GDB and invoking new-ui mi2 " + manager.getMi2PtyName()); Msg.debug(this, "Starting GDB and invoking new-ui mi2 " + manager.getMi2PtyName());
gdb = ptyUserGdb.getSlave() gdb = ptyUserGdb.getChild()
.session(new String[] { GdbManager.DEFAULT_GDB_CMD }, Map.of()); .session(new String[] { GdbManager.DEFAULT_GDB_CMD }, Map.of());
new ReaderThread().start(); new ReaderThread().start();
PrintWriter gdbCmd = new PrintWriter(ptyUserGdb.getMaster().getOutputStream()); PrintWriter gdbCmd = new PrintWriter(ptyUserGdb.getParent().getOutputStream());
gdbCmd.println("new-ui mi2 " + manager.getMi2PtyName()); gdbCmd.println("new-ui mi2 " + manager.getMi2PtyName());
gdbCmd.flush(); gdbCmd.flush();
return manager.runRC(); return manager.runRC();

View file

@ -21,6 +21,8 @@ import java.util.concurrent.CompletableFuture;
import org.junit.Ignore; import org.junit.Ignore;
import agent.gdb.manager.GdbManager; import agent.gdb.manager.GdbManager;
import agent.gdb.pty.PtyFactory;
import agent.gdb.pty.linux.LinuxPtyFactory;
@Ignore("Need compatible GDB version for CI") @Ignore("Need compatible GDB version for CI")
public class SpawnedCliGdbManagerTest extends AbstractGdbManagerTest { public class SpawnedCliGdbManagerTest extends AbstractGdbManagerTest {
@ -34,4 +36,10 @@ public class SpawnedCliGdbManagerTest extends AbstractGdbManagerTest {
throw new AssertionError(e); throw new AssertionError(e);
} }
} }
@Override
protected PtyFactory getPtyFactory() {
// TODO: Choose by host OS
return new LinuxPtyFactory();
}
} }

View file

@ -21,6 +21,8 @@ import java.util.concurrent.CompletableFuture;
import org.junit.Ignore; import org.junit.Ignore;
import agent.gdb.manager.GdbManager; import agent.gdb.manager.GdbManager;
import agent.gdb.pty.PtyFactory;
import agent.gdb.pty.linux.LinuxPtyFactory;
@Ignore("Need to install GDB 7.6.1 to the expected directory on CI") @Ignore("Need to install GDB 7.6.1 to the expected directory on CI")
public class SpawnedMi2Gdb7Dot6Dot1ManagerTest extends AbstractGdbManagerTest { public class SpawnedMi2Gdb7Dot6Dot1ManagerTest extends AbstractGdbManagerTest {
@ -34,4 +36,10 @@ public class SpawnedMi2Gdb7Dot6Dot1ManagerTest extends AbstractGdbManagerTest {
throw new AssertionError(e); throw new AssertionError(e);
} }
} }
@Override
protected PtyFactory getPtyFactory() {
// TODO: Choose by host OS
return new LinuxPtyFactory();
}
} }

View file

@ -21,6 +21,8 @@ import java.util.concurrent.CompletableFuture;
import org.junit.Ignore; import org.junit.Ignore;
import agent.gdb.manager.GdbManager; import agent.gdb.manager.GdbManager;
import agent.gdb.pty.PtyFactory;
import agent.gdb.pty.linux.LinuxPtyFactory;
@Ignore("Need compatible GDB version for CI") @Ignore("Need compatible GDB version for CI")
public class SpawnedMi2GdbManagerTest2 extends AbstractGdbManagerTest { public class SpawnedMi2GdbManagerTest2 extends AbstractGdbManagerTest {
@ -34,4 +36,10 @@ public class SpawnedMi2GdbManagerTest2 extends AbstractGdbManagerTest {
throw new AssertionError(e); throw new AssertionError(e);
} }
} }
@Override
protected PtyFactory getPtyFactory() {
// TODO: Choose by host OS
return new LinuxPtyFactory();
}
} }

View file

@ -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);
}
}
}

View file

@ -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();
}
}

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package agent.gdb.ffi.linux; package agent.gdb.pty.linux;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
@ -23,21 +23,22 @@ import java.util.*;
import org.junit.Test; import org.junit.Test;
import agent.gdb.pty.PtySession;
import ghidra.dbg.testutil.DummyProc; import ghidra.dbg.testutil.DummyProc;
public class PtyTest { public class LinuxPtyTest {
@Test @Test
public void testOpenClosePty() throws IOException { public void testOpenClosePty() throws IOException {
Pty pty = Pty.openpty(); LinuxPty pty = LinuxPty.openpty();
pty.close(); pty.close();
} }
@Test @Test
public void testMasterToSlave() throws IOException { public void testParentToChild() throws IOException {
try (Pty pty = Pty.openpty()) { try (LinuxPty pty = LinuxPty.openpty()) {
PrintWriter writer = new PrintWriter(pty.getMaster().getOutputStream()); PrintWriter writer = new PrintWriter(pty.getParent().getOutputStream());
BufferedReader reader = BufferedReader reader =
new BufferedReader(new InputStreamReader(pty.getSlave().getInputStream())); new BufferedReader(new InputStreamReader(pty.getChild().getInputStream()));
writer.println("Hello, World!"); writer.println("Hello, World!");
writer.flush(); writer.flush();
@ -46,11 +47,11 @@ public class PtyTest {
} }
@Test @Test
public void testSlaveToMaster() throws IOException { public void testChildToParent() throws IOException {
try (Pty pty = Pty.openpty()) { try (LinuxPty pty = LinuxPty.openpty()) {
PrintWriter writer = new PrintWriter(pty.getSlave().getOutputStream()); PrintWriter writer = new PrintWriter(pty.getChild().getOutputStream());
BufferedReader reader = BufferedReader reader =
new BufferedReader(new InputStreamReader(pty.getMaster().getInputStream())); new BufferedReader(new InputStreamReader(pty.getParent().getInputStream()));
writer.println("Hello, World!"); writer.println("Hello, World!");
writer.flush(); writer.flush();
@ -60,22 +61,24 @@ public class PtyTest {
@Test @Test
public void testSessionBash() throws IOException, InterruptedException { public void testSessionBash() throws IOException, InterruptedException {
try (Pty pty = Pty.openpty()) { try (LinuxPty pty = LinuxPty.openpty()) {
Process bash = pty.getSlave().session(new String[] { DummyProc.which("bash") }, null); PtySession bash =
pty.getMaster().getOutputStream().write("exit\n".getBytes()); pty.getChild().session(new String[] { DummyProc.which("bash") }, null);
assertEquals(0, bash.waitFor()); pty.getParent().getOutputStream().write("exit\n".getBytes());
assertEquals(0, bash.waitExited().intValue());
} }
} }
@Test @Test
public void testForkIntoNonExistent() throws IOException, InterruptedException { public void testForkIntoNonExistent() throws IOException, InterruptedException {
try (Pty pty = Pty.openpty()) { try (LinuxPty pty = LinuxPty.openpty()) {
Process dies = pty.getSlave().session(new String[] { "thisHadBetterNotExist" }, null); PtySession dies =
pty.getChild().session(new String[] { "thisHadBetterNotExist" }, null);
/** /**
* NOTE: Java subprocess dies with code 1 on unhandled exception. TODO: Is there a nice * NOTE: Java subprocess dies with code 1 on unhandled exception. TODO: Is there a nice
* way to distinguish whether the code is from java or the execed image? * way to distinguish whether the code is from java or the execed image?
*/ */
assertEquals(1, dies.waitFor()); assertEquals(1, dies.waitExited().intValue());
} }
} }
@ -109,11 +112,12 @@ public class PtyTest {
}; };
} }
public Thread runExitCheck(int expected, Process proc) { public Thread runExitCheck(int expected, PtySession session) {
Thread exitCheck = new Thread(() -> { Thread exitCheck = new Thread(() -> {
while (true) { while (true) {
try { try {
assertEquals("Early exit with wrong code", expected, proc.waitFor()); assertEquals("Early exit with wrong code", expected,
session.waitExited().intValue());
return; return;
} }
catch (InterruptedException e) { catch (InterruptedException e) {
@ -132,12 +136,12 @@ public class PtyTest {
env.put("PS1", "BASH:"); env.put("PS1", "BASH:");
env.put("PROMPT_COMMAND", ""); env.put("PROMPT_COMMAND", "");
env.put("TERM", ""); env.put("TERM", "");
try (Pty pty = Pty.openpty()) { try (LinuxPty pty = LinuxPty.openpty()) {
PtyMaster master = pty.getMaster(); LinuxPtyParent parent = pty.getParent();
PrintWriter writer = new PrintWriter(master.getOutputStream()); PrintWriter writer = new PrintWriter(parent.getOutputStream());
BufferedReader reader = loggingReader(master.getInputStream()); BufferedReader reader = loggingReader(parent.getInputStream());
Process bash = PtySession bash =
pty.getSlave().session(new String[] { DummyProc.which("bash"), "--norc" }, env); pty.getChild().session(new String[] { DummyProc.which("bash"), "--norc" }, env);
runExitCheck(3, bash); runExitCheck(3, bash);
writer.println("echo test"); writer.println("echo test");
@ -155,7 +159,7 @@ public class PtyTest {
assertTrue("Not 'exit 3' or 'BASH:exit 3': '" + line + "'", assertTrue("Not 'exit 3' or 'BASH:exit 3': '" + line + "'",
Set.of("BASH:exit 3", "exit 3").contains(line)); Set.of("BASH:exit 3", "exit 3").contains(line));
assertEquals(3, bash.waitFor()); assertEquals(3, bash.waitExited().intValue());
} }
} }
@ -165,12 +169,12 @@ public class PtyTest {
env.put("PS1", "BASH:"); env.put("PS1", "BASH:");
env.put("PROMPT_COMMAND", ""); env.put("PROMPT_COMMAND", "");
env.put("TERM", ""); env.put("TERM", "");
try (Pty pty = Pty.openpty()) { try (LinuxPty pty = LinuxPty.openpty()) {
PtyMaster master = pty.getMaster(); LinuxPtyParent parent = pty.getParent();
PrintWriter writer = new PrintWriter(master.getOutputStream()); PrintWriter writer = new PrintWriter(parent.getOutputStream());
BufferedReader reader = loggingReader(master.getInputStream()); BufferedReader reader = loggingReader(parent.getInputStream());
Process bash = PtySession bash =
pty.getSlave().session(new String[] { DummyProc.which("bash"), "--norc" }, env); pty.getChild().session(new String[] { DummyProc.which("bash"), "--norc" }, env);
runExitCheck(3, bash); runExitCheck(3, bash);
writer.println("echo test"); writer.println("echo test");
@ -210,7 +214,7 @@ public class PtyTest {
writer.flush(); writer.flush();
assertTrue(Set.of("BASH:exit 3", "exit 3").contains(reader.readLine())); assertTrue(Set.of("BASH:exit 3", "exit 3").contains(reader.readLine()));
assertEquals(3, bash.waitFor()); assertEquals(3, bash.waitExited().intValue());
} }
} }
} }

View file

@ -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");
}
}

View file

@ -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());
}
}
}

View file

@ -31,7 +31,7 @@ import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind;
import ghidra.dbg.target.TargetEventScope.TargetEventType; import ghidra.dbg.target.TargetEventScope.TargetEventType;
import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState;
import ghidra.dbg.testutil.*; import ghidra.dbg.testutil.*;
import ghidra.test.AbstractGhidraHeadlessIntegrationTest; import ghidra.test.AbstractGhidraHeadedIntegrationTest;
import ghidra.util.Msg; import ghidra.util.Msg;
/** /**
@ -41,7 +41,7 @@ import ghidra.util.Msg;
* <li>TODO: ensure registersUpdated(RegisterBank) immediately upon created(RegisterBank) ?</li> * <li>TODO: ensure registersUpdated(RegisterBank) immediately upon created(RegisterBank) ?</li>
* </ul> * </ul>
*/ */
public abstract class AbstractDebuggerModelTest extends AbstractGhidraHeadlessIntegrationTest public abstract class AbstractDebuggerModelTest extends AbstractGhidraHeadedIntegrationTest
implements TestDebuggerModelProvider, DebuggerModelTestUtils { implements TestDebuggerModelProvider, DebuggerModelTestUtils {
protected DummyProc dummy; protected DummyProc dummy;