initial commit

This commit is contained in:
DrFrugal 2024-01-22 05:51:31 +01:00
parent 35a1b96c9b
commit 62bfbbb16d
6 changed files with 429 additions and 0 deletions

2
.gitignore vendored
View File

@ -24,3 +24,5 @@
hs_err_pid* hs_err_pid*
replay_pid* replay_pid*
.project
.classpath

181
BulkUtils.java Normal file
View File

@ -0,0 +1,181 @@
import java.awt.BorderLayout;
import java.io.File;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextArea;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableColumn;
import javax.swing.table.TableRowSorter;
import ghidra.app.script.GhidraScript;
import ghidra.app.util.datatype.DataTypeSelectionDialog;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.data.DataType;
import ghidra.program.model.data.DataTypeManager;
import ghidra.program.model.lang.CompilerSpec;
import ghidra.program.model.lang.PrototypeModel;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.FunctionIterator;
import ghidra.util.data.DataTypeParser.AllowedDataTypes;
import ghidra.util.exception.CancelledException;
public class BulkUtils {
private static final Set<String> RESERVED_C_KEYWORDS = Set.of(
"auto", "break", "case", "char", "const", "continue", "default", "do", "double", "else",
"enum", "extern", "float", "for", "goto", "if", "inline", "int", "long", "register",
"restrict", "return", "short", "signed", "sizeof", "static", "struct", "switch",
"typedef", "union", "unsigned", "void", "volatile", "while", "_Alignas", "_Alignof",
"_Atomic", "_Bool", "_Complex", "_Generic", "_Imaginary", "_Noreturn", "_Static_assert",
"_Thread_local"
);
private static final Pattern C_PARAM_REGEX = Pattern.compile("[a-zA-Z_][a-zA-Z\\\\d_]*");
static File askForOutputFile(String suggestedFileName) {
JFileChooser fileChooser = new JFileChooser();
if(suggestedFileName != null) {
fileChooser.setSelectedFile(new File(suggestedFileName));
}
int fileChooserResult = fileChooser.showSaveDialog(null);
if(fileChooserResult == JFileChooser.APPROVE_OPTION) {
File outputFile = fileChooser.getSelectedFile();
if(outputFile.exists()) {
String question = "This file already exists, do you want to overwrite it?";
int confirmOverwriteResult = JOptionPane.showConfirmDialog(null, question, null, JOptionPane.YES_NO_OPTION);
if(confirmOverwriteResult != JOptionPane.YES_OPTION) {
return null;
}
}
return outputFile;
}
return null;
}
static boolean isValidCParameterName(String parameterName) {
if(parameterName == null || parameterName.isEmpty()) return false;
if(RESERVED_C_KEYWORDS.contains(parameterName)) return false;
return C_PARAM_REGEX.matcher(parameterName).matches();
}
static DataType askForDataType(GhidraScript gs, String title) {
DataTypeManager dataTypeManager = gs.getCurrentProgram().getDataTypeManager();
PluginTool tool = gs.getState().getTool();
DataTypeSelectionDialog typeDialog = new DataTypeSelectionDialog(tool, dataTypeManager, -1, AllowedDataTypes.FIXED_LENGTH);
if(title != null) {
typeDialog.setTitle(title);
}
tool.showDialog(typeDialog);
return typeDialog.getUserChosenDataType();
}
static void printDoneMessage(GhidraScript gs, int success, int fail) {
gs.println("Done: successful: " + success + " | failed: " + fail);
}
static String askForCallingConvention(GhidraScript gs) throws CancelledException {
CompilerSpec compilerSpec = gs.getCurrentProgram().getCompilerSpec();
PrototypeModel[] callingConventions = compilerSpec.getCallingConventions();
List<String> choices = new ArrayList<>();
choices.add(Function.DEFAULT_CALLING_CONVENTION_STRING);
choices.add(Function.UNKNOWN_CALLING_CONVENTION_STRING);
for(PrototypeModel callingConvention : callingConventions) {
choices.add(callingConvention.getName());
}
return gs.askChoice(null, "Calling convention:", choices, choices.get(0));
}
static Integer askForInteger(GhidraScript gs, String whatFor) throws CancelledException {
String input = gs.askString(null, whatFor);
try {
return Integer.parseInt(input, 10);
} catch(NumberFormatException nfe) {}
return null;
}
static Pattern askForRegex(GhidraScript gs, String whatToMatch) throws CancelledException {
String regex = gs.askString("Enter Regex", "Pattern to match " + whatToMatch + ":");
Pattern pattern = Pattern.compile(regex);
return pattern;
}
static List<Function> getFunctionsByRegex(GhidraScript gs, Pattern pattern) {
List<Function> functions = new ArrayList<>();
FunctionIterator iterator = gs.getCurrentProgram().getFunctionManager().getFunctions(true);
while (iterator.hasNext() && !gs.getMonitor().isCancelled()) {
Function function = iterator.next();
if (pattern.matcher(function.getName()).matches()) {
functions.add(function);
}
}
return functions;
}
static int removeFunctionsByCallingConvention(List<Function> functions, String callingConvention) {
int cnt = 0;
Iterator<Function> iterator = functions.iterator();
while(iterator.hasNext()) {
Function function = iterator.next();
if(function.getCallingConventionName().equals(callingConvention)) {
iterator.remove();
cnt++;
}
}
return cnt;
}
static int removeFunctionsByDataType(List<Function> functions, DataType dataType) {
int cnt = 0;
Iterator<Function> iterator = functions.iterator();
while(iterator.hasNext()) {
Function function = iterator.next();
if(function.getReturnType().equals(dataType)) {
iterator.remove();
cnt++;
}
}
return cnt;
}
static boolean showFunctionConfirmationDialog(List<Function> functions, String title, int skipped, String skippedString) {
DefaultTableModel tableModel = new DefaultTableModel(new String[] {"Name", "Entry Point"}, 0);
functions.forEach(function -> {
tableModel.addRow(new String[] {function.getName(), function.getEntryPoint().toString().toUpperCase()});
});
TableRowSorter<DefaultTableModel> sorter = new TableRowSorter<>(tableModel);
sorter.setSortsOnUpdates(true);
sorter.toggleSortOrder(0); // Sort by first column (Name)
JTable table = new JTable(tableModel);
table.setRowSorter(sorter);
TableColumn entryPointColumn = table.getColumnModel().getColumn(1);
int fixedWidthEntryPoint = 130;
entryPointColumn.setMinWidth(fixedWidthEntryPoint);
entryPointColumn.setMaxWidth(fixedWidthEntryPoint);
entryPointColumn.setPreferredWidth(fixedWidthEntryPoint);
JScrollPane scrollPane = new JScrollPane(table);
JPanel panel = new JPanel(new BorderLayout());
if(skippedString != null) {
JTextArea skippedText = new JTextArea(String.format(skippedString, skipped));
skippedText.setEditable(false);
panel.add(skippedText, BorderLayout.NORTH);
}
panel.add(scrollPane, BorderLayout.CENTER);
Object[] options = { "OK", "Cancel" };
JOptionPane optionPane = new JOptionPane(panel, JOptionPane.PLAIN_MESSAGE, JOptionPane.OK_CANCEL_OPTION, null, options, options[1]); // select cancel initially
JDialog confirmDialog = optionPane.createDialog(null, title);
confirmDialog.setResizable(true);
confirmDialog.setVisible(true);
return optionPane.getValue() == options[0];
}
}

