From f2074a588bb8aa42653011f7e062181ccf342069 Mon Sep 17 00:00:00 2001 From: redd1ng <20698029+redd1ng@users.noreply.github.com> Date: Mon, 30 Mar 2026 14:06:34 +0200 Subject: [PATCH] Support for Ghidra 12.0.4 and DRCOV v3/v5 - Accept DRCOV VERSION 2 and 3 file headers - Add module table version 5 support (adds preferred_base column) - Add missing regex parsers for module table versions 3 and 4 (Windows) - Fix case-insensitive module name matching for Windows DLLs - Demote checksum mismatch from hard error to warning (PE checksums are not comparable to MD5 hashes used by Ghidra) - Fix table/list UI not updating by wrapping fireTableDataChanged() in Swing.runLater() for Ghidra 12 EDT compliance - Prioritise -PGHIDRA_INSTALL_DIR flag over GHIDRA_INSTALL_DIR env var - Update CI and VS Code build config to Ghidra 12.0.4 --- .github/workflows/build_on_tag_push.yml | 6 +++--- .vscode/tasks.json | 2 +- lightkeeper/build.gradle | 8 ++++---- .../lightkeeper/io/file/DynamoRioFile.java | 10 +++++++--- .../lightkeeper/io/module/ModuleReader.java | 19 ++++++++++++++++--- .../model/coverage/CoverageModel.java | 7 ++++--- .../lightkeeper/model/list/CoverageList.java | 3 ++- .../model/table/CoverageTable.java | 3 ++- 8 files changed, 39 insertions(+), 19 deletions(-) diff --git a/.github/workflows/build_on_tag_push.yml b/.github/workflows/build_on_tag_push.yml index cab3c6a..975e011 100644 --- a/.github/workflows/build_on_tag_push.yml +++ b/.github/workflows/build_on_tag_push.yml @@ -1,9 +1,9 @@ name: Ghidra Extension Publish env: - ghidra-url: https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_11.3.1_build/ghidra_11.3.1_PUBLIC_20250219.zip - ghidra-zip-filename: ghidra_11.3.1_PUBLIC_20250219.zip - ghidra-directory: ghidra_11.3.1_PUBLIC + ghidra-url: https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_12.0.4_build/ghidra_12.0.4_PUBLIC_20260303.zip + ghidra-zip-filename: ghidra_12.0.4_PUBLIC_20260303.zip + ghidra-directory: ghidra_12.0.4_PUBLIC on: push: diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 620b43d..8120f4d 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -4,7 +4,7 @@ { "label": "Build", "type": "shell", - "command": "gradle -PGHIDRA_INSTALL_DIR=/opt/ghidra_11.3.1_PUBLIC", + "command": "gradle -PGHIDRA_INSTALL_DIR=/opt/ghidra_12.0.4_PUBLIC", "group": "build", "options": { "cwd": "${workspaceFolder}/lightkeeper" diff --git a/lightkeeper/build.gradle b/lightkeeper/build.gradle index 24bcf41..6b25268 100644 --- a/lightkeeper/build.gradle +++ b/lightkeeper/build.gradle @@ -32,12 +32,12 @@ //----------------------START "DO NOT MODIFY" SECTION------------------------------ def ghidraInstallDir -if (System.env.GHIDRA_INSTALL_DIR) { - ghidraInstallDir = System.env.GHIDRA_INSTALL_DIR -} -else if (project.hasProperty("GHIDRA_INSTALL_DIR")) { +if (project.hasProperty("GHIDRA_INSTALL_DIR")) { ghidraInstallDir = project.getProperty("GHIDRA_INSTALL_DIR") } +else if (System.env.GHIDRA_INSTALL_DIR) { + ghidraInstallDir = System.env.GHIDRA_INSTALL_DIR +} if (ghidraInstallDir) { apply from: new File(ghidraInstallDir).getCanonicalPath() + "/support/buildExtension.gradle" diff --git a/lightkeeper/src/main/java/lightkeeper/io/file/DynamoRioFile.java b/lightkeeper/src/main/java/lightkeeper/io/file/DynamoRioFile.java index 9219dfe..c361df3 100644 --- a/lightkeeper/src/main/java/lightkeeper/io/file/DynamoRioFile.java +++ b/lightkeeper/src/main/java/lightkeeper/io/file/DynamoRioFile.java @@ -20,7 +20,7 @@ import lightkeeper.io.module.ModuleReader; public class DynamoRioFile implements IEventListener { - protected final String HEADER = "DRCOV VERSION: 2"; + protected final Pattern HEADER_REGEX = Pattern.compile("^DRCOV VERSION: (?[23])$"); protected final Pattern FLAVOUR_REGEX = Pattern.compile("^DRCOV FLAVOR: (?.*)$"); protected final Pattern TABLE_REGEX = Pattern .compile("^Module Table: (version (?\\d+), count )?(?\\d+)$"); @@ -31,6 +31,7 @@ public class DynamoRioFile implements IEventListener { protected File file; protected String flavour; + protected int fileVersion; protected int tableVersion; protected int tableCount; protected ArrayList modules = new ArrayList<>(); @@ -78,9 +79,12 @@ private void readHeader(TaskMonitor monitor) throws CancelledException, IOExcept monitor.setMessage("Reading header"); var headerLine = reader.readLine(); addMessage(headerLine); - if (!headerLine.equals(HEADER)) { - throw new IOException(String.format("Invalid header: '%s' expected '%s'", headerLine, HEADER)); + var headerMatcher = HEADER_REGEX.matcher(headerLine); + if (!headerMatcher.matches()) { + throw new IOException(String.format("Invalid header: '%s'", headerLine)); } + fileVersion = Integer.parseInt(headerMatcher.group("version")); + addMessage(String.format("Detected file version: %d", fileVersion)); monitor.checkCancelled(); } diff --git a/lightkeeper/src/main/java/lightkeeper/io/module/ModuleReader.java b/lightkeeper/src/main/java/lightkeeper/io/module/ModuleReader.java index e85d5d5..b7e4953 100644 --- a/lightkeeper/src/main/java/lightkeeper/io/module/ModuleReader.java +++ b/lightkeeper/src/main/java/lightkeeper/io/module/ModuleReader.java @@ -22,18 +22,29 @@ public class ModuleReader { "^\\s*(?\\d+), (0x)?(?[0-9a-fA-F]+), (0x)?(?[0-9a-fA-F]+), (0x)?(?[0-9a-fA-F]+), (?.+)$"); protected final String COLUMN_3_HDR_WIN = "Columns: id, containing_id, start, end, entry, checksum, timestamp, path"; - protected final Pattern COLUMN_3_HDR_WIN_FMT = null; + protected final Pattern COLUMN_3_HDR_WIN_FMT = Pattern.compile( + "^\\s*(?\\d+), \\s*(?\\d+), (0x)?(?[0-9a-fA-F]+), (0x)?(?[0-9a-fA-F]+), (0x)?(?[0-9a-fA-F]+), (0x)?(?[0-9a-fA-F]+), (0x)?(?[0-9a-fA-F]+), (?.+)$"); protected final String COLUMN_3_HDR_LINUX = "Columns: id, containing_id, start, end, entry, path"; - protected final Pattern COLUMN_3_HDR_LINUX_FMT = null; + protected final Pattern COLUMN_3_HDR_LINUX_FMT = Pattern.compile( + "^\\s*(?\\d+), \\s*(?\\d+), (0x)?(?[0-9a-fA-F]+), (0x)?(?[0-9a-fA-F]+), (0x)?(?[0-9a-fA-F]+), (?.+)$"); protected final String COLUMN_4_HDR_WIN = "Columns: id, containing_id, start, end, entry, offset, checksum, timestamp, path"; - protected final Pattern COLUMN_4_HDR_WIN_FMT = null; + protected final Pattern COLUMN_4_HDR_WIN_FMT = Pattern.compile( + "^\\s*(?\\d+), \\s*(?\\d+), (0x)?(?[0-9a-fA-F]+), (0x)?(?[0-9a-fA-F]+), (0x)?(?[0-9a-fA-F]+), (0x)?(?[0-9a-fA-F]+), (0x)?(?[0-9a-fA-F]+), (0x)?(?[0-9a-fA-F]+), (?.+)$"); protected final String COLUMN_4_HDR_LINUX = "Columns: id, containing_id, start, end, entry, offset, path"; protected final Pattern COLUMN_4_HDR_LINUX_FMT = Pattern.compile( "^\\s*(?\\d+), \\s*(?\\d+), (0x)?(?[0-9a-fA-F]+), (0x)?(?[0-9a-fA-F]+), (0x)?(?[0-9a-fA-F]+), (0x)?(?[0-9a-fA-F]+), (?.+)$"); + protected final String COLUMN_5_HDR_WIN = "Columns: id, containing_id, start, end, entry, offset, preferred_base, checksum, timestamp, path"; + protected final Pattern COLUMN_5_HDR_WIN_FMT = Pattern.compile( + "^\\s*(?\\d+), \\s*(?\\d+), (0x)?(?[0-9a-fA-F]+), (0x)?(?[0-9a-fA-F]+), (0x)?(?[0-9a-fA-F]+), (0x)?(?[0-9a-fA-F]+), (0x)?(?[0-9a-fA-F]+), (0x)?(?[0-9a-fA-F]+), (0x)?(?[0-9a-fA-F]+), (?.+)$"); + + protected final String COLUMN_5_HDR_LINUX = "Columns: id, containing_id, start, end, entry, offset, preferred_base, path"; + protected final Pattern COLUMN_5_HDR_LINUX_FMT = Pattern.compile( + "^\\s*(?\\d+), \\s*(?\\d+), (0x)?(?[0-9a-fA-F]+), (0x)?(?[0-9a-fA-F]+), (0x)?(?[0-9a-fA-F]+), (0x)?(?[0-9a-fA-F]+), (0x)?(?[0-9a-fA-F]+), (?.+)$"); + protected final Pattern NULL_CHECKSUM = Pattern.compile("0+"); private List listeners = new ArrayList<>(); @@ -71,6 +82,8 @@ public ModuleReader(TaskMonitor monitor, BinaryLineReader reader, int tableVersi formats.add(new ModuleTriplet(3, COLUMN_3_HDR_LINUX, COLUMN_3_HDR_LINUX_FMT, true, false)); formats.add(new ModuleTriplet(4, COLUMN_4_HDR_WIN, COLUMN_4_HDR_WIN_FMT, true, true)); formats.add(new ModuleTriplet(4, COLUMN_4_HDR_LINUX, COLUMN_4_HDR_LINUX_FMT, true, false)); + formats.add(new ModuleTriplet(5, COLUMN_5_HDR_WIN, COLUMN_5_HDR_WIN_FMT, true, true)); + formats.add(new ModuleTriplet(5, COLUMN_5_HDR_LINUX, COLUMN_5_HDR_LINUX_FMT, true, false)); readColumnHeader(); diff --git a/lightkeeper/src/main/java/lightkeeper/model/coverage/CoverageModel.java b/lightkeeper/src/main/java/lightkeeper/model/coverage/CoverageModel.java index bd2701b..331947f 100644 --- a/lightkeeper/src/main/java/lightkeeper/model/coverage/CoverageModel.java +++ b/lightkeeper/src/main/java/lightkeeper/model/coverage/CoverageModel.java @@ -56,7 +56,7 @@ protected List getSelectedModules(DynamoRioFile file) throws IOExce var api = plugin.getApi(); var programFileName = api.getCurrentProgram().getName(); var selectedModules = file.getModules().stream() - .filter(m -> new File(m.getPath()).getName().trim().equals(programFileName)) + .filter(m -> new File(m.getPath()).getName().trim().equalsIgnoreCase(programFileName)) .collect(Collectors.toList()); if (selectedModules.isEmpty()) { addMessage(String.format("Found %d modules", file.getModules().size())); @@ -112,11 +112,12 @@ public void update(TaskMonitor monitor) throws CancelledException, IOException { var selectedModules = getSelectedModules(file); var misMatch = selectedModules.stream().filter(m -> m.getChecksum() != null) + .filter(m -> m.getChecksum().length() == md5.length()) .filter(m -> !m.getChecksum().equalsIgnoreCase(md5)).findFirst(); if (misMatch.isPresent()) { var module = misMatch.get(); - throw new IOException(String.format("Module entry '%s' has invalid checksum '%s'", module.getPath(), - module.getChecksum())); + addErrorMessage(String.format("Warning: module '%s' has checksum '%s' but loaded binary has MD5 '%s' - coverage may be inaccurate", + module.getPath(), module.getChecksum(), md5)); } Set ids = this.getSelectedModuleIds(selectedModules); diff --git a/lightkeeper/src/main/java/lightkeeper/model/list/CoverageList.java b/lightkeeper/src/main/java/lightkeeper/model/list/CoverageList.java index 82950f7..0dd11a5 100644 --- a/lightkeeper/src/main/java/lightkeeper/model/list/CoverageList.java +++ b/lightkeeper/src/main/java/lightkeeper/model/list/CoverageList.java @@ -5,6 +5,7 @@ import docking.widgets.table.AbstractSortedTableModel; import docking.widgets.table.ColumnSortState.SortDirection; import docking.widgets.table.TableSortStateEditor; +import ghidra.util.Swing; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; import lightkeeper.model.ICoverageModelListener; @@ -68,6 +69,6 @@ public Object getColumnValueForRow(CoverageListRow row, int columnIndex) { @Override public void modelChanged(TaskMonitor monitor) throws CancelledException { - fireTableDataChanged(); + Swing.runLater(() -> fireTableDataChanged()); } } diff --git a/lightkeeper/src/main/java/lightkeeper/model/table/CoverageTable.java b/lightkeeper/src/main/java/lightkeeper/model/table/CoverageTable.java index b633341..c9f907c 100644 --- a/lightkeeper/src/main/java/lightkeeper/model/table/CoverageTable.java +++ b/lightkeeper/src/main/java/lightkeeper/model/table/CoverageTable.java @@ -5,6 +5,7 @@ import docking.widgets.table.AbstractSortedTableModel; import docking.widgets.table.ColumnSortState.SortDirection; import docking.widgets.table.TableSortStateEditor; +import ghidra.util.Swing; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; import lightkeeper.model.ICoverageModelListener; @@ -70,6 +71,6 @@ public Object getColumnValueForRow(CoverageTableRow row, int columnIndex) { @Override public void modelChanged(TaskMonitor monitor) throws CancelledException { - fireTableDataChanged(); + Swing.runLater(() -> fireTableDataChanged()); } }