rea-agents 0.2.1
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/LICENSE +21 -0
- package/README.md +312 -0
- package/bridge/hopper_bridge.py +557 -0
- package/dist/application/AnalysisProvider.js +1 -0
- package/dist/application/BinarySession.js +173 -0
- package/dist/application/DirectAnalysis.js +31 -0
- package/dist/application/Doctor.js +110 -0
- package/dist/application/EnhancedTools.js +357 -0
- package/dist/application/EvidenceLedger.js +65 -0
- package/dist/application/HopperToolPort.js +1 -0
- package/dist/application/LoopbackReplay.js +84 -0
- package/dist/application/ProcessHarness.js +347 -0
- package/dist/application/Setup.js +240 -0
- package/dist/application/homebrew.js +19 -0
- package/dist/application/runtime.js +11 -0
- package/dist/cli.js +88 -0
- package/dist/config.js +86 -0
- package/dist/contracts/enhancedInputs.js +73 -0
- package/dist/contracts/toolContracts.js +144 -0
- package/dist/contracts/toolOutputSchemas.js +267 -0
- package/dist/domain/binaryTarget.js +321 -0
- package/dist/domain/errors.js +81 -0
- package/dist/domain/evidence.js +146 -0
- package/dist/domain/evidenceBundle.js +11 -0
- package/dist/domain/hopperValues.js +253 -0
- package/dist/domain/jsonValue.js +3 -0
- package/dist/domain/processCapture.js +252 -0
- package/dist/domain/result.js +4 -0
- package/dist/domain/symbolAnalysis.js +68 -0
- package/dist/hopper/BridgeLauncher.js +130 -0
- package/dist/hopper/HopperClient.js +314 -0
- package/dist/hopper/HopperProvider.js +66 -0
- package/dist/hopper/protocol.js +32 -0
- package/dist/identity.js +14 -0
- package/dist/logger.js +27 -0
- package/dist/main.js +81 -0
- package/dist/server/createServer.js +30 -0
- package/dist/server/registerEnhancedTools.js +53 -0
- package/dist/server/registerOfficialTools.js +59 -0
- package/dist/server/registerSessionTools.js +159 -0
- package/dist/server/toolLogging.js +11 -0
- package/dist/server/toolResult.js +44 -0
- package/package.json +109 -0
- package/scripts/rea.mjs +15 -0
- package/scripts/rebuild-native.mjs +22 -0
- package/skills/rea-analysis/SKILL.md +44 -0
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
"""Authenticated REA adapter executed on Hopper's dedicated Python thread.
|
|
2
|
+
|
|
3
|
+
The bootstrap injects ``REA_SOCKET`` and a random ``REA_TOKEN`` before executing
|
|
4
|
+
this file with Hopper's supported ``--python`` launcher option. Keep all Hopper
|
|
5
|
+
API access on this thread: moving dispatch to a worker can deadlock Hopper.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import hmac
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
import socket
|
|
13
|
+
|
|
14
|
+
MAX_LINE_BYTES = 10 * 1024 * 1024
|
|
15
|
+
BAD_ADDRESSES = (-1, 0xFFFFFFFFFFFFFFFF, None)
|
|
16
|
+
_selected_document = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _hex(value):
|
|
20
|
+
return "0x%x" % value
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _json_safe(value):
|
|
24
|
+
"""Project Hopper-specific Python values into the JSON protocol boundary."""
|
|
25
|
+
if value is None or isinstance(value, (bool, int, float, str)):
|
|
26
|
+
return value
|
|
27
|
+
if isinstance(value, (list, tuple)):
|
|
28
|
+
return [_json_safe(item) for item in value]
|
|
29
|
+
if isinstance(value, dict):
|
|
30
|
+
return {str(key): _json_safe(item) for key, item in value.items()}
|
|
31
|
+
return str(value)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _document(name=None):
|
|
35
|
+
"""Resolve an explicit or session-selected document without changing Hopper UI."""
|
|
36
|
+
global _selected_document
|
|
37
|
+
documents = Document.getAllDocuments()
|
|
38
|
+
if name is not None:
|
|
39
|
+
for candidate in documents:
|
|
40
|
+
if candidate.getDocumentName() == name:
|
|
41
|
+
return candidate
|
|
42
|
+
raise ValueError("Unknown Hopper document")
|
|
43
|
+
if _selected_document is not None:
|
|
44
|
+
for candidate in documents:
|
|
45
|
+
if candidate.getDocumentName() == _selected_document:
|
|
46
|
+
return candidate
|
|
47
|
+
current = Document.getCurrentDocument()
|
|
48
|
+
if current is None:
|
|
49
|
+
raise ValueError("No Hopper document is loaded")
|
|
50
|
+
return current
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _address(document, value=None):
|
|
54
|
+
"""Resolve hexadecimal addresses first, then fall back to Hopper symbol names."""
|
|
55
|
+
if value is None:
|
|
56
|
+
return document.getCurrentAddress()
|
|
57
|
+
if not isinstance(value, str):
|
|
58
|
+
raise ValueError("Address must be a string")
|
|
59
|
+
try:
|
|
60
|
+
return int(value, 16)
|
|
61
|
+
except ValueError:
|
|
62
|
+
result = document.getAddressForName(value)
|
|
63
|
+
if result in BAD_ADDRESSES:
|
|
64
|
+
raise ValueError("Unknown Hopper address or name")
|
|
65
|
+
return result
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _segment(document, address):
|
|
69
|
+
result = document.getSegmentAtAddress(address)
|
|
70
|
+
if result is None:
|
|
71
|
+
raise ValueError("Address is outside every segment")
|
|
72
|
+
return result
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _procedure(document, value=None):
|
|
76
|
+
address = document.getCurrentAddress() if value is None else _address(document, value)
|
|
77
|
+
result = _segment(document, address).getProcedureAtAddress(address)
|
|
78
|
+
if result is None:
|
|
79
|
+
raise ValueError("No procedure exists at the requested address")
|
|
80
|
+
return result
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _procedure_name(procedure):
|
|
84
|
+
entry = procedure.getEntryPoint()
|
|
85
|
+
return procedure.getSegment().getNameAtAddress(entry) or _hex(entry)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _procedure_identity(procedure):
|
|
89
|
+
return {"address": _hex(procedure.getEntryPoint()), "name": _procedure_name(procedure)}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _containing_procedure(document, address):
|
|
93
|
+
segment = document.getSegmentAtAddress(address)
|
|
94
|
+
if segment is None:
|
|
95
|
+
return None, "outside_segments"
|
|
96
|
+
procedure = segment.getProcedureAtAddress(address)
|
|
97
|
+
return (procedure, None) if procedure is not None else (None, "not_in_procedure")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _instruction_addresses(procedure, limit):
|
|
101
|
+
result = []
|
|
102
|
+
seen = set()
|
|
103
|
+
segment = procedure.getSegment()
|
|
104
|
+
truncated = False
|
|
105
|
+
for block in procedure.basicBlockIterator():
|
|
106
|
+
address = block.getStartingAddress()
|
|
107
|
+
end = block.getEndingAddress()
|
|
108
|
+
while address < end and address not in seen:
|
|
109
|
+
if len(result) >= limit:
|
|
110
|
+
truncated = True
|
|
111
|
+
return result, truncated
|
|
112
|
+
seen.add(address)
|
|
113
|
+
instruction = segment.getInstructionAtAddress(address)
|
|
114
|
+
if instruction is None:
|
|
115
|
+
break
|
|
116
|
+
result.append(address)
|
|
117
|
+
length = instruction.getInstructionLength()
|
|
118
|
+
if length <= 0:
|
|
119
|
+
break
|
|
120
|
+
address += length
|
|
121
|
+
return result, truncated
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _procedure_references(document, params):
|
|
125
|
+
procedure = _procedure(document, params.get("procedure"))
|
|
126
|
+
direction = params.get("direction", "outgoing")
|
|
127
|
+
offset = params.get("offset", 0)
|
|
128
|
+
limit = params.get("limit", 100)
|
|
129
|
+
max_instructions = params.get("max_instructions", 500)
|
|
130
|
+
if direction not in ("incoming", "outgoing"):
|
|
131
|
+
raise ValueError("direction must be incoming or outgoing")
|
|
132
|
+
if not isinstance(offset, int) or isinstance(offset, bool) or offset < 0:
|
|
133
|
+
raise ValueError("offset must be a non-negative integer")
|
|
134
|
+
if not isinstance(limit, int) or isinstance(limit, bool) or limit < 1 or limit > 500:
|
|
135
|
+
raise ValueError("limit must be an integer between 1 and 500")
|
|
136
|
+
if not isinstance(max_instructions, int) or isinstance(max_instructions, bool) or max_instructions < 1 or max_instructions > 5000:
|
|
137
|
+
raise ValueError("max_instructions must be an integer between 1 and 5000")
|
|
138
|
+
addresses, scan_truncated = _instruction_addresses(procedure, max_instructions)
|
|
139
|
+
edges = set()
|
|
140
|
+
for address in addresses:
|
|
141
|
+
segment = _segment(document, address)
|
|
142
|
+
references = segment.getReferencesFromAddress(address) if direction == "outgoing" else segment.getReferencesOfAddress(address)
|
|
143
|
+
for reference in references:
|
|
144
|
+
edges.add((address, reference) if direction == "outgoing" else (reference, address))
|
|
145
|
+
ordered = sorted(edges)
|
|
146
|
+
items = []
|
|
147
|
+
selected = ordered[offset:offset + limit]
|
|
148
|
+
for source, target in selected:
|
|
149
|
+
source_procedure, _ = _containing_procedure(document, source)
|
|
150
|
+
target_procedure, _ = _containing_procedure(document, target)
|
|
151
|
+
items.append({
|
|
152
|
+
"source_address": _hex(source),
|
|
153
|
+
"target_address": _hex(target),
|
|
154
|
+
"source_procedure": _procedure_identity(source_procedure) if source_procedure is not None else None,
|
|
155
|
+
"target_procedure": _procedure_identity(target_procedure) if target_procedure is not None else None,
|
|
156
|
+
"kind": _unavailable("Hopper's public Python API does not classify reference kinds"),
|
|
157
|
+
})
|
|
158
|
+
next_offset = offset + len(selected)
|
|
159
|
+
has_more = next_offset < len(ordered)
|
|
160
|
+
truncated = scan_truncated or has_more
|
|
161
|
+
return {
|
|
162
|
+
"procedure": _procedure_identity(procedure), "direction": direction,
|
|
163
|
+
"references": {"items": items, "total": None if scan_truncated else len(ordered), "returned": len(items), "truncated": truncated, "next_offset": next_offset if has_more and not scan_truncated else None},
|
|
164
|
+
"instructions_scanned": len(addresses), "instruction_scan_truncated": scan_truncated,
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _procedure_map(document):
|
|
169
|
+
result = {}
|
|
170
|
+
for segment in document.getSegmentsList():
|
|
171
|
+
for index in range(segment.getProcedureCount()):
|
|
172
|
+
procedure = segment.getProcedureAtIndex(index)
|
|
173
|
+
result[_hex(procedure.getEntryPoint())] = _procedure_name(procedure)
|
|
174
|
+
return result
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _strings(document):
|
|
178
|
+
result = {}
|
|
179
|
+
for segment in document.getSegmentsList():
|
|
180
|
+
for value, address in segment.getStringsList():
|
|
181
|
+
result[_hex(address)] = value
|
|
182
|
+
return result
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _page(values, offset, limit):
|
|
186
|
+
"""Return one deterministically address-sorted page without crossing an unbounded map."""
|
|
187
|
+
if not isinstance(offset, int) or isinstance(offset, bool) or offset < 0:
|
|
188
|
+
raise ValueError("offset must be a non-negative integer")
|
|
189
|
+
if not isinstance(limit, int) or isinstance(limit, bool) or limit < 1 or limit > 500:
|
|
190
|
+
raise ValueError("limit must be an integer between 1 and 500")
|
|
191
|
+
ordered = sorted(values.items(), key=lambda item: int(item[0], 16))
|
|
192
|
+
selected = ordered[offset:offset + limit]
|
|
193
|
+
total = len(ordered)
|
|
194
|
+
next_offset = offset + len(selected)
|
|
195
|
+
has_more = next_offset < total
|
|
196
|
+
return {
|
|
197
|
+
"items": [{"address": address, "value": value} for address, value in selected],
|
|
198
|
+
"offset": offset,
|
|
199
|
+
"limit": limit,
|
|
200
|
+
"total": total,
|
|
201
|
+
"next_offset": next_offset if has_more else None,
|
|
202
|
+
"has_more": has_more,
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _unavailable(reason):
|
|
207
|
+
"""Describe evidence the public Hopper API cannot truthfully provide."""
|
|
208
|
+
return {"available": False, "reason": reason}
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _offset(params, name):
|
|
212
|
+
value = params.get(name, 0)
|
|
213
|
+
if not isinstance(value, int) or isinstance(value, bool) or value < 0:
|
|
214
|
+
raise ValueError("%s must be a non-negative integer" % name)
|
|
215
|
+
return value
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _collection_offset(params, name):
|
|
219
|
+
values = params.get("collection_offset", {})
|
|
220
|
+
if not isinstance(values, dict):
|
|
221
|
+
raise ValueError("collection_offset must be an object")
|
|
222
|
+
value = values.get(name, 0)
|
|
223
|
+
if not isinstance(value, int) or isinstance(value, bool) or value < 0:
|
|
224
|
+
raise ValueError("collection_offset.%s must be a non-negative integer" % name)
|
|
225
|
+
return value
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _bounded(items, offset, limit, total=None, scan_truncated=False):
|
|
229
|
+
selected = items[offset:offset + limit]
|
|
230
|
+
known_total = len(items) if total is None and not scan_truncated else total
|
|
231
|
+
has_more = offset + len(selected) < len(items)
|
|
232
|
+
return {
|
|
233
|
+
"items": selected,
|
|
234
|
+
"total": known_total,
|
|
235
|
+
"returned": len(selected),
|
|
236
|
+
"truncated": scan_truncated or has_more,
|
|
237
|
+
"next_offset": offset + len(selected) if has_more else None,
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _name_map(document):
|
|
242
|
+
result = {}
|
|
243
|
+
for segment in document.getSegmentsList():
|
|
244
|
+
for address in segment.getNamedAddresses():
|
|
245
|
+
name = segment.getNameAtAddress(address)
|
|
246
|
+
if name is not None:
|
|
247
|
+
result[address] = name
|
|
248
|
+
return result
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _assembly(procedure, limit=None):
|
|
252
|
+
"""Render bounded assembly while guarding against malformed instruction cycles."""
|
|
253
|
+
lines = []
|
|
254
|
+
segment = procedure.getSegment()
|
|
255
|
+
seen = set()
|
|
256
|
+
for block in procedure.basicBlockIterator():
|
|
257
|
+
address = block.getStartingAddress()
|
|
258
|
+
end = block.getEndingAddress()
|
|
259
|
+
while address < end and address not in seen:
|
|
260
|
+
seen.add(address)
|
|
261
|
+
instruction = segment.getInstructionAtAddress(address)
|
|
262
|
+
if instruction is None:
|
|
263
|
+
break
|
|
264
|
+
arguments = [instruction.getFormattedArgument(index) for index in range(instruction.getArgumentCount())]
|
|
265
|
+
suffix = ", ".join(value for value in arguments if value is not None)
|
|
266
|
+
lines.append("%s: %s%s" % (_hex(address), instruction.getInstructionString(), (" " + suffix) if suffix else ""))
|
|
267
|
+
if limit is not None and len(lines) >= limit:
|
|
268
|
+
return "\n".join(lines)
|
|
269
|
+
length = instruction.getInstructionLength()
|
|
270
|
+
if length <= 0:
|
|
271
|
+
break
|
|
272
|
+
address += length
|
|
273
|
+
return "\n".join(lines)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _analyze_function(document, params):
|
|
277
|
+
"""Collect a bounded, single-pass function dossier for agent callers."""
|
|
278
|
+
procedure = _procedure(document, params.get("procedure"))
|
|
279
|
+
limit = params.get("limit", 100)
|
|
280
|
+
max_chars = params.get("max_pseudocode_chars", 20000)
|
|
281
|
+
max_instructions = params.get("max_instructions", 500)
|
|
282
|
+
pseudocode_offset = _offset(params, "pseudocode_offset")
|
|
283
|
+
assembly_offset = _offset(params, "assembly_offset")
|
|
284
|
+
if not isinstance(limit, int) or isinstance(limit, bool) or limit < 1 or limit > 500:
|
|
285
|
+
raise ValueError("limit must be an integer between 1 and 500")
|
|
286
|
+
if not isinstance(max_chars, int) or isinstance(max_chars, bool) or max_chars < 1 or max_chars > 100000:
|
|
287
|
+
raise ValueError("max_pseudocode_chars must be between 1 and 100000")
|
|
288
|
+
if not isinstance(max_instructions, int) or isinstance(max_instructions, bool) or max_instructions < 1 or max_instructions > 5000:
|
|
289
|
+
raise ValueError("max_instructions must be between 1 and 5000")
|
|
290
|
+
addresses, instruction_scan_truncated = _instruction_addresses(procedure, max_instructions)
|
|
291
|
+
blocks = []
|
|
292
|
+
all_blocks = list(procedure.basicBlockIterator())
|
|
293
|
+
for block in all_blocks:
|
|
294
|
+
successors = []
|
|
295
|
+
for index in range(block.getSuccessorCount()):
|
|
296
|
+
successor = block.getSuccessorAddressAtIndex(index)
|
|
297
|
+
if successor not in BAD_ADDRESSES:
|
|
298
|
+
successors.append(_hex(successor))
|
|
299
|
+
blocks.append({
|
|
300
|
+
"start": _hex(block.getStartingAddress()),
|
|
301
|
+
"end": _hex(block.getEndingAddress()),
|
|
302
|
+
"successors": sorted(set(successors), key=lambda value: int(value, 16)),
|
|
303
|
+
})
|
|
304
|
+
pseudo = procedure.decompile() or ""
|
|
305
|
+
assembly_lines = _assembly(procedure).splitlines() if params.get("include_assembly", False) else []
|
|
306
|
+
callers = sorted((_procedure_identity(item) for item in procedure.getAllCallerProcedures()), key=lambda item: int(item["address"], 16))
|
|
307
|
+
callees = sorted((_procedure_identity(item) for item in procedure.getAllCalleeProcedures()), key=lambda item: int(item["address"], 16))
|
|
308
|
+
comments = []
|
|
309
|
+
edges = set()
|
|
310
|
+
for address in addresses:
|
|
311
|
+
segment = _segment(document, address)
|
|
312
|
+
comment = segment.getCommentAtAddress(address)
|
|
313
|
+
inline_comment = segment.getInlineCommentAtAddress(address)
|
|
314
|
+
if comment:
|
|
315
|
+
comments.append({"address": _hex(address), "kind": "comment", "text": comment})
|
|
316
|
+
if inline_comment:
|
|
317
|
+
comments.append({"address": _hex(address), "kind": "inline", "text": inline_comment})
|
|
318
|
+
for target in segment.getReferencesFromAddress(address):
|
|
319
|
+
edges.add((address, target))
|
|
320
|
+
for source in segment.getReferencesOfAddress(address):
|
|
321
|
+
edges.add((source, address))
|
|
322
|
+
incoming = []
|
|
323
|
+
outgoing = []
|
|
324
|
+
procedure_addresses = set(addresses)
|
|
325
|
+
for source, target in sorted(edges):
|
|
326
|
+
source_procedure, _ = _containing_procedure(document, source)
|
|
327
|
+
target_procedure, _ = _containing_procedure(document, target)
|
|
328
|
+
item = {
|
|
329
|
+
"source_address": _hex(source),
|
|
330
|
+
"target_address": _hex(target),
|
|
331
|
+
"source_procedure": _procedure_identity(source_procedure) if source_procedure is not None else None,
|
|
332
|
+
"target_procedure": _procedure_identity(target_procedure) if target_procedure is not None else None,
|
|
333
|
+
"kind": _unavailable("Hopper's public Python API does not classify reference kinds"),
|
|
334
|
+
}
|
|
335
|
+
if target in procedure_addresses and source not in procedure_addresses:
|
|
336
|
+
incoming.append(item)
|
|
337
|
+
if source in procedure_addresses:
|
|
338
|
+
outgoing.append(item)
|
|
339
|
+
string_map = {int(address, 16): value for address, value in _strings(document).items()}
|
|
340
|
+
name_map = _name_map(document)
|
|
341
|
+
referenced_strings = []
|
|
342
|
+
referenced_names = []
|
|
343
|
+
for edge in outgoing:
|
|
344
|
+
target = int(edge["target_address"], 16)
|
|
345
|
+
if target in string_map:
|
|
346
|
+
referenced_strings.append({"address": edge["target_address"], "value": string_map[target], "source_address": edge["source_address"]})
|
|
347
|
+
if target in name_map:
|
|
348
|
+
referenced_names.append({"address": edge["target_address"], "value": name_map[target], "source_address": edge["source_address"]})
|
|
349
|
+
comments.sort(key=lambda item: (int(item["address"], 16), item["kind"]))
|
|
350
|
+
referenced_strings.sort(key=lambda item: (int(item["address"], 16), int(item["source_address"], 16)))
|
|
351
|
+
referenced_names.sort(key=lambda item: (int(item["address"], 16), int(item["source_address"], 16)))
|
|
352
|
+
pseudo_text = pseudo[pseudocode_offset:pseudocode_offset + max_chars]
|
|
353
|
+
pseudo_next = pseudocode_offset + len(pseudo_text)
|
|
354
|
+
def collection(name, items, scan_limited=False):
|
|
355
|
+
return _bounded(items, _collection_offset(params, name), limit, None, scan_limited)
|
|
356
|
+
return {
|
|
357
|
+
"procedure": {"address": _hex(procedure.getEntryPoint()), "name": _procedure_name(procedure), "signature": procedure.signatureString(), "locals": _json_safe(procedure.getLocalVariableList())},
|
|
358
|
+
"pseudocode": {"text": pseudo_text, "total_chars": len(pseudo), "returned_chars": len(pseudo_text), "truncated": pseudo_next < len(pseudo), "next_offset": pseudo_next if pseudo_next < len(pseudo) else None},
|
|
359
|
+
"assembly": _bounded(assembly_lines, assembly_offset, max_instructions),
|
|
360
|
+
"comments": collection("comments", comments, instruction_scan_truncated),
|
|
361
|
+
"callers": collection("callers", callers), "callees": collection("callees", callees),
|
|
362
|
+
"incoming_references": collection("incoming_references", incoming, instruction_scan_truncated),
|
|
363
|
+
"outgoing_references": collection("outgoing_references", outgoing, instruction_scan_truncated),
|
|
364
|
+
"referenced_strings": collection("referenced_strings", referenced_strings, instruction_scan_truncated),
|
|
365
|
+
"referenced_names": collection("referenced_names", referenced_names, instruction_scan_truncated),
|
|
366
|
+
"basic_blocks": collection("basic_blocks", blocks),
|
|
367
|
+
"instruction_scan": {"scanned": len(addresses), "truncated": instruction_scan_truncated},
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _dispatch(method, params):
|
|
372
|
+
"""Dispatch only the closed operation set implemented by REA's public tools."""
|
|
373
|
+
global _selected_document
|
|
374
|
+
if method == "health":
|
|
375
|
+
return {"name": "REA Hopper bridge", "version": "1.0.0"}
|
|
376
|
+
if method == "shutdown":
|
|
377
|
+
return {"shutdown": True}
|
|
378
|
+
if method == "list_documents":
|
|
379
|
+
return [document.getDocumentName() for document in Document.getAllDocuments()]
|
|
380
|
+
if method == "current_document":
|
|
381
|
+
return _document().getDocumentName()
|
|
382
|
+
if method == "set_current_document":
|
|
383
|
+
document = _document(params.get("document"))
|
|
384
|
+
_selected_document = document.getDocumentName()
|
|
385
|
+
return _selected_document
|
|
386
|
+
|
|
387
|
+
document = _document(params.get("document"))
|
|
388
|
+
|
|
389
|
+
if method == "analyze_function":
|
|
390
|
+
return _analyze_function(document, params)
|
|
391
|
+
if method == "resolve_containing_procedure":
|
|
392
|
+
address = _address(document, params.get("address"))
|
|
393
|
+
procedure, reason = _containing_procedure(document, address)
|
|
394
|
+
if procedure is None:
|
|
395
|
+
return {"query_address": _hex(address), "found": False, "procedure": None, "reason": reason}
|
|
396
|
+
return {"query_address": _hex(address), "found": True, "procedure": _procedure_identity(procedure)}
|
|
397
|
+
if method == "procedure_references":
|
|
398
|
+
return _procedure_references(document, params)
|
|
399
|
+
|
|
400
|
+
if method == "current_address":
|
|
401
|
+
return _hex(document.getCurrentAddress())
|
|
402
|
+
if method == "current_procedure":
|
|
403
|
+
return _procedure_name(_procedure(document))
|
|
404
|
+
if method == "goto_address":
|
|
405
|
+
address = _address(document, params.get("address"))
|
|
406
|
+
document.moveCursorAtAddress(address)
|
|
407
|
+
return _hex(address)
|
|
408
|
+
if method in ("address_name", "comment", "inline_comment", "xrefs"):
|
|
409
|
+
target = _address(document, params.get("address"))
|
|
410
|
+
segment = _segment(document, target)
|
|
411
|
+
if method == "address_name":
|
|
412
|
+
return segment.getNameAtAddress(target)
|
|
413
|
+
if method == "comment":
|
|
414
|
+
return segment.getCommentAtAddress(target)
|
|
415
|
+
if method == "inline_comment":
|
|
416
|
+
return segment.getInlineCommentAtAddress(target)
|
|
417
|
+
return [_hex(value) for value in segment.getReferencesOfAddress(target)]
|
|
418
|
+
if method in ("next_address", "prev_address"):
|
|
419
|
+
target = _address(document, params.get("address"))
|
|
420
|
+
if method == "next_address":
|
|
421
|
+
result = target + max(1, document.getObjectLength(target))
|
|
422
|
+
else:
|
|
423
|
+
result = document.getInstructionStart(max(0, target - 1))
|
|
424
|
+
if result in BAD_ADDRESSES:
|
|
425
|
+
raise ValueError("No adjacent address")
|
|
426
|
+
return _hex(result)
|
|
427
|
+
if method == "list_segments":
|
|
428
|
+
result = []
|
|
429
|
+
for segment in document.getSegmentsList():
|
|
430
|
+
start = segment.getStartingAddress()
|
|
431
|
+
sections = [{"name": section.getName(), "start": _hex(section.getStartingAddress()), "end": _hex(section.getStartingAddress() + section.getLength())} for section in segment.getSectionsList()]
|
|
432
|
+
result.append({
|
|
433
|
+
"name": segment.getName(),
|
|
434
|
+
"start": _hex(start),
|
|
435
|
+
"end": _hex(start + segment.getLength()),
|
|
436
|
+
"writable": None,
|
|
437
|
+
"executable": None,
|
|
438
|
+
"permissions": _unavailable(
|
|
439
|
+
"Hopper's public Python API does not expose segment permissions"
|
|
440
|
+
),
|
|
441
|
+
"sections": sections,
|
|
442
|
+
})
|
|
443
|
+
return result
|
|
444
|
+
if method == "list_procedures":
|
|
445
|
+
return _page(_procedure_map(document), params.get("offset", 0), params.get("limit", 100))
|
|
446
|
+
if method == "list_strings":
|
|
447
|
+
values = _strings(document)
|
|
448
|
+
requested = params.get("address")
|
|
449
|
+
if requested is not None:
|
|
450
|
+
key = _hex(_address(document, requested))
|
|
451
|
+
values = {key: values[key]} if key in values else {}
|
|
452
|
+
return _page(values, params.get("offset", 0), params.get("limit", 100))
|
|
453
|
+
if method == "list_names":
|
|
454
|
+
result = {}
|
|
455
|
+
for segment in document.getSegmentsList():
|
|
456
|
+
for item in segment.getNamedAddresses():
|
|
457
|
+
result[_hex(item)] = segment.getNameAtAddress(item)
|
|
458
|
+
requested = params.get("address")
|
|
459
|
+
if requested is not None:
|
|
460
|
+
key = _hex(_address(document, requested))
|
|
461
|
+
result = {key: result[key]} if key in result else {}
|
|
462
|
+
return _page(result, params.get("offset", 0), params.get("limit", 100))
|
|
463
|
+
if method in ("search_procedures", "search_strings"):
|
|
464
|
+
flags = 0 if params.get("case_sensitive", False) else re.IGNORECASE
|
|
465
|
+
expression = re.compile(params["pattern"], flags)
|
|
466
|
+
values = _procedure_map(document) if method == "search_procedures" else _strings(document)
|
|
467
|
+
return {key: value for key, value in values.items() if expression.search(value)}
|
|
468
|
+
if method.startswith("procedure_"):
|
|
469
|
+
procedure = _procedure(document, params.get("procedure"))
|
|
470
|
+
if method == "procedure_address":
|
|
471
|
+
return _hex(procedure.getEntryPoint())
|
|
472
|
+
if method == "procedure_assembly":
|
|
473
|
+
return _assembly(procedure)
|
|
474
|
+
if method == "procedure_pseudo_code":
|
|
475
|
+
return procedure.decompile()
|
|
476
|
+
if method == "procedure_callers":
|
|
477
|
+
return [_procedure_name(item) for item in procedure.getAllCallerProcedures()]
|
|
478
|
+
if method == "procedure_callees":
|
|
479
|
+
return [_procedure_name(item) for item in procedure.getAllCalleeProcedures()]
|
|
480
|
+
if method == "procedure_info":
|
|
481
|
+
blocks = list(procedure.basicBlockIterator())
|
|
482
|
+
length = sum(max(0, block.getEndingAddress() - block.getStartingAddress()) for block in blocks)
|
|
483
|
+
return {"name": _procedure_name(procedure), "entrypoint": _hex(procedure.getEntryPoint()), "basicblock_count": procedure.getBasicBlockCount(), "length": length, "signature": procedure.signatureString(), "locals": procedure.getLocalVariableList()}
|
|
484
|
+
if method == "set_address_name":
|
|
485
|
+
address = _address(document, params.get("address"))
|
|
486
|
+
return document.setNameAtAddress(address, params["name"])
|
|
487
|
+
if method == "set_addresses_names":
|
|
488
|
+
return {key: document.setNameAtAddress(_address(document, key), value) for key, value in params["names"].items()}
|
|
489
|
+
if method in ("set_comment", "set_inline_comment"):
|
|
490
|
+
address = _address(document, params.get("address"))
|
|
491
|
+
segment = _segment(document, address)
|
|
492
|
+
setter = segment.setCommentAtAddress if method == "set_comment" else segment.setInlineCommentAtAddress
|
|
493
|
+
getter = segment.getCommentAtAddress if method == "set_comment" else segment.getInlineCommentAtAddress
|
|
494
|
+
setter(address, params["comment"])
|
|
495
|
+
return getter(address) == params["comment"]
|
|
496
|
+
if method == "list_bookmarks":
|
|
497
|
+
return [{"address": _hex(item), "name": document.getBookmarkName(item)} for item in document.getBookmarks()]
|
|
498
|
+
if method == "set_bookmark":
|
|
499
|
+
address = _address(document, params.get("address"))
|
|
500
|
+
document.setBookmarkAtAddress(address, params.get("name"))
|
|
501
|
+
return document.hasBookmarkAtAddress(address)
|
|
502
|
+
if method == "unset_bookmark":
|
|
503
|
+
address = _address(document, params.get("address"))
|
|
504
|
+
document.removeBookmarkAtAddress(address)
|
|
505
|
+
return not document.hasBookmarkAtAddress(address)
|
|
506
|
+
raise ValueError("Unknown bridge method")
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def _serve_connection(connection):
|
|
510
|
+
"""Serve one size-bounded, capability-authenticated NDJSON connection."""
|
|
511
|
+
file = connection.makefile("rwb")
|
|
512
|
+
while True:
|
|
513
|
+
line = file.readline(MAX_LINE_BYTES + 1)
|
|
514
|
+
if not line or len(line) > MAX_LINE_BYTES:
|
|
515
|
+
break
|
|
516
|
+
request_id = None
|
|
517
|
+
should_stop = False
|
|
518
|
+
try:
|
|
519
|
+
request = json.loads(line.decode("utf-8"))
|
|
520
|
+
if set(request) != {"id", "token", "method", "params"}:
|
|
521
|
+
raise ValueError("Invalid bridge request shape")
|
|
522
|
+
request_id = request["id"]
|
|
523
|
+
if not isinstance(request["token"], str) or not hmac.compare_digest(request["token"], REA_TOKEN):
|
|
524
|
+
raise PermissionError("Invalid bridge capability")
|
|
525
|
+
result = _dispatch(request["method"], request["params"])
|
|
526
|
+
should_stop = request["method"] == "shutdown"
|
|
527
|
+
response = {"id": request_id, "result": _json_safe(result)}
|
|
528
|
+
except Exception as error:
|
|
529
|
+
response = {"id": request_id if isinstance(request_id, int) else 0, "error": {"code": -32000, "message": str(error)[:512]}}
|
|
530
|
+
file.write((json.dumps(response, separators=(",", ":")) + "\n").encode("utf-8"))
|
|
531
|
+
file.flush()
|
|
532
|
+
if should_stop:
|
|
533
|
+
break
|
|
534
|
+
file.close()
|
|
535
|
+
connection.close()
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _run():
|
|
539
|
+
"""Own a permission-restricted, single-client Unix socket for this bridge."""
|
|
540
|
+
if os.path.exists(REA_SOCKET):
|
|
541
|
+
os.unlink(REA_SOCKET)
|
|
542
|
+
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
543
|
+
server.bind(REA_SOCKET)
|
|
544
|
+
os.chmod(REA_SOCKET, 0o600)
|
|
545
|
+
server.listen(1)
|
|
546
|
+
try:
|
|
547
|
+
connection, _ = server.accept()
|
|
548
|
+
_serve_connection(connection)
|
|
549
|
+
finally:
|
|
550
|
+
server.close()
|
|
551
|
+
if os.path.exists(REA_SOCKET):
|
|
552
|
+
os.unlink(REA_SOCKET)
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
# Hopper's public objects are bound to its dedicated Python execution thread.
|
|
556
|
+
# Keep dispatch on that thread; moving calls to an arbitrary worker can deadlock.
|
|
557
|
+
_run()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|