scientific-method-engine 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. scientific_method_engine/__init__.py +5 -0
  2. scientific_method_engine/__main__.py +3 -0
  3. scientific_method_engine/cli.py +75 -0
  4. scientific_method_engine/ghidra/ClearNoReturnFunctions.java +22 -0
  5. scientific_method_engine/ghidra/CreateFunctions.java +27 -0
  6. scientific_method_engine/ghidra/ExportBoundedFlow.java +59 -0
  7. scientific_method_engine/ghidra/ExportFunctionFingerprints.java +135 -0
  8. scientific_method_engine/ghidra/ExportFunctionInventory.java +50 -0
  9. scientific_method_engine/ghidra/MergeFallThroughFragment.java +63 -0
  10. scientific_method_engine/ghidra/RecoverCitedFunctions.java +93 -0
  11. scientific_method_engine/ghidra/RepairReturningCallers.java +76 -0
  12. scientific_method_engine/ghidra/ReportCallArguments.java +65 -0
  13. scientific_method_engine/ghidra/ReportCallPaths.java +106 -0
  14. scientific_method_engine/ghidra/ReportCallSitesWithScalars.java +95 -0
  15. scientific_method_engine/ghidra/ReportCallsToRange.java +67 -0
  16. scientific_method_engine/ghidra/ReportConstantFirstArgumentCalls.java +55 -0
  17. scientific_method_engine/ghidra/ReportDataBytes.java +36 -0
  18. scientific_method_engine/ghidra/ReportDecompileMatches.java +71 -0
  19. scientific_method_engine/ghidra/ReportDecompileWindow.java +61 -0
  20. scientific_method_engine/ghidra/ReportFilePatternInMemory.java +102 -0
  21. scientific_method_engine/ghidra/ReportFirstArgumentCallSummary.java +63 -0
  22. scientific_method_engine/ghidra/ReportFunctionScalarConstants.java +56 -0
  23. scientific_method_engine/ghidra/ReportFunctionSummary.java +64 -0
  24. scientific_method_engine/ghidra/ReportInstructionContext.java +64 -0
  25. scientific_method_engine/ghidra/ReportInstructionWindow.java +36 -0
  26. scientific_method_engine/ghidra/ReportMemoryBlockForFileOffset.java +105 -0
  27. scientific_method_engine/ghidra/ReportMemoryBlocks.java +52 -0
  28. scientific_method_engine/ghidra/ReportRandomnessCandidates.java +74 -0
  29. scientific_method_engine/ghidra/ReportReferences.java +42 -0
  30. scientific_method_engine/ghidra/ReportScalarConstants.java +55 -0
  31. scientific_method_engine/ghidra/ReportStringReferences.java +102 -0
  32. scientific_method_engine/ghidra/ReportSymbolReferences.java +72 -0
  33. scientific_method_engine/x86/__init__.py +0 -0
  34. scientific_method_engine/x86/dispatch.py +51 -0
  35. scientific_method_engine/x86/image.py +172 -0
  36. scientific_method_engine/x86/machine.py +659 -0
  37. scientific_method_engine/x86/pe.py +96 -0
  38. scientific_method_engine/x86/reports.py +1259 -0
  39. scientific_method_engine/x86/trace.py +547 -0
  40. scientific_method_engine/x86/values.py +123 -0
  41. scientific_method_engine-0.1.0.dist-info/METADATA +110 -0
  42. scientific_method_engine-0.1.0.dist-info/RECORD +45 -0
  43. scientific_method_engine-0.1.0.dist-info/WHEEL +4 -0
  44. scientific_method_engine-0.1.0.dist-info/entry_points.txt +2 -0
  45. scientific_method_engine-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,5 @@
