Merge remote-tracking branch 'origin/GP-1977_Dan_terminalEmulation--SQUASHED'

This commit is contained in:
Ryan Kurtz 2023-09-05 10:48:28 -04:00
commit 09170c9e8b
98 changed files with 7972 additions and 141 deletions

View file

@ -16,6 +16,7 @@
package docking;
import java.awt.event.ActionEvent;
import java.util.List;
import javax.swing.AbstractAction;
import javax.swing.KeyStroke;
@ -24,12 +25,12 @@ import docking.action.DockingActionIf;
import docking.actions.KeyBindingUtils;
/**
* A class that can be used as an interface for using actions associated with keybindings. This
* A class that can be used as an interface for using actions associated with keybindings. This
* class is meant to only by used by internal Ghidra key event processing.
*/
public abstract class DockingKeyBindingAction extends AbstractAction {
private DockingActionIf docakbleAction;
private DockingActionIf dockingAction;
protected final KeyStroke keyStroke;
protected final Tool tool;
@ -37,7 +38,7 @@ public abstract class DockingKeyBindingAction extends AbstractAction {
public DockingKeyBindingAction(Tool tool, DockingActionIf action, KeyStroke keyStroke) {
super(KeyBindingUtils.parseKeyStroke(keyStroke));
this.tool = tool;
this.docakbleAction = action;
this.dockingAction = action;
this.keyStroke = keyStroke;
}
@ -63,7 +64,7 @@ public abstract class DockingKeyBindingAction extends AbstractAction {
ComponentProvider provider = tool.getActiveComponentProvider();
ActionContext context = getLocalContext(provider);
context.setSourceObject(e.getSource());
docakbleAction.actionPerformed(context);
dockingAction.actionPerformed(context);
}
protected ActionContext getLocalContext(ComponentProvider localProvider) {
@ -78,4 +79,8 @@ public abstract class DockingKeyBindingAction extends AbstractAction {
return new DefaultActionContext(localProvider, null);
}
public List<DockingActionIf> getActions() {
return List.of(dockingAction);
}
}

View file

@ -705,11 +705,13 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
placeholderManager.removeComponent(provider);
}
//==================================================================================================
// Package-level Action Methods
//==================================================================================================
Iterator<DockingActionIf> getComponentActions(ComponentProvider provider) {
/**
* Get the local actions installed on the given provider
*
* @param provider the provider
* @return an iterator over the actions
*/
public Iterator<DockingActionIf> getComponentActions(ComponentProvider provider) {
ComponentPlaceholder placeholder = getActivePlaceholder(provider);
if (placeholder != null) {
return placeholder.getActions();
@ -719,6 +721,10 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
return emptyList.iterator();
}
//==================================================================================================
// Package-level Action Methods
//==================================================================================================
void removeProviderAction(ComponentProvider provider, DockingActionIf action) {
ComponentPlaceholder placeholder = getActivePlaceholder(provider);
if (placeholder != null) {

View file

@ -318,6 +318,7 @@ public class MultipleKeyAction extends DockingKeyBindingAction {
return dwm.getActiveWindow();
}
@Override
public List<DockingActionIf> getActions() {
List<DockingActionIf> list = new ArrayList<>(actions.size());
for (ActionData actionData : actions) {

View file

@ -57,7 +57,7 @@ public class IndexedScrollPane extends JPanel implements IndexScrollListener {
}
/**
* Sets this scroll pane to never show scroll bars. This is useful when you want a container
* Sets this scroll pane to never show scroll bars. This is useful when you want a container
* whose view is always as big as the component in this scroll pane.
*/
public void setNeverScroll(boolean b) {
@ -67,6 +67,20 @@ public class IndexedScrollPane extends JPanel implements IndexScrollListener {
useViewSizeAsPreferredSize = b;
}
/**
* @see JScrollPane#setVerticalScrollBarPolicy(int)
*/
public void setVerticalScrollBarPolicy(int policy) {
scrollPane.setVerticalScrollBarPolicy(policy);
}
/**
* @see JScrollPane#setHorizontalScrollBarPolicy(int)
*/
public void setHorizontalScrollBarPolicy(int policy) {
scrollPane.setHorizontalScrollBarPolicy(policy);
}
private ViewToIndexMapper createIndexMapper() {
if (neverScroll) {
return new PreMappedViewToIndexMapper(scrollable);
@ -215,6 +229,7 @@ public class IndexedScrollPane extends JPanel implements IndexScrollListener {
return false;
}
@Override
public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation,
int direction) {
@ -296,8 +311,8 @@ public class IndexedScrollPane extends JPanel implements IndexScrollListener {
/**
* Sets whether the scroll wheel triggers scrolling <b>when over the scroll pane</b> of this
* class. When disabled, scrolling will still work when over the component inside of
* this class, but not when over the scroll bar.
* class. When disabled, scrolling will still work when over the component inside of this class,
* but not when over the scroll bar.
*
* @param enabled true to enable
*/

View file

@ -322,7 +322,7 @@ public abstract class ThemeManager {
FontValue font = currentValues.getFont(id);
if (font == null) {
error("No color value registered for: '" + id + "'");
error("No font value registered for: '" + id + "'");
return DEFAULT_FONT;
}
return font.get(currentValues);

View file

View file

@ -0,0 +1,30 @@
/* ###
* 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.
*/
import org.gradle.plugins.ide.eclipse.model.Container;
apply from: "$rootProject.projectDir/gradle/distributableGhidraModule.gradle"
apply from: "$rootProject.projectDir/gradle/javaProject.gradle"
apply from: "$rootProject.projectDir/gradle/jacocoProject.gradle"
apply from: "$rootProject.projectDir/gradle/javaTestProject.gradle"
apply from: "$rootProject.projectDir/gradle/javadoc.gradle"
apply plugin: 'eclipse'
eclipse.project.name = 'Framework Pty'
dependencies {
api project(':Framework-Debugging')
api "com.jcraft:jsch:0.1.55"
}

View file

@ -0,0 +1,4 @@
##VERSION: 2.0
##MODULE IP: Apache License 2.0
Module.manifest||GHIDRA||||END|
data/gui.palette.theme.properties||GHIDRA||||END|

View file

@ -0,0 +1,100 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.pty;
import java.io.IOException;
/**
* A pseudo-terminal
*
* <p>
* A pseudo-terminal is essentially a two way pipe where one end acts as the parent, and the other
* acts as the child. The process opening the pseudo-terminal is given a handle to both ends. The
* child end is generally given to a subprocess, possibly designating the pty as the controlling tty
* of a new session. This scheme is how, for example, an SSH daemon starts a new login shell. The
* shell is given the child end, and the parent end is presented to the SSH client.
*
* <p>
* This is more powerful than controlling a process via standard in and standard out. 1) Some
* programs detect whether or not stdin/out/err refer to the controlling tty. For example, a program
* should avoid prompting for passwords unless stdin is the controlling tty. Using a pty can provide
* a controlling tty that is not necessarily controlled by a user. 2) Terminals have other
* properties and can, e.g., send signals to the foreground process group (job) by sending special
* characters. Normal characters are passed to the child, but special characters may be interpreted
* by the terminal's <em>line discipline</em>. A rather common case is to send Ctrl-C (character
* 003). Using stdin, the subprocess simply reads 003. With a properly-configured pty and session,
* the subprocess is interrupted (sent SIGINT) instead.
*
* <p>
* This class opens a pseudo-terminal and presents both ends as individual handles. The parent end
* simply provides an input and output stream. These are typical byte-oriented streams, except that
* the data passes through the pty, subject to interpretation by the OS kernel. On Linux, this means
* the pty will apply the configured line discipline. Consult the host OS documentation for special
* character sequences.
*
* <p>
* The child end also provides the input and output streams, but it is uncommon to use them from the
* same process. More likely, subprocess is launched in a new session, configuring the child as the
* controlling terminal. Thus, the child handle provides methods for obtaining the child pty file
* name and/or spawning a new session. Once spawned, the parent end is used to control the session.
*
* <p>
* Example:
*
* <pre>
* Pty pty = factory.openpty();
* pty.getChild().session("bash");
*
* PrintWriter writer = new PrintWriter(pty.getParent().getOutputStream());
* writer.println("echo test");
* BufferedReader reader =
* new BufferedReader(new InputStreamReader(pty.getParent().getInputStream()));
* System.out.println(reader.readLine());
* System.out.println(reader.readLine());
*
* pty.close();
* </pre>
*/
public interface Pty extends AutoCloseable {
/**
* Get a handle to the parent side of the pty
*
* @return the parent handle
*/
PtyParent getParent();
/**
* Get a handle to the child side of the pty
*
* @return the child handle
*/
PtyChild getChild();
/**
* Closes both ends of the pty
*
* <p>
* This only closes this process's handles to the pty. For the parent end, this should be the
* only process with a handle. The child end may be opened by any number of other processes.
* More than likely, however, those processes will terminate once the parent end is closed,
* since reads or writes on the child will produce EOF or an error.
*
* @throws IOException if an I/O error occurs
*/
@Override
void close() throws IOException;
}

View file

@ -0,0 +1,88 @@
/* ###
* 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 ghidra.pty;
import java.io.IOException;
import java.util.*;
/**
* The child (UNIX "slave") end of a pseudo-terminal
*/
public interface PtyChild extends PtyEndpoint {
/**
* A terminal mode flag
*/
interface TermMode {
}
/**
* Mode flag for local echo
*/
enum Echo implements TermMode {
/**
* Input is echoed to output by the terminal itself.
*/
ON,
/**
* No local echo.
*/
OFF;
}
/**
* Spawn a subprocess in a new session whose controlling tty is this pseudo-terminal
*
* <p>
* This method or {@link #nullSession(Collection)} can only be invoked once per pty.
*
* @param args the image path and arguments
* @param env the environment
* @param mode the terminal mode. If a mode is not implemented, it may be silently ignored.
* @return a handle to the subprocess
* @throws IOException if the session could not be started
*/
PtySession session(String[] args, Map<String, String> env, Collection<TermMode> mode)
throws IOException;
default PtySession session(String[] args, Map<String, String> env, TermMode... mode)
throws IOException {
return session(args, env, List.of(mode));
}
/**
* Start a session without a real leader, instead obtaining the pty's name
*
* <p>
* This method or {@link #session(String[], Map, Collection)} can only be invoked once per pty.
* It must be called before anyone reads the parent's output stream, since obtaining the
* filename may be implemented by the parent sending commands to its child.
*
* <p>
* If the child end of the pty is on a remote system, this should be the file (or other
* resource) name as it would be accessed on that remote system.
*
* @param mode the terminal mode. If a mode is not implemented, it may be silently ignored.
* @return the file name
* @throws IOException if the session could not be started or the pty name could not be
* determined
*/
String nullSession(Collection<TermMode> mode) throws IOException;
default String nullSession(TermMode... mode) throws IOException {
return nullSession(List.of(mode));
}
}

View file

@ -0,0 +1,49 @@
/* ###
* 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 ghidra.pty;
import java.io.InputStream;
import java.io.OutputStream;
/**
* One end of a pseudo-terminal
*/
public interface PtyEndpoint {
/**
* Get the output stream for this end of the pty
*
* <p>
* Writes to this stream arrive on the input stream for the opposite end, subject to the
* terminal's line discipline.
*
* @return the output stream
* @throws UnsupportedOperationException if this end is not local
*/
OutputStream getOutputStream();
/**
* Get the input stream for this end of the pty
*
* <p>
* Writes to the output stream of the opposite end arrive here, subject to the terminal's line
* discipline.
*
* @return the input stream
* @throws UnsupportedOperationException if this end is not local
*/
InputStream getInputStream();
}

View file

@ -0,0 +1,57 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.pty;
import java.io.IOException;
import ghidra.framework.OperatingSystem;
import ghidra.pty.linux.LinuxPtyFactory;
import ghidra.pty.macos.MacosPtyFactory;
import ghidra.pty.windows.ConPtyFactory;
/**
* A mechanism for opening pseudo-terminals
*/
public interface PtyFactory {
/**
* Choose a factory of local pty's for the host operating system
*
* @return the factory
*/
static PtyFactory local() {
switch (OperatingSystem.CURRENT_OPERATING_SYSTEM) {
case MAC_OS_X:
return new MacosPtyFactory();
case LINUX:
return new LinuxPtyFactory();
case WINDOWS:
return new ConPtyFactory();
default:
throw new UnsupportedOperationException();
}
}
/**
* Open a new pseudo-terminal
*
* @return new new Pty
* @throws IOException for an I/O error, including cancellation
*/
Pty openpty() throws IOException;
String getDescription();
}

View file

@ -0,0 +1,23 @@
/* ###
* 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 ghidra.pty;
/**
* The parent (UNIX "master") end of a pseudo-terminal
*/
public interface PtyParent extends PtyEndpoint {
void setWindowSize(int cols, int rows);
}

View file

@ -0,0 +1,43 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.pty;
/**
* A session led by the child pty
*
* <p>
* This is typically a handle to the (local or remote) process designated as the "session leader"
*/
public interface PtySession {
/**
* Wait for the session leader to exit, returning its optional exit status code
*
* @return the status code, if applicable and implemented
* @throws InterruptedException if the wait is interrupted
*/
int waitExited() throws InterruptedException;
/**
* Take the greatest efforts to terminate the session (leader and descendants)
*
* <p>
* If this represents a remote session, this should strive to release the remote resources
* consumed by this session. If that is not possible, this should at the very least release
* whatever local resources are used in maintaining and controlling the remote session.
*/
void destroyForcibly();
}

View file

@ -0,0 +1,31 @@
/* ###
* 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 ghidra.pty.linux;
import com.sun.jna.LastErrorException;
import com.sun.jna.Native;
public interface Err {
PosixC BARE_POSIX = PosixC.BARE;
static int checkLt0(int result) {
if (result < 0) {
int errno = Native.getLastError();
throw new LastErrorException("[" + errno + "] " + BARE_POSIX.strerror(errno));
}
return result;
}
}

View file

@ -0,0 +1,81 @@
/* ###
* 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 ghidra.pty.linux;
import java.io.IOException;
import java.io.InputStream;
import com.sun.jna.Memory;
/**
* An input stream that wraps a native POSIX file descriptor
*
* <p>
* <b>WARNING:</b> This class makes use of jnr-ffi to invoke native functions. An invalid file
* descriptor is generally detected, but an incorrect, but valid file descriptor may cause undefined
* behavior.
*/
public class FdInputStream extends InputStream {
private static final PosixC LIB_POSIX = PosixC.INSTANCE;
private final int fd;
private boolean closed = false;
/**
* Wrap the given file descriptor in an {@link InputStream}
*
* @param fd the file descriptor
*/
FdInputStream(int fd) {
this.fd = fd;
}
@Override
public synchronized int read() throws IOException {
if (closed) {
throw new IOException("Stream closed");
}
byte[] buf = new byte[1];
if (0 == read(buf)) {
return -1;
}
return buf[0] & 0x0FF;
}
@Override
public synchronized int read(byte[] b) throws IOException {
return read(b, 0, b.length);
}
@Override
public synchronized int read(byte[] b, int off, int len) throws IOException {
if (closed) {
throw new IOException("Stream closed");
}
if (len == 0) {
return 0;
}
Memory buf = new Memory(len);
int ret = LIB_POSIX.read(fd, buf, len);
buf.read(0, b, off, ret);
return ret;
}
@Override
public synchronized void close() throws IOException {
closed = true;
}
}

View file

@ -0,0 +1,75 @@
/* ###
* 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 ghidra.pty.linux;
import java.io.IOException;
import java.io.OutputStream;
import com.sun.jna.Memory;
/**
* An output stream that wraps a native POSIX file descriptor
*
* <p>
* <b>WARNING:</b> This class makes use of jnr-ffi to invoke native functions. An invalid file
* descriptor is generally detected, but an incorrect, but valid file descriptor may cause undefined
* behavior.
*/
public class FdOutputStream extends OutputStream {
private static final PosixC LIB_POSIX = PosixC.INSTANCE;
private final int fd;
private boolean closed = false;
/**
* Wrap the given file descriptor in an {@link OutputStream}
*
* @param fd the file descriptor
*/
FdOutputStream(int fd) {
this.fd = fd;
}
@Override
public synchronized void write(int b) throws IOException {
write(new byte[] { (byte) b });
}
@Override
public synchronized void write(byte[] b) throws IOException {
write(b, 0, b.length);
}
@Override
public synchronized void write(byte[] b, int off, int len) throws IOException {
if (closed) {
throw new IOException("Stream closed");
}
Memory buf = new Memory(len);
buf.write(0, b, off, len);
int total = 0;
do {
int ret = LIB_POSIX.write(fd, buf, len - total);
total += ret;
}
while (total < len);
}
@Override
public synchronized void close() throws IOException {
closed = true;
}
}

View file

@ -0,0 +1,75 @@
/* ###
* 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 ghidra.pty.linux;
import java.io.IOException;
import com.sun.jna.*;
import com.sun.jna.ptr.IntByReference;
import ghidra.pty.Pty;
import ghidra.util.Msg;
public class LinuxPty implements Pty {
static final PosixC LIB_POSIX = PosixC.INSTANCE;
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();
Memory n = new Memory(1024);
Util.INSTANCE.openpty(p, c, n, null, null);
return new LinuxPty(p.getValue(), c.getValue(), 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.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;
}
LIB_POSIX.close(achild);
LIB_POSIX.close(aparent);
closed = true;
}
}

View file

@ -0,0 +1,166 @@
/* ###
* 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 ghidra.pty.linux;
import java.io.*;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.file.Paths;
import java.util.*;
import ghidra.pty.PtyChild;
import ghidra.pty.PtySession;
import ghidra.pty.linux.PosixC.Termios;
import ghidra.pty.local.LocalProcessPtySession;
import ghidra.util.Msg;
public class LinuxPtyChild extends LinuxPtyEndpoint implements PtyChild {
static final PosixC LIB_POSIX = PosixC.INSTANCE;
private final String name;
LinuxPtyChild(int fd, String name) {
super(fd);
this.name = name;
}
@Override
public String nullSession(Collection<TermMode> mode) {
applyMode(mode);
return name;
}
/**
* {@inheritDoc}
*
* @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.
*/
@Override
public PtySession session(String[] args, Map<String, String> env, Collection<TermMode> mode)
throws IOException {
return sessionUsingJavaLeader(args, env, mode);
}
protected PtySession sessionUsingJavaLeader(String[] args, Map<String, String> env,
Collection<TermMode> mode) throws IOException {
final List<String> argsList = new ArrayList<>();
String javaCommand =
System.getProperty("java.home") + File.separator + "bin" + File.separator + "java";
argsList.add(javaCommand);
argsList.add("-cp");
argsList.add(System.getProperty("java.class.path"));
argsList.add(LinuxPtySessionLeader.class.getCanonicalName());
argsList.add(name);
argsList.addAll(Arrays.asList(args));
ProcessBuilder builder = new ProcessBuilder(argsList);
if (env != null) {
builder.environment().putAll(env);
}
builder.inheritIO();
applyMode(mode);
try {
return new LocalProcessPtySession(builder.start());
}
catch (Exception e) {
Msg.error(this, "Could not start process with args " + Arrays.toString(args), e);
throw e;
}
}
protected PtySession sessionUsingPythonLeader(String[] args, Map<String, String> env)
throws IOException {
final List<String> argsList = new ArrayList<>();
argsList.add("python");
argsList.add("-m");
argsList.add("session");
argsList.add(name);
argsList.addAll(Arrays.asList(args));
ProcessBuilder builder = new ProcessBuilder(argsList);
if (env != null) {
builder.environment().putAll(env);
}
String sourceLoc = getSourceLocationForResource("session.py").getAbsolutePath();
//System.err.println("PYTHONPATH=" + sourceLoc);
builder.environment().put("PYTHONPATH", sourceLoc);
builder.inheritIO();
return new LocalProcessPtySession(builder.start());
}
public static File getSourceLocationForResource(String name) {
// TODO: Refactor this with SystemUtilities.getSourceLocationForClass()
URL url = LinuxPtyChild.class.getClassLoader().getResource(name);
String urlFile = url.getFile();
try {
urlFile = URLDecoder.decode(urlFile, "UTF-8");
}
catch (UnsupportedEncodingException e) {
// can't happen, since we know the encoding is correct
throw new AssertionError(e);
}
if ("file".equals(url.getProtocol())) {
int packageLevel = Paths.get(name).getNameCount();
File file = new File(urlFile);
for (int i = 0; i < packageLevel; i++) {
file = file.getParentFile();
}
return file;
}
if ("jar".equals(url.getProtocol())) {
// Running from Jar file
String jarPath = urlFile;
if (!jarPath.startsWith("file:")) {
return null;
}
// strip off the 'file:' prefix and the jar path suffix after the
// '!'
jarPath = jarPath.substring(5, jarPath.indexOf('!'));
return new File(jarPath);
}
return null;
}
private void applyMode(Collection<TermMode> mode) {
if (mode.contains(Echo.OFF)) {
disableEcho();
}
}
private void disableEcho() {
Termios.ByReference tmios = new Termios.ByReference();
LIB_POSIX.tcgetattr(fd, tmios);
tmios.c_lflag &= ~Termios.ECHO;
LIB_POSIX.tcsetattr(fd, Termios.TCSANOW, tmios);
}
}

View file

@ -0,0 +1,43 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.pty.linux;
import java.io.InputStream;
import java.io.OutputStream;
import ghidra.pty.PtyEndpoint;
public class LinuxPtyEndpoint implements PtyEndpoint {
protected final int fd;
private final FdOutputStream outputStream;
private final FdInputStream inputStream;
LinuxPtyEndpoint(int fd) {
this.fd = fd;
this.outputStream = new FdOutputStream(fd);
this.inputStream = new FdInputStream(fd);
}
@Override
public OutputStream getOutputStream() {
return outputStream;
}
@Override
public InputStream getInputStream() {
return inputStream;
}
}

View file

@ -0,0 +1,33 @@
/* ###
* 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 ghidra.pty.linux;
import java.io.IOException;
import ghidra.pty.Pty;
import ghidra.pty.PtyFactory;
public class LinuxPtyFactory implements PtyFactory {
@Override
public Pty openpty() throws IOException {
return LinuxPty.openpty();
}
@Override
public String getDescription() {
return "local (Linux)";
}
}

View file

@ -0,0 +1,38 @@
/* ###
* 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 ghidra.pty.linux;
import ghidra.pty.PtyParent;
import ghidra.pty.linux.PosixC.Winsize;
public class LinuxPtyParent extends LinuxPtyEndpoint implements PtyParent {
LinuxPtyParent(int fd) {
super(fd);
}
@Override
public void setWindowSize(int cols, int rows) {
if (cols > 0xffff || rows > 0xffff) {
throw new IllegalArgumentException(
"Dimensions limited to unsigned shorts. Got cols=" + cols + ",rows=" + rows);
}
Winsize.ByReference ws = new Winsize.ByReference();
ws.ws_col = (short) cols;
ws.ws_row = (short) rows;
ws.write();
PosixC.INSTANCE.ioctl(fd, Winsize.TIOCSWINSZ, ws.getPointer());
}
}

View file

@ -0,0 +1,85 @@
/* ###
* 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 ghidra.pty.linux;
import java.util.List;
public class LinuxPtySessionLeader {
private static final PosixC LIB_POSIX = PosixC.INSTANCE;
private static final int O_RDWR = 2; // TODO: Find this in libs
public static void main(String[] args) throws Exception {
LinuxPtySessionLeader leader = new LinuxPtySessionLeader();
leader.parseArgs(args);
leader.run();
}
protected String ptyPath;
protected List<String> subArgs;
protected void parseArgs(String[] args) {
ptyPath = args[0];
subArgs = List.of(args).subList(1, args.length);
}
protected void run() throws Exception {
/** This tells Linux to make this process the leader of a new session. */
LIB_POSIX.setsid();
/**
* Open the TTY. On Linux, the first TTY opened since becoming a session leader becomes the
* session's controlling TTY. Other platforms, e.g., BSD may require an explicit IOCTL.
*/
int bk = -1;
try {
int fd = LIB_POSIX.open(ptyPath, O_RDWR, 0);
/** Copy stderr to a backup descriptor, in case something goes wrong. */
int bkt = fd + 1;
LIB_POSIX.dup2(2, bkt);
bk = bkt;
/**
* Copy the TTY fd over all standard streams. This effectively redirects the leader's
* standard streams to the TTY.
*/
LIB_POSIX.dup2(fd, 0);
LIB_POSIX.dup2(fd, 1);
LIB_POSIX.dup2(fd, 2);
/**
* At this point, we are the session leader and the named TTY is the controlling PTY.
* Now, exec the specified image with arguments as the session leader. Recall, this
* replaces the image of this process.
*/
LIB_POSIX.execv(subArgs.get(0), subArgs.toArray(new String[0]));
}
catch (Throwable t) {
if (bk != -1) {
try {
int bkt = bk;
LIB_POSIX.dup2(bkt, 2);
}
catch (Throwable t2) {
// Catastrophic
System.exit(-1);
}
}
System.err.println("Could not execute " + subArgs.get(0) + ": " + t.getMessage());
System.exit(127);
}
}
}

View file

@ -0,0 +1,149 @@
/* ###
* 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 ghidra.pty.linux;
import com.sun.jna.*;
import com.sun.jna.Structure.FieldOrder;
/**
* Interface for POSIX functions in libc
*
* <p>
* The functions are not documented here. Instead see the POSIX manual pages.
*/
public interface PosixC extends Library {
@FieldOrder({ "ws_row", "ws_col", "ws_xpixel", "ws_ypixel" })
class Winsize extends Structure {
public static final int TIOCSWINSZ = 0x5414; // This may actually be Linux-specific
public short ws_row;
public short ws_col;
public short ws_xpixel; // Unused
public short ws_ypixel; // Unused
public static class ByReference extends Winsize implements Structure.ByReference {
}
}
@FieldOrder({ "c_iflag", "c_oflag", "c_cflag", "c_lflag", "c_line", "c_cc", "c_ispeed",
"c_ospeed" })
class Termios extends Structure {
public static final int TCSANOW = 0;
public static final int ECHO = 0000010; // Octal
public int c_iflag;
public int c_oflag;
public int c_cflag;
public int c_lflag;
public byte c_line;
public byte[] c_cc = new byte[32];
public int c_ispeed;
public int c_ospeed;
public static class ByReference extends Termios implements Structure.ByReference {
}
}
/**
* The bare library without error handling
*
* @see Util#BARE
*/
PosixC BARE = Native.load("c", PosixC.class);
PosixC INSTANCE = new PosixC() {
@Override
public String strerror(int errnum) {
return BARE.strerror(errnum);
}
@Override
public int close(int fd) {
return Err.checkLt0(BARE.close(fd));
}
@Override
public int read(int fd, Pointer buf, int len) {
return Err.checkLt0(BARE.read(fd, buf, len));
}
@Override
public int write(int fd, Pointer buf, int i) {
return Err.checkLt0(BARE.write(fd, buf, i));
}
@Override
public int setsid() {
return Err.checkLt0(BARE.setsid());
}
@Override
public int open(String path, int mode, int flags) {
return Err.checkLt0(BARE.open(path, mode, flags));
}
@Override
public int dup2(int oldfd, int newfd) {
return Err.checkLt0(BARE.dup2(oldfd, newfd));
}
@Override
public int execv(String path, String[] argv) {
return Err.checkLt0(BARE.execv(path, argv));
}
@Override
public int ioctl(int fd, int cmd, Pointer... args) {
return Err.checkLt0(BARE.ioctl(fd, cmd, args));
}
@Override
public int tcgetattr(int fd, Termios.ByReference termios_p) {
return Err.checkLt0(BARE.tcgetattr(fd, termios_p));
}
@Override
public int tcsetattr(int fd, int optional_actions, Termios.ByReference termios_p) {
return Err.checkLt0(BARE.tcsetattr(fd, optional_actions, termios_p));
}
};
String strerror(int errnum);
int close(int fd);
int read(int fd, Pointer buf, int len);
int write(int fd, Pointer buf, int i);
int setsid();
int open(String path, int mode, int flags);
int dup2(int oldfd, int newfd);
int execv(String path, String[] argv);
int ioctl(int fd, int cmd, Pointer... args);
int tcgetattr(int fd, Termios.ByReference termios_p);
int tcsetattr(int fd, int optional_actions, Termios.ByReference termios_p);
}

View file

@ -0,0 +1,62 @@
/* ###
* 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 ghidra.pty.linux;
import com.sun.jna.*;
import com.sun.jna.ptr.IntByReference;
/**
* The interface for linking to {@code openpty} via jna
*
* <p>
* See the UNIX manual pages
*/
public interface Util extends Library {
/**
* The bare library without error handling
*
* <p>
* For error handling, use {@link #INSTANCE}, or check for errors manually, perhaps using
* {@link Err}.
*
* <p>
* We cannot just use {@code throws }{@link LastErrorException} to handle errors, because the
* idiom it applies is not correct for errno on UNIX. See
* https://man7.org/linux/man-pages/man3/errno.3.html, in particular:
*
* <blockquote>The value in errno is significant only when the return value of the call
* indicated an error (i.e., -1 from most system calls; -1 or NULL from most library functions);
* a function that succeeds is allowed to change errno.</blockquote>
*
* <p>
* This actually happens on our test setup when invoking the native {@code openpty} from a
* Docker container. It returns 0, but sets errno. JNA will incorrectly interpret this as
* failure.
*/
Util BARE = Native.load("util", Util.class);
Util INSTANCE = new Util() {
@Override
public int openpty(IntByReference amaster, IntByReference aslave, Pointer name,
Pointer termp, Pointer winp) {
return Err.checkLt0(BARE.openpty(amaster, aslave, name, termp, winp));
}
};
int openpty(IntByReference amaster, IntByReference aslave, Pointer name, Pointer termp,
Pointer winp);
}

View file

@ -0,0 +1,41 @@
/* ###
* 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 ghidra.pty.local;
import ghidra.pty.PtySession;
import ghidra.util.Msg;
/**
* 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;
Msg.info(this, "local Pty session. PID = " + process.pid());
}
@Override
public int waitExited() throws InterruptedException {
return process.waitFor();
}
@Override
public void destroyForcibly() {
process.destroyForcibly();
}
}

View file

@ -0,0 +1,77 @@
/* ###
* 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 ghidra.pty.local;
import com.sun.jna.LastErrorException;
import com.sun.jna.platform.win32.Kernel32;
import com.sun.jna.platform.win32.WinBase;
import com.sun.jna.ptr.IntByReference;
import ghidra.pty.PtySession;
import ghidra.pty.windows.Handle;
import ghidra.util.Msg;
public class LocalWindowsNativeProcessPtySession implements PtySession {
//private final int pid;
//private final int tid;
private final Handle processHandle;
//private final Handle threadHandle;
public LocalWindowsNativeProcessPtySession(int pid, int tid, Handle processHandle,
Handle threadHandle) {
//this.pid = pid;
//this.tid = tid;
this.processHandle = processHandle;
//this.threadHandle = threadHandle;
Msg.info(this, "local Windows Pty session. PID = " + pid);
}
@Override
public int waitExited() throws InterruptedException {
while (true) {
switch (Kernel32.INSTANCE.WaitForSingleObject(processHandle.getNative(), -1)) {
case Kernel32.WAIT_OBJECT_0:
case Kernel32.WAIT_ABANDONED:
IntByReference lpExitCode = new IntByReference();
Kernel32.INSTANCE.GetExitCodeProcess(processHandle.getNative(), lpExitCode);
if (lpExitCode.getValue() != WinBase.STILL_ACTIVE) {
return lpExitCode.getValue();
}
case Kernel32.WAIT_TIMEOUT:
throw new AssertionError();
case Kernel32.WAIT_FAILED:
throw new LastErrorException(Kernel32.INSTANCE.GetLastError());
}
}
}
@Override
public void destroyForcibly() {
if (!Kernel32.INSTANCE.TerminateProcess(processHandle.getNative(), 1)) {
int error = Kernel32.INSTANCE.GetLastError();
switch (error) {
case Kernel32.ERROR_ACCESS_DENIED:
/**
* This indicates the process has already terminated. It's unclear to me whether
* or not that is the only possible cause of this error.
*/
return;
}
throw new LastErrorException(error);
}
}
}

View file

@ -0,0 +1,34 @@
/* ###
* 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 ghidra.pty.macos;
import java.io.IOException;
import ghidra.pty.Pty;
import ghidra.pty.PtyFactory;
import ghidra.pty.linux.LinuxPty;
public class MacosPtyFactory implements PtyFactory {
@Override
public Pty openpty() throws IOException {
return LinuxPty.openpty();
}
@Override
public String getDescription() {
return "local (MacOS)";
}
}

View file

@ -0,0 +1,246 @@
/* ###
* 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 ghidra.pty.ssh;
import java.io.IOException;
import java.util.Objects;
import java.util.concurrent.CancellationException;
import javax.swing.JOptionPane;
import org.apache.commons.text.StringEscapeUtils;
import com.jcraft.jsch.*;
import com.jcraft.jsch.ConfigRepository.Config;
import docking.DockingWindowManager;
import docking.widgets.PasswordDialog;
import ghidra.pty.PtyFactory;
import ghidra.util.Msg;
import ghidra.util.StringUtilities;
public class GhidraSshPtyFactory implements PtyFactory {
private static final String TITLE = "GDB via SSH";
private static final int WRAP_LEN = 80;
public static final String DEFAULT_HOSTNAME = "localhost";
public static final int DEFAULT_PORT = 22;
public static final String DEFAULT_USERNAME = "user";
public static final String DEFAULT_CONFIG_FILE = "~/.ssh/config";
private class RequireTTYAlwaysConfig implements Config {
private final Config delegate;
public RequireTTYAlwaysConfig(Config delegate) {
this.delegate = delegate;
}
@Override
public String getHostname() {
return delegate.getHostname();
}
@Override
public String getUser() {
return delegate.getUser();
}
@Override
public int getPort() {
return delegate.getPort();
}
@Override
public String getValue(String key) {
if ("RequestTTY".equals(key)) {
return "yes";
}
return delegate.getValue(key);
}
@Override
public String[] getValues(String key) {
if ("RequestTTY".equals(key)) {
return new String[] { "yes" };
}
return delegate.getValues(key);
}
}
private class RequireTTYAlwaysConfigRepo implements ConfigRepository {
private final ConfigRepository delegate;
public RequireTTYAlwaysConfigRepo(ConfigRepository delegate) {
this.delegate = delegate;
}
@Override
public Config getConfig(String host) {
if (delegate == null) {
return new RequireTTYAlwaysConfig(ConfigRepository.defaultConfig);
}
return new RequireTTYAlwaysConfig(delegate.getConfig(host));
}
}
private class GhidraUserInfo implements UserInfo {
private String password;
private String passphrase;
public String doPromptSecret(String prompt) {
PasswordDialog dialog =
new PasswordDialog(TITLE, "SSH", hostname, prompt, null, null);
DockingWindowManager.showDialog(dialog);
if (dialog.okWasPressed()) {
return new String(dialog.getPassword());
}
return null;
}
public String html(String message) {
// TODO: I shouldn't have to do this. Why won't swing wrap?
String wrapped = StringUtilities.wrapToWidth(message, WRAP_LEN);
return "<html><pre>" + StringEscapeUtils.escapeHtml4(wrapped).replace("\n", "<br>");
}
@Override
public String getPassphrase() {
return passphrase;
}
@Override
public String getPassword() {
return password;
}
@Override
public boolean promptPassword(String message) {
password = doPromptSecret(message);
return password != null;
}
@Override
public boolean promptPassphrase(String message) {
passphrase = doPromptSecret(message);
return passphrase != null;
}
@Override
public boolean promptYesNo(String message) {
return JOptionPane.showConfirmDialog(null, html(message), TITLE,
JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE) == JOptionPane.YES_OPTION;
}
@Override
public void showMessage(String message) {
JOptionPane.showMessageDialog(null, html(message), TITLE,
JOptionPane.INFORMATION_MESSAGE);
}
}
private String hostname = DEFAULT_HOSTNAME;
private int port = DEFAULT_PORT;
private String username = DEFAULT_USERNAME;
private String configFile = DEFAULT_CONFIG_FILE;
private Session session;
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 getConfigFile() {
return configFile;
}
public void setConfigFile(String configFile) {
this.configFile = configFile;
}
protected Session connectAndAuthenticate() throws IOException {
JSch jsch = new JSch();
ConfigRepository configRepo = null;
try {
configRepo = OpenSSHConfig.parseFile(configFile);
}
catch (IOException e) {
Msg.warn(this, "ssh config file " + configFile + " could not be parsed.");
// I guess the config file doesn't exist. Just go on
}
jsch.setConfigRepository(new RequireTTYAlwaysConfigRepo(configRepo));
try {
Session session =
jsch.getSession(username.length() == 0 ? null : username, hostname, port);
session.setUserInfo(new GhidraUserInfo());
session.connect();
return session;
}
catch (JSchException e) {
String message = e.getMessage();
if (message.equals("Auth cancel")) {
Msg.error(this, "SSH connection canceled");
throw new CancellationException("SSH connection canceled");
}
else if (message.startsWith("reject HostKey")) {
String cancelMessage = "SSH " + message;
Msg.error(this, cancelMessage);
throw new CancellationException(cancelMessage);
}
Msg.error(this, "SSH connection error");
throw new IOException("SSH connection error", e);
}
}
@Override
public SshPty openpty() throws IOException {
if (session == null) {
session = connectAndAuthenticate();
}
try {
return new SshPty((ChannelExec) session.openChannel("exec"));
}
catch (JSchException e) {
throw new IOException("SSH connection error", e);
}
}
@Override
public String getDescription() {
return "ssh:" + hostname + "(user=" + username + ",port=" + port + ")";
}
}

View file

@ -0,0 +1,57 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.pty.ssh;
import java.io.*;
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSchException;
import ghidra.pty.*;
public class SshPty implements Pty {
private final ChannelExec channel;
private final OutputStream out;
private final InputStream in;
private final SshPtyParent parent;
private final SshPtyChild child;
public SshPty(ChannelExec channel) throws JSchException, IOException {
this.channel = channel;
out = channel.getOutputStream();
in = channel.getInputStream();
parent = new SshPtyParent(channel, out, in);
child = new SshPtyChild(channel, out, in);
}
@Override
public PtyParent getParent() {
return parent;
}
@Override
public PtyChild getChild() {
return child;
}
@Override
public void close() throws IOException {
channel.disconnect();
}
}

View file

@ -0,0 +1,118 @@
/* ###
* 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 ghidra.pty.ssh;
import java.io.*;
import java.util.*;
import java.util.stream.Collectors;
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSchException;
import ghidra.dbg.util.ShellUtils;
import ghidra.pty.PtyChild;
import ghidra.util.Msg;
public class SshPtyChild extends SshPtyEndpoint implements PtyChild {
private String name;
public SshPtyChild(ChannelExec channel, OutputStream outputStream, InputStream inputStream) {
super(channel, outputStream, inputStream);
}
private String sttyString(Collection<TermMode> mode) {
StringBuilder sb = new StringBuilder();
if (mode.contains(Echo.OFF)) {
sb.append("-echo ");
}
else if (mode.contains(Echo.ON)) {
sb.append("echo ");
}
if (sb.isEmpty()) {
return "";
}
return "stty " + sb + "&& ";
}
@Override
public SshPtySession session(String[] args, Map<String, String> env, Collection<TermMode> mode)
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 = ShellUtils.generateLine(Arrays.asList(args));
channel.setCommand(sttyString(mode) + envStr + cmdStr);
try {
channel.connect();
}
catch (JSchException e) {
throw new IOException("SSH error", e);
}
return new SshPtySession(channel);
}
private String getTtyNameAndStartNullSession(Collection<TermMode> mode) throws IOException {
// NB. UNIX sleep is only required to support integer durations
channel.setCommand(sttyString(mode) +
"sh -c 'tty && ctrlc() { echo; } && trap ctrlc INT && while true; do sleep " +
Integer.MAX_VALUE + "; done'");
try {
channel.connect();
}
catch (JSchException e) {
throw new IOException("SSH error", e);
}
byte[] buf = new byte[1024]; // Should be plenty
for (int i = 0; i < 1024; i++) {
int chr = inputStream.read();
if (chr == '\n' || chr == -1) {
return new String(buf, 0, i + 1, "UTF-8").trim();
}
buf[i] = (byte) chr;
}
throw new IOException("Expected pty name. Got " + new String(buf, 0, 1024, "UTF-8"));
}
@Override
public String nullSession(Collection<TermMode> mode) throws IOException {
if (name == null) {
this.name = getTtyNameAndStartNullSession(mode);
if ("".equals(name)) {
throw new IOException("Could not determine child remote tty name");
}
}
Msg.debug(this, "Remote SSH pty: " + name);
return name;
}
@Override
public InputStream getInputStream() {
throw new UnsupportedOperationException("The child is not local");
}
@Override
public OutputStream getOutputStream() {
throw new UnsupportedOperationException("The child is not local");
}
}

View file

@ -0,0 +1,45 @@
/* ###
* 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 ghidra.pty.ssh;
import java.io.InputStream;
import java.io.OutputStream;
import com.jcraft.jsch.ChannelExec;
import ghidra.pty.PtyEndpoint;
public class SshPtyEndpoint implements PtyEndpoint {
protected final ChannelExec channel;
protected final OutputStream outputStream;
protected final InputStream inputStream;
public SshPtyEndpoint(ChannelExec channel, OutputStream outputStream, InputStream inputStream) {
this.channel = channel;
this.outputStream = outputStream;
this.inputStream = inputStream;
}
@Override
public OutputStream getOutputStream() {
return outputStream;
}
@Override
public InputStream getInputStream() {
return inputStream;
}
}

View file

@ -0,0 +1,34 @@
/* ###
* 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 ghidra.pty.ssh;
import java.io.InputStream;
import java.io.OutputStream;
import com.jcraft.jsch.ChannelExec;
import ghidra.pty.PtyParent;
public class SshPtyParent extends SshPtyEndpoint implements PtyParent {
public SshPtyParent(ChannelExec channel, OutputStream outputStream, InputStream inputStream) {
super(channel, outputStream, inputStream);
}
@Override
public void setWindowSize(int cols, int rows) {
channel.setPtySize(cols, rows, 0, 0);
}
}

View file

@ -0,0 +1,44 @@
/* ###
* 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 ghidra.pty.ssh;
import com.jcraft.jsch.Channel;
import ghidra.pty.PtySession;
public class SshPtySession implements PtySession {
private final Channel channel;
public SshPtySession(Channel channel) {
this.channel = channel;
}
@Override
public int waitExited() throws InterruptedException {
// Doesn't look like there's a clever way to wait. So do the spin sleep :(
while (!channel.isEOF()) {
Thread.sleep(1000);
}
// NB. May not be available
return channel.getExitStatus();
}
@Override
public void destroyForcibly() {
channel.disconnect();
}
}

View file

@ -0,0 +1,476 @@
/* ###
* 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 ghidra.pty.windows;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Stream;
// TODO: I shouldn't have to do any of this.
public class AnsiBufferedInputStream extends InputStream {
private static final Charset WINDOWS_1252 = Charset.forName("windows-1252");
private enum Mode {
CHARS,
ESC,
CSI,
CSI_p,
CSI_Q,
OSC,
WINDOW_TITLE,
WINDOW_TITLE_ESC;
}
private final InputStream in;
private int countIn = 0;
private ByteBuffer lineBaked = ByteBuffer.allocate(Short.MAX_VALUE);
private ByteBuffer lineBuf = ByteBuffer.allocate(Short.MAX_VALUE);
private ByteBuffer escBuf = ByteBuffer.allocate(1024);
private ByteBuffer titleBuf = ByteBuffer.allocate(255);
private Mode mode = Mode.CHARS;
public AnsiBufferedInputStream(InputStream in) {
if (in instanceof HandleInputStream) {
// Spare myself the 1-by-1 native calls
in = new BufferedInputStream(in);
}
this.in = in;
lineBuf.limit(0);
lineBaked.limit(0);
}
@Override
public int read() throws IOException {
if (lineBaked.hasRemaining()) {
return lineBaked.get();
}
if (readUntilBaked() < 0) {
return -1;
}
if (lineBaked.hasRemaining()) {
return lineBaked.get();
}
return -1; // EOF
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
if (!lineBaked.hasRemaining()) {
if (readUntilBaked() < 0) {
return -1;
}
}
int read = Math.min(lineBaked.remaining(), len);
lineBaked.get(b, off, read);
return read;
}
@Override
public void close() throws IOException {
in.close();
super.close();
}
protected int readUntilBaked() throws IOException {
while (!lineBaked.hasRemaining()) {
if (processNext() < 0) {
break;
}
}
if (!lineBaked.hasRemaining()) {
return -1;
}
return lineBaked.remaining();
}
protected void printDebugChar(byte c) {
if (0x20 <= c && c <= 0x7f) {
System.err.print(new String(new byte[] { c }));
}
else {
System.err.print(String.format("<%02x>", c & 0xff));
}
}
protected int processNext() throws IOException {
int ci = in.read();
if (ci == -1) {
return -1;
}
byte c = (byte) ci;
// printDebugChar(c);
switch (mode) {
case CHARS:
processChars(c);
break;
case ESC:
processEsc(c);
break;
case CSI:
processCsi(c);
break;
case CSI_p:
processCsiParamOrCommand(c);
break;
case CSI_Q:
processCsiQ(c);
break;
case OSC:
processOsc(c);
break;
case WINDOW_TITLE:
processWindowTitle(c);
break;
case WINDOW_TITLE_ESC:
processWindowTitleEsc(c);
break;
default:
throw new AssertionError();
}
countIn++;
return c;
}
/**
* There's not really a good way to know if any trailing space was intentional. For GDB/MI, that
* doesn't really matter.
*/
protected int guessEnd() {
for (int i = lineBuf.limit() - 1; i >= 0; i--) {
byte c = lineBuf.get(i);
if (c != 0x20 && c != 0) {
return i + 1;
}
}
return 0;
}
protected void bakeLine() {
lineBuf.position(0);
lineBuf.limit(guessEnd() + 1);
lineBuf.put(lineBuf.limit() - 1, (byte) '\n');
ByteBuffer temp = lineBaked;
lineBaked = lineBuf;
lineBuf = temp;
lineBuf.clear();
Arrays.fill(lineBuf.array(), (byte) 0);
lineBuf.limit(0);
}
protected void appendChar(byte c) {
int limit = lineBuf.limit();
if (lineBuf.position() == limit) {
lineBuf.limit(limit + 1);
}
lineBuf.put(c);
}
protected void processChars(byte c) {
switch (c) {
case 0x08:
if (lineBuf.get(lineBuf.position() - 1) == ' ') {
lineBuf.position(lineBuf.position() - 1);
}
break;
case '\n':
//appendChar(c);
bakeLine();
break;
case 0x1b:
mode = Mode.ESC;
break;
default:
appendChar(c);
break;
}
}
protected void processEsc(byte c) {
switch (c) {
case '[':
mode = Mode.CSI;
break;
case ']':
mode = Mode.OSC;
break;
default:
throw new AssertionError("Saw 'ESC " + c + "' at " + countIn);
}
}
protected void processCsi(byte c) {
switch (c) {
default:
processCsiParamOrCommand(c);
break;
case '?':
mode = Mode.CSI_Q;
break;
}
}
protected void processCsiParamOrCommand(byte c) {
switch (c) {
default:
escBuf.put(c);
break;
case 'A':
execCursorUp();
mode = Mode.CHARS;
break;
case 'B':
execCursorDown();
mode = Mode.CHARS;
break;
case 'C':
execCursorForward();
mode = Mode.CHARS;
break;
case 'D':
execCursorBackward();
mode = Mode.CHARS;
break;
case 'H':
execCursorPosition();
mode = Mode.CHARS;
break;
case 'J':
execEraseInDisplay();
mode = Mode.CHARS;
break;
case 'K':
execEraseInLine();
mode = Mode.CHARS;
break;
case 'X':
execEraseCharacter();
mode = Mode.CHARS;
break;
case 'm':
execSetGraphicsRendition();
mode = Mode.CHARS;
break;
}
}
protected void processCsiQ(byte c) {
String buf;
switch (c) {
default:
escBuf.put(c);
break;
case 'h':
buf = readAndClearEscBuf();
if ("12".equals(buf)) {
execTextCursorEnableBlinking();
escBuf.clear();
mode = Mode.CHARS;
}
else if ("25".equals(buf)) {
execTextCursorEnableModeShow();
escBuf.clear();
mode = Mode.CHARS;
}
else {
throw new AssertionError();
}
break;
case 'l':
buf = readAndClearEscBuf();
if ("12".equals(buf)) {
execTextCursorDisableBlinking();
escBuf.clear();
mode = Mode.CHARS;
}
else if ("25".equals(buf)) {
execTextCursorDisableModeShow();
escBuf.clear();
mode = Mode.CHARS;
}
break;
}
}
protected void processOsc(byte c) {
switch (c) {
default:
escBuf.put(c);
break;
case ';':
if (Set.of("0", "2").contains(readAndClearEscBuf())) {
mode = Mode.WINDOW_TITLE;
escBuf.clear();
break;
}
throw new AssertionError();
}
}
protected void processWindowTitle(byte c) {
switch (c) {
default:
titleBuf.put(c);
break;
case 0x07: // bell, even though MSDN says longer form preferred
execSetWindowTitle();
mode = Mode.CHARS;
break;
case 0x1b:
mode = Mode.WINDOW_TITLE_ESC;
break;
}
}
protected void processWindowTitleEsc(byte c) {
switch (c) {
case '\\':
execSetWindowTitle();
mode = Mode.CHARS;
break;
default:
throw new AssertionError("Saw <ST> ... ESC " + c + " at " + countIn);
}
}
protected String readAndClear(ByteBuffer buf) {
buf.flip();
String result = new String(buf.array(), buf.position(), buf.remaining(), WINDOWS_1252);
buf.clear();
return result;
}
protected String readAndClearEscBuf() {
return readAndClear(escBuf);
}
protected int parseNumericBuffer() {
String numeric = readAndClearEscBuf();
if (numeric.isEmpty()) {
return 0;
}
int result = Integer.parseInt(numeric);
return result;
}
protected int[] parseNumericListBuffer() {
String numericList = readAndClearEscBuf();
if (numericList.isEmpty()) {
return new int[] {};
}
return Stream.of(numericList.split(";"))
.mapToInt(Integer::parseInt)
.toArray();
}
protected void execCursorUp() {
throw new UnsupportedOperationException("Cursor Up");
}
protected void execCursorDown() {
throw new UnsupportedOperationException("Cursor Down");
}
protected void setPosition(int newPosition) {
if (lineBuf.limit() < newPosition) {
lineBuf.limit(newPosition);
}
lineBuf.position(newPosition);
}
protected void execCursorForward() {
int delta = parseNumericBuffer();
setPosition(lineBuf.position() + delta);
}
protected void execCursorBackward() {
int delta = parseNumericBuffer();
lineBuf.position(lineBuf.position() - delta);
}
protected void execCursorPosition() {
int[] yx = parseNumericListBuffer();
if (yx.length == 0) {
lineBuf.position(0);
return;
}
if (yx.length != 2) {
throw new AssertionError();
}
if (yx[0] != 1) {
throw new AssertionError();
}
lineBuf.position(yx[1] - 1);
}
protected void execTextCursorEnableBlinking() {
// Don't care
}
protected void execTextCursorDisableBlinking() {
// Don't care
}
protected void execTextCursorEnableModeShow() {
// Don't care
}
protected void execTextCursorDisableModeShow() {
// Don't care
}
protected void execEraseInDisplay() {
// Because I have only one line, right?
execEraseInLine();
}
protected void execEraseInLine() {
switch (parseNumericBuffer()) {
case 0:
Arrays.fill(lineBuf.array(), lineBuf.position(), lineBuf.capacity(), (byte) 0);
break;
case 1:
Arrays.fill(lineBuf.array(), 0, lineBuf.position() + 1, (byte) 0);
break;
case 2:
Arrays.fill(lineBuf.array(), (byte) 0);
break;
}
}
protected void execEraseCharacter() {
int count = parseNumericBuffer();
Arrays.fill(lineBuf.array(), lineBuf.position(), lineBuf.position() + count, (byte) ' ');
}
protected void execSetGraphicsRendition() {
// TODO: Maybe echo these or provide callbacks
// Otherwise, don't care
escBuf.clear();
}
protected void execSetWindowTitle() {
// Msg.info(this, "Title: " + readAndClear(titleBuf));
// TODO: Maybe a callback. Otherwise, don't care
titleBuf.clear();
}
}

View file

@ -0,0 +1,112 @@
/* ###
* 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 ghidra.pty.windows;
import java.io.IOException;
import com.sun.jna.platform.win32.Kernel32;
import com.sun.jna.platform.win32.WinDef.DWORD;
import com.sun.jna.platform.win32.WinNT.HANDLEByReference;
import ghidra.pty.*;
import ghidra.pty.windows.jna.ConsoleApiNative;
import ghidra.pty.windows.jna.ConsoleApiNative.COORD;
import com.sun.jna.platform.win32.COM.COMUtils;
public class ConPty implements Pty {
static final DWORD DW_ZERO = new DWORD(0);
static final DWORD DW_ONE = new DWORD(1);
static final DWORD PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE = new DWORD(0x20016);
static final DWORD EXTENDED_STARTUPINFO_PRESENT =
new DWORD(Kernel32.EXTENDED_STARTUPINFO_PRESENT);
private static final COORD SIZE = new COORD();
static {
SIZE.X = Short.MAX_VALUE;
SIZE.Y = 1;
}
private final Pipe pipeToChild;
private final Pipe pipeFromChild;
private final PseudoConsoleHandle pseudoConsoleHandle;
private boolean closed = false;
private final ConPtyParent parent;
private final ConPtyChild child;
public static ConPty openpty() {
// Create communication channels
Pipe pipeToChild = Pipe.createPipe();
Pipe pipeFromChild = Pipe.createPipe();
// Close the child-connected ends after creating the pseudoconsole
// Keep the parent-connected ends, because we're the parent
HANDLEByReference lphPC = new HANDLEByReference();
COMUtils.checkRC(ConsoleApiNative.INSTANCE.CreatePseudoConsole(
SIZE,
pipeToChild.getReadHandle().getNative(),
pipeFromChild.getWriteHandle().getNative(),
DW_ZERO,
lphPC));
return new ConPty(pipeToChild, pipeFromChild, new PseudoConsoleHandle(lphPC.getValue()));
}
public ConPty(Pipe pipeToChild, Pipe pipeFromChild, PseudoConsoleHandle pseudoConsoleHandle) {
this.pipeToChild = pipeToChild;
this.pipeFromChild = pipeFromChild;
this.pseudoConsoleHandle = pseudoConsoleHandle;
// TODO: See if this can all be combined with named pipes.
// Would be nice if that's sufficient to support new-ui
this.parent = new ConPtyParent(pipeToChild.getWriteHandle(), pipeFromChild.getReadHandle());
this.child = new ConPtyChild(pipeFromChild.getWriteHandle(), pipeToChild.getReadHandle(),
pseudoConsoleHandle);
}
@Override
public PtyParent getParent() {
return parent;
}
@Override
public PtyChild getChild() {
return child;
}
@Override
public synchronized void close() throws IOException {
if (closed) {
return;
}
try {
pseudoConsoleHandle.close();
pipeToChild.close();
pipeFromChild.close();
}
catch (IOException e) {
throw e;
}
catch (Exception e) {
throw new IOException(e);
}
closed = true;
}
}

View file

@ -0,0 +1,112 @@
/* ###
* 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 ghidra.pty.windows;
import java.io.IOException;
import java.util.*;
import com.sun.jna.*;
import com.sun.jna.platform.win32.Kernel32;
import com.sun.jna.platform.win32.WinBase;
import com.sun.jna.platform.win32.WinBase.PROCESS_INFORMATION;
import com.sun.jna.platform.win32.WinDef.*;
import com.sun.jna.platform.win32.WinNT.HANDLE;
import ghidra.dbg.util.ShellUtils;
import ghidra.pty.PtyChild;
import ghidra.pty.local.LocalWindowsNativeProcessPtySession;
import ghidra.pty.windows.jna.ConsoleApiNative;
import ghidra.pty.windows.jna.ConsoleApiNative.STARTUPINFOEX;
public class ConPtyChild extends ConPtyEndpoint implements PtyChild {
private final Handle pseudoConsoleHandle;
public ConPtyChild(Handle writeHandle, Handle readHandle, Handle pseudoConsoleHandle) {
super(writeHandle, readHandle);
this.pseudoConsoleHandle = pseudoConsoleHandle;
}
protected STARTUPINFOEX prepareStartupInfo() {
STARTUPINFOEX si = new STARTUPINFOEX();
si.StartupInfo.cb = new DWORD(si.size());
si.StartupInfo.hStdOutput = new HANDLE();
si.StartupInfo.hStdError = new HANDLE();
si.StartupInfo.hStdInput = new HANDLE();
si.StartupInfo.dwFlags = WinBase.STARTF_USESTDHANDLES;
// Discover the size required for the thread attrs list and allocate
UINTByReference bytesRequired = new UINTByReference();
// NB. This will "fail." See Remarks on MSDN.
ConsoleApiNative.INSTANCE.InitializeProcThreadAttributeList(
null, ConPty.DW_ONE, ConPty.DW_ZERO, bytesRequired);
// NB. Memory frees itself in .finalize()
si.lpAttributeList = new Memory(bytesRequired.getValue().intValue());
// Initialize it
if (!ConsoleApiNative.INSTANCE.InitializeProcThreadAttributeList(
si.lpAttributeList, ConPty.DW_ONE, ConPty.DW_ZERO, bytesRequired)
.booleanValue()) {
throw new LastErrorException(Kernel32.INSTANCE.GetLastError());
}
// Set the pseudoconsole information into the list
if (!ConsoleApiNative.INSTANCE.UpdateProcThreadAttribute(
si.lpAttributeList, ConPty.DW_ZERO,
ConPty.PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
new PVOID(pseudoConsoleHandle.getNative().getPointer()),
new DWORD(Native.POINTER_SIZE),
null, null).booleanValue()) {
throw new LastErrorException(Kernel32.INSTANCE.GetLastError());
}
return si;
}
@Override
public LocalWindowsNativeProcessPtySession session(String[] args, Map<String, String> env,
Collection<TermMode> mode) throws IOException {
/**
* TODO: How to incorporate environment into CreateProcess?
*
* TODO: How to control local echo?
*/
STARTUPINFOEX si = prepareStartupInfo();
PROCESS_INFORMATION pi = new PROCESS_INFORMATION();
if (!ConsoleApiNative.INSTANCE.CreateProcessW(
null /*lpApplicationName*/,
new WString(ShellUtils.generateLine(Arrays.asList(args))),
null /*lpProcessAttributes*/,
null /*lpThreadAttributes*/,
false /*bInheritHandles*/,
ConPty.EXTENDED_STARTUPINFO_PRESENT /*dwCreationFlags*/,
null /*lpEnvironment*/,
null /*lpCurrentDirectory*/,
si /*lpStartupInfo*/,
pi /*lpProcessInformation*/).booleanValue()) {
throw new LastErrorException(Kernel32.INSTANCE.GetLastError());
}
return new LocalWindowsNativeProcessPtySession(pi.dwProcessId.intValue(),
pi.dwThreadId.intValue(),
new Handle(pi.hProcess), new Handle(pi.hThread));
}
@Override
public String nullSession(Collection<TermMode> mode) throws IOException {
throw new UnsupportedOperationException("ConPTY does not have a name");
}
}

View file

@ -0,0 +1,41 @@
/* ###
* 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 ghidra.pty.windows;
import java.io.InputStream;
import java.io.OutputStream;
import ghidra.pty.PtyEndpoint;
public class ConPtyEndpoint implements PtyEndpoint {
protected InputStream inputStream;
protected OutputStream outputStream;
public ConPtyEndpoint(Handle writeHandle, Handle readHandle) {
this.inputStream = new HandleInputStream(readHandle);
this.outputStream = new HandleOutputStream(writeHandle);
}
@Override
public OutputStream getOutputStream() {
return outputStream;
}
@Override
public InputStream getInputStream() {
return inputStream;
}
}

View file

@ -0,0 +1,33 @@
/* ###
* 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 ghidra.pty.windows;
import java.io.IOException;
import ghidra.pty.Pty;
import ghidra.pty.PtyFactory;
public class ConPtyFactory implements PtyFactory {
@Override
public Pty openpty() throws IOException {
return ConPty.openpty();
}
@Override
public String getDescription() {
return "local (Windows)";
}
}

View file

@ -0,0 +1,30 @@
/* ###
* 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 ghidra.pty.windows;
import ghidra.pty.PtyParent;
import ghidra.util.Msg;
public class ConPtyParent extends ConPtyEndpoint implements PtyParent {
public ConPtyParent(Handle writeHandle, Handle readHandle) {
super(writeHandle, readHandle);
}
@Override
public void setWindowSize(int rows, int cols) {
Msg.error(this, "Pty window size not implemented on Windows");
}
}

View file

@ -0,0 +1,66 @@
/* ###
* 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 ghidra.pty.windows;
import java.lang.ref.Cleaner;
import com.sun.jna.LastErrorException;
import com.sun.jna.platform.win32.Kernel32;
import com.sun.jna.platform.win32.WinNT.HANDLE;
public class Handle implements AutoCloseable {
private static final Cleaner CLEANER = Cleaner.create();
protected static class State implements Runnable {
protected final HANDLE handle;
protected State(HANDLE handle) {
this.handle = handle;
}
@Override
public void run() {
if (!Kernel32.INSTANCE.CloseHandle(handle)) {
throw new LastErrorException(Kernel32.INSTANCE.GetLastError());
}
}
}
private final State state;
private final Cleaner.Cleanable cleanable;
public Handle(HANDLE handle) {
this.state = newState(handle);
this.cleanable = CLEANER.register(this, state);
}
protected State newState(HANDLE handle) {
return new State(handle);
}
@Override
public void close() throws Exception {
cleanable.clean();
}
public HANDLE getNative() {
HANDLE handle = state.handle;
if (handle == null) {
throw new IllegalStateException("This handle is no longer valid");
}
return handle;
}
}

View file

@ -0,0 +1,94 @@
/* ###
* 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 ghidra.pty.windows;
import java.io.IOException;
import java.io.InputStream;
import com.sun.jna.LastErrorException;
import com.sun.jna.platform.win32.Kernel32;
import com.sun.jna.ptr.IntByReference;
public class HandleInputStream extends InputStream {
private final Handle handle;
private boolean closed = false;
HandleInputStream(Handle handle) {
this.handle = handle;
}
@Override
public synchronized int read() throws IOException {
if (closed) {
throw new IOException("Stream closed");
}
byte[] buf = new byte[1];
if (0 == read(buf)) {
return -1;
}
return buf[0] & 0x0FF;
}
protected void waitPipeConnected() {
if (Kernel32.INSTANCE.ConnectNamedPipe(handle.getNative(), null)) {
return; // We waited, and now we're connected
}
int error = Kernel32.INSTANCE.GetLastError();
if (error == Kernel32.ERROR_PIPE_CONNECTED) {
return; // We got the connection before we waited. OK
}
throw new LastErrorException(error);
}
@Override
public synchronized int read(byte[] b) throws IOException {
if (closed) {
throw new IOException("Stream closed");
}
IntByReference dwRead = new IntByReference();
while (!Kernel32.INSTANCE.ReadFile(handle.getNative(), b, b.length, dwRead, null)) {
int error = Kernel32.INSTANCE.GetLastError();
switch (error) {
case Kernel32.ERROR_BROKEN_PIPE:
return -1;
case Kernel32.ERROR_PIPE_LISTENING:
/**
* Well, we know we're dealing with a listening pipe, now. Wait for a client,
* then try reading again.
*/
waitPipeConnected();
continue;
}
throw new IOException("Could not read",
new LastErrorException(error));
}
return dwRead.getValue();
}
@Override
public synchronized int read(byte[] b, int off, int len) throws IOException {
byte[] temp = new byte[len];
int read = read(temp);
System.arraycopy(temp, 0, b, off, read);
return read;
}
@Override
public synchronized void close() throws IOException {
closed = true;
}
}

