rea-agents 2.5.0 → 3.0.0
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.
- package/README.md +14 -4
- package/bridge/ghidra/ReaGhidraBridge.java +477 -4
- package/bridge/hopper_bridge.py +54 -4
- package/dist/application/AnalysisContextQueries.js +130 -0
- package/dist/application/AnalysisSnapshotCache.js +4 -2
- package/dist/application/ArtifactInventory/scanCanonical.js +1 -1
- package/dist/application/BinarySession.js +5 -4
- package/dist/application/BinarySessionComposition.js +21 -0
- package/dist/application/BinarySessionRecords.js +79 -9
- package/dist/application/CapabilityInventory.js +16 -2
- package/dist/application/ClientRegistrationStatus.js +4 -1
- package/dist/application/DirectAnalysis.js +6 -2
- package/dist/application/EnhancedTools.js +37 -7
- package/dist/application/EvidenceLedger.js +45 -16
- package/dist/application/JavaScriptArtifactAnalysis.js +13 -4
- package/dist/application/LazyAnalysisProvider.js +77 -0
- package/dist/application/LoopbackReplay.js +1 -1
- package/dist/application/NativeApiInspection.js +80 -0
- package/dist/application/SessionProviderRouter.js +6 -6
- package/dist/application/SetupClientConfiguration.js +4 -2
- package/dist/application/runtime.js +26 -9
- package/dist/browser/PlaywrightBrowserScenarioProvider.js +8 -2
- package/dist/catalogIdentity.js +3 -3
- package/dist/cli/artifactCommands.js +5 -3
- package/dist/cli/coreAnalysisCommands.js +41 -0
- package/dist/cliCommandNames.js +1 -0
- package/dist/contracts/artifactToolContracts.js +8 -3
- package/dist/contracts/enhancedInputs.js +10 -0
- package/dist/contracts/functionWorkflowToolContracts.js +30 -0
- package/dist/contracts/promptContracts.js +14 -15
- package/dist/contracts/toolContractExamples.js +3 -0
- package/dist/contracts/toolContracts.js +21 -4
- package/dist/contracts/toolEffects.js +4 -0
- package/dist/contracts/toolOutputSchemaGroups.js +50 -10
- package/dist/domain/analysisErrorPresentation.js +8 -2
- package/dist/domain/analysisErrorProjection.js +1 -0
- package/dist/domain/bytecodeProvider.js +179 -0
- package/dist/domain/conformancePackage.js +169 -0
- package/dist/domain/conformanceReplay.js +104 -0
- package/dist/domain/conformanceTrustGate.js +193 -0
- package/dist/domain/customProtocolCapture.js +137 -0
- package/dist/domain/eventProcessTree.js +214 -0
- package/dist/domain/evidenceBundle.js +47 -0
- package/dist/domain/hopperStartupFailure.js +1 -1
- package/dist/domain/hopperValues.js +4 -1
- package/dist/domain/inputIssueProjection.js +7 -1
- package/dist/domain/javascriptSemanticAnalysis.js +8 -12
- package/dist/domain/javascriptSourceParser.js +15 -0
- package/dist/domain/javascriptStaticAnalysis.js +8 -12
- package/dist/domain/mobileApplicationInvestigation.js +159 -0
- package/dist/domain/nativeApiBoundary.js +130 -0
- package/dist/domain/objcSwiftMetadata.js +150 -0
- package/dist/domain/packageAnalysis.js +123 -0
- package/dist/domain/peInspection.js +196 -0
- package/dist/domain/protocolCapture.js +243 -0
- package/dist/dotnet/ManagedStaticProvider.js +34 -5
- package/dist/generatedMcpToolCatalog.js +37 -0
- package/dist/generatedPackageMetadata.js +4 -4
- package/dist/ghidra/GhidraDefaults.js +1 -1
- package/dist/ghidra/GhidraFunctionValues.js +27 -1
- package/dist/ghidra/GhidraProviderCapabilities.js +40 -38
- package/dist/hopper/HopperClient.js +26 -2
- package/dist/hopper/HopperProvider.js +42 -30
- package/dist/hopper/HopperRequestQueue.js +32 -4
- package/dist/hopper/HopperResponseStream.js +2 -2
- package/dist/hopper/protocol.js +63 -21
- package/dist/main/transport.js +2 -5
- package/dist/main.js +14 -12
- package/dist/mcpDoctor.js +2 -2
- package/dist/mcpStartupPolicy.js +14 -0
- package/dist/server/LazyToolCatalog.js +134 -0
- package/dist/server/createServer.js +64 -171
- package/dist/server/hydrateServerTools.js +191 -0
- package/dist/server/registerEnhancedTools.js +13 -3
- package/dist/server/registerEvidenceResources.js +50 -101
- package/dist/server/registerInvestigationTools.js +1 -2
- package/dist/server/registerJavaScriptApplicationGraphResource.js +0 -4
- package/dist/server/registerReconstructionObligationLedgerResource.js +0 -7
- package/dist/server/registerReconstructionReadinessResource.js +0 -7
- package/dist/server/registerSessionRecordTools.js +82 -12
- package/dist/server/registerSessionStatusTool.js +1 -1
- package/dist/server/registerSessionTools.js +21 -1
- package/dist/server/toolRegistrationOptions.js +21 -6
- package/dist/server/toolResult.js +12 -0
- package/package.json +21 -10
- package/scripts/package-runner-bootstrap.mjs +72 -0
- package/scripts/rea.mjs +11 -1
- package/skills/reverse-engineer-anything/SKILL.md +10 -3
package/README.md
CHANGED
|
@@ -103,6 +103,13 @@ detected agents, then asks which capabilities to set up: agent integration
|
|
|
103
103
|
Nothing is preselected. Choosing agent integration opens a second empty
|
|
104
104
|
checklist for the specific detected agents that should receive a registration.
|
|
105
105
|
|
|
106
|
+
If bare `npx` finds a project-local REA package, its dispatcher restarts setup
|
|
107
|
+
through `rea-agents@latest` before planning any changes. This prevents an older
|
|
108
|
+
local dependency from silently writing stale integration state. An already
|
|
109
|
+
published older release cannot contain that dispatcher protection; run
|
|
110
|
+
`npx -y rea-agents@latest setup` once to repair registrations created by one.
|
|
111
|
+
Intentional rollbacks remain available through an exact package request.
|
|
112
|
+
|
|
106
113
|
REA keeps the journey inline so its history remains in the terminal. Selecting
|
|
107
114
|
a capability does not select every detected target or authorize a change.
|
|
108
115
|
Before anything changes, REA validates existing configuration, prints exact
|
|
@@ -374,7 +381,7 @@ REA handles the app analysis in steps 1–5. The agent performs step 6 with its
|
|
|
374
381
|
| Tool family | Count | Examples |
|
|
375
382
|
| ------------------------- | ----: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
376
383
|
| Native inspection | 36 | procedures, pseudocode, assembly, strings, names, segments, callers, callees, xrefs, annotations, bounded byte reads, file-offset translation |
|
|
377
|
-
| Investigation workflows |
|
|
384
|
+
| Investigation workflows | 13 | `binary_overview`, `analyze_function`, `inspect_native_api`, `batch_decompile`, `trace_feature`, exact string-to-code lookup, bounded call paths, call graphs, Swift and Objective-C discovery |
|
|
378
385
|
| Native macOS utilities | 5 | Mach-O metadata, code signatures, plists, architectures, Swift demangling; Hopper-free and provenance-bearing |
|
|
379
386
|
| Artifact graph | 3 | bounded provider-neutral inspection, deterministic directory/ZIP/APK/IPA/MSIX/AppX/ASAR inventory, and explicitly selected extraction into an absent owned tree |
|
|
380
387
|
| Managed PE/CLI | 8 | PE/CLI identity, metadata members, CIL hashes, P/Invoke/native-boundary declarations and verification, application-graph projection, decompiler reconstruction import, token remapping, runtime-correlation plans, and version comparison |
|
|
@@ -382,7 +389,7 @@ REA handles the app analysis in steps 1–5. The agent performs step 6 with its
|
|
|
382
389
|
| Electron analysis | 4 | passive root-confined observation, bounded static application mapping, and evidence-backed static/runtime reconciliation |
|
|
383
390
|
| JavaScript runtime | 2 | approved attach-only Node/Electron Inspector target discovery plus bounded script and execution-context observation without evaluation or instrumentation |
|
|
384
391
|
| Application workflows | 12 | bounded cross-layer traces, unique-only version matching, historical-source to bundle mapping, static export return-shape comparison, approved Linux-isolated extracted-module replay, managed-runtime characterization, reconstruction coverage closure, deterministic obligation ledgers, and end-to-end readiness conformance |
|
|
385
|
-
| Workspace and observation |
|
|
392
|
+
| Workspace and observation | 22 | target lifecycle, Evidence v2 bundle snapshots, aggregate navigation/address context, direct finite replay-machine evaluation, process/artifact/function comparison, evidence-linked residual-unknown lifecycle |
|
|
386
393
|
|
|
387
394
|
The public interface describes what the agent is trying to learn. Providers decide how to answer. macOS utilities handle common semantic inspection without launching Hopper; Hopper handles deeper native analysis; the process harness implements controlled behavioral capture.
|
|
388
395
|
|
|
@@ -530,7 +537,7 @@ Setup detects Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf,
|
|
|
530
537
|
"mcpServers": {
|
|
531
538
|
"rea": {
|
|
532
539
|
"command": "npx",
|
|
533
|
-
"args": ["-y", "rea-agents@
|
|
540
|
+
"args": ["-y", "rea-agents@3.0.0", "mcp"]
|
|
534
541
|
}
|
|
535
542
|
}
|
|
536
543
|
}
|
|
@@ -787,7 +794,10 @@ Any agent that can run a local MCP server can use the manual configuration. Setu
|
|
|
787
794
|
|
|
788
795
|
## Development
|
|
789
796
|
|
|
790
|
-
See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, architecture,
|
|
797
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, architecture, and release
|
|
798
|
+
instructions, and [docs/testing.md](docs/testing.md) for the behavioral test
|
|
799
|
+
depths, focused developer commands, coverage floors, and CI evidence. PR CI
|
|
800
|
+
publishes generated API documentation as the `api-docs` workflow artifact.
|
|
791
801
|
|
|
792
802
|
`npm run verify:agent` runs brandless native, JavaScript-application, managed,
|
|
793
803
|
and browser prompts through a real local Codex CLI. Its JSON report measures
|
|
@@ -55,6 +55,7 @@ import ghidra.program.model.block.CodeBlockIterator;
|
|
|
55
55
|
import ghidra.program.model.block.CodeBlockReference;
|
|
56
56
|
import ghidra.program.model.block.CodeBlockReferenceIterator;
|
|
57
57
|
import ghidra.program.model.data.StringDataInstance;
|
|
58
|
+
import ghidra.program.model.data.DataType;
|
|
58
59
|
import ghidra.program.model.listing.CommentType;
|
|
59
60
|
import ghidra.program.model.listing.Data;
|
|
60
61
|
import ghidra.program.model.listing.DataIterator;
|
|
@@ -70,15 +71,25 @@ import ghidra.program.model.symbol.ReferenceIterator;
|
|
|
70
71
|
import ghidra.program.model.symbol.SourceType;
|
|
71
72
|
import ghidra.program.model.symbol.Symbol;
|
|
72
73
|
import ghidra.program.model.symbol.SymbolIterator;
|
|
74
|
+
import ghidra.program.model.pcode.FunctionPrototype;
|
|
75
|
+
import ghidra.program.model.pcode.HighFunction;
|
|
76
|
+
import ghidra.program.model.pcode.HighSymbol;
|
|
77
|
+
import ghidra.program.model.pcode.JumpTable;
|
|
73
78
|
|
|
74
79
|
public final class ReaGhidraBridge extends HeadlessScript {
|
|
75
|
-
private static final int BRIDGE_VERSION =
|
|
80
|
+
private static final int BRIDGE_VERSION = 6;
|
|
76
81
|
private static final int MAX_DESCRIPTOR_BYTES = 16 * 1024;
|
|
77
82
|
private static final int MAX_REQUEST_CHARACTERS = 256 * 1024;
|
|
78
83
|
private static final int MAX_RESPONSE_BYTES = 1024 * 1024;
|
|
84
|
+
private static final int RESPONSE_ENVELOPE_RESERVE_BYTES = 32 * 1024;
|
|
85
|
+
private static final int MAX_NATIVE_API_RESPONSE_BYTES = 192 * 1024;
|
|
79
86
|
private static final int MAX_INVENTORY_ITEMS = 1_000_000;
|
|
80
87
|
private static final int MAX_FUNCTION_ITEMS = 100_000;
|
|
81
88
|
private static final int MAX_FUNCTION_INSTRUCTIONS = 100_000;
|
|
89
|
+
private static final int MAX_NATIVE_API_PARAMETERS = 64;
|
|
90
|
+
private static final int MAX_NATIVE_API_JUMP_TABLES = 4;
|
|
91
|
+
private static final int MAX_NATIVE_API_LOAD_TABLES = 4;
|
|
92
|
+
private static final int MAX_NATIVE_API_MAPPINGS = 32;
|
|
82
93
|
private static final int DECOMPILE_TIMEOUT_SECONDS = 30;
|
|
83
94
|
private static final int DECOMPILE_PAYLOAD_MBYTES = 8;
|
|
84
95
|
private static final int MAX_LIST_VALUE_CODE_POINTS = 1_024;
|
|
@@ -377,7 +388,9 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
|
|
377
388
|
private JsonElement procedurePseudocode(JsonObject params) throws Exception {
|
|
378
389
|
requireKeys(params, Set.of("document", "procedure"));
|
|
379
390
|
requireDocument(params);
|
|
380
|
-
String value =
|
|
391
|
+
String value = decompiledText(
|
|
392
|
+
decompile(resolveProcedure(requireString(params, "procedure")))
|
|
393
|
+
);
|
|
381
394
|
return value == null ? JsonNull.INSTANCE : GSON.toJsonTree(value);
|
|
382
395
|
}
|
|
383
396
|
|
|
@@ -576,7 +589,8 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
|
|
576
589
|
);
|
|
577
590
|
JsonObject offsets = requireCollectionOffsets(params);
|
|
578
591
|
InstructionScan scan = scanInstructions(function, maximumInstructions);
|
|
579
|
-
|
|
592
|
+
DecompileResults decompilation = decompile(function);
|
|
593
|
+
String pseudocode = decompiledText(decompilation);
|
|
580
594
|
if (pseudocode == null) {
|
|
581
595
|
pseudocode = "";
|
|
582
596
|
}
|
|
@@ -662,6 +676,19 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
|
|
662
676
|
instructionScan.addProperty("scanned", scan.instructions.size());
|
|
663
677
|
instructionScan.addProperty("truncated", scan.truncated);
|
|
664
678
|
result.add("instruction_scan", instructionScan);
|
|
679
|
+
int nativeApiBudget = Math.min(
|
|
680
|
+
MAX_NATIVE_API_RESPONSE_BYTES,
|
|
681
|
+
Math.max(
|
|
682
|
+
0,
|
|
683
|
+
MAX_RESPONSE_BYTES -
|
|
684
|
+
encodedUtf8Length(result) -
|
|
685
|
+
RESPONSE_ENVELOPE_RESERVE_BYTES
|
|
686
|
+
)
|
|
687
|
+
);
|
|
688
|
+
result.add(
|
|
689
|
+
"native_api",
|
|
690
|
+
nativeApiBoundary(function, decompilation, nativeApiBudget)
|
|
691
|
+
);
|
|
665
692
|
JsonArray limitations = new JsonArray();
|
|
666
693
|
limitations.add(
|
|
667
694
|
"Unresolved computed or indirect flows without target addresses are not represented as reference edges."
|
|
@@ -971,7 +998,446 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
|
|
971
998
|
return matches.get(0);
|
|
972
999
|
}
|
|
973
1000
|
|
|
974
|
-
private
|
|
1001
|
+
private JsonObject nativeApiBoundary(
|
|
1002
|
+
Function function,
|
|
1003
|
+
DecompileResults decompilation,
|
|
1004
|
+
int maximumBytes
|
|
1005
|
+
) throws Exception {
|
|
1006
|
+
if (decompilation == null || decompilation.getHighFunction() == null) {
|
|
1007
|
+
return unavailableNativeApiBoundary(
|
|
1008
|
+
"Ghidra has no HighFunction model for an external or bodyless procedure"
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
HighFunction highFunction = decompilation.getHighFunction();
|
|
1013
|
+
FunctionPrototype prototype = highFunction.getFunctionPrototype();
|
|
1014
|
+
String signatureSource = function
|
|
1015
|
+
.getSignatureSource()
|
|
1016
|
+
.name()
|
|
1017
|
+
.toLowerCase(Locale.ROOT);
|
|
1018
|
+
JsonObject result = new JsonObject();
|
|
1019
|
+
result.addProperty("available", true);
|
|
1020
|
+
result.addProperty("provenance", "ghidra-high-function");
|
|
1021
|
+
result.addProperty("signature_source", signatureSource);
|
|
1022
|
+
String callingConvention = prototype.getModelName();
|
|
1023
|
+
result.addProperty(
|
|
1024
|
+
"calling_convention",
|
|
1025
|
+
callingConvention == null || callingConvention.isBlank()
|
|
1026
|
+
? "unknown"
|
|
1027
|
+
: callingConvention
|
|
1028
|
+
);
|
|
1029
|
+
result.add(
|
|
1030
|
+
"return_type",
|
|
1031
|
+
inferredBoundaryType(
|
|
1032
|
+
"return",
|
|
1033
|
+
null,
|
|
1034
|
+
null,
|
|
1035
|
+
prototype.getReturnType(),
|
|
1036
|
+
prototype.getReturnStorage().toString(),
|
|
1037
|
+
false,
|
|
1038
|
+
signatureSource
|
|
1039
|
+
)
|
|
1040
|
+
);
|
|
1041
|
+
|
|
1042
|
+
JsonArray parameters = new JsonArray();
|
|
1043
|
+
result.add("parameters", parameters);
|
|
1044
|
+
result.addProperty("parameters_truncated", false);
|
|
1045
|
+
|
|
1046
|
+
JsonArray jumpTables = new JsonArray();
|
|
1047
|
+
result.add("jump_tables", jumpTables);
|
|
1048
|
+
result.addProperty("jump_tables_truncated", false);
|
|
1049
|
+
|
|
1050
|
+
JsonObject pseudocode = new JsonObject();
|
|
1051
|
+
pseudocode.addProperty("classification", "decompiler-generated-non-source");
|
|
1052
|
+
pseudocode.addProperty("compilable", false);
|
|
1053
|
+
result.add("pseudocode", pseudocode);
|
|
1054
|
+
|
|
1055
|
+
JsonArray artifacts = new JsonArray();
|
|
1056
|
+
artifacts.add("recovered-signature");
|
|
1057
|
+
artifacts.add("register-and-stack-variables");
|
|
1058
|
+
artifacts.add("compiler-generated-control-flow");
|
|
1059
|
+
artifacts.add("pointer-arithmetic");
|
|
1060
|
+
artifacts.add("pseudocode");
|
|
1061
|
+
result.add("decompiler_artifacts", artifacts);
|
|
1062
|
+
|
|
1063
|
+
JsonArray limitations = new JsonArray();
|
|
1064
|
+
limitations.add(
|
|
1065
|
+
"Types and calling conventions are Ghidra decompiler observations, not source declarations."
|
|
1066
|
+
);
|
|
1067
|
+
limitations.add(
|
|
1068
|
+
"Register variables, compiler-generated blocks, and pointer arithmetic remain decompiler artifacts."
|
|
1069
|
+
);
|
|
1070
|
+
limitations.add(
|
|
1071
|
+
"Pseudocode is neither original source nor guaranteed to compile."
|
|
1072
|
+
);
|
|
1073
|
+
result.add("limitations", limitations);
|
|
1074
|
+
if (encodedUtf8Length(result) > maximumBytes) {
|
|
1075
|
+
return unavailableNativeApiBoundary(
|
|
1076
|
+
"The remaining Ghidra response budget cannot contain a structured native API boundary"
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
int parameterCount = Math.min(
|
|
1081
|
+
prototype.getNumParams(),
|
|
1082
|
+
MAX_NATIVE_API_PARAMETERS
|
|
1083
|
+
);
|
|
1084
|
+
for (int index = 0; index < parameterCount; index += 1) {
|
|
1085
|
+
HighSymbol parameter = prototype.getParam(index);
|
|
1086
|
+
if (parameter == null) {
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
parameters.add(
|
|
1090
|
+
inferredBoundaryType(
|
|
1091
|
+
"parameter",
|
|
1092
|
+
index,
|
|
1093
|
+
parameter.getName(),
|
|
1094
|
+
parameter.getDataType(),
|
|
1095
|
+
parameter.getStorage().toString(),
|
|
1096
|
+
parameter.isTypeLocked(),
|
|
1097
|
+
signatureSource
|
|
1098
|
+
)
|
|
1099
|
+
);
|
|
1100
|
+
if (encodedUtf8Length(result) > maximumBytes) {
|
|
1101
|
+
parameters.remove(parameters.size() - 1);
|
|
1102
|
+
break;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
result.addProperty(
|
|
1106
|
+
"parameters_truncated",
|
|
1107
|
+
prototype.getNumParams() > parameters.size()
|
|
1108
|
+
);
|
|
1109
|
+
|
|
1110
|
+
JumpTable[] recoveredJumpTables = highFunction.getJumpTables();
|
|
1111
|
+
int jumpTableCount = Math.min(
|
|
1112
|
+
recoveredJumpTables.length,
|
|
1113
|
+
MAX_NATIVE_API_JUMP_TABLES
|
|
1114
|
+
);
|
|
1115
|
+
for (int index = 0; index < jumpTableCount; index += 1) {
|
|
1116
|
+
jumpTables.add(inferredJumpTable(recoveredJumpTables[index]));
|
|
1117
|
+
if (encodedUtf8Length(result) > maximumBytes) {
|
|
1118
|
+
jumpTables.remove(jumpTables.size() - 1);
|
|
1119
|
+
break;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
result.addProperty(
|
|
1123
|
+
"jump_tables_truncated",
|
|
1124
|
+
recoveredJumpTables.length > jumpTables.size()
|
|
1125
|
+
);
|
|
1126
|
+
return result;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
private static JsonObject unavailableNativeApiBoundary(String reason) {
|
|
1130
|
+
JsonObject unavailable = new JsonObject();
|
|
1131
|
+
unavailable.addProperty("available", false);
|
|
1132
|
+
unavailable.addProperty("reason", reason);
|
|
1133
|
+
JsonArray unknowns = new JsonArray();
|
|
1134
|
+
unknowns.add("Return and parameter boundary types are unavailable.");
|
|
1135
|
+
unknowns.add("Jump-table mappings are unavailable.");
|
|
1136
|
+
unavailable.add("residual_unknowns", unknowns);
|
|
1137
|
+
return unavailable;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
private static int encodedUtf8Length(JsonElement value) {
|
|
1141
|
+
return GSON.toJson(value).getBytes(StandardCharsets.UTF_8).length;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
private static JsonObject inferredBoundaryType(
|
|
1145
|
+
String role,
|
|
1146
|
+
Integer ordinal,
|
|
1147
|
+
String name,
|
|
1148
|
+
DataType dataType,
|
|
1149
|
+
String storage,
|
|
1150
|
+
boolean typeLocked,
|
|
1151
|
+
String signatureSource
|
|
1152
|
+
) {
|
|
1153
|
+
JsonObject result = new JsonObject();
|
|
1154
|
+
result.addProperty("role", role);
|
|
1155
|
+
if (ordinal == null) {
|
|
1156
|
+
result.add("ordinal", JsonNull.INSTANCE);
|
|
1157
|
+
}
|
|
1158
|
+
else {
|
|
1159
|
+
result.addProperty("ordinal", ordinal);
|
|
1160
|
+
}
|
|
1161
|
+
if (name == null || name.isBlank()) {
|
|
1162
|
+
result.add("name", JsonNull.INSTANCE);
|
|
1163
|
+
}
|
|
1164
|
+
else {
|
|
1165
|
+
result.addProperty("name", name);
|
|
1166
|
+
}
|
|
1167
|
+
result.addProperty("data_type", dataType.getDisplayName());
|
|
1168
|
+
int dataTypeLength = dataType.getLength();
|
|
1169
|
+
if (dataTypeLength < 0) {
|
|
1170
|
+
result.add("size_bytes", JsonNull.INSTANCE);
|
|
1171
|
+
}
|
|
1172
|
+
else {
|
|
1173
|
+
result.addProperty("size_bytes", dataTypeLength);
|
|
1174
|
+
}
|
|
1175
|
+
if (storage.isBlank()) {
|
|
1176
|
+
result.add("storage", JsonNull.INSTANCE);
|
|
1177
|
+
}
|
|
1178
|
+
else {
|
|
1179
|
+
result.addProperty("storage", storage);
|
|
1180
|
+
}
|
|
1181
|
+
result.addProperty(
|
|
1182
|
+
"confidence",
|
|
1183
|
+
inferenceConfidence(signatureSource, typeLocked)
|
|
1184
|
+
);
|
|
1185
|
+
|
|
1186
|
+
JsonArray evidence = new JsonArray();
|
|
1187
|
+
evidence.add(
|
|
1188
|
+
inferenceEvidence(
|
|
1189
|
+
"signature-source",
|
|
1190
|
+
"ghidra-function-manager",
|
|
1191
|
+
"Function signature source is " + signatureSource + "."
|
|
1192
|
+
)
|
|
1193
|
+
);
|
|
1194
|
+
evidence.add(
|
|
1195
|
+
inferenceEvidence(
|
|
1196
|
+
"decompiler-type",
|
|
1197
|
+
"ghidra-high-function",
|
|
1198
|
+
"HighFunction recovered data type " + dataType.getDisplayName() + "."
|
|
1199
|
+
)
|
|
1200
|
+
);
|
|
1201
|
+
if (typeLocked) {
|
|
1202
|
+
evidence.add(
|
|
1203
|
+
inferenceEvidence(
|
|
1204
|
+
"type-lock",
|
|
1205
|
+
"ghidra-high-symbol",
|
|
1206
|
+
"The decompiler marks this parameter type as locked."
|
|
1207
|
+
)
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
result.add("evidence", evidence);
|
|
1211
|
+
|
|
1212
|
+
JsonArray artifacts = new JsonArray();
|
|
1213
|
+
artifacts.add("recovered-signature");
|
|
1214
|
+
if (!storage.isBlank()) {
|
|
1215
|
+
artifacts.add("register-or-stack-storage");
|
|
1216
|
+
}
|
|
1217
|
+
if (name != null && (name.startsWith("param_") || name.startsWith("unaff_"))) {
|
|
1218
|
+
artifacts.add("compiler-generated-variable");
|
|
1219
|
+
}
|
|
1220
|
+
result.add("decompiler_artifacts", artifacts);
|
|
1221
|
+
return result;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
private JsonObject inferredJumpTable(JumpTable jumpTable) throws Exception {
|
|
1225
|
+
JsonObject result = new JsonObject();
|
|
1226
|
+
result.addProperty(
|
|
1227
|
+
"dispatch_address",
|
|
1228
|
+
canonicalAddress(jumpTable.getSwitchAddress())
|
|
1229
|
+
);
|
|
1230
|
+
JumpTable.LoadTable[] loadTables = jumpTable.getLoadTables();
|
|
1231
|
+
List<Address> referencedDataAddresses = loadTables.length == 0
|
|
1232
|
+
? dispatchPathDataReferences(jumpTable)
|
|
1233
|
+
: List.of();
|
|
1234
|
+
JsonArray dataSources = new JsonArray();
|
|
1235
|
+
int loadTableCount = Math.min(
|
|
1236
|
+
loadTables.length,
|
|
1237
|
+
MAX_NATIVE_API_LOAD_TABLES
|
|
1238
|
+
);
|
|
1239
|
+
for (int index = 0; index < loadTableCount; index += 1) {
|
|
1240
|
+
JumpTable.LoadTable loadTable = loadTables[index];
|
|
1241
|
+
JsonObject source = new JsonObject();
|
|
1242
|
+
source.addProperty("address", canonicalAddress(loadTable.getAddress()));
|
|
1243
|
+
source.addProperty("provenance", "ghidra-decompiler-load-table");
|
|
1244
|
+
source.addProperty("entry_size_bytes", loadTable.getSize());
|
|
1245
|
+
source.addProperty("entry_count", loadTable.getNum());
|
|
1246
|
+
source.addProperty("confidence", "high");
|
|
1247
|
+
JsonArray evidence = new JsonArray();
|
|
1248
|
+
evidence.add(
|
|
1249
|
+
inferenceEvidence(
|
|
1250
|
+
"jump-table",
|
|
1251
|
+
"ghidra-high-function",
|
|
1252
|
+
"Decompiler load table starts at " +
|
|
1253
|
+
canonicalAddress(loadTable.getAddress()) +
|
|
1254
|
+
"."
|
|
1255
|
+
)
|
|
1256
|
+
);
|
|
1257
|
+
source.add("evidence", evidence);
|
|
1258
|
+
dataSources.add(source);
|
|
1259
|
+
}
|
|
1260
|
+
int referencedDataCount = Math.min(
|
|
1261
|
+
referencedDataAddresses.size(),
|
|
1262
|
+
MAX_NATIVE_API_LOAD_TABLES - loadTableCount
|
|
1263
|
+
);
|
|
1264
|
+
for (int index = 0; index < referencedDataCount; index += 1) {
|
|
1265
|
+
Address address = referencedDataAddresses.get(index);
|
|
1266
|
+
JsonObject source = new JsonObject();
|
|
1267
|
+
source.addProperty("address", canonicalAddress(address));
|
|
1268
|
+
source.addProperty(
|
|
1269
|
+
"provenance",
|
|
1270
|
+
"ghidra-dispatch-block-data-reference"
|
|
1271
|
+
);
|
|
1272
|
+
source.add("entry_size_bytes", JsonNull.INSTANCE);
|
|
1273
|
+
source.add("entry_count", JsonNull.INSTANCE);
|
|
1274
|
+
source.addProperty("confidence", "medium");
|
|
1275
|
+
JsonArray evidence = new JsonArray();
|
|
1276
|
+
evidence.add(
|
|
1277
|
+
inferenceEvidence(
|
|
1278
|
+
"jump-table",
|
|
1279
|
+
"ghidra-reference-manager",
|
|
1280
|
+
"The dispatch block or an immediate predecessor contains a typed data reference to " +
|
|
1281
|
+
canonicalAddress(address) +
|
|
1282
|
+
"."
|
|
1283
|
+
)
|
|
1284
|
+
);
|
|
1285
|
+
source.add("evidence", evidence);
|
|
1286
|
+
dataSources.add(source);
|
|
1287
|
+
}
|
|
1288
|
+
result.add("data_sources", dataSources);
|
|
1289
|
+
result.addProperty(
|
|
1290
|
+
"data_sources_truncated",
|
|
1291
|
+
loadTables.length > loadTableCount ||
|
|
1292
|
+
referencedDataAddresses.size() > referencedDataCount
|
|
1293
|
+
);
|
|
1294
|
+
|
|
1295
|
+
Address[] targets = jumpTable.getCases();
|
|
1296
|
+
Integer[] labels = jumpTable.getLabelValues();
|
|
1297
|
+
JsonArray mappings = new JsonArray();
|
|
1298
|
+
int mappingCount = Math.min(targets.length, MAX_NATIVE_API_MAPPINGS);
|
|
1299
|
+
for (int index = 0; index < mappingCount; index += 1) {
|
|
1300
|
+
JsonObject mapping = new JsonObject();
|
|
1301
|
+
if (labels == null || index >= labels.length || labels[index] == null) {
|
|
1302
|
+
mapping.add("case_value", JsonNull.INSTANCE);
|
|
1303
|
+
}
|
|
1304
|
+
else {
|
|
1305
|
+
mapping.addProperty("case_value", labels[index]);
|
|
1306
|
+
}
|
|
1307
|
+
String targetAddress = canonicalAddress(targets[index]);
|
|
1308
|
+
mapping.addProperty("target_address", targetAddress);
|
|
1309
|
+
JsonArray dataAddresses = new JsonArray();
|
|
1310
|
+
for (int sourceIndex = 0; sourceIndex < loadTableCount; sourceIndex += 1) {
|
|
1311
|
+
dataAddresses.add(
|
|
1312
|
+
canonicalAddress(loadTables[sourceIndex].getAddress())
|
|
1313
|
+
);
|
|
1314
|
+
}
|
|
1315
|
+
mapping.add("data_addresses", dataAddresses);
|
|
1316
|
+
mapping.addProperty("confidence", "medium");
|
|
1317
|
+
JsonArray evidence = new JsonArray();
|
|
1318
|
+
evidence.add(
|
|
1319
|
+
inferenceEvidence(
|
|
1320
|
+
"jump-table",
|
|
1321
|
+
"ghidra-high-function",
|
|
1322
|
+
"Recovered target " +
|
|
1323
|
+
targetAddress +
|
|
1324
|
+
" from dispatch " +
|
|
1325
|
+
canonicalAddress(jumpTable.getSwitchAddress()) +
|
|
1326
|
+
"."
|
|
1327
|
+
)
|
|
1328
|
+
);
|
|
1329
|
+
mapping.add("evidence", evidence);
|
|
1330
|
+
mappings.add(mapping);
|
|
1331
|
+
}
|
|
1332
|
+
result.add("mappings", mappings);
|
|
1333
|
+
result.addProperty("mappings_truncated", targets.length > mappingCount);
|
|
1334
|
+
|
|
1335
|
+
JsonArray limitations = new JsonArray();
|
|
1336
|
+
if (loadTables.length == 0 && referencedDataAddresses.isEmpty()) {
|
|
1337
|
+
limitations.add(
|
|
1338
|
+
"Ghidra recovered targets but exposed no backing load-table or dispatch-block data address."
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
if (loadTables.length == 0 && !referencedDataAddresses.isEmpty()) {
|
|
1342
|
+
limitations.add(
|
|
1343
|
+
"Load-table metadata was absent; table-level candidate ownership is inferred from typed data references in the dispatch block and its immediate control-flow predecessors, but candidates are not assigned to individual mappings."
|
|
1344
|
+
);
|
|
1345
|
+
}
|
|
1346
|
+
if (labels == null || labels.length != targets.length) {
|
|
1347
|
+
limitations.add(
|
|
1348
|
+
"Case labels were unavailable or incomplete; null labels are preserved."
|
|
1349
|
+
);
|
|
1350
|
+
}
|
|
1351
|
+
if (
|
|
1352
|
+
loadTables.length > loadTableCount ||
|
|
1353
|
+
referencedDataAddresses.size() > referencedDataCount ||
|
|
1354
|
+
targets.length > mappingCount
|
|
1355
|
+
) {
|
|
1356
|
+
limitations.add(
|
|
1357
|
+
"Native API jump-table observations reached REA's bounded output limits."
|
|
1358
|
+
);
|
|
1359
|
+
}
|
|
1360
|
+
result.add("limitations", limitations);
|
|
1361
|
+
return result;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
private List<Address> dispatchPathDataReferences(JumpTable jumpTable)
|
|
1365
|
+
throws Exception {
|
|
1366
|
+
BasicBlockModel model = new BasicBlockModel(currentProgram, false);
|
|
1367
|
+
CodeBlock dispatchBlock = model.getFirstCodeBlockContaining(
|
|
1368
|
+
jumpTable.getSwitchAddress(),
|
|
1369
|
+
monitor
|
|
1370
|
+
);
|
|
1371
|
+
if (dispatchBlock == null) {
|
|
1372
|
+
return List.of();
|
|
1373
|
+
}
|
|
1374
|
+
List<CodeBlock> blocks = new ArrayList<>();
|
|
1375
|
+
blocks.add(dispatchBlock);
|
|
1376
|
+
CodeBlockReferenceIterator sources = dispatchBlock.getSources(monitor);
|
|
1377
|
+
while (sources.hasNext()) {
|
|
1378
|
+
monitor.checkCancelled();
|
|
1379
|
+
CodeBlock sourceBlock = sources.next().getSourceBlock();
|
|
1380
|
+
if (sourceBlock != null) {
|
|
1381
|
+
blocks.add(sourceBlock);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
Set<Address> addresses = new HashSet<>();
|
|
1385
|
+
for (CodeBlock block : blocks) {
|
|
1386
|
+
InstructionIterator instructions = currentProgram
|
|
1387
|
+
.getListing()
|
|
1388
|
+
.getInstructions(block, true);
|
|
1389
|
+
while (instructions.hasNext()) {
|
|
1390
|
+
monitor.checkCancelled();
|
|
1391
|
+
Instruction instruction = instructions.next();
|
|
1392
|
+
for (
|
|
1393
|
+
Reference reference : currentProgram
|
|
1394
|
+
.getReferenceManager()
|
|
1395
|
+
.getReferencesFrom(instruction.getAddress())
|
|
1396
|
+
) {
|
|
1397
|
+
if (
|
|
1398
|
+
reference.getReferenceType().isData() &&
|
|
1399
|
+
currentProgram
|
|
1400
|
+
.getMemory()
|
|
1401
|
+
.getLoadedAndInitializedAddressSet()
|
|
1402
|
+
.contains(reference.getToAddress())
|
|
1403
|
+
) {
|
|
1404
|
+
addresses.add(reference.getToAddress());
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
List<Address> ordered = new ArrayList<>(addresses);
|
|
1410
|
+
ordered.sort(Address::compareTo);
|
|
1411
|
+
return List.copyOf(ordered);
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
private static JsonObject inferenceEvidence(
|
|
1415
|
+
String kind,
|
|
1416
|
+
String source,
|
|
1417
|
+
String detail
|
|
1418
|
+
) {
|
|
1419
|
+
JsonObject evidence = new JsonObject();
|
|
1420
|
+
evidence.addProperty("kind", kind);
|
|
1421
|
+
evidence.addProperty("source", source);
|
|
1422
|
+
evidence.addProperty("detail", detail);
|
|
1423
|
+
return evidence;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
private static String inferenceConfidence(
|
|
1427
|
+
String signatureSource,
|
|
1428
|
+
boolean typeLocked
|
|
1429
|
+
) {
|
|
1430
|
+
if (
|
|
1431
|
+
typeLocked ||
|
|
1432
|
+
signatureSource.equals("user_defined") ||
|
|
1433
|
+
signatureSource.equals("imported")
|
|
1434
|
+
) {
|
|
1435
|
+
return "high";
|
|
1436
|
+
}
|
|
1437
|
+
return signatureSource.equals("analysis") ? "medium" : "low";
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
private DecompileResults decompile(Function function) {
|
|
975
1441
|
if (function.isExternal() || function.getBody().isEmpty()) {
|
|
976
1442
|
return null;
|
|
977
1443
|
}
|
|
@@ -995,6 +1461,13 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
|
|
995
1461
|
"Ghidra decompilation failed: " + boundedMessage(results.getErrorMessage())
|
|
996
1462
|
);
|
|
997
1463
|
}
|
|
1464
|
+
return results;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
private static String decompiledText(DecompileResults results) {
|
|
1468
|
+
if (results == null) {
|
|
1469
|
+
return null;
|
|
1470
|
+
}
|
|
998
1471
|
DecompiledFunction value = results.getDecompiledFunction();
|
|
999
1472
|
return value == null ? null : value.getC();
|
|
1000
1473
|
}
|
package/bridge/hopper_bridge.py
CHANGED
|
@@ -1046,29 +1046,65 @@ def _serve_connection(connection):
|
|
|
1046
1046
|
if not line or len(line) > MAX_LINE_BYTES:
|
|
1047
1047
|
break
|
|
1048
1048
|
request_id = None
|
|
1049
|
+
authenticated = False
|
|
1049
1050
|
should_stop = False
|
|
1050
1051
|
try:
|
|
1051
1052
|
request = json.loads(line.decode("utf-8"))
|
|
1052
1053
|
if set(request) != {"id", "token", "method", "params"}:
|
|
1053
1054
|
raise ValueError("Invalid bridge request shape")
|
|
1054
1055
|
request_id = request["id"]
|
|
1056
|
+
if isinstance(request_id, bool) or not isinstance(request_id, int) or request_id < 0:
|
|
1057
|
+
raise ValueError("Invalid bridge request id")
|
|
1055
1058
|
if not isinstance(request["token"], str) or not hmac.compare_digest(request["token"], REA_TOKEN):
|
|
1056
1059
|
raise PermissionError("Invalid bridge capability")
|
|
1060
|
+
authenticated = True
|
|
1061
|
+
_write_message(file, {"id": request_id, "event": {
|
|
1062
|
+
"type": "progress",
|
|
1063
|
+
"phase": "hopper_bridge",
|
|
1064
|
+
"completed": 0,
|
|
1065
|
+
"total": 1,
|
|
1066
|
+
"message": "Hopper bridge started request",
|
|
1067
|
+
}})
|
|
1057
1068
|
result = _dispatch(request["method"], request["params"])
|
|
1058
1069
|
should_stop = request["method"] == "shutdown_document" or (
|
|
1059
1070
|
request["method"] == "shutdown" and not result.get("cleanup_required", False)
|
|
1060
1071
|
)
|
|
1061
|
-
|
|
1072
|
+
safe_result = _json_safe(result)
|
|
1073
|
+
_write_message(file, {"id": request_id, "event": {
|
|
1074
|
+
"type": "progress",
|
|
1075
|
+
"phase": "hopper_bridge",
|
|
1076
|
+
"completed": 1,
|
|
1077
|
+
"total": 1,
|
|
1078
|
+
"message": "Hopper bridge completed request",
|
|
1079
|
+
"terminal": True,
|
|
1080
|
+
}})
|
|
1081
|
+
response = {"id": request_id, "result": safe_result}
|
|
1062
1082
|
except Exception as error:
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1083
|
+
diagnostic = _safe_diagnostic(error)
|
|
1084
|
+
if authenticated:
|
|
1085
|
+
_write_message(file, {"id": request_id, "event": {
|
|
1086
|
+
"type": "diagnostic",
|
|
1087
|
+
"error": diagnostic,
|
|
1088
|
+
}})
|
|
1089
|
+
response_id = request_id if (
|
|
1090
|
+
isinstance(request_id, int)
|
|
1091
|
+
and not isinstance(request_id, bool)
|
|
1092
|
+
and request_id >= 0
|
|
1093
|
+
) else 0
|
|
1094
|
+
response = {"id": response_id, "error": diagnostic}
|
|
1095
|
+
_write_message(file, response)
|
|
1066
1096
|
if should_stop:
|
|
1067
1097
|
break
|
|
1068
1098
|
file.close()
|
|
1069
1099
|
connection.close()
|
|
1070
1100
|
|
|
1071
1101
|
|
|
1102
|
+
def _write_message(file, message):
|
|
1103
|
+
"""Write and flush one compact bridge message."""
|
|
1104
|
+
file.write((json.dumps(message, separators=(",", ":")) + "\n").encode("utf-8"))
|
|
1105
|
+
file.flush()
|
|
1106
|
+
|
|
1107
|
+
|
|
1072
1108
|
def _diagnostic_type(error):
|
|
1073
1109
|
if isinstance(error, CapabilityUnavailableError):
|
|
1074
1110
|
return "capability_unavailable"
|
|
@@ -1079,6 +1115,20 @@ def _diagnostic_type(error):
|
|
|
1079
1115
|
return "bridge_exception"
|
|
1080
1116
|
|
|
1081
1117
|
|
|
1118
|
+
def _safe_diagnostic(error):
|
|
1119
|
+
"""Project one exception without retaining provider or credential text."""
|
|
1120
|
+
diagnostic_type = _diagnostic_type(error)
|
|
1121
|
+
if diagnostic_type == "capability_unavailable":
|
|
1122
|
+
message = str(error)[:512]
|
|
1123
|
+
elif diagnostic_type == "authorization":
|
|
1124
|
+
message = "Invalid bridge capability"
|
|
1125
|
+
elif diagnostic_type == "invalid_request":
|
|
1126
|
+
message = "Invalid Hopper bridge request"
|
|
1127
|
+
else:
|
|
1128
|
+
message = "%s: Hopper bridge operation failed" % type(error).__name__[:128]
|
|
1129
|
+
return {"code": -32000, "message": message, "type": diagnostic_type}
|
|
1130
|
+
|
|
1131
|
+
|
|
1082
1132
|
def _run():
|
|
1083
1133
|
"""Own a permission-restricted, single-client Unix socket for this bridge."""
|
|
1084
1134
|
if os.path.exists(REA_SOCKET):
|