From 4d710ce2bc49d2860b73bc2d040eafba647753f3 Mon Sep 17 00:00:00 2001 From: Dan <46821332+nsadeveloper789@users.noreply.github.com> Date: Fri, 16 Apr 2021 15:41:58 -0400 Subject: [PATCH] GP-568: Factored pty interfaces, change terms, implements GDB over SSH --- .../gdb/GdbInJvmDebuggerModelFactory.java | 8 +- .../gdb/GdbOverSshDebuggerModelFactory.java | 130 ++++++++++++ .../main/java/agent/gdb/ffi/linux/Pty.java | 153 -------------- .../gdb/gadp/impl/GdbGadpServerImpl.java | 4 +- .../java/agent/gdb/manager/GdbManager.java | 17 +- .../gdb/manager/impl/GdbManagerImpl.java | 46 +++-- .../agent/gdb/model/impl/GdbModelImpl.java | 5 +- .../src/main/java/agent/gdb/pty/Pty.java | 100 +++++++++ .../src/main/java/agent/gdb/pty/PtyChild.java | 56 +++++ .../gdb/{ffi/linux => pty}/PtyEndpoint.java | 25 +-- .../main/java/agent/gdb/pty/PtyFactory.java | 32 +++ .../PtyMaster.java => pty/PtyParent.java} | 9 +- .../main/java/agent/gdb/pty/PtySession.java | 43 ++++ .../gdb/{ffi => pty}/linux/FdInputStream.java | 8 +- .../{ffi => pty}/linux/FdOutputStream.java | 8 +- .../java/agent/gdb/pty/linux/LinuxPty.java | 87 ++++++++ .../linux/LinuxPtyChild.java} | 75 ++++--- .../agent/gdb/pty/linux/LinuxPtyEndpoint.java | 43 ++++ .../agent/gdb/pty/linux/LinuxPtyFactory.java | 28 +++ .../agent/gdb/pty/linux/LinuxPtyParent.java | 24 +++ .../linux/LinuxPtySessionLeader.java} | 10 +- .../agent/gdb/{ffi => pty}/linux/Util.java | 2 +- .../gdb/pty/local/LocalProcessPtySession.java | 39 ++++ .../gdb/pty/ssh/GhidraSshHostKeyVerifier.java | 54 +++++ .../gdb/pty/ssh/GhidraSshPtyFactory.java | 134 ++++++++++++ .../main/java/agent/gdb/pty/ssh/SshPty.java | 46 +++++ .../java/agent/gdb/pty/ssh/SshPtyChild.java | 96 +++++++++ .../agent/gdb/pty/ssh/SshPtyEndpoint.java | 42 ++++ .../java/agent/gdb/pty/ssh/SshPtyParent.java | 27 +++ .../java/agent/gdb/pty/ssh/SshPtySession.java | 57 +++++ .../manager/impl/AbstractGdbManagerTest.java | 43 ++-- .../manager/impl/JoinedGdbManagerTest.java | 23 ++- .../impl/SpawnedCliGdbManagerTest.java | 8 + .../SpawnedMi2Gdb7Dot6Dot1ManagerTest.java | 8 + .../impl/SpawnedMi2GdbManagerTest2.java | 8 + .../agent/gdb/model/ssh/SshGdbModelHost.java | 42 ++++ .../model/ssh/SshModelForGdbFactoryTest.java | 36 ++++ .../linux/LinuxPtyTest.java} | 72 ++++--- .../agent/gdb/pty/ssh/SshExperimentsTest.java | 195 ++++++++++++++++++ .../java/agent/gdb/pty/ssh/SshPtyTest.java | 60 ++++++ .../dbg/test/AbstractDebuggerModelTest.java | 4 +- 41 files changed, 1588 insertions(+), 319 deletions(-) create mode 100644 Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/GdbOverSshDebuggerModelFactory.java delete mode 100644 Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/ffi/linux/Pty.java create mode 100644 Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/Pty.java create mode 100644 Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/PtyChild.java rename Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/{ffi/linux => pty}/PtyEndpoint.java (74%) create mode 100644 Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/PtyFactory.java rename Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/{ffi/linux/PtyMaster.java => pty/PtyParent.java} (79%) create mode 100644 Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/PtySession.java rename Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/{ffi => pty}/linux/FdInputStream.java (88%) rename Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/{ffi => pty}/linux/FdOutputStream.java (88%) create mode 100644 Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/LinuxPty.java rename Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/{ffi/linux/PtySlave.java => pty/linux/LinuxPtyChild.java} (59%) create mode 100644 Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/LinuxPtyEndpoint.java create mode 100644 Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/LinuxPtyFactory.java create mode 100644 Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/LinuxPtyParent.java rename Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/{ffi/linux/PtySessionLeader.java => pty/linux/LinuxPtySessionLeader.java} (93%) rename Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/{ffi => pty}/linux/Util.java (97%) create mode 100644 Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/local/LocalProcessPtySession.java create mode 100644 Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/GhidraSshHostKeyVerifier.java create mode 100644 Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/GhidraSshPtyFactory.java create mode 100644 Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPty.java create mode 100644 Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPtyChild.java create mode 100644 Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPtyEndpoint.java create mode 100644 Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPtyParent.java create mode 100644 Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPtySession.java create mode 100644 Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/ssh/SshGdbModelHost.java create mode 100644 Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/ssh/SshModelForGdbFactoryTest.java rename Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/{ffi/linux/PtyTest.java => pty/linux/LinuxPtyTest.java} (69%) create mode 100644 Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/pty/ssh/SshExperimentsTest.java create mode 100644 Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/pty/ssh/SshPtyTest.java diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/GdbInJvmDebuggerModelFactory.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/GdbInJvmDebuggerModelFactory.java index 2c690a30e3..e13ca8de2d 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/GdbInJvmDebuggerModelFactory.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/GdbInJvmDebuggerModelFactory.java @@ -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 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); } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/GdbOverSshDebuggerModelFactory.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/GdbOverSshDebuggerModelFactory.java new file mode 100644 index 0000000000..8c990af2fc --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/GdbOverSshDebuggerModelFactory.java @@ -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 gdbCommandOption = + Property.fromAccessors(String.class, this::getGdbCommand, this::setGdbCommand); + + private boolean existing = false; + @FactoryOption("Use existing session via new-ui") + public final Property useExistingOption = + Property.fromAccessors(boolean.class, this::isUseExisting, this::setUseExisting); + + private String hostname = "localhost"; + @FactoryOption("SSH hostname") + public final Property hostnameOption = + Property.fromAccessors(String.class, this::getHostname, this::setHostname); + + private int port = 22; + @FactoryOption("SSH TCP port") + public final Property portOption = + Property.fromAccessors(Integer.class, this::getPort, this::setPort); + + private String username = "user"; + @FactoryOption("SSH username") + public final Property usernameOption = + Property.fromAccessors(String.class, this::getUsername, this::setUsername); + + private String keyFile = ""; + @FactoryOption("SSH identity (blank for password auth)") + public final Property keyFileOption = + Property.fromAccessors(String.class, this::getKeyFile, this::setKeyFile); + + @Override + public CompletableFuture 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; + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/ffi/linux/Pty.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/ffi/linux/Pty.java deleted file mode 100644 index 32cf1d146b..0000000000 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/ffi/linux/Pty.java +++ /dev/null @@ -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 line discipline. 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: - * - *
- * 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();
- * 
- */ -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; - } -} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/gadp/impl/GdbGadpServerImpl.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/gadp/impl/GdbGadpServerImpl.java index 28657278d5..caa1a8f802 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/gadp/impl/GdbGadpServerImpl.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/gadp/impl/GdbGadpServerImpl.java @@ -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); } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/GdbManager.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/GdbManager.java index ccf8ec9522..f21d1d5a31 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/GdbManager.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/GdbManager.java @@ -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; } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/GdbManagerImpl.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/GdbManagerImpl.java index 7ec5249333..6bc5ac652f 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/GdbManagerImpl.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/GdbManagerImpl.java @@ -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 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, 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() { diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelImpl.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelImpl.java index 35c7b035a8..b3ac42854d 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelImpl.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelImpl.java @@ -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 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); diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/Pty.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/Pty.java new file mode 100644 index 0000000000..a1f1c1e1f9 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/Pty.java @@ -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 + * + *