View file

@ -0,0 +1,92 @@
/* ###
* 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 ghidra.pty.windows;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import com.sun.jna.LastErrorException;
import com.sun.jna.platform.win32.Kernel32;
import com.sun.jna.ptr.IntByReference;
public class HandleOutputStream extends OutputStream {
private final Handle handle;
private boolean closed = false;
public HandleOutputStream(Handle handle) {
this.handle = handle;
}
@Override
public synchronized void write(int b) throws IOException {
write(new byte[] { (byte) b });
}
@Override
public synchronized void write(byte[] b) throws IOException {
if (closed) {
throw new IOException("Stream closed");
}
int total = 0;
do {
IntByReference dwWritten = new IntByReference();
if (!Kernel32.INSTANCE.WriteFile(handle.getNative(), b, b.length, dwWritten, null)) {
throw new IOException("Could not write",
new LastErrorException(Kernel32.INSTANCE.GetLastError()));
}
total += dwWritten.getValue();
}
while (total < b.length);
}
@Override
public synchronized void write(byte[] b, int off, int len) throws IOException {
// Abstraction of Windows API given by JNA doesn't have offset
// In C, could add offset to lpBuffer, but not here :(
write(Arrays.copyOfRange(b, off, off + len));
}
@Override
public synchronized void close() throws IOException {
closed = true;
}
/**
* Check whether this handle has buffered output
*
* <p>
* Windows can get touchy when trying to flush handles that are not actually buffered. If the
* wrapped handle is not buffered, then this method must return false, otherwise, any attempt to
* flush this stream will result in {@code ERROR_INVALID_HANDLE}.
*
* @return
*/
protected boolean isBuffered() {
return true;
}
@Override
public void flush() throws IOException {
if (!isBuffered()) {
return;
}
if (!(Kernel32.INSTANCE.FlushFileBuffers(handle.getNative()))) {
throw new LastErrorException(Kernel32.INSTANCE.GetLastError());
}
}
}