58
ExportFunctionsBulk.java Normal file
View File

@ -0,0 +1,58 @@
//TODO write a description for this script
//@author
//@category Export
//@keybinding
//@menupath
//@toolbar bomb
import java.io.File;
import java.io.PrintWriter;
import java.util.List;
import java.util.regex.Pattern;
import ghidra.app.decompiler.DecompInterface;
import ghidra.app.decompiler.DecompileOptions;
import ghidra.app.decompiler.DecompileResults;
import ghidra.app.script.GhidraScript;
import ghidra.program.model.listing.Function;
public class ExportFunctionsBulk extends GhidraScript {
@Override
public void run() throws Exception {
Pattern pattern = BulkUtils.askForRegex(this, "entire function name");
List<Function> functions = BulkUtils.getFunctionsByRegex(this, pattern);
if(functions.size() == 0) {
println("Abort: no functions match the provided regex");
return;
}
String programName = currentProgram.getName();
DecompInterface decompiler = new DecompInterface();
DecompileOptions options = new DecompileOptions();
options.setMaxWidth(1024); // easier parsing if everything is in one line
decompiler.setOptions(options);
decompiler.openProgram(currentProgram);
String fileName = programName + ".c";
File outputFile = BulkUtils.askForOutputFile(fileName);
if(outputFile == null){
println("Abort: export dialog has been cancelled");
return;
}
PrintWriter decompWriter = new PrintWriter(outputFile);
int decompileTimeout = 10; // seconds
functions.forEach(function -> {
decompWriter.write("// ADDRESS - 0x" + function.getEntryPoint().toString().toUpperCase() + "\n");
DecompileResults decompileResults = decompiler.decompileFunction(function, decompileTimeout, null);
String decompiledCode = decompileResults.getDecompiledFunction().getC();
decompWriter.println(decompiledCode.trim() + "\n\n");
});
decompWriter.close();
println(functions.size() + " matching function(s) exported to " + outputFile.getAbsolutePath());
}
}