1
+ """Bounded instruction-derived x86 evidence reports."""
2
+
3
+ # The reader passes this number with every prepared config it pipes to the engine. A reader and
4
+ # engine that disagree on it refuse to run instead of exchanging relocation data in another shape.
5
+ PREPARED_PROTOCOL = 1
@@ -0,0 +1,3 @@
1
+ from .cli import run
2
+
3
+ run()
@@ -0,0 +1,75 @@
1
+ """Read-only bounded x86 evidence reports. Keep configs and reports in GAME_DIR, uncommitted."""
2
+ import json
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ from . import PREPARED_PROTOCOL
7
+
8
+ CONFIG_LIMIT = 1024 * 1024
9
+ PREPARED_CONFIG_LIMIT = 16 * 1024 * 1024
10
+ USAGE = ("Usage: scientific-method-engine <operand|operand-candidates|target|bounds|owner|callees|trace|uses|arguments|"
11
+ "effects|returns|memory|incoming|guards|allocation|dispatch> <config.json|->\n"
12
+ " scientific-method-engine ghidra-scripts")
13
+
14
+
15
+ def ghidra_scripts():
16
+ """The directory to pass to Ghidra's analyzeHeadless -scriptPath."""
17
+ return Path(__file__).resolve().parent / "ghidra"
18
+
19
+
20
+ def main(argv):
21
+ """Run one command (``argv`` excludes the program name) and print its JSON report."""
22
+ from .x86.image import read_source
23
+ from .x86.reports import run_report
24
+ if argv == ["ghidra-scripts"]:
25
+ print(ghidra_scripts())
26
+ return
27
+ if len(argv) != 2:
28
+ raise ValueError(USAGE)
29
+ command, config_path = argv
30
+ reject_derived = False
31
+ if config_path == "-":
32
+ # The reader caps its input at 1 MiB, then adds every source relocation.
33
+ limit, label = PREPARED_CONFIG_LIMIT, "Prepared config exceeds 16 MiB"
34
+ text = sys.stdin.read(limit + 1)
35
+ base = Path.cwd()
36
+ else:
37
+ limit, label = CONFIG_LIMIT, "Config exceeds 1 MiB"
38
+ path = Path(config_path).resolve()
39
+ if path.stat().st_size > limit:
40
+ raise ValueError(label)
41
+ text, base = path.read_text(encoding="utf-8"), path.parent
42
+ reject_derived = True
43
+ if len(text.encode("utf-8")) > limit:
44
+ raise ValueError(label)
45
+ config = json.loads(text)
46
+ if not isinstance(config, dict):
47
+ raise ValueError("Config must be an object")
48
+ if reject_derived and "overlayExports" in config:
49
+ # Only the reader's MZ/FBOV loader (stdin mode) derives overlay exports from source tables.
50
+ raise ValueError("overlayExports is source-derived and cannot be supplied")
51
+ if config_path == "-":
52
+ protocol = config.pop("preparedProtocol", None)
53
+ if protocol != PREPARED_PROTOCOL:
54
+ raise ValueError(f"Reader sent prepared config protocol {protocol}; this engine reads protocol "
55
+ f"{PREPARED_PROTOCOL}. Install matching @scientific-method/executable-reader and "
56
+ "scientific-method-engine releases.")
57
+ elif "preparedProtocol" in config:
58
+ raise ValueError("preparedProtocol is set by the reader and cannot be supplied")
59
+ data, identity = read_source(config, base)
60
+ result = run_report(data, config, command)
61
+ print(json.dumps({"schema": "bounded-x86-v1", "decoder": "capstone 5.0.7", "sourceIdentity": identity,
62
+ "status": "Conditional static report; never promotes an evidence entry", **result}, indent=2))
63
+
64
+
65
+ def run():
66
+ """Entry point of the ``scientific-method-engine`` command; exits with 1 on any error."""
67
+ try:
68
+ main(sys.argv[1:])
69
+ except (ValueError, TypeError, KeyError, OSError, ImportError) as error:
70
+ print("Evidence report: " + str(error), file=sys.stderr)
71
+ sys.exit(1)
72
+
73
+
74
+ if __name__ == "__main__":
75
+ run()
@@ -0,0 +1,22 @@
1
+ // Clears incorrect no-return markings at explicitly supplied function addresses.
2
+ // @category Restoration
3
+
4
+ import ghidra.app.script.GhidraScript;
5
+ import ghidra.program.model.address.Address;
6
+ import ghidra.program.model.listing.Function;
7
+
8
+ public class ClearNoReturnFunctions extends GhidraScript {
9
+ @Override
10
+ protected void run() throws Exception {
11
+ for (String argument : getScriptArgs()) {
12
+ Address address = toAddr(argument);
13
+ Function function = getFunctionContaining(address);
14
+ if (function == null) {
15
+ printerr("No function contains " + address);
16
+ continue;
17
+ }
18
+ function.setNoReturn(false);
19
+ println("Cleared no-return on " + function.getName() + " at " + function.getEntryPoint());
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,27 @@
1
+ // Creates functions at explicitly supplied indirect-call targets.
2
+ // @category Restoration
3
+
4
+ import ghidra.app.script.GhidraScript;
5
+ import ghidra.program.model.address.Address;
6
+ import ghidra.program.model.listing.Function;
7
+
8
+ public class CreateFunctions extends GhidraScript {
9
+ @Override
10
+ protected void run() throws Exception {
11
+ String[] arguments = getScriptArgs();
12
+ if (arguments.length == 0) {
13
+ printerr("Pass one or more function entry addresses.");
14
+ return;
15
+ }
16
+ for (String argument : arguments) {
17
+ Address address = toAddr(argument);
18
+ Function function = getFunctionAt(address);
19
+ if (function == null) function = createFunction(address, null);
20
+ if (function == null) {
21
+ printerr("Could not create function at " + address);
22
+ continue;
23
+ }
24
+ println("FUNCTION " + function.getName() + " " + function.getEntryPoint());
25
+ }
26
+ }
27
+ }
@@ -0,0 +1,59 @@
1
+ // Exports instruction metadata for one entry. Output belongs in ignored analysis/original/.
2
+ // @category Restoration
3
+ import ghidra.app.script.GhidraScript;
4
+ import ghidra.program.model.address.Address;
5
+ import ghidra.program.model.listing.Instruction;
6
+ import ghidra.program.model.listing.Function;
7
+ import java.nio.file.*;
8
+ import java.nio.charset.StandardCharsets;
9
+ import java.util.*;
10
+
11
+ public class ExportBoundedFlow extends GhidraScript {
12
+ @Override protected void run() throws Exception {
13
+ String[] args = getScriptArgs();
14
+ if (args.length != 3) throw new IllegalArgumentException("Supply entry, instruction limit (1..10000), ignored analysis/original/output.json");
15
+ Address entry = toAddr(args[0]);
16
+ int limit = Integer.parseInt(args[1]);
17
+ if (limit < 1 || limit > 10000) throw new IllegalArgumentException("Limit must be 1..10000");
18
+ Path output = Path.of(args[2]).toAbsolutePath().normalize();
19
+ if (!output.toString().replace('\\', '/').contains("/analysis/original/")) throw new IllegalArgumentException("Flow reports stay under ignored analysis/original/");
20
+ Set<Address> visited = new HashSet<>();
21
+ Deque<Address> pending = new ArrayDeque<>();
22
+ List<String> rows = new ArrayList<>();
23
+ Set<Long> entries = new TreeSet<>();
24
+ entries.add(entry.getOffset()); pending.push(entry);
25
+ while (!pending.isEmpty()) {
26
+ if (monitor.isCancelled()) throw new InterruptedException("Cancelled");
27
+ Address at = pending.pop();
28
+ if (visited.contains(at)) continue;
29
+ if (visited.size() >= limit) break; // Missing edge targets remain explicit review gaps.
30
+ visited.add(at);
31
+ Function owner = currentProgram.getFunctionManager().getFunctionContaining(at);
32
+ Function exact = currentProgram.getFunctionManager().getFunctionAt(at);
33
+ if (!at.equals(entry) && exact != null) { entries.add(at.getOffset()); continue; }
34
+ Instruction ins = currentProgram.getListing().getInstructionAt(at);
35
+ if (ins == null) continue;
36
+ List<Long> next = new ArrayList<>(), calls = new ArrayList<>();
37
+ var type = ins.getFlowType();
38
+ Address fall = ins.getFallThrough();
39
+ if (fall != null) { next.add(fall.getOffset()); pending.push(fall); }
40
+ for (Address target : ins.getFlows()) {
41
+ if (type.isCall()) calls.add(target.getOffset());
42
+ else { next.add(target.getOffset()); pending.push(target); }
43
+ }
44
+ String kind = type.isComputed() ? "indirect" : type.isCall() ? "call" : type.isTerminal() ? "terminal" : type.isJump() ? "branch" : "ordinary";
45
+ // A terminal mnemonic alone is insufficient to identify interrupt/OS semantics.
46
+ String mnemonic = ins.getMnemonicString().toUpperCase(Locale.ROOT);
47
+ if (mnemonic.startsWith("RET")) kind = "return";
48
+ boolean hardware = mnemonic.matches("(?:IN|OUT)(?:S[BDW]?)?(?:\\.REP\\w*)?|INT(?:[13O])?|HLT");
49
+ rows.add("{\"start\":" + at.getOffset() + ",\"size\":" + ins.getLength()
50
+ + ",\"kind\":\"" + kind + "\",\"next\":" + next + ",\"calls\":" + calls
51
+ + ",\"externalEffects\":" + (hardware ? "[\"instruction requires hardware/OS review\"]" : "[]")
52
+ + ",\"owner\":" + (owner == null ? "null" : owner.getEntryPoint().getOffset()) + "}");
53
+ }
54
+ Files.createDirectories(output.getParent());
55
+ String text = "{\"entries\":" + entries + ",\"instructions\":[" + String.join(",", rows) + "]}";
56
+ Files.writeString(output, text, StandardCharsets.UTF_8, StandardOpenOption.CREATE_NEW);
57
+ println("Exported " + rows.size() + " instruction metadata records. Ghidra addresses are view-specific; retain the import mapping separately.");
58
+ }
59
+ }
@@ -0,0 +1,135 @@
1
+ // Exports normalized function and instruction fingerprints for cross-version mapping.
2
+ // @category Restoration
3
+
4
+ import java.io.BufferedWriter;
5
+ import java.io.File;
6
+ import java.io.FileWriter;
7
+ import java.nio.charset.StandardCharsets;
8
+ import java.security.MessageDigest;
9
+ import java.util.ArrayList;
10
+ import java.util.List;
11
+
12
+ import ghidra.app.script.GhidraScript;
13
+ import ghidra.program.model.address.Address;
14
+ import ghidra.program.model.lang.Register;
15
+ import ghidra.program.model.listing.Function;
16
+ import ghidra.program.model.listing.FunctionIterator;
17
+ import ghidra.program.model.listing.Instruction;
18
+ import ghidra.program.model.listing.InstructionIterator;
19
+ import ghidra.program.model.scalar.Scalar;
20
+
21
+ public class ExportFunctionFingerprints extends GhidraScript {
22
+ @Override
23
+ protected void run() throws Exception {
24
+ String[] args = getScriptArgs();
25
+ if (args.length != 1) {
26
+ throw new IllegalArgumentException("Expected output TSV path.");
27
+ }
28
+
29
+ File output = new File(args[0]);
30
+ File parent = output.getParentFile();
31
+ if (parent != null) {
32
+ parent.mkdirs();
33
+ }
34
+
35
+ try (BufferedWriter writer = new BufferedWriter(new FileWriter(output, StandardCharsets.UTF_8))) {
36
+ writer.write("kind\tfunction\taddress\tend\tsize\tinstructionCount\tfingerprint\tsignature\n");
37
+ FunctionIterator functions = currentProgram.getFunctionManager().getFunctions(true);
38
+ while (functions.hasNext() && !monitor.isCancelled()) {
39
+ Function function = functions.next();
40
+ List<Instruction> instructions = new ArrayList<>();
41
+ InstructionIterator iterator = currentProgram.getListing()
42
+ .getInstructions(function.getBody(), true);
43
+ StringBuilder functionSignature = new StringBuilder();
44
+ while (iterator.hasNext()) {
45
+ Instruction instruction = iterator.next();
46
+ instructions.add(instruction);
47
+ functionSignature.append(normalize(instruction)).append(';');
48
+ }
49
+
50
+ String entry = hex(function.getEntryPoint());
51
+ writer.write(String.join("\t",
52
+ "F", entry, entry, hex(function.getBody().getMaxAddress()),
53
+ Long.toString(function.getBody().getNumAddresses()),
54
+ Integer.toString(instructions.size()), sha256(functionSignature.toString()),
55
+ functionSignature.toString()));
56
+ writer.newLine();
57
+
58
+ for (Instruction instruction : instructions) {
59
+ String signature = normalize(instruction);
60
+ writer.write(String.join("\t",
61
+ "I", entry, hex(instruction.getAddress()),
62
+ hex(instruction.getMaxAddress()), Integer.toString(instruction.getLength()),
63
+ "1", sha256(signature), signature));
64
+ writer.newLine();
65
+ }
66
+ }
67
+
68
+ InstructionIterator allInstructions = currentProgram.getListing().getInstructions(true);
69
+ while (allInstructions.hasNext() && !monitor.isCancelled()) {
70
+ Instruction instruction = allInstructions.next();
71
+ Function function = currentProgram.getFunctionManager()
72
+ .getFunctionContaining(instruction.getAddress());
73
+ String owner = function == null ? "" : hex(function.getEntryPoint());
74
+ String signature = normalize(instruction);
75
+ writer.write(String.join("\t",
76
+ "A", owner, hex(instruction.getAddress()),
77
+ hex(instruction.getMaxAddress()), Integer.toString(instruction.getLength()),
78
+ "1", sha256(signature), signature));
79
+ writer.newLine();
80
+ }
81
+ }
82
+ println("Wrote " + output.getAbsolutePath());
83
+ }
84
+
85
+ // A scalar that names an address in the program is relocatable between versions, so it is
86
+ // fingerprinted as an address rather than by value.
87
+ private boolean isProgramAddress(long value) {
88
+ try {
89
+ return currentProgram.getMemory().contains(toAddr(value));
90
+ } catch (RuntimeException outOfRange) {
91
+ return false;
92
+ }
93
+ }
94
+
95
+ private String normalize(Instruction instruction) {
96
+ StringBuilder result = new StringBuilder(instruction.getMnemonicString().toLowerCase());
97
+ for (int operand = 0; operand < instruction.getNumOperands(); operand++) {
98
+ result.append('|');
99
+ Object[] objects = instruction.getOpObjects(operand);
100
+ for (Object object : objects) {
101
+ if (object instanceof Register register) {
102
+ result.append('R').append(register.getName().toLowerCase());
103
+ } else if (object instanceof Address address) {
104
+ result.append(address.isMemoryAddress() ? "A" : "X");
105
+ } else if (object instanceof Scalar scalar) {
106
+ long value = scalar.getSignedValue();
107
+ long unsigned = scalar.getUnsignedValue();
108
+ if (instruction.getFlowType().isFlow() || isProgramAddress(unsigned)) {
109
+ result.append('A');
110
+ } else {
111
+ result.append('S').append(value);
112
+ }
113
+ } else {
114
+ result.append('O').append(object.getClass().getSimpleName());
115
+ }
116
+ result.append(',');
117
+ }
118
+ }
119
+ return result.toString();
120
+ }
121
+
122
+ private static String hex(Address address) {
123
+ return String.format("%08x", address.getOffset());
124
+ }
125
+
126
+ private static String sha256(String value) throws Exception {
127
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
128
+ byte[] hash = digest.digest(value.getBytes(StandardCharsets.UTF_8));
129
+ StringBuilder text = new StringBuilder(hash.length * 2);
130
+ for (byte b : hash) {
131
+ text.append(String.format("%02x", b & 0xff));
132
+ }
133
+ return text.toString();
134
+ }
135
+ }
@@ -0,0 +1,50 @@
1
+ // Exports only function starts and body sizes for the committed coverage inventory.
2
+ // @category Restoration
3
+
4
+ import java.io.BufferedWriter;
5
+ import java.nio.charset.StandardCharsets;
6
+ import java.nio.file.Files;
7
+ import java.nio.file.Path;
8
+
9
+ import ghidra.app.script.GhidraScript;
10
+ import ghidra.program.model.listing.Function;
11
+ import ghidra.program.model.listing.FunctionIterator;
12
+
13
+ public class ExportFunctionInventory extends GhidraScript {
14
+ @Override
15
+ protected void run() throws Exception {
16
+ String[] arguments = getScriptArgs();
17
+ if (arguments.length != 1) {
18
+ throw new IllegalArgumentException("Supply one output TSV path.");
19
+ }
20
+
21
+ Path output = Path.of(arguments[0]).toAbsolutePath().normalize();
22
+ if (Files.exists(output)) {
23
+ throw new IllegalArgumentException("Inventory output already exists: " + output);
24
+ }
25
+ Files.createDirectories(output.getParent());
26
+
27
+ long count = 0;
28
+ Path temporary = Files.createTempFile(output.getParent(), "inventory-", ".partial");
29
+ try {
30
+ try (BufferedWriter writer = Files.newBufferedWriter(temporary, StandardCharsets.UTF_8)) {
31
+ writer.write("start\tsize\n");
32
+ FunctionIterator functions = currentProgram.getFunctionManager().getFunctions(true);
33
+ while (functions.hasNext()) {
34
+ if (monitor.isCancelled()) {
35
+ throw new InterruptedException("Function inventory export cancelled.");
36
+ }
37
+ Function function = functions.next();
38
+ writer.write(function.getEntryPoint().toString());
39
+ writer.write('\t');
40
+ writer.write(Long.toString(function.getBody().getNumAddresses()));
41
+ writer.write('\n');
42
+ count++;
43
+ }
44
+ }
45
+ Files.move(temporary, output); // No replacement, and no final file until traversal completes.
46
+ }
47
+ finally { Files.deleteIfExists(temporary); }
48
+ println("Exported " + count + " function starts and body sizes to " + output);
49
+ }
50
+ }
@@ -0,0 +1,63 @@
1
+ // Merges a verified orphan function fragment reached by its parent's fall-through.
2
+ // @category Restoration
3
+
4
+ import ghidra.app.cmd.function.CreateFunctionCmd;
5
+ import ghidra.app.script.GhidraScript;
6
+ import ghidra.program.model.address.Address;
7
+ import ghidra.program.model.listing.Function;
8
+ import ghidra.program.model.listing.Instruction;
9
+ import ghidra.program.model.symbol.Reference;
10
+ import ghidra.program.model.symbol.ReferenceIterator;
11
+
12
+ public class MergeFallThroughFragment extends GhidraScript {
13
+ @Override
14
+ protected void run() throws Exception {
15
+ String[] args = getScriptArgs();
16
+ if (args.length != 2) {
17
+ throw new IllegalArgumentException("Pass parent and orphan fragment function entries.");
18
+ }
19
+ Address parentEntry = toAddr(args[0]);
20
+ Address fragmentEntry = toAddr(args[1]);
21
+ Function parent = getFunctionAt(parentEntry);
22
+ Function fragment = getFunctionAt(fragmentEntry);
23
+ if (parent == null || fragment == null || parent.equals(fragment)) {
24
+ throw new IllegalArgumentException("Pass two different existing function entries.");
25
+ }
26
+ Instruction previous = currentProgram.getListing().getInstructionBefore(fragmentEntry);
27
+ Address expectedNext = fragmentEntry;
28
+ int bridgeInstructions = 0;
29
+ while (previous != null && expectedNext.equals(previous.getFallThrough())) {
30
+ Function owner = getFunctionContaining(previous.getAddress());
31
+ if (parent.equals(owner)) break;
32
+ if (owner != null || previous.getFlowType().isCall() ||
33
+ previous.getFlowType().isJump() || previous.getFlowType().isTerminal() ||
34
+ ++bridgeInstructions > 64) {
35
+ throw new IllegalArgumentException("Fragment bridge rejected at " + previous.getAddress() + " owner=" + (owner == null ? "none" : owner.getEntryPoint()) + " flow=" + previous.getFlowType());
36
+ }
37
+ expectedNext = previous.getAddress();
38
+ previous = currentProgram.getListing().getInstructionBefore(expectedNext);
39
+ }
40
+ if (previous == null || !expectedNext.equals(previous.getFallThrough()) ||
41
+ !parent.getBody().contains(previous.getAddress())) {
42
+ throw new IllegalArgumentException("Fragment is not reached by its parent's adjacent fall-through.");
43
+ }
44
+ ReferenceIterator references = currentProgram.getReferenceManager().getReferencesTo(fragmentEntry);
45
+ while (references.hasNext()) {
46
+ Reference reference = references.next();
47
+ if (!parent.getBody().contains(reference.getFromAddress()) ||
48
+ reference.getReferenceType().isCall() || reference.getReferenceType().isData()) {
49
+ throw new IllegalArgumentException("Fragment entry has an independent caller/data reference: " + reference);
50
+ }
51
+ }
52
+ long previousSize = parent.getBody().getNumAddresses();
53
+ println("MERGE fragment=" + fragmentEntry + " parent=" + parentEntry +
54
+ " preceding=" + previous.getAddress() + " bridgeInstructions=" + bridgeInstructions + " fragmentBytes=" + fragment.getBody().getNumAddresses());
55
+ currentProgram.getFunctionManager().removeFunction(fragmentEntry);
56
+ CreateFunctionCmd.fixupFunctionBody(currentProgram, parent, monitor);
57
+ if (!parent.getBody().contains(fragmentEntry)) {
58
+ throw new IllegalStateException("Recomputed parent body does not include the fragment entry.");
59
+ }
60
+ println("PARENT " + parentEntry + " bodyBytes=" + previousSize +
61
+ " -> " + parent.getBody().getNumAddresses() + " end=" + parent.getBody().getMaxAddress());
62
+ }
63
+ }
@@ -0,0 +1,93 @@
1
+ // Disassembles and recovers functions at a version-specific list of cited entries.
2
+ // Arguments: a file of 8-digit hex addresses, one per line or in a CSV column, and the optional
3
+ // zero-based column. Addresses outside executable memory blocks are counted and skipped.
4
+ // @category Restoration
5
+
6
+ import java.io.BufferedReader;
7
+ import java.io.File;
8
+ import java.io.FileReader;
9
+
10
+ import ghidra.app.script.GhidraScript;
11
+ import ghidra.program.model.address.Address;
12
+ import ghidra.program.model.listing.Function;
13
+ import ghidra.program.model.listing.Instruction;
14
+ import ghidra.program.model.mem.MemoryBlock;
15
+
16
+ public class RecoverCitedFunctions extends GhidraScript {
17
+ @Override
18
+ protected void run() throws Exception {
19
+ String[] arguments = getScriptArgs();
20
+ if (arguments.length < 1 || arguments.length > 2) {
21
+ throw new IllegalArgumentException(
22
+ "Expected an address/CSV file and optional zero-based CSV column.");
23
+ }
24
+ int column = arguments.length == 2 ? Integer.parseInt(arguments[1]) : 0;
25
+ if (column < 0) {
26
+ throw new IllegalArgumentException("The CSV column is zero-based and cannot be negative.");
27
+ }
28
+
29
+ int recovered = 0;
30
+ int alreadyDefined = 0;
31
+ int failed = 0;
32
+ int skipped = 0;
33
+ try (BufferedReader reader = new BufferedReader(new FileReader(new File(arguments[0])))) {
34
+ String line;
35
+ while ((line = reader.readLine()) != null && !monitor.isCancelled()) {
36
+ line = line.strip();
37
+ if (line.isEmpty() || line.startsWith("#")) {
38
+ continue;
39
+ }
40
+ if (line.contains(",")) {
41
+ String[] fields = line.split(",", -1);
42
+ // A CSV written as "a, b" pads its fields; the padding is not part of the address.
43
+ if (column >= fields.length || !fields[column].strip().matches("(?i)[0-9a-f]{8}")) {
44
+ continue;
45
+ }
46
+ line = fields[column].strip();
47
+ }
48
+ if (!line.matches("(?i)[0-9a-f]{8}")) {
49
+ continue;
50
+ }
51
+ Address address = toAddr(line);
52
+ MemoryBlock block = currentProgram.getMemory().getBlock(address);
53
+ if (block == null || !block.isExecute()) {
54
+ skipped++;
55
+ continue;
56
+ }
57
+ Function function = getFunctionAt(address);
58
+ Instruction instruction = getInstructionAt(address);
59
+
60
+ // A previous address-only pass may have left a one-byte, empty function.
61
+ // Remove only that unusable placeholder before asking Ghidra to disassemble.
62
+ if (function != null && instruction == null &&
63
+ function.getBody().getNumAddresses() == 1) {
64
+ removeFunction(function);
65
+ function = null;
66
+ }
67
+
68
+ if (instruction == null && !disassemble(address)) {
69
+ printerr("Could not disassemble " + address);
70
+ failed++;
71
+ continue;
72
+ }
73
+ if (function == null) {
74
+ function = createFunction(address, null);
75
+ }
76
+ if (function == null) {
77
+ function = getFunctionContaining(address);
78
+ }
79
+ if (function == null) {
80
+ printerr("Could not create function " + address);
81
+ failed++;
82
+ } else if (function.getEntryPoint().equals(address)) {
83
+ recovered++;
84
+ } else {
85
+ alreadyDefined++;
86
+ }
87
+ }
88
+ }
89
+ println("Recovered entries: " + recovered +
90
+ "; already inside functions: " + alreadyDefined + "; failures: " + failed +
91
+ "; outside executable memory: " + skipped);
92
+ }
93
+ }
@@ -0,0 +1,76 @@
1
+ // Repairs explicitly verified returning calls and recomputes their caller body.
2
+ // @category Restoration
3
+
4
+ import java.util.ArrayList;
5
+ import java.util.List;
6
+ import ghidra.app.cmd.function.CreateFunctionCmd;
7
+ import ghidra.app.script.GhidraScript;
8
+ import ghidra.program.model.address.Address;
9
+ import ghidra.program.model.listing.FlowOverride;
10
+ import ghidra.program.model.listing.Function;
11
+ import ghidra.program.model.listing.Instruction;
12
+
13
+ public class RepairReturningCallers extends GhidraScript {
14
+ @Override
15
+ protected void run() throws Exception {
16
+ String[] args = getScriptArgs();
17
+ if (args.length < 3) {
18
+ throw new IllegalArgumentException("Pass callee entry, caller entry, then verified call addresses.");
19
+ }
20
+ Address calleeAddress = toAddr(args[0]);
21
+ Address callerAddress = toAddr(args[1]);
22
+ Function callee = getFunctionAt(calleeAddress);
23
+ Function caller = getFunctionAt(callerAddress);
24
+ if (callee == null || caller == null) {
25
+ throw new IllegalArgumentException("Callee and caller must be existing function entries.");
26
+ }
27
+ List<Instruction> calls = new ArrayList<>();
28
+ for (int index = 2; index < args.length; index++) {
29
+ Address callAddress = toAddr(args[index]);
30
+ Instruction call = getInstructionAt(callAddress);
31
+ if (call == null || !call.getMnemonicString().equalsIgnoreCase("CALL") ||
32
+ callAddress.compareTo(callerAddress) < 0) {
33
+ throw new IllegalArgumentException("Not a verified caller CALL: " + callAddress);
34
+ }
35
+ Address[] targets = call.getDefaultFlows();
36
+ if (targets.length != 1 || !targets[0].equals(calleeAddress)) {
37
+ throw new IllegalArgumentException("CALL does not target supplied callee: " + callAddress);
38
+ }
39
+ Function owner = getFunctionContaining(callAddress);
40
+ if (owner != null && !owner.equals(caller)) {
41
+ throw new IllegalArgumentException("CALL belongs to another function: " + callAddress);
42
+ }
43
+ FlowOverride override = call.getFlowOverride();
44
+ if (override != FlowOverride.NONE && override != FlowOverride.CALL_RETURN) {
45
+ throw new IllegalArgumentException("Unexpected flow override at " + callAddress + ": " + override);
46
+ }
47
+ Address expectedFallThrough = callAddress.add(call.getLength());
48
+ if (call.isFallThroughOverridden() && call.getFallThrough() != null &&
49
+ !call.getFallThrough().equals(expectedFallThrough)) {
50
+ throw new IllegalArgumentException("Unexpected custom fall-through at " + callAddress);
51
+ }
52
+ calls.add(call);
53
+ }
54
+ long previousSize = caller.getBody().getNumAddresses();
55
+ callee.setNoReturn(false);
56
+ for (Instruction call : calls) {
57
+ println("REPAIR " + call.getAddress() + " override=" + call.getFlowOverride() +
58
+ " fallThrough=" + call.getFallThrough());
59
+ call.setFlowOverride(FlowOverride.NONE);
60
+ call.clearFallThroughOverride();
61
+ Address next = call.getAddress().add(call.getLength());
62
+ if (getInstructionAt(next) == null && !disassemble(next)) {
63
+ throw new IllegalStateException("Could not disassemble caller continuation at " + next);
64
+ }
65
+ }
66
+ CreateFunctionCmd.fixupFunctionBody(currentProgram, caller, monitor);
67
+ println("CALLER " + caller.getEntryPoint() + " bodyBytes=" + previousSize +
68
+ " -> " + caller.getBody().getNumAddresses() + " end=" + caller.getBody().getMaxAddress());
69
+ for (Instruction call : calls) {
70
+ if (call.getFallThrough() == null ||
71
+ !caller.getBody().contains(call.getAddress().add(call.getLength()))) {
72
+ throw new IllegalStateException("Caller continuation remains missing after repair: " + call.getAddress());
73
+ }
74
+ }
75
+ }
76
+ }