diff --git a/.gitignore b/.gitignore index 9154f4c..3c60c08 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,5 @@ hs_err_pid* replay_pid* +.project +.classpath \ No newline at end of file diff --git a/BulkUtils.java b/BulkUtils.java new file mode 100644 index 0000000..2673195 --- /dev/null +++ b/BulkUtils.java @@ -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 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 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 getFunctionsByRegex(GhidraScript gs, Pattern pattern) { + List 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 functions, String callingConvention) { + int cnt = 0; + Iterator iterator = functions.iterator(); + while(iterator.hasNext()) { + Function function = iterator.next(); + if(function.getCallingConventionName().equals(callingConvention)) { + iterator.remove(); + cnt++; + } + } + return cnt; + } + + static int removeFunctionsByDataType(List functions, DataType dataType) { + int cnt = 0; + Iterator iterator = functions.iterator(); + while(iterator.hasNext()) { + Function function = iterator.next(); + if(function.getReturnType().equals(dataType)) { + iterator.remove(); + cnt++; + } + } + return cnt; + } + + static boolean showFunctionConfirmationDialog(List 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 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]; + } + +} diff --git a/ExportFunctionsBulk.java b/ExportFunctionsBulk.java new file mode 100644 index 0000000..d537784 --- /dev/null +++ b/ExportFunctionsBulk.java @@ -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 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()); + + } +} diff --git a/SetCallingConventionBulk.java b/SetCallingConventionBulk.java new file mode 100644 index 0000000..8edc8da --- /dev/null +++ b/SetCallingConventionBulk.java @@ -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 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); + } + +} diff --git a/SetFuncParamByIndexBulk.java b/SetFuncParamByIndexBulk.java new file mode 100644 index 0000000..38909b7 --- /dev/null +++ b/SetFuncParamByIndexBulk.java @@ -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 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); + + } + +} diff --git a/SetReturnTypeBulk.java b/SetReturnTypeBulk.java new file mode 100644 index 0000000..c67ebf6 --- /dev/null +++ b/SetReturnTypeBulk.java @@ -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 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); + + } + +}