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

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

View File

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

View File

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

View File

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

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

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
* limitations under the License.
*/
package agent.gdb.ffi.linux;
package agent.gdb.pty;
/**
* The master end of a pseudo-terminal
* The parent (UNIX "master") end of a pseudo-terminal
*/
public class PtyMaster extends PtyEndpoint {
PtyMaster(int fd) {
super(fd);
}
public interface PtyParent extends PtyEndpoint {
}

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

View File

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

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

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