+ * 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. + * + *

+ * 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 line discipline. 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 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. + * + *

+ * 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. + * + *

+ * Example: + * + *

+ * 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();
+ * 
+ */ +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 + * + *

+ * 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; +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/PtyChild.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/PtyChild.java new file mode 100644 index 0000000000..83bd674c7f --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/PtyChild.java @@ -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 + * + *

+ * 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 env) throws IOException; + + /** + * Start a session without a real leader, instead obtaining the pty's name + * + *

+ * 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. + * + *

+ * 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; +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/ffi/linux/PtyEndpoint.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/PtyEndpoint.java similarity index 74% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/ffi/linux/PtyEndpoint.java rename to Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/PtyEndpoint.java index 4ec078b624..db782c5b0d 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/ffi/linux/PtyEndpoint.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/PtyEndpoint.java @@ -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 * + *

* 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 * + *

* 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(); } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/PtyFactory.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/PtyFactory.java new file mode 100644 index 0000000000..67cfd7fb89 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/PtyFactory.java @@ -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; +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/ffi/linux/PtyMaster.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/PtyParent.java similarity index 79% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/ffi/linux/PtyMaster.java rename to Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/PtyParent.java index 3914936c34..58fee345d5 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/ffi/linux/PtyMaster.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/PtyParent.java @@ -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 { } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/PtySession.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/PtySession.java new file mode 100644 index 0000000000..e24a04c8ec --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/PtySession.java @@ -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 + * + *

+ * 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) + * + *

+ * 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(); +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/ffi/linux/FdInputStream.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/FdInputStream.java similarity index 88% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/ffi/linux/FdInputStream.java rename to Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/FdInputStream.java index df359b469f..ef1c5f5fcc 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/ffi/linux/FdInputStream.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/FdInputStream.java @@ -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. + *

+ * 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. */ public class FdInputStream extends InputStream { private static final POSIX LIB_POSIX = POSIXFactory.getNativePOSIX(); diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/ffi/linux/FdOutputStream.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/FdOutputStream.java similarity index 88% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/ffi/linux/FdOutputStream.java rename to Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/FdOutputStream.java index 2b35ec0b40..e458397a20 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/ffi/linux/FdOutputStream.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/FdOutputStream.java @@ -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. + *

+ * 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. */ public class FdOutputStream extends OutputStream { private static final POSIX LIB_POSIX = POSIXFactory.getNativePOSIX(); diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/LinuxPty.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/LinuxPty.java new file mode 100644 index 0000000000..785384b726 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/LinuxPty.java @@ -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; + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/ffi/linux/PtySlave.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/LinuxPtyChild.java similarity index 59% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/ffi/linux/PtySlave.java rename to Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/LinuxPtyChild.java index 2172404e67..dfee448b27 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/ffi/linux/PtySlave.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/LinuxPtyChild.java @@ -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 env) throws IOException { + @Override + public PtySession session(String[] args, Map env) throws IOException { return sessionUsingJavaLeader(args, env); } - protected Process sessionUsingJavaLeader(String[] args, Map env) + protected PtySession sessionUsingJavaLeader(String[] args, Map env) throws IOException { final List 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 env) + protected PtySession sessionUsingPythonLeader(String[] args, Map env) throws IOException { final List 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"); diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/LinuxPtyEndpoint.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/LinuxPtyEndpoint.java new file mode 100644 index 0000000000..39413b8fbb --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/LinuxPtyEndpoint.java @@ -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; + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/LinuxPtyFactory.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/LinuxPtyFactory.java new file mode 100644 index 0000000000..d5ddfb1ec6 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/LinuxPtyFactory.java @@ -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(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/LinuxPtyParent.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/LinuxPtyParent.java new file mode 100644 index 0000000000..86604373c4 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/LinuxPtyParent.java @@ -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); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/ffi/linux/PtySessionLeader.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/LinuxPtySessionLeader.java similarity index 93% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/ffi/linux/PtySessionLeader.java rename to Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/LinuxPtySessionLeader.java index f52c8f373c..695e50c30d 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/ffi/linux/PtySessionLeader.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/LinuxPtySessionLeader.java @@ -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; diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/ffi/linux/Util.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/Util.java similarity index 97% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/ffi/linux/Util.java rename to Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/Util.java index f19e799ebf..1a27479c69 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/ffi/linux/Util.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/Util.java @@ -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; diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/local/LocalProcessPtySession.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/local/LocalProcessPtySession.java new file mode 100644 index 0000000000..70b56fe51b --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/local/LocalProcessPtySession.java @@ -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(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/GhidraSshHostKeyVerifier.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/GhidraSshHostKeyVerifier.java new file mode 100644 index 0000000000..c49b67c1a6 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/GhidraSshHostKeyVerifier.java @@ -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", + "The server " + hostname + " is not known. " + + "It is highly recommended you log in to the server using a standard " + + "SSH client to confirm the host key first.

" + + "Do you want to continue?"); + return response == OptionDialog.YES_OPTION; + case KnownHosts.HOSTKEY_HAS_CHANGED: + Msg.showError(this, null, "SSH Server Host Key Changed", + "The server " + hostname + " has a different key than before!" + + "Use a standard SSH client to resolve the issue."); + return false; + default: + throw new IllegalStateException(); + } + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/GhidraSshPtyFactory.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/GhidraSshPtyFactory.java new file mode 100644 index 0000000000..576d2fc064 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/GhidraSshPtyFactory.java @@ -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()); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPty.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPty.java new file mode 100644 index 0000000000..acf05aa5dc --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPty.java @@ -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(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPtyChild.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPtyChild.java new file mode 100644 index 0000000000..aaebf53782 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPtyChild.java @@ -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 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"); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPtyEndpoint.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPtyEndpoint.java new file mode 100644 index 0000000000..b308bf2af8 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPtyEndpoint.java @@ -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; + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPtyParent.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPtyParent.java new file mode 100644 index 0000000000..ef44f169b0 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPtyParent.java @@ -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); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPtySession.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPtySession.java new file mode 100644 index 0000000000..050cc29903 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPtySession.java @@ -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(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/manager/impl/AbstractGdbManagerTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/manager/impl/AbstractGdbManagerTest.java index 806457f06c..1f2db9f917 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/manager/impl/AbstractGdbManagerTest.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/manager/impl/AbstractGdbManagerTest.java @@ -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 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 selEvtIdsTemp = new ArrayList<>(); AsyncReference, 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 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 procs = waitOn(mgr.listAvailableProcesses()); List 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 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")); diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/manager/impl/JoinedGdbManagerTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/manager/impl/JoinedGdbManagerTest.java index 4d598ebef0..2530118336 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/manager/impl/JoinedGdbManagerTest.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/manager/impl/JoinedGdbManagerTest.java @@ -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 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(); diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/manager/impl/SpawnedCliGdbManagerTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/manager/impl/SpawnedCliGdbManagerTest.java index 3b4100d671..5fd1fe0401 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/manager/impl/SpawnedCliGdbManagerTest.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/manager/impl/SpawnedCliGdbManagerTest.java @@ -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(); + } } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/manager/impl/SpawnedMi2Gdb7Dot6Dot1ManagerTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/manager/impl/SpawnedMi2Gdb7Dot6Dot1ManagerTest.java index e6a36e3047..70eacd6bd4 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/manager/impl/SpawnedMi2Gdb7Dot6Dot1ManagerTest.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/manager/impl/SpawnedMi2Gdb7Dot6Dot1ManagerTest.java @@ -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(); + } } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/manager/impl/SpawnedMi2GdbManagerTest2.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/manager/impl/SpawnedMi2GdbManagerTest2.java index 328d747400..a62411aa28 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/manager/impl/SpawnedMi2GdbManagerTest2.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/manager/impl/SpawnedMi2GdbManagerTest2.java @@ -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(); + } } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/ssh/SshGdbModelHost.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/ssh/SshGdbModelHost.java new file mode 100644 index 0000000000..3091359e86 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/ssh/SshGdbModelHost.java @@ -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 getFactoryOptions() { + try { + return Map.ofEntries(Map.entry("SSH username", SshPtyTest.promptUser())); + } + catch (CancelledException e) { + throw new AssertionError("Cancelled", e); + } + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/ssh/SshModelForGdbFactoryTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/ssh/SshModelForGdbFactoryTest.java new file mode 100644 index 0000000000..7bbaa556ce --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/ssh/SshModelForGdbFactoryTest.java @@ -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(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/ffi/linux/PtyTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/pty/linux/LinuxPtyTest.java similarity index 69% rename from Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/ffi/linux/PtyTest.java rename to Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/pty/linux/LinuxPtyTest.java index 100ad8c12e..46697ccfe8 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/ffi/linux/PtyTest.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/pty/linux/LinuxPtyTest.java @@ -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()); } } } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/pty/ssh/SshExperimentsTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/pty/ssh/SshExperimentsTest.java new file mode 100644 index 0000000000..bdfba163a9 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/pty/ssh/SshExperimentsTest.java @@ -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 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 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"); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/pty/ssh/SshPtyTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/pty/ssh/SshPtyTest.java new file mode 100644 index 0000000000..192cc13221 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/pty/ssh/SshPtyTest.java @@ -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 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()); + } + } +} diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelTest.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelTest.java index 53ffc527ce..b0e2150480 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelTest.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelTest.java @@ -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; *

  • TODO: ensure registersUpdated(RegisterBank) immediately upon created(RegisterBank) ?
  • * */ -public abstract class AbstractDebuggerModelTest extends AbstractGhidraHeadlessIntegrationTest +public abstract class AbstractDebuggerModelTest extends AbstractGhidraHeadedIntegrationTest implements TestDebuggerModelProvider, DebuggerModelTestUtils { protected DummyProc dummy;