View File

@ -0,0 +1,52 @@
//TODO write a description for this script
//@author
//@category _NEW_
//@keybinding
//@menupath
//@toolbar bomb
import java.util.List;
import java.util.regex.Pattern;
import ghidra.app.script.GhidraScript;
import ghidra.program.model.listing.Function;
public class SetCallingConventionBulk extends GhidraScript {
public void run() throws Exception {
String callingConvention = BulkUtils.askForCallingConvention(this);
Pattern pattern = BulkUtils.askForRegex(this, "entire function name");
List<Function> matchingFunctions = BulkUtils.getFunctionsByRegex(this, pattern);
int skipped = BulkUtils.removeFunctionsByCallingConvention(matchingFunctions, callingConvention);
int numOfMatches = matchingFunctions.size();
// only abort if there are no matches AND no matches have been skipped, so the users understands why there are no matches
if(numOfMatches == 0 && skipped == 0){
println("Abort: no matches found");
return;
}
String title = "Set calling convention of " + matchingFunctions.size() + " function(s) to " + callingConvention;
boolean confirmed = BulkUtils.showFunctionConfirmationDialog(matchingFunctions, title, skipped, "Number of matches skipped (calling convention already matches): %d");
if (!confirmed) {
println("Abort: negative confirmation");
return;
}
int success = 0;
int fail = 0;
for (Function func : matchingFunctions) {
try {
func.setCallingConvention(callingConvention);
success++;
} catch (Exception e) {
println("Failed to update function " + func.getName() + " at " + func.getEntryPoint() + " - " + e.getMessage());
fail++;
}
}
BulkUtils.printDoneMessage(this, success, fail);
}
}

View File