View file

@ -0,0 +1,54 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.pty.windows;
import com.sun.jna.LastErrorException;
import com.sun.jna.platform.win32.Kernel32;
import com.sun.jna.platform.win32.WinBase.SECURITY_ATTRIBUTES;
import com.sun.jna.platform.win32.WinNT.HANDLEByReference;
public class Pipe {
public static Pipe createPipe() {
HANDLEByReference pRead = new HANDLEByReference();
HANDLEByReference pWrite = new HANDLEByReference();
if (!Kernel32.INSTANCE.CreatePipe(pRead, pWrite, new SECURITY_ATTRIBUTES(), 0)) {
throw new LastErrorException(Kernel32.INSTANCE.GetLastError());
}
return new Pipe(new Handle(pRead.getValue()), new Handle(pWrite.getValue()));
}
private final Handle readHandle;
private final Handle writeHandle;
private Pipe(Handle read, Handle write) {
this.readHandle = read;
this.writeHandle = write;
}
public Handle getReadHandle() {
return readHandle;
}
public Handle getWriteHandle() {
return writeHandle;
}
public void close() throws Exception {
writeHandle.close();
readHandle.close();
}
}

View file

@ -0,0 +1,43 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.pty.windows;
import com.sun.jna.platform.win32.WinNT.HANDLE;
import ghidra.pty.windows.jna.ConsoleApiNative;
public class PseudoConsoleHandle extends Handle {
protected static class PseudoConsoleState extends State {
public PseudoConsoleState(HANDLE handle) {
super(handle);
}
@Override
public void run() {
ConsoleApiNative.INSTANCE.ClosePseudoConsole(handle);
}
}
public PseudoConsoleHandle(HANDLE handle) {
super(handle);
}
@Override
protected State newState(HANDLE handle) {
return new PseudoConsoleState(handle);
}
}

