rea-agents 6.0.0 → 6.1.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 +23 -17
- package/bridge/android/ReaJadxBridge.java +14 -5
- package/bridge/ghidra/ReaGhidraBridge.java +173 -47
- package/bridge/hopper_bridge.py +310 -128
- package/bridge/mitmproxy/capture.py +0 -2
- package/bridge/native/rea_lldb_tracer.py +630 -94
- package/bridge/process/ProcessRunTokenReader.swift +39 -5
- package/bridge/pwntools/recorded_crash_impl.py +4 -0
- package/dist/android/JadxConfiguration.js +196 -33
- package/dist/android/JadxMcpTransport.js +4 -0
- package/dist/android/JadxProtocol.js +6 -0
- package/dist/android/JadxProvider.js +19 -2
- package/dist/android/JadxSession.js +23 -4
- package/dist/application/AnalysisContextQueries.js +8 -35
- package/dist/application/AnalysisDocument.js +32 -0
- package/dist/application/AppBundleExecutable.js +67 -21
- package/dist/application/CallPathTracing.js +19 -3
- package/dist/application/CapabilityInventory.js +6 -3
- package/dist/application/ClientConfigurationDocument.js +871 -6
- package/dist/application/ClientRegistrationIdentity.js +26 -5
- package/dist/application/ClientRegistrationStatus.js +29 -14
- package/dist/application/DirectAnalysis.js +27 -37
- package/dist/application/Doctor.js +4 -2
- package/dist/application/DoctorScope.js +25 -13
- package/dist/application/EnhancedLiteralTracing.js +22 -25
- package/dist/application/EnhancedTools.js +13 -11
- package/dist/application/EvidenceBundleFiles.js +23 -11
- package/dist/application/FunctionAnalysisEvidence.js +2 -3
- package/dist/application/InventoryProjectionEvidence.js +4 -3
- package/dist/application/InvestigationProviders.js +3 -2
- package/dist/application/JsonFiles.js +50 -28
- package/dist/application/ProcedureAddressResolution.js +11 -0
- package/dist/application/ReconstructionCoverageService.js +7 -4
- package/dist/application/ReconstructionObligationLedgerService.js +4 -3
- package/dist/application/Setup.js +13 -1
- package/dist/application/SetupClientConfiguration.js +10 -15
- package/dist/application/SetupHost.js +5 -2
- package/dist/application/SetupPlan.js +5 -3
- package/dist/application/SupportedClients.js +34 -1
- package/dist/application/Uninstall.js +4 -3
- package/dist/application/Update.js +18 -3
- package/dist/application/UpdateRuntime.js +46 -25
- package/dist/application/WebModuleTraceService.js +4 -2
- package/dist/application/WebNetworkCaptureService.js +4 -2
- package/dist/application/WebRuntimeService.js +5 -3
- package/dist/application/WebScriptExportService.js +2 -2
- package/dist/application/WebSourceLocationService.js +4 -2
- package/dist/application/WorkflowEvidence.js +82 -0
- package/dist/application/binary/AnalysisProviderEvaluation.js +21 -5
- package/dist/application/binary/AnalysisProviderRegistry.js +2 -0
- package/dist/application/binary/AnalysisSnapshotCache.js +3 -6
- package/dist/application/binary/BinarySession.js +95 -68
- package/dist/application/binary/BinarySessionOpen.js +3 -3
- package/dist/application/binary/BinarySessionRecords.js +25 -3
- package/dist/application/binary/BinarySessionStatus.js +17 -1
- package/dist/application/binary/CompositeProvider.js +13 -0
- package/dist/application/binary/LazyAnalysisProvider.js +3 -0
- package/dist/application/binaryDiagnostics/BinaryLayoutService.js +4 -2
- package/dist/application/binaryDiagnostics/RecordedCrashService.js +4 -2
- package/dist/application/evm/EvmInterfaceService.js +4 -2
- package/dist/application/investigation/EvidenceLedger.js +21 -7
- package/dist/application/investigation/InvestigationRecords.js +8 -0
- package/dist/application/javascript/JavaScriptAnalysisControl.js +34 -0
- package/dist/application/javascript/JavaScriptApplicationEvidence.js +24 -3
- package/dist/application/javascript/JavaScriptApplicationEvidenceGraph.js +19 -2
- package/dist/application/javascript/JavaScriptApplicationService.js +22 -15
- package/dist/application/javascript/JavaScriptApplicationWorkflowService.js +5 -5
- package/dist/application/javascript/JavaScriptArtifactAnalysis.js +5 -4
- package/dist/application/javascript/JavaScriptArtifactGraphBuilder.js +39 -7
- package/dist/application/javascript/JavaScriptArtifactPathResolution.js +6 -3
- package/dist/application/javascript/JavaScriptArtifactReconstruction.js +11 -3
- package/dist/application/javascript/JavaScriptModuleRelationships.js +37 -8
- package/dist/application/javascript/JavaScriptRecoveryService.js +4 -2
- package/dist/application/javascript/JavaScriptRuntimeObservationService.js +3 -1
- package/dist/application/javascript/JavaScriptRuntimeReconciliationService.js +3 -3
- package/dist/application/javascript/JavaScriptSemanticGraphBuilder.js +63 -61
- package/dist/application/javascript/JavaScriptSemanticGraphValueProjection.js +23 -1
- package/dist/application/managed/ManagedApplicationGraphService.js +4 -3
- package/dist/application/managed/ManagedMemberComparisonService.js +7 -4
- package/dist/application/managed/ManagedNativeVerificationService.js +4 -3
- package/dist/application/managed/ManagedReconstructionService.js +4 -3
- package/dist/application/process/ProcessCli.js +14 -6
- package/dist/application/process/ProcessEvidence.js +4 -3
- package/dist/application/workflowInputError.js +18 -0
- package/dist/artifacts/ArtifactProvider.js +12 -2
- package/dist/artifacts/MachOSliceArtifactReader.js +98 -14
- package/dist/artifacts/NativeDmgArtifactReader.js +13 -6
- package/dist/artifacts/apple/AppleAssetCatalogAnalysis.js +22 -3
- package/dist/artifacts/apple/InterfaceBuilderAnalysis.js +176 -56
- package/dist/artifacts/apple/InterfaceBuilderDecodeBudget.js +195 -0
- package/dist/artifacts/apple/KeyedArchiveReader.js +36 -22
- package/dist/artifacts/apple/MachoLoadCommandReader.js +92 -31
- package/dist/artifacts/apple/NibArchive.js +81 -14
- package/dist/artifacts/apple/NibViewHierarchy.js +10 -3
- package/dist/artifacts/apple/PlistNumberLiterals.js +164 -0
- package/dist/artifacts/inventory/ArtifactGraphConstruction.js +13 -8
- package/dist/artifacts/inventory/scanCanonical.js +11 -14
- package/dist/artifacts/javascript/JavaScriptArtifactFiles.js +6 -2
- package/dist/browser/CdpCaptureCompleteness.js +2 -0
- package/dist/browser/CdpCaptureEventHandlers.js +76 -11
- package/dist/browser/CdpCaptureEventTypes.js +5 -1
- package/dist/browser/CdpCaptureEvents.js +25 -0
- package/dist/browser/CdpConnection.js +30 -11
- package/dist/browser/CdpOptionalCommand.js +2 -3
- package/dist/browser/CdpPageCapture.js +23 -1
- package/dist/browser/PlaywrightElectronActiveActions.js +32 -23
- package/dist/browser/PlaywrightElectronActiveProjection.js +141 -0
- package/dist/browser/PlaywrightElectronActiveProvider.js +123 -124
- package/dist/browser/WebSourceMapFetcher.js +426 -72
- package/dist/browser/assets/LocalWebScriptArtifacts.js +2 -2
- package/dist/browser/history/CaptureFailures.js +37 -1
- package/dist/browser/history/CaptureRelease.js +5 -0
- package/dist/browser/history/HarCaptureAdapter.js +3 -3
- package/dist/browser/history/HarJson.js +3 -2
- package/dist/browser/history/HistoricalCaptureDecoder.js +4 -2
- package/dist/cli/analysisSelector.js +23 -0
- package/dist/cli/applicationCommands.js +2 -3
- package/dist/cli/coreBinaryCommands.js +70 -12
- package/dist/cli/electronCommands.js +7 -6
- package/dist/cli/javascriptApplicationAnalysis.js +7 -3
- package/dist/cli/jsonOutput.js +4 -99
- package/dist/cli/nativeCallCommands.js +2 -3
- package/dist/cli/webRuntimeCommands.js +8 -8
- package/dist/cli.js +2 -1
- package/dist/cliCommandNames.js +0 -1
- package/dist/cliJsonInput.js +9 -4
- package/dist/config/environment.js +4 -0
- package/dist/config/ghidraStartupTimeout.js +17 -0
- package/dist/config/mcpResponseBudget.js +22 -0
- package/dist/config/parseConfig.js +2 -1
- package/dist/contracts/enhancedInputs.js +5 -3
- package/dist/contracts/enhancedToolContracts.js +3 -3
- package/dist/contracts/hopperMemoryContracts.js +2 -5
- package/dist/contracts/javascript/electronToolContracts.js +1 -1
- package/dist/contracts/managed/managedToolContracts.js +2 -4
- package/dist/contracts/native/nativeToolContracts.js +4 -2
- package/dist/contracts/officialToolContracts.js +14 -15
- package/dist/contracts/promptContracts.js +3 -3
- package/dist/contracts/sessionLifecycleInputs.js +3 -7
- package/dist/contracts/sessionStatusContract.js +2 -4
- package/dist/contracts/sessionToolContracts.js +4 -6
- package/dist/contracts/sessionToolSchemas.js +2 -5
- package/dist/contracts/toolContractExamples.js +0 -1
- package/dist/contracts/toolContractHelpers.js +4 -1
- package/dist/contracts/toolEffects.js +0 -1
- package/dist/contracts/toolOutputSchemaGroups.js +28 -27
- package/dist/contracts/toolOutputSchemaPrimitives.js +18 -0
- package/dist/contracts/toolSchemaMetadata.js +69 -10
- package/dist/contracts/webNetworkCaptureToolContracts.js +1 -1
- package/dist/domain/analysisErrorBase.js +8 -3
- package/dist/domain/analysisErrorCore.js +3 -1
- package/dist/domain/analysisErrorPresentation.js +72 -17
- package/dist/domain/analysisErrorProjection.js +106 -27
- package/dist/domain/android/androidAnalysis.js +2 -4
- package/dist/domain/android/androidApplication.js +17 -9
- package/dist/domain/apple/appleApplication.js +41 -14
- package/dist/domain/apple/dyldEnvironment.js +73 -0
- package/dist/domain/apple/dylibResolution.js +120 -9
- package/dist/domain/apple/dylibResolutionFindings.js +9 -7
- package/dist/domain/apple/interfaceBuilderGraph.js +25 -13
- package/dist/domain/apple/interfaceBuilderKeyedArchive.js +13 -3
- package/dist/domain/apple/keyedArchive.js +9 -1
- package/dist/domain/apple/machoContainer.js +54 -0
- package/dist/domain/apple/plistNumbers.js +76 -0
- package/dist/domain/artifactIdentity.js +29 -0
- package/dist/domain/artifactInventoryEvidence.js +19 -24
- package/dist/domain/bridgeCandidateProjection.js +71 -0
- package/dist/domain/browserCompleteness.js +1 -0
- package/dist/domain/browserObservationError.js +24 -5
- package/dist/domain/browserScenarioValues.js +2 -6
- package/dist/domain/bundleComparison.js +3 -2
- package/dist/domain/canonicalJsonDigestSteps.js +133 -0
- package/dist/domain/canonicalOrdering.js +9 -5
- package/dist/domain/changedBehavior.js +6 -6
- package/dist/domain/comparisonSourceEvidence.js +40 -0
- package/dist/domain/configurationErrors.js +1 -1
- package/dist/domain/evidence.js +64 -9
- package/dist/domain/evidenceBundle.js +42 -1
- package/dist/domain/evidenceErrors.js +12 -1
- package/dist/domain/explicitSensitiveFailure.js +17 -1
- package/dist/domain/firmware/firmwareAnalysis.js +6 -7
- package/dist/domain/hopperErrors.js +23 -2
- package/dist/domain/hopperValues.js +51 -8
- package/dist/domain/immutableJson.js +114 -0
- package/dist/domain/inputIssueProjection.js +3 -0
- package/dist/domain/javascript/electronActiveObservation.js +30 -4
- package/dist/domain/javascript/electronStaticAnalysisValues.js +6 -12
- package/dist/domain/javascript/javascriptApplicationAnalysis.js +71 -23
- package/dist/domain/javascript/javascriptApplicationGraph.js +13 -0
- package/dist/domain/javascript/javascriptAstValues.js +20 -0
- package/dist/domain/javascript/javascriptRecovery.js +3 -8
- package/dist/domain/javascript/javascriptSemanticAnalysis.js +97 -2
- package/dist/domain/javascript/javascriptSemanticCoverage.js +6 -5
- package/dist/domain/javascript/javascriptSemanticDataEffectHelpers.js +9 -17
- package/dist/domain/javascript/javascriptSemanticDataEffects.js +22 -5
- package/dist/domain/javascript/javascriptSemanticGraph.js +13 -0
- package/dist/domain/javascript/javascriptSemanticGraphSchemas.js +1 -0
- package/dist/domain/javascript/javascriptSemanticPrimitives.js +25 -17
- package/dist/domain/javascript/javascriptSemanticQuerySchemas.js +8 -4
- package/dist/domain/javascript/javascriptSemanticResourceLimits.js +77 -0
- package/dist/domain/javascript/javascriptSemanticValues.js +83 -11
- package/dist/domain/javascript/javascriptSourceParser.js +6 -2
- package/dist/domain/javascript/javascriptStaticAnalysis.js +22 -17
- package/dist/domain/javascript/javascriptStaticAnalysisBundler.js +5 -3
- package/dist/domain/javascript/javascriptStaticAnalysisCalls.js +14 -4
- package/dist/domain/javascript/javascriptStaticAnalysisFindings.js +66 -7
- package/dist/domain/javascript/javascriptStaticAnalysisHelpers.js +28 -16
- package/dist/domain/javascript/javascriptStaticAnalysisState.js +3 -2
- package/dist/domain/jsonSerialization.js +109 -0
- package/dist/domain/jsonValue.js +90 -6
- package/dist/domain/localPath.js +6 -0
- package/dist/domain/managed/managedApplicationGraph.js +10 -10
- package/dist/domain/managed/managedInspectionEvidence.js +16 -0
- package/dist/domain/managed/managedMemberComparisonMatch.js +5 -9
- package/dist/domain/managed/managedNativeVerification.js +3 -9
- package/dist/domain/native/fileOffsetPartialObservation.js +1 -0
- package/dist/domain/native/nativeCallObservation.js +33 -11
- package/dist/domain/native/nativeCallPartialObservation.js +33 -0
- package/dist/domain/process/processCapture.js +8 -4
- package/dist/domain/process/processComparison.js +38 -4
- package/dist/domain/process/processScenario.js +11 -10
- package/dist/domain/propertyListKeys.js +1 -1
- package/dist/domain/propertyListXmlText.js +19 -0
- package/dist/domain/providerAdapterError.js +1 -1
- package/dist/domain/providerOperationHealth.js +54 -0
- package/dist/domain/reconstructionVerification.js +6 -43
- package/dist/domain/reconstructionVerificationSchemas.js +2 -2
- package/dist/domain/staticRuntimeCorrelation.js +6 -43
- package/dist/domain/webBundleAnalysis.js +20 -8
- package/dist/domain/webBundleAnalyzerInspection.js +13 -8
- package/dist/domain/webBundleAnalyzerResult.js +41 -2
- package/dist/domain/webRuntime.js +19 -5
- package/dist/domain/webSourceLocation.js +1 -0
- package/dist/firmware/FirmwareFiles.js +1 -1
- package/dist/generatedPackageMetadata.js +1 -1
- package/dist/ghidra/GhidraAnalysisProfile.js +5 -0
- package/dist/ghidra/GhidraClient.js +47 -14
- package/dist/ghidra/GhidraDiagnostics.js +7 -7
- package/dist/ghidra/GhidraFunctionValues.js +10 -7
- package/dist/ghidra/GhidraInstallation.js +18 -21
- package/dist/ghidra/GhidraInventoryValues.js +14 -4
- package/dist/ghidra/GhidraJavaLaunch.js +62 -0
- package/dist/ghidra/GhidraLauncher.js +33 -16
- package/dist/ghidra/GhidraProviderCapabilities.js +14 -2
- package/dist/ghidra/GhidraProviderClient.js +23 -12
- package/dist/ghidra/GhidraSessionError.js +11 -7
- package/dist/ghidra/GhidraTargetSnapshot.js +47 -12
- package/dist/ghidra/GhidraTransport.js +20 -0
- package/dist/hopper/BridgeLauncher.js +31 -11
- package/dist/hopper/HopperAnalysisProfile.js +62 -4
- package/dist/hopper/HopperCleanup.js +15 -3
- package/dist/hopper/HopperClient.js +222 -14
- package/dist/hopper/HopperFileOffset.js +237 -0
- package/dist/hopper/HopperMachOImage.js +196 -0
- package/dist/hopper/HopperOperationStage.js +6 -0
- package/dist/hopper/HopperProvider.js +63 -9
- package/dist/hopper/HopperProviderCapabilities.js +19 -1
- package/dist/hopper/HopperRegexSearch.js +137 -0
- package/dist/hopper/HopperRegexWorker.js +27 -0
- package/dist/hopper/HopperRequestQueue.js +68 -19
- package/dist/hopper/HopperTargetLease.js +3 -2
- package/dist/inspector/JavaScriptRuntimeScope.js +9 -1
- package/dist/inspector/V8InspectorCaptureProjection.js +8 -3
- package/dist/inspector/V8InspectorProvider.js +47 -11
- package/dist/javascript/sourceMaps/DecodedSourceMap.js +47 -0
- package/dist/javascript/sourceMaps/SourceMapCommand.js +16 -0
- package/dist/javascript/sourceMaps/SourceMapFormat.js +79 -31
- package/dist/javascript/sourceMaps/TraceSourceMap.js +14 -28
- package/dist/mcpDoctor.js +62 -2
- package/dist/native/AppleDispatchCoverage.js +47 -6
- package/dist/native/AppleDispatchDecodeFacts.js +6 -0
- package/dist/native/AppleDispatchMetadata.js +6 -3
- package/dist/native/AppleMachoFixups.js +48 -24
- package/dist/native/AppleMachoSelection.js +32 -16
- package/dist/native/AppleObjcCategories.js +6 -2
- package/dist/native/AppleObjcDispatchFacets.js +5 -2
- package/dist/native/AppleObjcProperties.js +2 -1
- package/dist/native/AppleSwiftDispatchFacets.js +27 -21
- package/dist/native/AppleSwiftVtables.js +21 -15
- package/dist/native/CommandRunner.js +81 -84
- package/dist/native/LldbCallTracer.js +365 -102
- package/dist/native/LldbRetainedObservations.js +178 -0
- package/dist/native/NativeCallObservation.js +62 -3
- package/dist/native/NativeMacOSProvider.js +98 -17
- package/dist/native/SignatureTargetBinding.js +155 -0
- package/dist/native/parsers/lipo.js +71 -1
- package/dist/native/pwndbg/PwndbgCoreContext.js +25 -5
- package/dist/native/pwntools/RecordedCrashStructureBindings.js +2 -0
- package/dist/process/DarwinProcessRunTokenReader.js +8 -9
- package/dist/process/ExecFileOutput.js +20 -0
- package/dist/process/OwnedCommand.js +7 -2
- package/dist/process/ProcessOwnership.js +203 -53
- package/dist/process/ProcessOwnershipObservation.js +49 -4
- package/dist/process/ProviderProcess.js +38 -7
- package/dist/process/capture/ProcessCaptureError.js +2 -2
- package/dist/process/capture/ProcessCaptureLifecycle.js +100 -34
- package/dist/server/EvidenceMcpServer.js +146 -0
- package/dist/server/createServer.js +15 -8
- package/dist/server/registerAndroidTools.js +2 -4
- package/dist/server/registerApplicationTools/helpers.js +2 -4
- package/dist/server/registerArtifactComparisonTool.js +11 -7
- package/dist/server/registerBinaryDiagnosticsTools.js +2 -4
- package/dist/server/registerBrowserScenarioTool.js +2 -4
- package/dist/server/registerBrowserTools.js +2 -4
- package/dist/server/registerBundleComparisonTool.js +2 -3
- package/dist/server/registerCloseLifecycleTool.js +8 -18
- package/dist/server/registerElectronTools.js +2 -4
- package/dist/server/registerEnhancedTools.js +18 -59
- package/dist/server/registerEvidenceTools.js +2 -4
- package/dist/server/registerEvmTools.js +2 -4
- package/dist/server/registerFirmwareTools.js +2 -4
- package/dist/server/registerFunctionComparisonTool.js +10 -6
- package/dist/server/registerInvestigationTools.js +7 -7
- package/dist/server/registerJavaScriptRecoveryTool.js +2 -4
- package/dist/server/registerJavaScriptRuntimeObservationTools.js +2 -4
- package/dist/server/registerManagedWorkflowTools/compareManagedMembers.js +2 -4
- package/dist/server/registerManagedWorkflowTools/importManagedReconstruction.js +2 -4
- package/dist/server/registerManagedWorkflowTools/projectManagedApplicationGraph.js +2 -4
- package/dist/server/registerManagedWorkflowTools/verifyManagedNativeBoundaries.js +2 -4
- package/dist/server/registerOfficialTools.js +2 -4
- package/dist/server/registerProcessComparisonTool.js +71 -45
- package/dist/server/registerRecordedCrashTools.js +2 -4
- package/dist/server/registerSessionRecordTools.js +5 -6
- package/dist/server/registerSessionStatusTool.js +17 -3
- package/dist/server/registerSessionTools.js +11 -3
- package/dist/server/registerWebModuleTool.js +2 -4
- package/dist/server/registerWebNetworkCaptureTool.js +2 -4
- package/dist/server/registerWebRuntimeTools.js +3 -7
- package/dist/server/registerWebScriptTool.js +2 -4
- package/dist/server/registerWebSourceLocationTool.js +2 -4
- package/dist/server/runDerivedOperation.js +15 -3
- package/dist/server/sessionAvailabilityPolicy.js +0 -1
- package/dist/server/toolRegistrationOptions.js +2 -2
- package/dist/server/toolResult.js +62 -10
- package/dist/server/toolResultEncoding.js +33 -0
- package/install.sh +1 -1
- package/native/windows/build/manifest.json +1 -1
- package/package.json +8 -3
- package/scripts/electron-active-hook-boundaries.cjs +18 -20
- package/scripts/electron-active-hook.cjs +241 -37
- package/skills/reverse-engineer-anything/SKILL.md +11 -3
- package/skills/reverse-engineer-anything/references/javascript-applications.md +8 -1
- package/dist/ghidra/GhidraDefaults.js +0 -2
package/bridge/hopper_bridge.py
CHANGED
|
@@ -8,12 +8,12 @@ API access on this thread: moving dispatch to a worker can deadlock Hopper.
|
|
|
8
8
|
import json
|
|
9
9
|
import hmac
|
|
10
10
|
import os
|
|
11
|
-
import re
|
|
12
11
|
import socket
|
|
13
12
|
from typing import Any, Optional, Protocol, Sequence
|
|
14
13
|
|
|
15
14
|
BAD_ADDRESSES = (-1, 0xFFFFFFFFFFFFFFFF, None)
|
|
16
|
-
|
|
15
|
+
# The native label setter truncates NSString names at this UTF-16 extent.
|
|
16
|
+
HOPPER_SYMBOL_NAME_UTF16_UNITS = 1024
|
|
17
17
|
_rea_session_document = None
|
|
18
18
|
_search_inventory_cache = {}
|
|
19
19
|
_pseudocode_cache = {}
|
|
@@ -89,9 +89,11 @@ def _session_document():
|
|
|
89
89
|
global _rea_session_document
|
|
90
90
|
documents = _api().documents()
|
|
91
91
|
if _rea_session_document is not None:
|
|
92
|
+
# Hopper recreates Python wrappers; equality compares native handles.
|
|
93
|
+
# Retain our bound wrapper so cache identity stays stable across calls.
|
|
92
94
|
return (
|
|
93
95
|
_rea_session_document
|
|
94
|
-
if any(document
|
|
96
|
+
if any(document == _rea_session_document for document in documents)
|
|
95
97
|
else None
|
|
96
98
|
)
|
|
97
99
|
return _bind_session_document(documents)
|
|
@@ -174,46 +176,89 @@ def _json_safe(value):
|
|
|
174
176
|
|
|
175
177
|
|
|
176
178
|
def _document(name=None):
|
|
177
|
-
"""
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
179
|
+
"""Keep observations and mutations bound to the active REA target."""
|
|
180
|
+
documents = _api().documents()
|
|
181
|
+
session_document = _session_document()
|
|
182
|
+
if session_document is None:
|
|
183
|
+
raise CapabilityUnavailableError("The active REA target's Hopper document is no longer open; reopen the target with open_binary")
|
|
181
184
|
if name is not None:
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
return current
|
|
189
|
-
raise InvalidRequestError("Hopper document name is ambiguous")
|
|
185
|
+
# A document name is display metadata; scope it to the bound native
|
|
186
|
+
# handle even when another open target happens to have the same name.
|
|
187
|
+
if session_document.getDocumentName() == name:
|
|
188
|
+
return session_document
|
|
189
|
+
if any(candidate.getDocumentName() == name for candidate in documents):
|
|
190
|
+
raise InvalidRequestError("Hopper document belongs to another target; use open_binary to select that target")
|
|
190
191
|
raise InvalidRequestError("Unknown Hopper document")
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
192
|
+
return session_document
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _api_text(value, field):
|
|
196
|
+
"""Validate text before passing it to Hopper's native UTF-8 string API."""
|
|
197
|
+
if not isinstance(value, str):
|
|
198
|
+
raise InvalidRequestError("%s must be a string" % field)
|
|
199
|
+
if "\0" in value:
|
|
200
|
+
raise InvalidRequestError("%s contains a NUL character, which Hopper's Python API cannot represent" % field)
|
|
201
|
+
try:
|
|
202
|
+
value.encode("utf-8")
|
|
203
|
+
except UnicodeEncodeError as error:
|
|
204
|
+
raise InvalidRequestError("%s contains an unpaired Unicode surrogate, which Hopper's Python API cannot represent" % field) from error
|
|
205
|
+
return value
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _validate_name_targets(document, names):
|
|
209
|
+
"""Keep Hopper's unique-label moves within the explicitly selected addresses."""
|
|
210
|
+
targets = {address for _, address, _ in names}
|
|
211
|
+
if len(targets) != len(names):
|
|
212
|
+
raise InvalidRequestError("Batch rename selects the same address more than once; use one canonical address per target")
|
|
213
|
+
labels = set()
|
|
214
|
+
for _, address, name in names:
|
|
215
|
+
_segment(document, address)
|
|
216
|
+
if not name:
|
|
217
|
+
continue
|
|
218
|
+
if name in labels:
|
|
219
|
+
raise InvalidRequestError("Batch rename assigns the same name more than once: %s" % name)
|
|
220
|
+
labels.add(name)
|
|
221
|
+
owner = document.getAddressForName(name)
|
|
222
|
+
if owner not in BAD_ADDRESSES and owner != address and owner not in targets:
|
|
223
|
+
raise InvalidRequestError("Name %s is already assigned to %s; include that address in the batch with a replacement name" % (name, _hex(owner)))
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _symbol_name(value, field):
|
|
227
|
+
"""Reject native label truncation without altering the caller's name."""
|
|
228
|
+
value = _api_text(value, field)
|
|
229
|
+
if len(value.encode("utf-16-le")) // 2 > HOPPER_SYMBOL_NAME_UTF16_UNITS:
|
|
230
|
+
raise InvalidRequestError("%s exceeds Hopper's %s UTF-16 code-unit symbol-name limit" % (field, HOPPER_SYMBOL_NAME_UTF16_UNITS))
|
|
231
|
+
return value
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _name_matches(document, address, name):
|
|
235
|
+
observed = _segment(document, address).getNameAtAddress(address)
|
|
236
|
+
return observed == name or (not name and observed is None)
|
|
202
237
|
|
|
203
238
|
|
|
204
239
|
def _address(document, value=None):
|
|
205
|
-
"""
|
|
240
|
+
"""Keep explicit hexadecimal coordinates distinct from symbol names."""
|
|
206
241
|
if value is None:
|
|
207
242
|
return document.getCurrentAddress()
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
243
|
+
_api_text(value, "Address")
|
|
244
|
+
if value.lower().startswith("0x"):
|
|
245
|
+
try:
|
|
246
|
+
address = int(value, 16)
|
|
247
|
+
except ValueError as error:
|
|
248
|
+
raise InvalidRequestError("Invalid hexadecimal Hopper address: %s" % value) from error
|
|
249
|
+
else:
|
|
250
|
+
# Names such as `add` and `face` are valid hexadecimal strings too.
|
|
251
|
+
# Prefer an observed symbol, retaining bare-hex compatibility only
|
|
252
|
+
# when Hopper has no symbol with the supplied name.
|
|
253
|
+
address = document.getAddressForName(value)
|
|
254
|
+
if address in BAD_ADDRESSES:
|
|
255
|
+
try:
|
|
256
|
+
address = int(value, 16)
|
|
257
|
+
except ValueError as error:
|
|
258
|
+
raise InvalidRequestError("Unknown Hopper address or name: %s" % value) from error
|
|
259
|
+
if not 0 <= address < 0xFFFFFFFFFFFFFFFF:
|
|
260
|
+
raise InvalidRequestError("Address is outside Hopper's usable unsigned 64-bit range: %s" % value)
|
|
261
|
+
return address
|
|
217
262
|
|
|
218
263
|
|
|
219
264
|
def _segment(document, address):
|
|
@@ -249,14 +294,18 @@ def _procedure_identity(procedure):
|
|
|
249
294
|
|
|
250
295
|
|
|
251
296
|
def _procedure_locals(procedure):
|
|
252
|
-
"""
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
297
|
+
"""Preserve public local-variable names and observed stack displacements."""
|
|
298
|
+
result = []
|
|
299
|
+
for local in procedure.getLocalVariableList():
|
|
300
|
+
name = local.name()
|
|
301
|
+
displacement = local.displacement()
|
|
302
|
+
result.append({
|
|
303
|
+
"description": "%s (stack displacement %s)" % (name if name is not None else "Unnamed local", displacement),
|
|
304
|
+
"name": name,
|
|
305
|
+
"stack_displacement": str(displacement) if displacement is not None else None,
|
|
256
306
|
"provenance": "hopper-public-python-api",
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
]
|
|
307
|
+
})
|
|
308
|
+
return result
|
|
260
309
|
|
|
261
310
|
|
|
262
311
|
def _containing_procedure(document, address):
|
|
@@ -267,26 +316,75 @@ def _containing_procedure(document, address):
|
|
|
267
316
|
return (procedure, None) if procedure is not None else (None, "not_in_procedure")
|
|
268
317
|
|
|
269
318
|
|
|
319
|
+
def _basic_block_end(procedure, block):
|
|
320
|
+
"""Normalize native block endpoints to an exclusive byte address."""
|
|
321
|
+
owner_at = getattr(procedure, "getBasicBlockAtAddress", None)
|
|
322
|
+
if not callable(owner_at):
|
|
323
|
+
raise CapabilityUnavailableError(
|
|
324
|
+
"Instruction enumeration requires Hopper basic-block ownership"
|
|
325
|
+
)
|
|
326
|
+
end = block.getEndingAddress()
|
|
327
|
+
# Hopper 6.1 returns the final instruction address, despite the Python
|
|
328
|
+
# documentation describing an exclusive end. Membership distinguishes
|
|
329
|
+
# that representation from an actual exclusive endpoint in other builds.
|
|
330
|
+
if owner_at(end) == block:
|
|
331
|
+
instruction = procedure.getSegment().getInstructionAtAddress(end)
|
|
332
|
+
if instruction is not None and instruction.getInstructionLength() > 0:
|
|
333
|
+
return end + instruction.getInstructionLength()
|
|
334
|
+
return end
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _block_instruction_addresses(procedure, block):
|
|
338
|
+
"""Walk either endpoint convention using Hopper's block ownership API.
|
|
339
|
+
|
|
340
|
+
Some Hopper builds return the final instruction start, despite the public
|
|
341
|
+
API documenting an exclusive end. Equality compares fresh native wrappers;
|
|
342
|
+
probing ownership also keeps an exclusive endpoint in the next block out.
|
|
343
|
+
"""
|
|
344
|
+
segment = procedure.getSegment()
|
|
345
|
+
address = block.getStartingAddress()
|
|
346
|
+
end = _basic_block_end(procedure, block)
|
|
347
|
+
while address < end:
|
|
348
|
+
if procedure.getBasicBlockAtAddress(address) != block:
|
|
349
|
+
break
|
|
350
|
+
instruction = segment.getInstructionAtAddress(address)
|
|
351
|
+
if instruction is None:
|
|
352
|
+
break
|
|
353
|
+
length = instruction.getInstructionLength()
|
|
354
|
+
if length <= 0 or address + length > end:
|
|
355
|
+
break
|
|
356
|
+
yield address
|
|
357
|
+
address += length
|
|
358
|
+
|
|
359
|
+
|
|
270
360
|
def _instruction_addresses(procedure):
|
|
271
361
|
result = []
|
|
272
362
|
seen = set()
|
|
273
|
-
segment = procedure.getSegment()
|
|
274
363
|
for block in procedure.basicBlockIterator():
|
|
275
|
-
address
|
|
276
|
-
|
|
277
|
-
|
|
364
|
+
for address in _block_instruction_addresses(procedure, block):
|
|
365
|
+
if address in seen:
|
|
366
|
+
continue
|
|
278
367
|
seen.add(address)
|
|
279
|
-
instruction = segment.getInstructionAtAddress(address)
|
|
280
|
-
if instruction is None:
|
|
281
|
-
break
|
|
282
368
|
result.append(address)
|
|
283
|
-
length = instruction.getInstructionLength()
|
|
284
|
-
if length <= 0:
|
|
285
|
-
break
|
|
286
|
-
address += length
|
|
287
369
|
return result
|
|
288
370
|
|
|
289
371
|
|
|
372
|
+
def _native_calls(procedure, direction):
|
|
373
|
+
references = procedure.getAllCallees() if direction == "outgoing" else procedure.getAllCallers()
|
|
374
|
+
kinds = {0: "none", 1: "unknown", 2: "direct", 3: "objective_c"}
|
|
375
|
+
calls = {}
|
|
376
|
+
unresolved = []
|
|
377
|
+
for reference in references:
|
|
378
|
+
source, target, kind = reference.fromAddress(), reference.toAddress(), reference.type()
|
|
379
|
+
if source in BAD_ADDRESSES:
|
|
380
|
+
continue
|
|
381
|
+
if target in BAD_ADDRESSES:
|
|
382
|
+
unresolved.append({"address": _hex(source), "reason": "Native CallReference has no resolved target (type %s)" % kind})
|
|
383
|
+
continue
|
|
384
|
+
calls[(source, target)] = {"classification": kinds.get(kind, "unknown"), "provider_type": kind, "provenance": "hopper-public-python-api:CallReference"}
|
|
385
|
+
return calls, sorted(unresolved, key=lambda item: int(item["address"], 16))
|
|
386
|
+
|
|
387
|
+
|
|
290
388
|
def _procedure_references(document, params):
|
|
291
389
|
procedure = _procedure(document, params.get("procedure"))
|
|
292
390
|
direction = params.get("direction", "outgoing")
|
|
@@ -299,6 +397,8 @@ def _procedure_references(document, params):
|
|
|
299
397
|
references = segment.getReferencesFromAddress(address) if direction == "outgoing" else segment.getReferencesOfAddress(address)
|
|
300
398
|
for reference in references:
|
|
301
399
|
edges.add((address, reference) if direction == "outgoing" else (reference, address))
|
|
400
|
+
calls, unresolved = _native_calls(procedure, direction)
|
|
401
|
+
edges.update(calls)
|
|
302
402
|
ordered = sorted(edges)
|
|
303
403
|
items = []
|
|
304
404
|
for source, target in ordered:
|
|
@@ -309,16 +409,15 @@ def _procedure_references(document, params):
|
|
|
309
409
|
"target_address": _hex(target),
|
|
310
410
|
"source_procedure": _procedure_identity(source_procedure) if source_procedure is not None else None,
|
|
311
411
|
"target_procedure": _procedure_identity(target_procedure) if target_procedure is not None else None,
|
|
312
|
-
"kind": _unavailable("Hopper
|
|
412
|
+
"kind": _unavailable("Hopper exposes partial CallReference classification, but not detailed reference flags"),
|
|
413
|
+
**({"call": calls[(source, target)]} if (source, target) in calls else {}),
|
|
313
414
|
})
|
|
314
415
|
return {
|
|
315
416
|
"procedure": _procedure_identity(procedure), "direction": direction,
|
|
316
417
|
"reference_kinds_available": False,
|
|
317
|
-
#
|
|
318
|
-
#
|
|
319
|
-
|
|
320
|
-
# unknown coverage rather than claiming the list is exhaustive.
|
|
321
|
-
"unresolved_calls": [],
|
|
418
|
+
# These are native CallReference objects, not proof that every unresolved
|
|
419
|
+
# dispatch site has been enumerated by Hopper.
|
|
420
|
+
"unresolved_calls": unresolved,
|
|
322
421
|
"references": items,
|
|
323
422
|
}
|
|
324
423
|
|
|
@@ -333,11 +432,61 @@ def _procedure_map(document):
|
|
|
333
432
|
|
|
334
433
|
|
|
335
434
|
def _strings(document):
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
435
|
+
return {address: record["value"] for address, record in _string_inventory(document).items()}
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _string_record(segment, address, display):
|
|
439
|
+
"""Read one native typed object, retaining its shortened display separately."""
|
|
440
|
+
length = segment.getObjectLength(address)
|
|
441
|
+
end = segment.getStartingAddress() + segment.getLength()
|
|
442
|
+
if isinstance(length, bool) or not isinstance(length, int) or length <= 0 or length > end - address:
|
|
443
|
+
raise CapabilityUnavailableError("Hopper reported an invalid string object extent at %s" % _hex(address))
|
|
444
|
+
raw = segment.readBytes(address, length)
|
|
445
|
+
if not isinstance(raw, (bytes, bytearray)) or len(raw) != length:
|
|
446
|
+
raise CapabilityUnavailableError("Hopper could not read the complete string object at %s" % _hex(address))
|
|
447
|
+
kind = segment.getTypeAtAddress(address)
|
|
448
|
+
encodings = ("utf-8", "latin-1") if kind == segment.TYPE_ASCII else ("utf-16-le", "utf-16-be") if kind == segment.TYPE_UNICODE else ()
|
|
449
|
+
# Native display escaping is not invertible (a literal backslash-n and a
|
|
450
|
+
# newline can render alike). Decode byte strings directly; for UTF-16,
|
|
451
|
+
# compare rendered candidates to establish an unambiguous byte order.
|
|
452
|
+
candidates = []
|
|
453
|
+
for encoding in encodings:
|
|
454
|
+
terminator = b"\x00\x00" if encoding.startswith("utf-16") else b"\x00"
|
|
455
|
+
terminated = raw.endswith(terminator)
|
|
456
|
+
payload = raw[:-len(terminator)] if terminated else raw
|
|
457
|
+
try:
|
|
458
|
+
value = payload.decode(encoding)
|
|
459
|
+
except UnicodeDecodeError:
|
|
460
|
+
continue
|
|
461
|
+
record = {"value": value, "provider_value": display, "string": {"encoding": encoding, "encoding_status": "inferred", "termination": "present_or_not_required" if terminated else "missing", "byte_length": length}}
|
|
462
|
+
if kind == segment.TYPE_ASCII:
|
|
463
|
+
return record
|
|
464
|
+
rendered = value.replace('"', '\\"').replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t")
|
|
465
|
+
if rendered == display or (display.endswith("\u2026") and display[:-1] and rendered.startswith(display[:-1])):
|
|
466
|
+
candidates.append(record)
|
|
467
|
+
if candidates and len({candidate["value"] for candidate in candidates}) == 1:
|
|
468
|
+
return candidates[0]
|
|
469
|
+
raise CapabilityUnavailableError("Hopper's string display cannot be reconciled with its typed bytes at %s; inspect the bytes and encoding explicitly" % _hex(address))
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def _string_inventory(document, addresses=None):
|
|
473
|
+
key = (id(document), "string_objects")
|
|
474
|
+
objects = _search_inventory_cache.get(key)
|
|
475
|
+
if objects is None:
|
|
476
|
+
objects = {address: (segment, display) for segment in document.getSegmentsList() for display, address in segment.getStringsList()}
|
|
477
|
+
_search_inventory_cache[key] = objects
|
|
478
|
+
key = (id(document), "string_records")
|
|
479
|
+
records = _search_inventory_cache.setdefault(key, {})
|
|
480
|
+
selected = sorted(objects if addresses is None else objects.keys() & addresses)
|
|
481
|
+
for address in selected:
|
|
482
|
+
name = _hex(address)
|
|
483
|
+
if name not in records:
|
|
484
|
+
segment, display = objects[address]
|
|
485
|
+
try:
|
|
486
|
+
records[name] = _string_record(segment, address, display)
|
|
487
|
+
except CapabilityUnavailableError as error:
|
|
488
|
+
records[name] = {"value": display, "provider_value": display, "decoding": _unavailable(str(error))}
|
|
489
|
+
return {_hex(address): records[_hex(address)] for address in selected}
|
|
341
490
|
|
|
342
491
|
|
|
343
492
|
def _invalidate_search_inventory(document):
|
|
@@ -397,18 +546,14 @@ def _search_results(document, kind, params):
|
|
|
397
546
|
needle = pattern if case_sensitive else pattern.casefold()
|
|
398
547
|
matches = lambda value: needle in (value if case_sensitive else value.casefold())
|
|
399
548
|
else:
|
|
400
|
-
|
|
401
|
-
expression = re.compile(pattern, 0 if case_sensitive else re.IGNORECASE)
|
|
402
|
-
except (re.error, OverflowError) as error:
|
|
403
|
-
raise InvalidRequestError("Invalid regex pattern") from error
|
|
404
|
-
matches = lambda value: expression.search(value) is not None
|
|
549
|
+
raise CapabilityUnavailableError("Regex matching requires REA's supervised worker; the native bridge only executes literal searches")
|
|
405
550
|
|
|
406
551
|
selected = []
|
|
407
552
|
for item in _search_inventory(document, kind):
|
|
408
553
|
if not matches(item[1]):
|
|
409
554
|
continue
|
|
410
555
|
selected.append(item)
|
|
411
|
-
return [
|
|
556
|
+
return [{"address": address, **_string_inventory(document)[address]} for address, _ in selected] if kind == "string" else [
|
|
412
557
|
{
|
|
413
558
|
"address": address,
|
|
414
559
|
"value": value,
|
|
@@ -450,25 +595,11 @@ def _render_instruction(segment, address):
|
|
|
450
595
|
|
|
451
596
|
def _assembly(procedure):
|
|
452
597
|
"""Render assembly while guarding against malformed instruction cycles."""
|
|
453
|
-
lines = []
|
|
454
598
|
segment = procedure.getSegment()
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
while address < end and address not in seen:
|
|
460
|
-
seen.add(address)
|
|
461
|
-
instruction = segment.getInstructionAtAddress(address)
|
|
462
|
-
if instruction is None:
|
|
463
|
-
break
|
|
464
|
-
line = _render_instruction(segment, address)
|
|
465
|
-
if line is not None:
|
|
466
|
-
lines.append(line)
|
|
467
|
-
length = instruction.getInstructionLength()
|
|
468
|
-
if length <= 0:
|
|
469
|
-
break
|
|
470
|
-
address += length
|
|
471
|
-
return "\n".join(lines)
|
|
599
|
+
return "\n".join(
|
|
600
|
+
line for address in _instruction_addresses(procedure)
|
|
601
|
+
if (line := _render_instruction(segment, address)) is not None
|
|
602
|
+
)
|
|
472
603
|
|
|
473
604
|
|
|
474
605
|
def _read_function_instructions(document, params):
|
|
@@ -505,7 +636,7 @@ def _analyze_function(document, params):
|
|
|
505
636
|
successors.append(_hex(successor))
|
|
506
637
|
blocks.append({
|
|
507
638
|
"start": _hex(block.getStartingAddress()),
|
|
508
|
-
"end": _hex(block
|
|
639
|
+
"end": _hex(_basic_block_end(procedure, block)),
|
|
509
640
|
"successors": sorted(set(successors), key=lambda value: int(value, 16)),
|
|
510
641
|
})
|
|
511
642
|
pseudo = _pseudocode(document, procedure) or ""
|
|
@@ -529,6 +660,10 @@ def _analyze_function(document, params):
|
|
|
529
660
|
incoming = []
|
|
530
661
|
outgoing = []
|
|
531
662
|
procedure_addresses = set(addresses)
|
|
663
|
+
outgoing_calls, unresolved_calls = _native_calls(procedure, "outgoing")
|
|
664
|
+
incoming_calls, _ = _native_calls(procedure, "incoming")
|
|
665
|
+
calls = {**incoming_calls, **outgoing_calls}
|
|
666
|
+
edges.update(calls)
|
|
532
667
|
for source, target in sorted(edges):
|
|
533
668
|
source_procedure, _ = _containing_procedure(document, source)
|
|
534
669
|
target_procedure, _ = _containing_procedure(document, target)
|
|
@@ -537,16 +672,14 @@ def _analyze_function(document, params):
|
|
|
537
672
|
"target_address": _hex(target),
|
|
538
673
|
"source_procedure": _procedure_identity(source_procedure) if source_procedure is not None else None,
|
|
539
674
|
"target_procedure": _procedure_identity(target_procedure) if target_procedure is not None else None,
|
|
540
|
-
"kind": _unavailable("Hopper
|
|
675
|
+
"kind": _unavailable("Hopper exposes partial CallReference classification, but not detailed reference flags"),
|
|
676
|
+
**({"call": calls[(source, target)]} if (source, target) in calls else {}),
|
|
541
677
|
}
|
|
542
678
|
if target in procedure_addresses and source not in procedure_addresses:
|
|
543
679
|
incoming.append(item)
|
|
544
680
|
if source in procedure_addresses:
|
|
545
681
|
outgoing.append(item)
|
|
546
|
-
string_map = {
|
|
547
|
-
int(address, 16): value
|
|
548
|
-
for address, value in _search_inventory(document, "string")
|
|
549
|
-
}
|
|
682
|
+
string_map = {int(address, 16): record for address, record in _string_inventory(document, {int(edge["target_address"], 16) for edge in outgoing}).items()}
|
|
550
683
|
name_map = {
|
|
551
684
|
int(address, 16): value
|
|
552
685
|
for address, value in _search_inventory(document, "name")
|
|
@@ -556,7 +689,7 @@ def _analyze_function(document, params):
|
|
|
556
689
|
for edge in outgoing:
|
|
557
690
|
target = int(edge["target_address"], 16)
|
|
558
691
|
if target in string_map:
|
|
559
|
-
referenced_strings.append({"address": edge["target_address"],
|
|
692
|
+
referenced_strings.append({"address": edge["target_address"], **string_map[target], "source_address": edge["source_address"]})
|
|
560
693
|
if target in name_map:
|
|
561
694
|
referenced_names.append({"address": edge["target_address"], "value": name_map[target], "source_address": edge["source_address"]})
|
|
562
695
|
comments.sort(key=lambda item: (int(item["address"], 16), item["kind"]))
|
|
@@ -574,13 +707,14 @@ def _analyze_function(document, params):
|
|
|
574
707
|
"callers": callers, "callees": callees,
|
|
575
708
|
"incoming_references": incoming,
|
|
576
709
|
"outgoing_references": outgoing,
|
|
710
|
+
"unresolved_calls": unresolved_calls,
|
|
577
711
|
"referenced_strings": referenced_strings,
|
|
578
712
|
"referenced_names": referenced_names,
|
|
579
713
|
"basic_blocks": blocks,
|
|
580
714
|
"limitations": [
|
|
581
|
-
"
|
|
715
|
+
"Native CallReference classifications are observed where available; detailed reference flags remain unknown.",
|
|
582
716
|
"Hopper's public Python API does not expose equivalent external or thunk classification in this dossier.",
|
|
583
|
-
"
|
|
717
|
+
"Native calls without resolved target addresses are not represented as edges; enumeration beyond reported CallReference objects is unknown.",
|
|
584
718
|
"Pseudocode and assembly are provider-specific representations, not original source.",
|
|
585
719
|
],
|
|
586
720
|
}
|
|
@@ -588,7 +722,6 @@ def _analyze_function(document, params):
|
|
|
588
722
|
|
|
589
723
|
def _dispatch(method, params):
|
|
590
724
|
"""Dispatch only the closed operation set implemented by REA's public tools."""
|
|
591
|
-
global _selected_document
|
|
592
725
|
if method == "health":
|
|
593
726
|
return {"name": "REA Hopper bridge", "version": "1.0.0", "run_id": REA_RUN_ID}
|
|
594
727
|
if method in ("shutdown", "shutdown_document"):
|
|
@@ -622,14 +755,9 @@ def _dispatch(method, params):
|
|
|
622
755
|
"document_closed": document_closed,
|
|
623
756
|
}
|
|
624
757
|
if method == "list_documents":
|
|
625
|
-
return [document.getDocumentName() for document in
|
|
758
|
+
return [document.getDocumentName() for document in _api().documents()]
|
|
626
759
|
if method == "current_document":
|
|
627
760
|
return _document().getDocumentName()
|
|
628
|
-
if method == "set_current_document":
|
|
629
|
-
document = _document(params.get("document"))
|
|
630
|
-
_selected_document = document.getDocumentName()
|
|
631
|
-
return _selected_document
|
|
632
|
-
|
|
633
761
|
document = _document(params.get("document"))
|
|
634
762
|
if method in EXHAUSTIVE_ANALYSIS_METHODS:
|
|
635
763
|
_api().require_analysis_complete(document, method)
|
|
@@ -650,10 +778,17 @@ def _dispatch(method, params):
|
|
|
650
778
|
"Hopper's public Python API does not expose readBytes"
|
|
651
779
|
)
|
|
652
780
|
address = _address(document, params.get("address"))
|
|
653
|
-
|
|
781
|
+
segment = _segment(document, address)
|
|
782
|
+
# Document.readBytes delegates to one Segment and returns False when
|
|
783
|
+
# the requested range crosses its end. Preserve the readable prefix.
|
|
784
|
+
available = segment.getStartingAddress() + segment.getLength() - address
|
|
785
|
+
value = reader(address, min(length, available))
|
|
786
|
+
if value is False or value is None:
|
|
787
|
+
raise InvalidRequestError("Hopper could not read the requested byte range at %s" % _hex(address))
|
|
654
788
|
if not isinstance(value, (bytes, bytearray)):
|
|
655
789
|
raise CapabilityUnavailableError(
|
|
656
|
-
"Hopper
|
|
790
|
+
"Hopper readBytes returned %s for the requested %s-byte range at %s"
|
|
791
|
+
% (type(value).__name__, length, _hex(address))
|
|
657
792
|
)
|
|
658
793
|
data = bytes(value)
|
|
659
794
|
return {
|
|
@@ -678,7 +813,26 @@ def _dispatch(method, params):
|
|
|
678
813
|
or offset < 0
|
|
679
814
|
):
|
|
680
815
|
raise InvalidRequestError("Address has no authoritative file-offset mapping")
|
|
681
|
-
|
|
816
|
+
inverse = getattr(document, "getAddressFromFileOffset", None)
|
|
817
|
+
if not callable(inverse):
|
|
818
|
+
raise CapabilityUnavailableError(
|
|
819
|
+
"Hopper's public Python API cannot verify the file-offset mapping by reverse lookup"
|
|
820
|
+
)
|
|
821
|
+
# Synthetic memory can report offset zero, which belongs to the actual
|
|
822
|
+
# file header. An integer alone does not prove source-file provenance.
|
|
823
|
+
if inverse(offset) != address:
|
|
824
|
+
raise InvalidRequestError("Address has no authoritative file-offset mapping: reverse lookup disagrees at %s" % _hex(address))
|
|
825
|
+
if offset > 9007199254740991:
|
|
826
|
+
raise CapabilityUnavailableError("Provider file offset exceeds the exact JSON integer range")
|
|
827
|
+
header_address = inverse(0)
|
|
828
|
+
reader = getattr(document, "readBytes", None)
|
|
829
|
+
header = reader(header_address, 12) if header_address not in BAD_ADDRESSES and document.getSegmentAtAddress(header_address) is not None and callable(reader) else None
|
|
830
|
+
return {
|
|
831
|
+
"address": _hex(address),
|
|
832
|
+
"provider_file_offset": offset,
|
|
833
|
+
"provider_source_path": document.getExecutableFilePath(),
|
|
834
|
+
"provider_image_header_hex": bytes(header).hex() if isinstance(header, (bytes, bytearray)) and len(header) == 12 else None,
|
|
835
|
+
}
|
|
682
836
|
if method == "resolve_containing_procedure":
|
|
683
837
|
address = _address(document, params.get("address"))
|
|
684
838
|
procedure, reason = _containing_procedure(document, address)
|
|
@@ -694,8 +848,13 @@ def _dispatch(method, params):
|
|
|
694
848
|
return _procedure_name(_procedure(document))
|
|
695
849
|
if method == "goto_address":
|
|
696
850
|
address = _address(document, params.get("address"))
|
|
851
|
+
segment = _segment(document, address)
|
|
852
|
+
expected = segment.getInstructionStart(address)
|
|
697
853
|
document.moveCursorAtAddress(address)
|
|
698
|
-
|
|
854
|
+
observed = document.getCurrentAddress()
|
|
855
|
+
if observed != (address if expected in BAD_ADDRESSES else expected):
|
|
856
|
+
raise InvalidRequestError("Hopper cursor did not reach the requested object at %s; observed %s" % (_hex(address), _hex(observed)))
|
|
857
|
+
return _hex(observed)
|
|
699
858
|
if method in ("address_name", "comment", "inline_comment", "xrefs"):
|
|
700
859
|
target = _address(document, params.get("address"))
|
|
701
860
|
segment = _segment(document, target)
|
|
@@ -708,12 +867,22 @@ def _dispatch(method, params):
|
|
|
708
867
|
return [_hex(value) for value in segment.getReferencesOfAddress(target)]
|
|
709
868
|
if method in ("next_address", "prev_address"):
|
|
710
869
|
target = _address(document, params.get("address"))
|
|
870
|
+
segment = _segment(document, target)
|
|
711
871
|
if method == "next_address":
|
|
712
|
-
|
|
872
|
+
start = segment.getInstructionStart(target)
|
|
873
|
+
if start in BAD_ADDRESSES:
|
|
874
|
+
raise InvalidRequestError("No analyzed object exists at the requested address")
|
|
875
|
+
length = segment.getObjectLength(start)
|
|
876
|
+
if length in BAD_ADDRESSES or length <= 0:
|
|
877
|
+
raise InvalidRequestError("No next analyzed object exists at the requested address")
|
|
878
|
+
result = start + length
|
|
713
879
|
else:
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
880
|
+
previous_segment = document.getSegmentAtAddress(target - 1) if target > 0 else None
|
|
881
|
+
if previous_segment is None:
|
|
882
|
+
raise InvalidRequestError("No previous address exists in mapped memory")
|
|
883
|
+
result = previous_segment.getInstructionStart(target - 1)
|
|
884
|
+
if result in BAD_ADDRESSES or document.getSegmentAtAddress(result) is None:
|
|
885
|
+
raise InvalidRequestError("No adjacent analyzed object exists in mapped memory")
|
|
717
886
|
return _hex(result)
|
|
718
887
|
if method == "list_segments":
|
|
719
888
|
result = []
|
|
@@ -747,12 +916,9 @@ def _dispatch(method, params):
|
|
|
747
916
|
if method == "list_procedures":
|
|
748
917
|
return [{"address": address, "value": value} for address, value in _search_inventory(document, "procedure")]
|
|
749
918
|
if method == "list_strings":
|
|
750
|
-
values = dict(_search_inventory(document, "string"))
|
|
751
919
|
requested = params.get("address")
|
|
752
|
-
if requested is
|
|
753
|
-
|
|
754
|
-
values = {key: values[key]} if key in values else {}
|
|
755
|
-
return [{"address": address, "value": value} for address, value in values.items()]
|
|
920
|
+
values = _string_inventory(document, None if requested is None else {_address(document, requested)})
|
|
921
|
+
return [{"address": address, **record} for address, record in values.items()]
|
|
756
922
|
if method == "list_names":
|
|
757
923
|
result = dict(_search_inventory(document, "name"))
|
|
758
924
|
requested = params.get("address")
|
|
@@ -777,7 +943,7 @@ def _dispatch(method, params):
|
|
|
777
943
|
return sorted((_hex(item.getEntryPoint()) for item in procedure.getAllCalleeProcedures()), key=lambda value: int(value, 16))
|
|
778
944
|
if method == "procedure_info":
|
|
779
945
|
blocks = list(procedure.basicBlockIterator())
|
|
780
|
-
length = sum(max(0, block
|
|
946
|
+
length = sum(max(0, _basic_block_end(procedure, block) - block.getStartingAddress()) for block in blocks)
|
|
781
947
|
return {
|
|
782
948
|
"name": _procedure_name(procedure),
|
|
783
949
|
"entrypoint": _hex(procedure.getEntryPoint()),
|
|
@@ -790,35 +956,51 @@ def _dispatch(method, params):
|
|
|
790
956
|
}
|
|
791
957
|
if method == "set_address_name":
|
|
792
958
|
address = _address(document, params.get("address"))
|
|
959
|
+
name = _symbol_name(params["name"], "Name")
|
|
960
|
+
_validate_name_targets(document, [(params.get("address"), address, name)])
|
|
793
961
|
try:
|
|
794
|
-
result = document.setNameAtAddress(address,
|
|
962
|
+
result = document.setNameAtAddress(address, name)
|
|
795
963
|
finally:
|
|
796
964
|
_invalidate_search_inventory(document)
|
|
797
965
|
_invalidate_pseudocode(document)
|
|
798
|
-
return result
|
|
966
|
+
return bool(result) and _name_matches(document, address, name)
|
|
799
967
|
if method == "set_addresses_names":
|
|
968
|
+
# Validate all destinations before applying any annotation. A malformed
|
|
969
|
+
# later address must not discard the result of an earlier mutation.
|
|
970
|
+
names = [(key, _address(document, key), _symbol_name(value, "Name for %s" % key)) for key, value in params["names"].items()]
|
|
971
|
+
_validate_name_targets(document, names)
|
|
800
972
|
try:
|
|
801
|
-
result = {key: document.setNameAtAddress(
|
|
973
|
+
result = {key: document.setNameAtAddress(address, value) for key, address, value in names}
|
|
802
974
|
finally:
|
|
803
975
|
_invalidate_search_inventory(document)
|
|
804
976
|
_invalidate_pseudocode(document)
|
|
805
|
-
return result
|
|
977
|
+
return {key: bool(result[key]) and _name_matches(document, address, value) for key, address, value in names}
|
|
806
978
|
if method in ("set_comment", "set_inline_comment"):
|
|
807
979
|
address = _address(document, params.get("address"))
|
|
980
|
+
comment = _api_text(params["comment"], "Comment")
|
|
808
981
|
segment = _segment(document, address)
|
|
809
982
|
setter = segment.setCommentAtAddress if method == "set_comment" else segment.setInlineCommentAtAddress
|
|
810
983
|
getter = segment.getCommentAtAddress if method == "set_comment" else segment.getInlineCommentAtAddress
|
|
811
|
-
setter(address,
|
|
984
|
+
setter(address, comment)
|
|
812
985
|
_invalidate_pseudocode(document)
|
|
813
|
-
|
|
986
|
+
observed = getter(address)
|
|
987
|
+
return observed == comment or (comment == "" and observed is None)
|
|
814
988
|
if method == "list_bookmarks":
|
|
815
989
|
return [{"address": _hex(item), "name": document.getBookmarkName(item)} for item in document.getBookmarks()]
|
|
816
990
|
if method == "set_bookmark":
|
|
817
991
|
address = _address(document, params.get("address"))
|
|
818
|
-
document
|
|
992
|
+
_segment(document, address)
|
|
993
|
+
name = params.get("name")
|
|
994
|
+
if name is not None:
|
|
995
|
+
_api_text(name, "Bookmark name")
|
|
996
|
+
document.setBookmarkAtAddress(address, name)
|
|
819
997
|
return document.hasBookmarkAtAddress(address)
|
|
820
998
|
if method == "unset_bookmark":
|
|
821
999
|
address = _address(document, params.get("address"))
|
|
1000
|
+
# Permit removing a legacy orphan bookmark, but do not report success
|
|
1001
|
+
# for an unmapped address with no bookmark to remove.
|
|
1002
|
+
if not document.hasBookmarkAtAddress(address):
|
|
1003
|
+
_segment(document, address)
|
|
822
1004
|
document.removeBookmarkAtAddress(address)
|
|
823
1005
|
return not document.hasBookmarkAtAddress(address)
|
|
824
1006
|
raise InvalidRequestError("Unknown bridge method")
|
|
@@ -913,14 +1095,14 @@ def _diagnostic_type(error):
|
|
|
913
1095
|
|
|
914
1096
|
|
|
915
1097
|
def _safe_diagnostic(error):
|
|
916
|
-
"""
|
|
1098
|
+
"""Preserve bridge validation reasons while excluding transport credentials."""
|
|
917
1099
|
diagnostic_type = _diagnostic_type(error)
|
|
918
1100
|
if diagnostic_type == "capability_unavailable":
|
|
919
1101
|
message = str(error)
|
|
920
1102
|
elif diagnostic_type == "authorization":
|
|
921
1103
|
message = "Invalid bridge capability"
|
|
922
1104
|
elif diagnostic_type == "invalid_request":
|
|
923
|
-
message =
|
|
1105
|
+
message = str(error)
|
|
924
1106
|
else:
|
|
925
1107
|
message = "%s: Hopper bridge operation failed" % type(error).__name__
|
|
926
1108
|
return {"code": -32000, "message": message, "type": diagnostic_type}
|