@ -0,0 +1,76 @@
//Sets function parameters matching a provided regex at a provided index.
//@author DrFrugal
//@category Functions
//@keybinding
//@menupath
//@toolbar bomb
import java.util.List;
import java.util.regex.Pattern;
import ghidra.app.script.GhidraScript;
import ghidra.program.model.data.DataType;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.Parameter;
import ghidra.program.model.symbol.SourceType;
import ghidra.util.exception.DuplicateNameException;
import ghidra.util.exception.InvalidInputException;
public class SetFuncParamByIndexBulk extends GhidraScript {
public void run() throws Exception {
Pattern pattern = BulkUtils.askForRegex(this, "entire function name");
List<Function> matchingFunctions = BulkUtils.getFunctionsByRegex(this, pattern);
boolean confirmed = BulkUtils.showFunctionConfirmationDialog(matchingFunctions, "Confirm " + matchingFunctions.size() + " matches", -1, "Please confirm these matches before we proceed.");
if (!confirmed) {
println("Abort: negative confirmation");
return;
}
Integer paramIndex = BulkUtils.askForInteger(this, "Parameter index:");
if(paramIndex == null) {
println("Abort: index couldn't be parsed");
return;
}
DataType paramType = BulkUtils.askForDataType(this, "Parameter type");
if (paramType == null) {
println("Abort: no type selected");
return;
}
String paramName = askString(null, "Parameter name:");
if(!BulkUtils.isValidCParameterName(paramName)) {
println("Abort: invalid C parameter name");
return;
}
int success = 0;
int fail = 0;
for(Function function : matchingFunctions) {
Parameter param = function.getParameter(paramIndex);
if(param == null) {
println("Unable to retrieve parameter in " + function.getName());
fail++;
continue;
}
if(param.isAutoParameter()) {
println("Skipping auto parameter in " + function.getName());
fail++;
continue;
}
try {
param.setName(paramName, SourceType.USER_DEFINED);
success++;
} catch (DuplicateNameException | InvalidInputException e) {
printerr("Failed setting parameter name in " + function.getName());
fail++;
}
}
BulkUtils.printDoneMessage(this, success, fail);
}
}

60
SetReturnTypeBulk.java Normal file
View File

@ -0,0 +1,60 @@
//Select a fixed-size data type, and then provide regex to select functions whose return type should be set to it.
//Results of that regex will be shown in a dialog for confirmation.
//@author DrFrugal
//@category Functions
//@keybinding
//@menupath
//@toolbar bomb
import java.util.List;
import java.util.regex.Pattern;
import ghidra.app.script.GhidraScript;
import ghidra.program.model.data.DataType;
import ghidra.program.model.listing.Function;
import ghidra.program.model.symbol.SourceType;
public class SetReturnTypeBulk extends GhidraScript {
@Override
public void run() throws Exception {
DataType selectedDataType = BulkUtils.askForDataType(this, "Return type");
if (selectedDataType == null) {
println("Abort: no type selected");
return;
}
Pattern pattern = BulkUtils.askForRegex(this, "entire function name");
List<Function> matchingFunctions = BulkUtils.getFunctionsByRegex(this, pattern);
int numOfTypeAlreadyCorrect = BulkUtils.removeFunctionsByDataType(matchingFunctions, selectedDataType);
int numOfMatches = matchingFunctions.size();
// only abort if there are no matches AND no matches have been skipped, so the users understands why there are no matches
if(numOfMatches == 0 && numOfTypeAlreadyCorrect == 0){
println("Abort: no matches found");
return;
}
String title = "Set return type of " + matchingFunctions.size() + " function(s) to " + selectedDataType.getSourceArchive().getName() + selectedDataType.getPathName();
boolean confirmed = BulkUtils.showFunctionConfirmationDialog(matchingFunctions, title, numOfTypeAlreadyCorrect, "Number of matches skipped (return type already matches): %d");
if (!confirmed) {
println("Abort: negative confirmation");
return;
}
int success = 0;
int fail = 0;
for (Function func : matchingFunctions) {
try {
func.setReturnType(selectedDataType, SourceType.USER_DEFINED);
success++;
} catch (Exception e) {
println("Failed to update function " + func.getName() + " at " + func.getEntryPoint() + " - " + e.getMessage());
fail++;
}
}
BulkUtils.printDoneMessage(this, success, fail);
}
}