View file

@ -0,0 +1,160 @@
/* ###
* 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 ghidra.pty.windows.jna;
import java.util.List;
import com.sun.jna.*;
import com.sun.jna.platform.win32.WinBase;
import com.sun.jna.platform.win32.WinDef.*;
import com.sun.jna.platform.win32.WinNT.*;
import com.sun.jna.win32.StdCallLibrary;
public interface ConsoleApiNative extends StdCallLibrary {
ConsoleApiNative INSTANCE = Native.load("Kernel32.dll", ConsoleApiNative.class);
BOOL FAIL = new BOOL(false);
BOOL CreatePipe(HANDLEByReference hReadPipe, HANDLEByReference hWritePipe,
SECURITY_ATTRIBUTES.ByReference lpPipeAttributes, DWORD nSize);
HRESULT CreatePseudoConsole(COORD.ByValue size, HANDLE hInput, HANDLE hOutput,
DWORD dwFlags,
HANDLEByReference phPC);
void ClosePseudoConsole(HANDLE hPC);
BOOL InitializeProcThreadAttributeList(Pointer lpAttributeList,
DWORD dwAttributeCount, DWORD dwFlags, UINTByReference lpSize);
BOOL UpdateProcThreadAttribute(
Pointer lpAttributeList,
DWORD dwFlags,
DWORD Attribute,
PVOID lpValue,
DWORD cbSize,
PVOID lpPreviousValue,
ULONGLONGByReference lpReturnSize);
BOOL CreateProcessW(
WString lpApplicationName,
WString lpCommandLine,
WinBase.SECURITY_ATTRIBUTES lpProcessAttributes,
WinBase.SECURITY_ATTRIBUTES lpThreadAttributes,
boolean bInheritHandles,
DWORD dwCreationFlags,
Pointer lpEnvironment,
WString lpCurrentDirectory,
STARTUPINFOEX lpStartupInfo,
WinBase.PROCESS_INFORMATION lpProcessInformation);
/*
BOOL GetConsoleMode(
HANDLE hConsoleMode,
DWORDByReference dwMode);
BOOL CreateProcessWithTokenW(
HANDLE hToken,
DWORD dwLogonFlags,
WString lpApplicationName,
WString lpCommandLine,
DWORD dwCreationFlags,
Pointer lpEnvironment,
WString lpCurrentDirectory,
STARTUPINFOEX lpStartupInfo,
WinBase.PROCESS_INFORMATION lpProcessInformation);
BOOL LogonUserW(
WString lpUsername,
WString lpDomain,
WString lpPassword,
DWORD dwLogonType,
DWORD dwLogonProvider,
HANDLEByReference phToken);
*/
public static class COORD extends Structure implements Structure.ByValue {
public static class ByReference extends COORD
implements Structure.ByReference {
}
public static final List<String> FIELDS = createFieldsOrder("X", "Y");
public short X;
public short Y;
@Override
protected List<String> getFieldOrder() {
return FIELDS;
}
}
public static class SECURITY_ATTRIBUTES extends Structure {
public static class ByReference extends SECURITY_ATTRIBUTES
implements Structure.ByReference {
}
public static final List<String> FIELDS = createFieldsOrder(
"nLength", "lpSecurityDescriptor", "bInheritedHandle");
public DWORD nLength;
public ULONGLONG lpSecurityDescriptor;
public BOOL bInheritedHandle;
@Override
protected List<String> getFieldOrder() {
return FIELDS;
}
}
public static class PROC_THREAD_ATTRIBUTE_LIST extends Structure {
public static class ByReference extends PROC_THREAD_ATTRIBUTE_LIST
implements Structure.ByReference {
}
public static final List<String> FIELDS = createFieldsOrder(
"dwFlags", "Size", "Count", "Reserved", "Unknown");
public DWORD dwFlags;
public ULONG Size;
public ULONG Count;
public ULONG Reserved;
public ULONGLONG Unknown;
//public PROC_THREAD_ATTRIBUTE_ENTRY Entries[0];
@Override
protected List<String> getFieldOrder() {
return FIELDS;
}
}
public static class STARTUPINFOEX extends Structure {
public static class ByReference extends STARTUPINFOEX
implements Structure.ByReference {
}
public static final List<String> FIELDS = createFieldsOrder(
"StartupInfo", "lpAttributeList");
public WinBase.STARTUPINFO StartupInfo;
public Pointer lpAttributeList;
@Override
protected List<String> getFieldOrder() {
return FIELDS;
}
}
}

