Merge remote-tracking branch 'origin/patch'

This commit is contained in:
Ryan Kurtz
2026-06-23 09:53:01 -04:00
5 changed files with 271 additions and 20 deletions

View File

@@ -22,7 +22,7 @@ import com.sun.jdi.connect.AttachingConnector;
import com.sun.jdi.connect.Connector;
import com.sun.jdi.connect.Connector.Argument;
import ghidra.pty.ShellUtils;
import ghidra.pty.ShellUtils.Shell;
public class JdiArguments {
enum Mode {
@@ -101,7 +101,7 @@ public class JdiArguments {
}
String cp = env.get("OPT_TARGET_CLASSPATH");
if (!cp.isBlank()) {
args.get("options").setValue("-cp " + ShellUtils.generateArgument(cp));
args.get("options").setValue("-cp " + Shell.LOCAL.generateArgument(cp));
}
}
}

View File

@@ -50,6 +50,7 @@ import ghidra.program.model.address.Address;
import ghidra.program.model.listing.Program;
import ghidra.program.util.ProgramLocation;
import ghidra.pty.*;
import ghidra.pty.ShellUtils.Shell;
import ghidra.trace.model.Trace;
import ghidra.trace.model.TraceLocation;
import ghidra.util.*;
@@ -475,7 +476,7 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
parent.getInputStream(), parent.getOutputStream());
List<String> withoutPath = ShellUtils.removePath(commandLine);
terminal.setSubTitle(ShellUtils.generateLine(withoutPath));
terminal.setSubTitle(ShellUtils.generateLine(withoutPath, Shell.DISPLAY));
TerminalListener resizeListener = new TerminalListener() {
@Override
public void resized(short cols, short rows) {
@@ -490,8 +491,19 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
terminal.addTerminalListener(resizeListener);
env.put("TERM", "xterm-256color");
PtySession session =
pty.getChild().session(commandLine.toArray(String[]::new), env, workingDirectory);
PtySession session;
try {
session =
pty.getChild().session(commandLine.toArray(String[]::new), env, workingDirectory);
}
catch (Throwable t) {
terminal.terminated(-1);
pty.close();
for (TerminalSession ss : subordinates) {
ss.terminate();
}
throw t;
}
Thread waiter = new Thread(() -> {
try {
@@ -718,6 +730,7 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
* terminates early
*/
monitor.setMessage("Waiting for connection");
monitor.addCancelledListener(acceptor::cancel);
connection = acceptOrSessionEnds(acceptor, backEnd);
connection.registerTerminals(sessions.values());
monitor.increment();

View File

@@ -442,7 +442,9 @@ public class TerminalProvider extends ComponentProviderAdapter {
Swing.runIfSwingOrRunLater(() -> {
terminated = true;
panel.terminalListeners.invoke().terminated(exitcode);
removeLocalAction(actionTerminate);
if (actionTerminate != null) {
removeLocalAction(actionTerminate);
}
panel.terminalListeners.clear();
panel.setOutputCallback(buf -> {
});

View File

@@ -19,11 +19,28 @@ import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Collectors;
import ghidra.framework.OperatingSystem;
import ghidra.framework.Platform;
public class ShellUtils {
enum State {
NORMAL, NORMAL_ESCAPE, DQUOTE, DQUOTE_ESCAPE, SQUOTE, SQUOTE_ESCAPE;
}
/**
* Parse a command line into an argument list
* <p>
* LATER: This is meant to mimic UNIX- / C-style arguments, but that's probably not appropriate
* on all systems. We should either:
*
* <ol>
* <li>Let the offer specify how <code>@args</code> ought to be treated.</li>
* <li>Let the header annotations for <code>@args</code> include some extra specifier.</li>
* </ol>
*
* @param args the arguments as a single string
* @return the list of split arguments
*/
public static List<String> parseArgs(String args) {
List<String> argsList = new ArrayList<>();
StringBuilder curArg = new StringBuilder();
@@ -123,29 +140,207 @@ public class ShellUtils {
return List.copyOf(copy);
}
public static String generateLine(List<String> args) {
public static String generateLine(List<String> args, Shell shell) {
if (args.isEmpty()) {
return "";
}
StringBuilder line = new StringBuilder(generateArgument(args.get(0)));
StringBuilder line = new StringBuilder(shell.generateArgument(args.get(0)));
for (int i = 1; i < args.size(); i++) {
String a = args.get(i);
line.append(" " + generateArgument(a));
line.append(" " + shell.generateArgument(a));
}
return line.toString();
}
public static String generateArgument(String a) {
if (a.contains(" ")) {
if (a.contains("\"")) {
if (a.contains("'")) {
return "\"" + a.replace("\"", "\\\"") + "\"";
/**
* A target shell for command-line arguments
* <p>
* This determines how arguments are quoted and/or escaped. This should be set based on the
* shell that is going to receive the actual commands, which may or may not be the local shell.
* In many cases, it is the local shell, but please ensure for remote cases, the correct shell
* is specified.
*/
public enum Shell {
/**
* For display purposes only. DO NOT pass to any actual shell.
*/
DISPLAY {
@Override
public String generateArgument(String a) {
if (a.contains(" ")) {
if (a.contains("\"")) {
if (a.contains("'")) {
return '"' + a.replace("\"", "\\\"") + '"';
}
return "'" + a + "'";
}
return '"' + a + '"';
}
return "'" + a + "'";
return a;
}
return "\"" + a + "\"";
},
/**
* Unix shells that follow the same conventions as "sh". This is most Unix shells.
*/
UNIX_SH {
@Override
public String generateArgument(String a) {
StringBuilder b = new StringBuilder();
for (int i = 0; i < a.length(); i++) {
char c = a.charAt(i);
boolean esc = switch (c) {
case '\t', ' ', // Whitespace
'&', '|', ';', '`', '(', ')', // Syntax, command separators
'<', '>', // Redirection
'$', // Variable substitution
'#', // Comments
'[', ']', '?', '*', // File globbing
'"', '\'', // Quotes
'\\' // The escape character itself
-> true;
default -> false;
};
if (esc) {
b.append('\\');
}
b.append(c);
}
return b.toString();
}
},
/**
* Plain Windows command-line arguments for the C runtime
* <p>
* See <href a=
* "https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-commandlinetoargvw">CommandLineToArgvW
* function</a>
*/
WINDOWS {
/**
* Derived from <a href=
* "https://learn.microsoft.com/en-us/archive/blogs/twistylittlepassagesallalike/everyone-quotes-command-line-arguments-the-wrong-way">Everyone
* quotes command line arguments the wrong way</a>
* <p>
* The section titled "The Correct Solution" invites its readers to "translate it into
* your language and coding style off choice."
*/
@Override
public String generateArgument(String a) {
if (!a.isEmpty() && a.indexOf(' ') == -1 && a.indexOf('\t') == -1 &&
a.indexOf('\n') == -1 && a.indexOf('"') == -1) {
return a;
}
StringBuilder b = new StringBuilder();
b.append('"');
for (int i = 0;; i++) {
int nBackSlash = 0;
while (i < a.length() && a.charAt(i) == '\\') {
i++;
nBackSlash++;
}
if (i == a.length()) {
/**
* We reached the end of the argument while counting backslashes. Escape
* them all. The terminating " we add at the end of this method will be
* interpreted as a metacharacter.
*/
b.append("\\".repeat(nBackSlash * 2));
break;
}
else if (a.charAt(i) == '"') {
/**
* Sequence of backslashes ends in a ". Escape them all, including the ".
*/
b.append("\\".repeat(nBackSlash * 2 + 1));
b.append(a.charAt(i));
}
else {
/**
* They're just literal backslashes. Do not escape them. Be sure to add the
* current character, too.
*/
b.append("\\".repeat(nBackSlash)); // No *2
b.append(a.charAt(i));
}
}
b.append('"');
return b.toString();
}
},
/**
* The Windows cmd.exe shell.
* <p>
* <b>NOTE:</b> It seems to me using this with {@link ShellUtils#generateLine(List, Shell)}
* is futile, if the intent is to use specific argument numbers in the batch file, e.g.,
* <code>%1</code>. If you make clear certain constraints to the user, maybe it's suitable,
* but especially involving quotes, it's not possible to encode any arbitrary string. It
* seems the cmd shell is primarily concerned with just passing the arguments along to child
* processes, as encoded, and then the child figures out the parsing. That said, if the
* child process parses to argc/argv, then so long as the <em>full</em> command line is
* passed through the batch file, it should work as intended. However, if grabbing
* individual arguments, they cannot be reliably controlled.
* <p>
* LATER: There may be a way to factor the escaping part separately from the argument
* catenation part, so that special logic can be applied here to better guarantee argument
* numbering, but then there's still the issue if the final target is expected to parse to
* argc/argv, if that can be encoded reliably.
*/
WINDOWS_CMD {
@Override
public String generateArgument(String a) {
String quoted = WINDOWS.generateArgument(a);
StringBuilder b = new StringBuilder();
for (int i = 0; i < quoted.length(); i++) {
char c = quoted.charAt(i);
/**
* The list and rationale for each metacharacter comes from the same blogpost:
* <a href=
* "https://learn.microsoft.com/en-us/archive/blogs/twistylittlepassagesallalike/everyone-quotes-command-line-arguments-the-wrong-way">Everyone
* quotes command line arguments the wrong way</a>
*/
boolean esc = switch (c) {
case '(', ')', '%', '!', '<', '>', '&', '|', // Metacharacters
'"', // Prevent cmd from interpreting quotes
'^' // The escape character itself
-> true;
default -> false;
};
if (esc) {
b.append('^');
}
b.append(c);
}
return b.toString();
}
};
/**
* Get the probable shell for the given operating system
*
* @param os the operating system
* @return the shell, probably
*/
public static Shell forOs(OperatingSystem os) {
return switch (os) {
case OperatingSystem.WINDOWS -> Shell.WINDOWS;
default -> Shell.UNIX_SH;
};
}
return a;
/**
* The local shell, probably
*/
public static final Shell LOCAL = forOs(Platform.CURRENT_PLATFORM.getOperatingSystem());
/**
* Escape and/or quote a single command-line argument
*
* @param a the argument
* @return the argument formed in such a way that the shell will interpret it as the given
* string in one argument
*/
public abstract String generateArgument(String a);
}
public static String generateEnvBlock(Map<String, String> env) {

View File

@@ -28,6 +28,7 @@ import com.sun.jna.platform.win32.WinNT.HANDLE;
import ghidra.pty.PtyChild;
import ghidra.pty.ShellUtils;
import ghidra.pty.ShellUtils.Shell;
import ghidra.pty.local.LocalWindowsNativeProcessPtySession;
import ghidra.pty.windows.jna.ConsoleApiNative;
import ghidra.pty.windows.jna.ConsoleApiNative.STARTUPINFOEX;
@@ -75,12 +76,51 @@ public class ConPtyChild extends ConPtyEndpoint implements PtyChild {
return si;
}
private boolean isImplicitCmd(String[] args) {
if (args.length < 1) {
return false; // Really shouldn't, but let Windows decide how to fail
}
String lower0 = args[0].toLowerCase();
if (lower0.endsWith(".bat") || lower0.endsWith(".cmd")) {
return true;
}
/**
* I'm on the fence about this. While it's pretty clear that invoking a .bat file, which
* implicitly prefixes <code>cmd /c</code>, ought to escape the metacharacters, I'm not
* certain about when a user explicitly invokes <code>cmd /c</code>. I think it should let
* the metacharacters through, i.e., cmd should be permitted to do what the user probably
* intended. Still, if someone using this API unwittingly puts the <code>cmd /c</code>
* prefix on a user-supplied command line without sanitizing, they could create a
* vulnerability.
*/
/*if (args.length < 2) {
return false;
}
if (!"/c".equals(args[1])) {
return false;
}
if ("cmd".equals(lower0) || "cmd.exe".equals(lower0) || lower0.endsWith("\\cmd") ||
lower0.endsWith("\\cmd.exe")) {
return true;
}*/
return false;
}
/**
* {@inheritDoc}
* <p>
* <b>WARNING:</b> If arg[0], i.e., the application name, is a batch file, Windows will
* automatically invoke it using <code>cmd /c</code>. This method is aware of this implicit
* invocation and, upon detecting it, will appropriately escape cmd's metacharacters.
* <em>However</em>, if a client explicitly invokes <code>cmd /c</code> with any part of the
* command line formed from user-supplied arguments, IT MUST sanitize those arguments itself.
* This can be achieved using {@link Shell#generateArgument(String)} of
* {@link Shell#WINDOWS_CMD}.
*/
@Override
public LocalWindowsNativeProcessPtySession session(String[] args, Map<String, String> env,
File workingDirectory, Collection<TermMode> mode) throws IOException {
/**
* TODO: How to incorporate environment into CreateProcess?
*
* TODO: How to control local echo?
*/
@@ -92,7 +132,8 @@ public class ConPtyChild extends ConPtyEndpoint implements PtyChild {
STARTUPINFOEX si = prepareStartupInfo();
PROCESS_INFORMATION pi = new PROCESS_INFORMATION();
String commandLine = ShellUtils.generateLine(Arrays.asList(args));
Shell shell = isImplicitCmd(args) ? Shell.WINDOWS_CMD : Shell.WINDOWS;
String commandLine = ShellUtils.generateLine(Arrays.asList(args), shell);
if (!ConsoleApiNative.INSTANCE.CreateProcessW(
null /*lpApplicationName*/,