//A script to headlessly create FIDB files based on all programs within the given project. Based on "CreateMultipleLibraries.java" by the Ghidra team, inspired by Threatrack's work (https://blog.threatrack.de/2019/09/20/ghidra-fid-generator/) //@author Max 'Libra' Kersten for Trellix //@category FunctionID //@keybinding //@menupath //@toolbar import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TreeSet; import ghidra.app.script.GhidraScript; import ghidra.feature.fid.db.FidDB; import ghidra.feature.fid.db.FidFile; import ghidra.feature.fid.db.FidFileManager; import ghidra.feature.fid.db.LibraryRecord; import ghidra.feature.fid.service.FidPopulateResult; import ghidra.feature.fid.service.FidPopulateResult.Disposition; import ghidra.feature.fid.service.FidPopulateResultReporter; import ghidra.feature.fid.service.FidService; import ghidra.feature.fid.service.Location; import ghidra.framework.model.DomainFile; import ghidra.framework.model.DomainFolder; import ghidra.program.database.ProgramContentHandler; import ghidra.program.model.lang.LanguageID; import ghidra.program.model.mem.MemoryAccessException; import ghidra.util.Msg; import ghidra.util.exception.CancelledException; import ghidra.util.exception.VersionException; import ghidra.util.task.TaskMonitor; public class AutomaticallyCreateFunctionIdDatabases extends GhidraScript { /** * The Function ID service to be used */ private FidService fidService; /** * The path to the log file where all logs are written (appended) to */ private Path logFile; /** * Logs a message to Ghidra's console and to the log file in the specified * output folder * * @param message the message to print and write */ private void log(String message, boolean isError) { /* * Write the (error) message to Ghidra's console, depending on the given boolean */ if (isError) { printerr(message); } else { println(message); } /* * Try to append data to the log file */ try { // Append the error tag if the message is an error if (isError) { message = "ERROR: " + message; } // Append a newline to the message message += "\n"; // Write the data to the log file Files.write(logFile, message.getBytes(), StandardOpenOption.APPEND); } catch (IOException e) { // Ignore exceptions, the console log already contains the logged information } } /** * The reporter class, which implements the required Ghidra report class */ class FidReporter implements FidPopulateResultReporter { @Override public void report(FidPopulateResult result) { // Check if a result is given, if not, simply return if (result == null) { return; } // Get the library record from the result LibraryRecord libraryRecord = result.getLibraryRecord(); // Get the library family name, version, and variant String libraryFamilyName = libraryRecord.getLibraryFamilyName(); String libraryVersion = libraryRecord.getLibraryVersion(); String libraryVariant = libraryRecord.getLibraryVariant(); // Log the obtained information log(libraryFamilyName + ':' + libraryVersion + ':' + libraryVariant, false); // Log the attempted, added, and excluded count for the generated FIDB log(result.getTotalAttempted() + " total functions visited", false); log(result.getTotalAdded() + " total functions added", false); log(result.getTotalExcluded() + " total functions excluded", false); // Log all exclusions log("Breakdown of exclusions:", false); for (Entry entry : result.getFailures().entrySet()) { if (entry.getKey() != Disposition.INCLUDED) { log(" " + entry.getKey() + ": " + entry.getValue(), false); } } // Log all unresolved symbols log("List of unresolved symbols:", false); TreeSet symbols = new TreeSet<>(); for (Location location : result.getUnresolvedSymbols()) { symbols.add(location.getFunctionName()); } for (String symbol : symbols) { log(" " + symbol, false); } } } /** * Parses the symbols within the given file, reading it line-by-line where each * line is assumed to be a symbol * * @param commonSymbolsFile the file to read * @return a list of symbols to exclude when making FunctionID signatures * @throws IOException * @throws CancelledException */ private List parseSymbols(File commonSymbolsFile) throws IOException, CancelledException { // Check if the common symbols file exists if (commonSymbolsFile.exists() == false) { return null; } // If it exists, read the file BufferedReader reader = new BufferedReader(new FileReader(commonSymbolsFile)); // Create a new linked list to store the common symbols in List commonSymbols = new LinkedList<>(); // Read a single line from the file String line = reader.readLine(); // Iterate over all lines while (line != null) { /* * If the monitor isn't cancelled, continue, else throw an exception which * causes the whole script to halt */ monitor.checkCancelled(); // If the line of the length is not zero if (line.length() != 0) { // Add it to the list commonSymbols.add(line); } // Read the next line line = reader.readLine(); } // Once the entire file has been read, close the file access reader.close(); // Return all common symbols return commonSymbols; } /** * Recursively generate a mapping for all programs within the given folder and * its sub-folders * * @param folder the folder to start in * @param languageIdMapping the LanguageID mapping which is populated within * each call * @param rawLanguageIds the raw LanguageIDs * @param languageIds the LanguageID * @param compilerSpecMapping the compiler spec mapping * @throws CancelledException */ private void generateLanguageIdProgramMapping(Map> languageIdMapping, DomainFolder folder, Set rawLanguageIds, Set languageIds, Map> compilerSpecMapping) throws CancelledException { // Iterate over all domain files within the given folder for (DomainFile domainFile : folder.getFiles()) { /* * As within any longer loop, a cancel check is present to allow a user to * cancel the script in a timely manner */ monitor.checkCancelled(); /* * Only if the file is of the "PROGRAM" type, it is to be used */ if (domainFile.getContentType().equals(ProgramContentHandler.PROGRAM_CONTENT_TYPE)) { Map metadata = domainFile.getMetadata(); String languageId = metadata.get("Language ID"); if (languageId != null) { rawLanguageIds.add(languageId); /* * An example of a raw language ID is "x86:LE:32:default (3.0)". Splitting by * the space removes the version number, giving the language ID that is required * for later use. */ String filteredLanguageId = languageId.split(" ")[0]; languageIds.add(filteredLanguageId); /* * Attempt to get the value from the mapping for the given key. If the result is * null, no such key (nor value) is present in the mapping. */ List domainFiles = languageIdMapping.get(filteredLanguageId); /* * If the result is null, there is no key in the mapping, meaning it is new. A * new list is created, and the key and value are added to the mapping. */ if (domainFiles == null) { domainFiles = new ArrayList<>(); domainFiles.add(domainFile); languageIdMapping.put(filteredLanguageId, domainFiles); } else { /* * Since the list from the mapping is not null, the key exists with a value. Add * the current file to the list, and overwrite the mapping's entry with the new * value for the given key. */ domainFiles.add(domainFile); languageIdMapping.put(filteredLanguageId, domainFiles); } } } } // Iterate over all folders within the given folder for (DomainFolder domainFolder : folder.getFolders()) { monitor.checkCancelled(); generateLanguageIdProgramMapping(languageIdMapping, domainFolder, rawLanguageIds, languageIds, compilerSpecMapping); } } /** * Populates an FIDB file based on the given list of programs for the given * language ID, excluding the given common symbols * * @param fidDb the database to populate * @param languageID the LanguageID to match with * @param programs the programs to parse * @param commonSymbols the common symbols to exclude * @param reporter the report to use when returning results * @param projectName the project name to use within the database * @param projectVersion the version to use within the database * @throws CancelledException */ private void createLibraryFromPrograms(FidDB fidDb, LanguageID languageID, List programs, List commonSymbols, FidReporter reporter, String projectName, String projectVersion) throws CancelledException { /* * Set the library name, version, and variant */ String libraryName = projectName; String libraryVersion = projectVersion; String libraryVariant = languageID.getIdAsString(); // Create the message String message = "[" + libraryVariant + "] Generating \"" + libraryName + " (" + libraryVersion + ")\" based on " + programs.size() + " files"; // Log the message log(message, false); // Set the monitor message to inform the analyst when running via a GUI monitor.setMessage(message); try { // Call the FID service to create populate the new library FidPopulateResult result = fidService.createNewLibraryFromPrograms(fidDb, libraryName, libraryVersion, libraryVariant, programs, null, languageID, null, commonSymbols, TaskMonitor.DUMMY); // report the results via the reporter reporter.report(result); } catch (MemoryAccessException e) { Msg.showError(this, null, "Unexpected memory access exception", "Please notify the Ghidra team:", e); } catch (VersionException e) { Msg.showError(this, null, "Version Exception", "One of the programs in your domain folder cannot be upgraded: " + e.getMessage()); } catch (IllegalStateException e) { Msg.showError(this, null, "Illegal State Exception", "Unknown error: " + e.getMessage()); } catch (IOException e) { Msg.showError(this, null, "FidDb IOException", "Please notify the Ghidra team:", e); } } /** * Creates a new FunctionID database file * * @param fidbFile the file to be created * @param fidFileName the file name to use * @return the newly created FIDB file with write permissions * @throws IOException * @throws VersionException */ private FidDB createFidDB(File fidbFile, String fidFileName) throws IOException, VersionException { // Declare the object FidFile fidFile = null; // Get the FID file manager FidFileManager fidFileManager = FidFileManager.getInstance(); // Create a new database fidFileManager.createNewFidDatabase(fidbFile); // Get all user added FIDB files List fids = fidFileManager.getUserAddedFiles(); // Iterate over all FIDB files for (FidFile fid : fids) { // If the name equals the given file name if (fid.getName().equalsIgnoreCase(fidFileName)) { // If the match is there, set the return variable and break the loop fidFile = fid; break; } } // If no matching file was found, something went wrong and null is returned if (fidFile == null) { return null; } // Return the FIDB file with write permissions return fidFile.getFidDB(true); } @Override protected void run() throws Exception { /* * A note about the askSomething functions. This script is intended to be * executed by Ghidra in its headless mode, since the GUI serves no purpose in * this script. Any askSomething function will take an argument from the * command-line interface when running headless. As such, it allows one to use * the command-line interface in an easy manner, or with the help of a * properties file. In this case, all askSomething functions are listed in the * start of the run-method, since the command-line interface argument order * matters. This is a convenient way for users to understand what arguments are * to be provided when running this script headless, without going over all code * within this script. */ /* * Ask for the project name, which is to be used in the to-be created FIDBs */ String projectName = askString("Project name", "What is the project name?"); /* * Ask for the project version, which is to be used in the to-be created FIDBs */ String projectVersion = askString("Project version", "What is the project version?"); /* * Ask for the output folder. This will be the location where the FIDB files * along with the log file will be written to. */ String outputFolder = askString("Desired output folder", "What is the desired output folder?"); // Create a file object for the output folder File outputFolderFile = new File(outputFolder); // If the output folder does not exist, create all required folders if (outputFolderFile.exists() == false) { outputFolderFile.mkdirs(); } /* * Get the absolute path for the output folder, excluding a trailing folder * separator */ outputFolder = outputFolderFile.getAbsolutePath(); /* * Ask if a common symbols file is to be used */ boolean useCommonSymbolsFile = askYesNo("Common symbols file usage", "Do you want to use a common symbols file?"); /* * The path to the symbols file, if desired */ String symbolsFilePath = null; /* * The file object which refers to the provided path */ File commonSymbolsFile = null; /* * The list of symbols */ List symbols = null; /* * If a common symbols file is desired, the path to said file is requested, * after which a file object pointing to said location is instantiated. Lastly, * the symbols are parsed, and stored in the symbols list. If the common symbols * file cannot be found, the script returns early with a related error message */ if (useCommonSymbolsFile) { symbolsFilePath = askString("Common symbols file location", "What is the location of the common symbols file?"); log("Received the following location for the common symbols file: " + symbolsFilePath, false); commonSymbolsFile = new File(symbolsFilePath); if (commonSymbolsFile.exists() == false) { log("Cannot find the common symbols file:" + symbolsFilePath, true); return; } symbols = parseSymbols(commonSymbolsFile); } /* * Initialise the FunctionID service */ fidService = new FidService(); /* * Initialises the log file object */ File log = new File(outputFolder + File.separator + "log.txt"); /* * Check if the log file exists. If it does, no action is required, as data can * be appended to it. If it does not, the file itself is created */ if (log.exists() == false) { log.createNewFile(); } /* * Set the log file, which is used by the log function */ logFile = log.toPath(); /* * Logging is possible from this point */ log("Received project name: " + projectName, false); log("Received project version: " + projectVersion, false); log("Received output folder path: " + outputFolder, false); log("Received choice with regards to the usage of a common symbols file: " + useCommonSymbolsFile, false); /* * Initialise a new report instance, to later report on the FIDB generation * results */ FidReporter reporter = new FidReporter(); /* * Gets the root folder of the current project, allowing the recursive calls * later on to start at the root node */ DomainFolder rootFolder = state.getProject().getProjectData().getRootFolder(); /* * Contains a mapping of the original compiler spec IDs for the given list of * files */ Map> compilerSpecMapping = new HashMap<>(); /* * Contains all language IDs without version information */ Set languageIds = new HashSet<>(); /* * Contains all language IDs with version information */ Set rawLanguageIds = new HashSet<>(); /* * Initialises the mapping of language IDs and their respective domain files */ Map> languageIdProgramMapping = new HashMap<>(); /* * Generate a mapping, stored in a global variable, which contains unique * language ID strings as a key, and a list of programs associated with said * language ID as value. This mapping is used later on to make FIDBs for each * language ID, with the respective files. */ generateLanguageIdProgramMapping(languageIdProgramMapping, rootFolder, rawLanguageIds, languageIds, compilerSpecMapping); /* * Create a log message with all unique language IDs, including their version */ String collectLanguageIds = "Collected language IDs and their versions:\n"; for (String id : rawLanguageIds) { collectLanguageIds += "\t" + id + "\n"; } /* * Write the message to Ghidra's console and the log file */ log(collectLanguageIds, false); /* * Create a string which is to be logged, which contains each unique language ID * and the number of files that are to be processed for said language ID */ String languageIdFileMessage = ""; for (Map.Entry> entry : languageIdProgramMapping.entrySet()) { String languageID = entry.getKey(); List domainFiles = entry.getValue(); languageIdFileMessage += languageID + " contains " + domainFiles.size() + " files\n"; } // Log the language ID and file summary log(languageIdFileMessage, false); /* * Iterate over the language IDs without version information */ for (String id : languageIds) { /* * Replace the colon with a dot to avoid invalid file name errors. The replace * all function uses regular expressions for both arguments, meaning that the * dot has to be escaped */ String fileName = id.replaceAll(":", "\\.") + ".fidb"; /* * The FIDB file name is the file name that the file will have on disk, based on * the project name, project version, and the language ID */ String fidFileName = projectName.replaceAll(" ", "\\.") + "_" + projectVersion.replaceAll(" ", "\\.") + "_" + fileName; // Create a file object for the FIDB file File fidbFile = new File(outputFolder + File.separator + fidFileName); /* * The createNewFidDatabase function requires the file to NOT exist, meaning it * needs to be deleted if it exists */ if (fidbFile.exists()) { String error = "The FIDB file already exists: " + fidbFile.getAbsolutePath(); log(error, true); return; } /* * Create the FIDB file */ FidDB fidDb = createFidDB(fidbFile, fidFileName); /* * If createFidDB returns null, the file could not be found, and the execution * needs to exit */ if (fidDb == null) { log("Cannot find any FIDB file named \"" + fidFileName + "\"", true); return; } /* * Create a language ID object, which is used and required during the generation * of the FIDB */ LanguageID languageID = new LanguageID(id); /* * A list of all programs which fall under the given language ID, within the * given Ghidra project */ List programs = languageIdProgramMapping.get(id); /* * Generate the hashes for functions for each program in the list, for the given * language ID */ createLibraryFromPrograms(fidDb, languageID, programs, symbols, reporter, projectName, projectVersion); // Save the database, otherwise all changes will be lost fidDb.saveDatabase("Saving", monitor); // Close the FIDB fidDb.close(); } } }