View file

@ -0,0 +1,73 @@
/* ###
* 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 ghidra.pty;
import static org.junit.Assert.assertEquals;
import java.io.*;
public class AbstractPtyTest {
public Thread pump(InputStream is, OutputStream os) {
Thread t = new Thread(() -> {
byte[] buf = new byte[1];
while (true) {
int len;
try {
len = is.read(buf);
if (len == -1) {
return;
}
os.write(buf, 0, len);
}
catch (IOException e) {
throw new AssertionError(e);
}
}
});
t.setDaemon(true);
t.start();
return t;
}
public BufferedReader loggingReader(InputStream is) {
return new BufferedReader(new InputStreamReader(is)) {
@Override
public String readLine() throws IOException {
String line = super.readLine();
System.out.println("log: " + line);
return line;
}
};
}
public Thread runExitCheck(int expected, PtySession session) {
Thread exitCheck = new Thread(() -> {
while (true) {
try {
assertEquals("Early exit with wrong code", expected,
session.waitExited());
return;
}
catch (InterruptedException e) {
System.err.println("Exit check interrupted");
}
}
});
exitCheck.setDaemon(true);
exitCheck.start();
return exitCheck;
}
}

View file

@ -0,0 +1,215 @@
/* ###
* 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 ghidra.pty.linux;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
import java.io.*;
import java.util.*;
import org.junit.Before;
import org.junit.Test;
import ghidra.dbg.testutil.DummyProc;
import ghidra.framework.OperatingSystem;
import ghidra.pty.AbstractPtyTest;
import ghidra.pty.PtyChild.Echo;
import ghidra.pty.PtySession;
public class LinuxPtyTest extends AbstractPtyTest {
@Before
public void checkLinux() {
assumeTrue(OperatingSystem.LINUX == OperatingSystem.CURRENT_OPERATING_SYSTEM);
}
@Test
public void testOpenClosePty() throws IOException {
LinuxPty pty = LinuxPty.openpty();
pty.close();
}
@Test
public void testParentToChild() throws IOException {
try (LinuxPty pty = LinuxPty.openpty()) {
PrintWriter writer = new PrintWriter(pty.getParent().getOutputStream());
BufferedReader reader =
new BufferedReader(new InputStreamReader(pty.getChild().getInputStream()));
writer.println("Hello, World!");
writer.flush();
assertEquals("Hello, World!", reader.readLine());
}
}
@Test
public void testChildToParent() throws IOException {
try (LinuxPty pty = LinuxPty.openpty()) {
PrintWriter writer = new PrintWriter(pty.getChild().getOutputStream());
BufferedReader reader =
new BufferedReader(new InputStreamReader(pty.getParent().getInputStream()));
writer.println("Hello, World!");
writer.flush();
assertEquals("Hello, World!", reader.readLine());
}
}
@Test
public void testSessionBash() throws IOException, InterruptedException {
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());
}
}
@Test
public void testForkIntoNonExistent() throws IOException, InterruptedException {
try (LinuxPty pty = LinuxPty.openpty()) {
PtySession dies =
pty.getChild().session(new String[] { "thisHadBetterNotExist" }, null);
/**
* Choice of 127 is based on bash setting "exit code" to 127 for "command not found"
*/
assertEquals(127, dies.waitExited());
}
}
@Test
public void testSessionBashEchoTest() throws IOException, InterruptedException {
Map<String, String> env = new HashMap<>();
env.put("PS1", "BASH:");
env.put("PROMPT_COMMAND", "");
env.put("TERM", "");
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");
writer.flush();
String line;
do {
line = reader.readLine();
}
while (!"test".equals(line));
writer.println("exit 3");
writer.flush();
line = reader.readLine();
assertTrue("Not 'exit 3' or 'BASH:exit 3': '" + line + "'",
Set.of("BASH:exit 3", "exit 3").contains(line));
assertEquals(3, bash.waitExited());
}
}
@Test
public void testSessionBashInterruptCat() throws IOException, InterruptedException {
Map<String, String> env = new HashMap<>();
env.put("PS1", "BASH:");
env.put("PROMPT_COMMAND", "");
env.put("TERM", "");
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");
writer.flush();
String line;
do {
line = reader.readLine();
}
while (!"test".equals(line));
writer.println("cat");
writer.flush();
line = reader.readLine();
assertTrue("Not 'cat' or 'BASH:cat': '" + line + "'",
Set.of("BASH:cat", "cat").contains(line));
writer.println("Hello, cat!");
writer.flush();
assertEquals("Hello, cat!", reader.readLine()); // echo back
assertEquals("Hello, cat!", reader.readLine()); // cat back
writer.write(3); // should interrupt
writer.flush();
do {
line = reader.readLine();
}
while (!"^C".equals(line));
writer.println("echo test");
writer.flush();
do {
line = reader.readLine();
}
while (!"test".equals(line));
writer.println("exit 3");
writer.flush();
assertTrue(Set.of("BASH:exit 3", "exit 3").contains(reader.readLine()));
assertEquals(3, bash.waitExited());
}
}
@Test
public void testLocalEchoOn() throws IOException {
try (LinuxPty pty = LinuxPty.openpty()) {
pty.getChild().nullSession();
PrintWriter writer = new PrintWriter(pty.getParent().getOutputStream());
BufferedReader reader =
new BufferedReader(new InputStreamReader(pty.getParent().getInputStream()));
writer.println("Hello, World!");
writer.flush();
assertEquals("Hello, World!", reader.readLine());
}
}
@Test
public void testLocalEchoOff() throws IOException {
try (LinuxPty pty = LinuxPty.openpty()) {
pty.getChild().nullSession(Echo.OFF);
PrintWriter writerP = new PrintWriter(pty.getParent().getOutputStream());
PrintWriter writerC = new PrintWriter(pty.getChild().getOutputStream());
BufferedReader reader =
new BufferedReader(new InputStreamReader(pty.getParent().getInputStream()));
writerP.println("Hello, World!");
writerP.flush();
writerC.println("Good bye!");
writerC.flush();
assertEquals("Good bye!", reader.readLine());
}
}
}

