diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d05bbbf..93242e3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,54 +2,66 @@ name: build on: push: - branches: [ "main" ] + branches: [ "*" ] pull_request: - branches: [ "main" ] + branches: [ "*" ] + release: + types: [ published ] + jobs: build: runs-on: ubuntu-latest strategy: matrix: - # Root directory for doing Ghidra work (building, etc.) - root: ["/tmp/ghidra"] - # Ghidra build version(s) - version: ["10.4", "11.0.2"] - include: - - version: "10.4" - release_url: "https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_10.4_build" - filename: "ghidra_10.4_PUBLIC_20230928.zip" - directory: "ghidra_10.4_PUBLIC" - - version: "11.0.2" - release_url: "https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_11.0.2_build" - filename: "ghidra_11.0.2_PUBLIC_20240326.zip" - directory: "ghidra_11.0.2_PUBLIC" + version: + - "11.1.2" steps: - - uses: actions/checkout@v3 + - name: Clone Repository + uses: actions/checkout@v4 + + - name: Install Java + uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: "21" - - name: Download Ghidra - run: | - wget -P ${{matrix.root}} -q ${{matrix.release_url}}/${{matrix.filename}} - unzip -d ${{matrix.root}} -q ${{matrix.root}}/${{matrix.filename}} + - name: Install Gradle + uses: gradle/actions/setup-gradle@v3 - - name: Setup Java - uses: actions/setup-java@v3 + - name: Install Ghidra ${{matrix.version}} + uses: antoniovazquezblanco/setup-ghidra@v2.0.3 with: - java-version: 17 - distribution: temurin + version: ${{matrix.version}} - name: Build plugin via gradle - run: gradle -PGHIDRA_INSTALL_DIR=${{matrix.root}}/${{matrix.directory}} -PZIP_NAME_PREFIX=ghidra_${{matrix.version}} + run: gradle -PZIP_NAME_PREFIX=ghidra_${{matrix.version}} - # Uploading a ZIP file as an artifact creates a double-ZIP - - name: Fix artifact ZIP - run: unzip -d dist/${{matrix.version}} dist/*_${{matrix.version}}_*.zip + - name: Rename artifact ZIP + run: mv dist/*_${{matrix.version}}_*.zip dist/ghidra_${{matrix.version}}_${{github.event.repository.name}}.zip - # Upload the unzipped contents as the artifact to create a Ghidra-loadable ZIP file - - name: Upload artifact - uses: actions/upload-artifact@v3 + - name: Upload artifacts + uses: actions/upload-artifact@v4 with: - name: ghidra_${{matrix.version}}_Cartographer - path: dist/${{matrix.version}}/* - if-no-files-found: error + name: ghidra_${{matrix.version}}_${{github.event.repository.name}} + path: dist/*${{github.event.repository.name}}.zip + + release: + permissions: + contents: write + runs-on: ubuntu-latest + needs: build + if: github.event_name == 'release' && github.event.action == 'published' + + steps: + - name: Clone Repository + uses: actions/checkout@v4 + + - name: Download binaries + uses: actions/download-artifact@v4 + + - name: Upload release ZIP + env: + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + run: gh release upload ${{github.event.release.tag_name}} *${{github.event.repository.name}}/*.zip diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 8b24c91..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: release - -on: - release: - types: [published] - -# Required for updating published release contents -permissions: - contents: write - -jobs: - release: - runs-on: ubuntu-latest - strategy: - matrix: - # Root directory for doing Ghidra work (building, etc.) - root: ["/tmp/ghidra"] - # Repository name ("Cartographer") - name: ["${{github.event.repository.name}}"] - # Ghidra build version(s) - version: ["10.4", "11.0.2"] - include: - - version: "10.4" - release_url: "https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_10.4_build" - filename: "ghidra_10.4_PUBLIC_20230928.zip" - directory: "ghidra_10.4_PUBLIC" - - version: "11.0.2" - release_url: "https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_11.0.2_build" - filename: "ghidra_11.0.2_PUBLIC_20240326.zip" - directory: "ghidra_11.0.2_PUBLIC" - - steps: - - uses: actions/checkout@v3 - - - name: Download Ghidra - run: | - wget -P ${{matrix.root}} -q ${{matrix.release_url}}/${{matrix.filename}} - unzip -d ${{matrix.root}} -q ${{matrix.root}}/${{matrix.filename}} - - - name: Setup Java - uses: actions/setup-java@v3 - with: - java-version: 17 - distribution: temurin - - - name: Build plugin via gradle - run: gradle -PGHIDRA_INSTALL_DIR=${{matrix.root}}/${{matrix.directory}} - - - name: Rename ZIP for upload - run: mv dist/*_${{matrix.version}}_*.zip dist/ghidra_${{matrix.version}}_${{matrix.name}}.zip - - - name: Upload release ZIP - env: - GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} - run: gh release upload ${{github.event.release.tag_name}} dist/ghidra_${{matrix.version}}_${{matrix.name}}.zip diff --git a/src/main/java/cartographer/CartographerModel.java b/src/main/java/cartographer/CartographerModel.java index 11cf43b..cf61d01 100644 --- a/src/main/java/cartographer/CartographerModel.java +++ b/src/main/java/cartographer/CartographerModel.java @@ -201,7 +201,7 @@ public String getColumnName() { @Override public String getValue(TableRowObject rowObject, Settings settings, Object data, ServiceProvider services) throws IllegalArgumentException { - return String.format("0x%08X", rowObject.getFunctionAddress().getUnsignedOffset()); + return String.format("0x%08X", rowObject.getFunctionAddress().getUnsignedOffset()); } } diff --git a/src/main/java/cartographer/CartographerPlugin.java b/src/main/java/cartographer/CartographerPlugin.java index bbb7d35..48c5f92 100644 --- a/src/main/java/cartographer/CartographerPlugin.java +++ b/src/main/java/cartographer/CartographerPlugin.java @@ -67,7 +67,7 @@ @PluginInfo( status = PluginStatus.RELEASED, packageName = MiscellaneousPluginPackage.NAME, - category = PluginCategoryNames.DECOMPILER, + category = PluginCategoryNames.ANALYSIS, shortDescription = "Code coverage parser", description = "Plugin for loading and processing code coverage data." ) @@ -299,39 +299,10 @@ public void actionPerformed(ActionContext context) { throw new AssertionError(e.getMessage()); } - // Only process if no errors were encountered - if (file.getStatusCode() != CoverageFile.STATUS.OK) { - Utils.showError( - file.getStatusCode().toString(), - file.getStatusMessage() - ); - return; + // Attempt to process the code coverage file + if (!processCoverageFile(file)) { + return; } - - // Load the coverage file data - if (!loadCoverageFile(file)) { - return; - } - - // Set the selected file for the provider - provider.setSelectedFile(file); - - // Set to loaded - loaded = true; - - // Reload the model - provider.setFileLoadedFlag(); - provider.getModel().reload(); - - // Associate the model with the file - file.setModel(provider.getModel()); - - // Give the loaded file a unique ID - file.setId(loadedFiles.size()); - file.setAlphaId(Utils.idToAlpha(loadedFiles.size())); - - // Add the file data to the list of loaded files - loadedFiles.put(file.getId(), file); }); } }; @@ -502,7 +473,7 @@ public void colorizeListing(CoverageFile file) { * * @return True if successfully loaded coverage file, false if not */ - private boolean loadCoverageFile(CoverageFile file) { + public boolean loadCoverageFile(CoverageFile file) { // Clear out function map file.clearCoverageFunctions(); @@ -553,7 +524,7 @@ private boolean loadCoverageFile(CoverageFile file) { ); // Bail if no option was chosen - if (response == null) { + if (response == null) { return false; } @@ -593,6 +564,53 @@ else if (file.getType().equals("ezcov")) { return true; } + + /** + * Processes the given code coverage file. + * + * @param file Coverage file to process + * + * @return Whether or not the coverage file was successfully processed + */ + public boolean processCoverageFile(CoverageFile file) { + + // Only process if no errors were encountered + if (file.getStatusCode() != CoverageFile.STATUS.OK) { + Utils.showError( + file.getStatusCode().toString(), + file.getStatusMessage() + ); + return false; + } + + // Load the coverage file data + if (!loadCoverageFile(file)) { + return false; + } + + // Set the selected file for the provider + provider.setSelectedFile(file); + + // Set to loaded + loaded = true; + + // Reload the model + provider.setFileLoadedFlag(); + provider.getModel().reload(); + + // Associate the model with the file + file.setModel(provider.getModel()); + + // Give the loaded file a unique ID + file.setId(loadedFiles.size()); + file.setAlphaId(Utils.idToAlpha(loadedFiles.size())); + + // Add the file data to the list of loaded files + loadedFiles.put(file.getId(), file); + + // Successfully processed + return true; + } /** * Gets the provider for the plugin. diff --git a/src/main/java/cartographer/CoverageFile.java b/src/main/java/cartographer/CoverageFile.java index 88b3715..406c32f 100644 --- a/src/main/java/cartographer/CoverageFile.java +++ b/src/main/java/cartographer/CoverageFile.java @@ -42,6 +42,7 @@ public class CoverageFile { public enum STATUS { OK("File loaded."), HEADER_ERROR("Unknown file header."), + DRCOV_MODULE_VERSION_ERROR("Unknown DrCov Module version."), DRCOV_MODULE_TABLE_ERROR("DrCov Module error."), DRCOV_BBTABLE_ERROR("DrCov BB Table error."), DRCOV_MODULE_ERROR("DrCov Module error."), @@ -178,28 +179,25 @@ private void parseDrCovFile(RandomAccessFile reader) throws IOException { // Update strings if any matches were found if (match.find()) { - version = Integer.parseInt((match.group(1) != null) ? match.group(1) : "0"); + // Default to version 1 if module table version doesn't exist + version = Integer.parseInt((match.group(1) != null) ? match.group(1) : "1"); numModules = Integer.parseInt(match.group(2)); } + + // Skip column names if this wasn't a version 1 DRCOV module set + if (version > 1) { + reader.readLine(); + } - // Skip the column names - reader.readLine(); - - // Parse version 5 modules + // Parse each module for (int i = 0; i < numModules; i++) { // Read each module line = reader.readLine(); String[] moduleData = line.split(","); - - // Get the parsed data - int moduleId = Integer.parseInt(moduleData[0].trim()); - int parentId = Integer.parseInt(moduleData[1].trim()); - int base = Integer.parseInt(moduleData[5].trim(), 16); - String name = moduleData[moduleData.length-1].trim(); - - // Create a new DrCov module - DrCovModule module = new DrCovModule(moduleId, parentId, base, name); + + // Read the DRCOV module data and add it to the list of modules + DrCovModule module = parseDrCovModule(moduleData); drcovModules.add(module); } @@ -252,7 +250,7 @@ private void parseDrCovFile(RandomAccessFile reader) throws IOException { match = Pattern.compile("module\\[\\s*(\\d+)\\]: 0x([0-9a-fA-F]+?),\\s*(\\d+)").matcher(line); if (match.find()) { int moduleId = Integer.parseInt(match.group(1)) & 0xFFFF; - int offset = Integer.parseInt(match.group(2), 16); + int offset = Integer.parseUnsignedInt(match.group(2), 16); short size = Short.parseShort(match.group(3)); // Make sure the module ID is valid @@ -268,6 +266,50 @@ private void parseDrCovFile(RandomAccessFile reader) throws IOException { populateModules(drcovModules); } + /** + * Parses a DRCOV module. + * + * @param moduleData List of module string data + * + * @return DRCOV module data + */ + private DrCovModule parseDrCovModule(String[] moduleData) { + + // Module ID is always at position 0 + int moduleId = Integer.parseInt(moduleData[0].trim()); + + // File path always comes last + String name = moduleData[moduleData.length-1].trim(); + + // Default parent ID and base position for versions 1 and 2 + int parentId = 0; + int basePos = 1; + + // Process versions 3 and above + if (version > 2) { + + // Versions 3, 4, and 5 all have the parent ID at position 1 + parentId = Integer.parseInt(moduleData[1].trim()); + + // Version 3 has the base address at position 2 + if (version == 3) { + basePos = 2; + } + + // Versions 4 and 5 have the base address at position 5 + else { + basePos = 5; + } + } + + // Get the base address based on its position + long base = Long.parseUnsignedLong(moduleData[basePos].trim().replace("0x", ""), 16); + + // Create and return the DrCovModule from the parsed module entry + DrCovModule module = new DrCovModule(moduleId, parentId, base, name); + return module; + } + /** * Populates the map of usable modules with those read from the DRCOV file. * @@ -289,7 +331,11 @@ private void populateModules(List modList) { // Add each block to the new module for (BasicBlock block : module.getBasicBlocks()) { - newMod.addBlock(block.offset + module.base, block.size, block.moduleId); + // Add the base address to the offset if this was a v5 module + if (version == 5) { + block.offset += module.base; + } + newMod.addBlock(block.offset, block.size, block.moduleId); } // Add the new module to the module list @@ -333,7 +379,7 @@ private EzCovModule parseEzCovFile(RandomAccessFile reader) throws IOException { String addressSpace = match.group(3); // Create a block from the data - module.addBlock((int)offset, size, addressSpace); + module.addBlock(offset, size, addressSpace); } // Read the next line @@ -388,7 +434,7 @@ public class DrCovModule extends CodeCoverageModule { private int moduleId; private int parentId; - private int base; + private long base; private String name; /** @@ -399,7 +445,7 @@ public class DrCovModule extends CodeCoverageModule { * @param base Base memory address * @param name Name of the file */ - public DrCovModule(int moduleId, int parentId, int base, String name) { + public DrCovModule(int moduleId, int parentId, long base, String name) { this.moduleId = moduleId; this.parentId = parentId; this.base = base; @@ -413,7 +459,7 @@ public DrCovModule(int moduleId, int parentId, int base, String name) { * @param size Size of the block in bytes * @param module Module ID */ - private void addBlock(int offset, short size, int module) { + private void addBlock(long offset, short size, int module) { BasicBlock basicBlock = new BasicBlock(offset, size, module); this.getBasicBlocks().add(basicBlock); } @@ -438,7 +484,7 @@ public EzCovModule() { * @param size Size of the block in bytes * @param addressSpace Address space of the block */ - private void addBlock(int offset, short size, String addressSpace) { + private void addBlock(long offset, short size, String addressSpace) { BasicBlock basicBlock = new BasicBlock(offset, size, addressSpace); this.getBasicBlocks().add(basicBlock); } @@ -448,7 +494,7 @@ private void addBlock(int offset, short size, String addressSpace) { * Represents a basic block. */ public class BasicBlock { - private int offset; + private long offset; private short size; private int moduleId; private AddressSpace addressSpace; @@ -460,7 +506,7 @@ public class BasicBlock { * @param size Size of the block in bytes * @param moduleId Module ID */ - public BasicBlock(int offset, short size, int moduleId) { + public BasicBlock(long offset, short size, int moduleId) { this.offset = offset; this.size = size; this.moduleId = moduleId; @@ -473,7 +519,7 @@ public BasicBlock(int offset, short size, int moduleId) { * @param size Size of the block in bytes * @param addressSpace Address space of the block */ - public BasicBlock(int offset, short size, String addressSpace) { + public BasicBlock(long offset, short size, String addressSpace) { this.offset = offset; this.size = size; this.addressSpace = CartographerPlugin.getAddressSpace(addressSpace); @@ -485,7 +531,7 @@ public BasicBlock(int offset, short size, String addressSpace) { * @param offset Memory offset of the block * @param size Size of the block in bytes */ - public BasicBlock(int offset, short size) { + public BasicBlock(long offset, short size) { this.offset = offset; this.size = size; } @@ -517,7 +563,7 @@ public void populateBlocks(Program program) { } // Get the address within the address space - Address address = block.addressSpace.getAddressInThisSpaceOnly(Integer.toUnsignedLong(block.offset)); + Address address = block.addressSpace.getAddressInThisSpaceOnly(block.offset); // Check if address is relative to the address space offset Address spaceOffset = block.addressSpace.getMinAddress(); diff --git a/src/main/java/cartographer/CoverageFunction.java b/src/main/java/cartographer/CoverageFunction.java index 7cc70ad..c78f8ae 100644 --- a/src/main/java/cartographer/CoverageFunction.java +++ b/src/main/java/cartographer/CoverageFunction.java @@ -20,7 +20,7 @@ import ghidra.program.model.address.AddressRange; import ghidra.program.model.address.AddressSet; import ghidra.program.model.address.AddressSetView; -import ghidra.program.model.block.BasicBlockModel; +import ghidra.program.model.block.SimpleBlockModel; import ghidra.program.model.block.CodeBlock; import ghidra.program.model.block.CodeBlockIterator; import ghidra.program.model.block.CodeBlockModel; @@ -111,7 +111,7 @@ public void process() { instructionsHit = 0; Program fnProgram = function.getProgram(); - CodeBlockModel blockModel = new BasicBlockModel(fnProgram); + CodeBlockModel blockModel = new SimpleBlockModel(fnProgram); AddressSetView body = function.getBody(); diff --git a/src/main/java/cartographer/Utils.java b/src/main/java/cartographer/Utils.java index 097ff22..42586a3 100644 --- a/src/main/java/cartographer/Utils.java +++ b/src/main/java/cartographer/Utils.java @@ -175,7 +175,7 @@ public static int alphaToId(String alphaId) { int charVal = (alphaId.charAt(i) - 'A') + 1; // Multiply the value using its current place in the string - output += charVal * Math.pow(26, (processLength - i)); + output += charVal * Math.pow(26, (processLength - i)); } // Add the value of the last character to the output