View file

@ -0,0 +1,102 @@
/* ###
* 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 ghidra.pty.ssh;
import static org.junit.Assert.assertEquals;
import static org.junit.Assume.assumeFalse;
import java.io.*;
import org.junit.Before;
import org.junit.Test;
import ghidra.app.script.AskDialog;
import ghidra.pty.PtyChild.Echo;
import ghidra.pty.PtySession;
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.setUsername(promptUser());
}
public static String promptUser() throws CancelledException {
AskDialog<String> dialog = new AskDialog<>("SSH", "Username:", AskDialog.STRING, "");
if (dialog.isCanceled()) {
throw new CancelledException();
}
return dialog.getValueAsString();
}
public static class StreamPumper extends Thread {
private final InputStream in;
private final OutputStream out;
public StreamPumper(InputStream in, OutputStream out) {
setDaemon(true);
this.in = in;
this.out = out;
}
@Override
public void run() {
byte[] buf = new byte[1024];
try {
while (true) {
int len = in.read(buf);
if (len <= 0) {
break;
}
out.write(buf, 0, len);
}
}
catch (IOException e) {
}
}
}
@Test
public void testSessionBash() throws IOException, InterruptedException {
try (SshPty pty = factory.openpty()) {
PtySession bash = pty.getChild().session(new String[] { "bash" }, null);
OutputStream out = pty.getParent().getOutputStream();
out.write("exit\n".getBytes("UTF-8"));
out.flush();
new StreamPumper(pty.getParent().getInputStream(), System.out).start();
assertEquals(0, bash.waitExited());
}
}
@Test
public void testDisableEcho() throws IOException, InterruptedException {
try (SshPty pty = factory.openpty()) {
PtySession bash =
pty.getChild().session(new String[] { "bash" }, null, Echo.OFF);
OutputStream out = pty.getParent().getOutputStream();
out.write("exit\n".getBytes("UTF-8"));
out.flush();
new StreamPumper(pty.getParent().getInputStream(), System.out).start();
assertEquals(0, bash.waitExited());
}
}
}

View file

@ -0,0 +1,194 @@
/* ###
* 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 ghidra.pty.windows;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;
import java.io.*;
import java.lang.ProcessBuilder.Redirect;
import org.junit.Before;
import org.junit.Test;
import com.sun.jna.LastErrorException;
import ghidra.dbg.testutil.DummyProc;
import ghidra.framework.OperatingSystem;
import ghidra.pty.*;
public class ConPtyTest extends AbstractPtyTest {
@Before
public void checkWindows() {
assumeTrue(OperatingSystem.WINDOWS == OperatingSystem.CURRENT_OPERATING_SYSTEM);
}
@Test
public void testSessionCmd() throws IOException, InterruptedException {
try (Pty pty = ConPty.openpty()) {
PtySession cmd = pty.getChild().session(new String[] { DummyProc.which("cmd") }, null);
pty.getParent().getOutputStream().write("exit\r\n".getBytes());
assertEquals(0, cmd.waitExited());
}
}
@Test
public void testSessionNonExistent() throws IOException, InterruptedException {
try (Pty pty = ConPty.openpty()) {
pty.getChild().session(new String[] { "thisHadBetterNoExist" }, null);
fail();
}
catch (LastErrorException e) {
assertEquals(2, e.getErrorCode());
}
}
@Test
public void testSessionCmdEchoTest() throws IOException, InterruptedException {
try (Pty pty = ConPty.openpty()) {
PtyParent parent = pty.getParent();
PrintWriter writer = new PrintWriter(parent.getOutputStream());
BufferedReader reader = loggingReader(parent.getInputStream());
PtySession cmd = pty.getChild().session(new String[] { DummyProc.which("cmd") }, null);
runExitCheck(3, cmd);
writer.println("echo test");
writer.flush();
String line;
do {
line = reader.readLine();
}
while (!"test".equals(line));
writer.println("exit 3");
writer.flush();
assertEquals(3, cmd.waitExited());
}
}
@Test
public void testSessionGdbLineLength() throws IOException, InterruptedException {
try (Pty pty = ConPty.openpty()) {
PtyParent parent = pty.getParent();
PrintWriter writer = new PrintWriter(parent.getOutputStream());
BufferedReader reader = loggingReader(parent.getInputStream());
PtySession gdb =
pty.getChild().session(new String[] { "C:\\msys64\\mingw64\\bin\\gdb.exe" }, null);
writer.println(
"echo This line is cleary much, much, much, much, much, much, much, much, much " +
" longer than 80 characters");
writer.flush();
String line;
do {
line = reader.readLine();
}
while (!"test".equals(line));
}
}
@Test
public void testGdbInterruptPlain() throws Exception {
ProcessBuilder builder = new ProcessBuilder("C:\\msys64\\mingw64\\bin\\gdb.exe");
builder.redirectOutput(Redirect.PIPE);
builder.redirectInput(Redirect.PIPE);
builder.redirectErrorStream(true);
Process gdb = builder.start();
PrintWriter writer = new PrintWriter(gdb.getOutputStream());
pump(gdb.getInputStream(), System.err);
System.out.println("Testing");
writer.println("echo test");
writer.println("set new-console on");
System.out.println("Launching notepad");
writer.println("file C:\\\\Windows\\\\notepad.exe");
writer.println("run");
writer.flush();
System.out.println("Waiting");
Thread.sleep(3000);
System.out.println("Interrupting");
writer.write(3);
writer.println();
writer.flush();
System.out.println("Killing");
writer.println("kill");
writer.flush();
writer.println("y");
writer.flush();
}
@Test
public void testGdbInterruptConPty() throws Exception {
try (Pty pty = ConPty.openpty()) {
PtyParent parent = pty.getParent();
PrintWriter writer = new PrintWriter(parent.getOutputStream());
//BufferedReader reader = loggingReader(parent.getInputStream());
PtySession gdb =
pty.getChild().session(new String[] { "C:\\msys64\\mingw64\\bin\\gdb.exe" }, null);
pump(parent.getInputStream(), System.err);
System.out.println("Testing");
writer.println("echo test");
writer.println("set new-console on");
System.out.println("Launching notepad");
writer.println("file C:\\\\Windows\\\\notepad.exe");
writer.println("run");
writer.flush();
System.out.println("Waiting");
Thread.sleep(3000);
System.out.println("Interrupting");
writer.write(3);
writer.println();
writer.flush();
System.out.println("Killing");
writer.println("kill");
writer.flush();
writer.println("y");
writer.flush();
Thread.sleep(100000);
}
}
@Test
public void testGdbMiConPty() throws Exception {
try (Pty pty = ConPty.openpty()) {
PtyParent parent = pty.getParent();
PrintWriter writer = new PrintWriter(parent.getOutputStream());
//BufferedReader reader = loggingReader(parent.getInputStream());
PtySession gdb = pty.getChild()
.session(new String[] { "C:\\msys64\\mingw64\\bin\\gdb.exe", "-i", "mi2" },
null);
InputStream inputStream = parent.getInputStream();
inputStream = new AnsiBufferedInputStream(inputStream);
pump(inputStream, System.out);
writer.println("-interpreter-exec console \"echo test\"");
writer.println("-interpreter-exec console \"quit\"");
writer.flush();
gdb.waitExited();
//System.out.println("Exited");
}
}
}

View file

@ -0,0 +1,68 @@
/* ###
* 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 ghidra.pty.windows;
import java.io.*;
import com.sun.jna.LastErrorException;
import com.sun.jna.platform.win32.Kernel32;
import com.sun.jna.platform.win32.WinNT.HANDLE;
public class NamedPipeTest {
protected Handle checkHandle(HANDLE handle) {
if (Kernel32.INVALID_HANDLE_VALUE.equals(handle)) {
throw new LastErrorException(Kernel32.INSTANCE.GetLastError());
}
return new Handle(handle);
}
// @Test
/**
* Experiment with MinGW GDB, named pipes, and {@code new-ui}.
*
* <p>
* Run this test, start GDB in a command shell, then issue
* "{@code new-ui mi2 \\\\.\\pipe\\GhidraGDB}". With GDB 11.1, GDB will print "New UI allocated"
* to the console, and I'll receive {@code =thread-group-added,id="i1"} on the pipe. However,
* GDB never seems to process my {@code -add-inferior} command. Furthermore, GDB freezes and no
* longer accepts input on the console, either.
*/
public void testExpNamedPipes() throws Exception {
Handle hPipe = checkHandle(Kernel32.INSTANCE.CreateNamedPipe(
"\\\\.\\pipe\\GhidraGDB" /*lpName*/,
Kernel32.PIPE_ACCESS_DUPLEX /*dwOpenMode*/,
Kernel32.PIPE_TYPE_BYTE | Kernel32.PIPE_WAIT /*dwPipeMode*/,
Kernel32.PIPE_UNLIMITED_INSTANCES /*nMaxInstances*/,
1024 /*nOutBufferSize*/,
1024 /*nInBufferSize*/,
0 /*nDefaultTimeOut*/,
null /*lpSecurityAttributes*/));
try (PrintWriter writer = new PrintWriter(new HandleOutputStream(hPipe));
BufferedReader reader =
new BufferedReader(new InputStreamReader(new HandleInputStream(hPipe)))) {
writer.println("-add-inferior");
writer.flush();
String line;
while (null != (line = reader.readLine())) {
System.out.println(line);
}
}
}
}