pcode-graph 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pcode_graph/__init__.py +0 -0
- pcode_graph/__main__.py +3 -0
- pcode_graph/analysis.py +395 -0
- pcode_graph/arch.py +10 -0
- pcode_graph/asm.py +60 -0
- pcode_graph/charac.py +97 -0
- pcode_graph/checker.py +120 -0
- pcode_graph/cli.py +175 -0
- pcode_graph/emulator.py +419 -0
- pcode_graph/gnn_exporter.py +208 -0
- pcode_graph/graph.py +234 -0
- pcode_graph/lief_importer.py +299 -0
- pcode_graph/log.py +36 -0
- pcode_graph/maker.py +369 -0
- pcode_graph/nx_exporter.py +20 -0
- pcode_graph/pcode.py +187 -0
- pcode_graph/registers.py +2929 -0
- pcode_graph/simplifier.py +185 -0
- pcode_graph/torch_utils.py +30 -0
- pcode_graph/translator.py +235 -0
- pcode_graph/utils.py +15 -0
- pcode_graph/view.py +65 -0
- pcode_graph/visitor.py +103 -0
- pcode_graph-1.0.0.dist-info/METADATA +205 -0
- pcode_graph-1.0.0.dist-info/RECORD +28 -0
- pcode_graph-1.0.0.dist-info/WHEEL +4 -0
- pcode_graph-1.0.0.dist-info/entry_points.txt +2 -0
- pcode_graph-1.0.0.dist-info/licenses/LICENSE +21 -0
pcode_graph/__init__.py
ADDED
|
File without changes
|
pcode_graph/__main__.py
ADDED
pcode_graph/analysis.py
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Iterator, override
|
|
4
|
+
from pypcode import OpCode, PcodeOp, Varnode
|
|
5
|
+
from pcode_graph.arch import Arch
|
|
6
|
+
from pcode_graph.pcode import OpCodes, dump_operation, from_native_opcode
|
|
7
|
+
from pcode_graph.visitor import PcodeVisitor
|
|
8
|
+
|
|
9
|
+
OpIndex = int
|
|
10
|
+
EntryIndex = -1
|
|
11
|
+
UnknownCodeIndex = -2
|
|
12
|
+
ExitIndex = -3
|
|
13
|
+
|
|
14
|
+
Address = int
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def dump_index(i: OpIndex) -> str:
|
|
18
|
+
match i:
|
|
19
|
+
case -1:
|
|
20
|
+
return "entry"
|
|
21
|
+
case -2:
|
|
22
|
+
return "elsewhere"
|
|
23
|
+
case -3:
|
|
24
|
+
return "exit"
|
|
25
|
+
case _:
|
|
26
|
+
assert i >= 0
|
|
27
|
+
return str(i)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class VarKinds(Enum):
|
|
31
|
+
# Register or named P-Code temporary
|
|
32
|
+
Register = 0
|
|
33
|
+
# P-Code temporary
|
|
34
|
+
Unique = 1
|
|
35
|
+
# Numerical constant
|
|
36
|
+
Constant = 2
|
|
37
|
+
# PC-relative address value
|
|
38
|
+
Ram = 3
|
|
39
|
+
# Dummy access to the memory
|
|
40
|
+
IndirectAccess = 4
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True, order=True)
|
|
44
|
+
class Var:
|
|
45
|
+
"""Hashable and comparable wrapper for temporaries, registers, constants and memory."""
|
|
46
|
+
|
|
47
|
+
kind: VarKinds
|
|
48
|
+
size: int = field(compare=False)
|
|
49
|
+
value: int
|
|
50
|
+
pretty_name: str = field(compare=False)
|
|
51
|
+
|
|
52
|
+
def __str__(self) -> str:
|
|
53
|
+
return self.pretty_name
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def wrap(var: Varnode) -> Var:
|
|
57
|
+
match var.space.name:
|
|
58
|
+
case "register":
|
|
59
|
+
return Var(VarKinds.Register, var.size, var.offset, var.getRegisterName())
|
|
60
|
+
case "unique":
|
|
61
|
+
return Var(VarKinds.Unique, var.size, var.offset, f"${var.offset}")
|
|
62
|
+
case "const":
|
|
63
|
+
return Var(VarKinds.Constant, var.size, var.offset, f"${var.offset}")
|
|
64
|
+
case "ram":
|
|
65
|
+
return Var(VarKinds.Ram, var.size, var.offset, f"[0x{var.offset:x}]")
|
|
66
|
+
case _ as name:
|
|
67
|
+
raise ValueError(f"Unexpected space for input variable: {name}")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# Special variable singleton
|
|
71
|
+
MEMORY = Var(VarKinds.IndirectAccess, 0, 0, "MEMORY")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class RichPcodeList:
|
|
76
|
+
"""A class to run static analysis on a list of P-Code operations."""
|
|
77
|
+
|
|
78
|
+
# Source architecture
|
|
79
|
+
arch: Arch
|
|
80
|
+
|
|
81
|
+
# List of P-Code operations
|
|
82
|
+
operations: list[PcodeOp]
|
|
83
|
+
|
|
84
|
+
# CFG edges: src/dst indexes on the previous list.
|
|
85
|
+
# Order of successors matters for cbranch, call, etc.
|
|
86
|
+
predecessors: dict[OpIndex, set[OpIndex]] = field(default_factory=dict)
|
|
87
|
+
successors: dict[OpIndex, list[OpIndex]] = field(default_factory=dict)
|
|
88
|
+
|
|
89
|
+
# First P-Code operation for each instruction address
|
|
90
|
+
addresses: dict[Address, OpIndex] = field(default_factory=dict)
|
|
91
|
+
|
|
92
|
+
# Reachable operation flags
|
|
93
|
+
reachable: list[bool] = field(default_factory=list)
|
|
94
|
+
|
|
95
|
+
# Reaching definitions. For each operation, by variable, list of:
|
|
96
|
+
# - definitions (= writing operation index) that reach this operation,
|
|
97
|
+
# - or EntryIndex (for initial definitions of inputs).
|
|
98
|
+
reaching_defs: list[dict[Var, set[OpIndex]]] = field(default_factory=list)
|
|
99
|
+
|
|
100
|
+
def __post_init__(self):
|
|
101
|
+
InstructionAddressCollector(self)()
|
|
102
|
+
CFGBuilder(self)()
|
|
103
|
+
UnreachableCodeFinder(self)()
|
|
104
|
+
ReachingDefBuilder(self)()
|
|
105
|
+
|
|
106
|
+
def __str__(self) -> str:
|
|
107
|
+
return self.dump()
|
|
108
|
+
|
|
109
|
+
def dump(self) -> str:
|
|
110
|
+
"""Dumps the program and analysis results in a markdown table."""
|
|
111
|
+
|
|
112
|
+
s = "|index|op|preds|succs|input defs|reachable|exit def|\n"
|
|
113
|
+
exit_defs = self.get_exit_defs()
|
|
114
|
+
|
|
115
|
+
s += "|--|--|--|--|--|--|--|\n"
|
|
116
|
+
for i, op in enumerate(self.operations):
|
|
117
|
+
s += f"|{i}|{dump_operation(op)}|"
|
|
118
|
+
s += ", ".join(dump_index(p) for p in self.predecessors.get(i, [])) + "|"
|
|
119
|
+
s += ", ".join(dump_index(p) for p in self.successors.get(i, [])) + "|"
|
|
120
|
+
var_defs = []
|
|
121
|
+
for var, defs in self.iter_input_defs(i):
|
|
122
|
+
if defs:
|
|
123
|
+
var_defs.append(
|
|
124
|
+
str(var) + " from " + " or ".join(dump_index(d) for d in defs)
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
s += ", ".join(var_defs) + "|"
|
|
128
|
+
if self.reachable[i]:
|
|
129
|
+
s += "x"
|
|
130
|
+
s += "|"
|
|
131
|
+
exit_def_here = []
|
|
132
|
+
for var, defs in exit_defs.items():
|
|
133
|
+
for j in defs:
|
|
134
|
+
if i == j:
|
|
135
|
+
exit_def_here.append(str(var))
|
|
136
|
+
if exit_def_here:
|
|
137
|
+
s += ", ".join(exit_def_here)
|
|
138
|
+
s += "|\n"
|
|
139
|
+
|
|
140
|
+
return s
|
|
141
|
+
|
|
142
|
+
def get_defs(self, index: OpIndex, var: Var) -> set[OpIndex]:
|
|
143
|
+
"""Returns all the operations defining the value at given operation of the given variable."""
|
|
144
|
+
|
|
145
|
+
return self.reaching_defs[index].get(var, set())
|
|
146
|
+
|
|
147
|
+
def get_exit_defs(self) -> dict[Var, set[OpIndex]]:
|
|
148
|
+
"""Computes side effects of the program."""
|
|
149
|
+
|
|
150
|
+
exit_defs: dict[Var, set[OpIndex]] = {}
|
|
151
|
+
|
|
152
|
+
for index, op in enumerate(self.operations):
|
|
153
|
+
if not self.reachable[index]:
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
if not {UnknownCodeIndex, ExitIndex}.intersection(
|
|
157
|
+
self.successors.get(index, [])
|
|
158
|
+
):
|
|
159
|
+
# Not an exit
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
for var, defs in self.reaching_defs[index].items():
|
|
163
|
+
if var.kind == VarKinds.Unique:
|
|
164
|
+
# Ignore P-Code temporary
|
|
165
|
+
continue
|
|
166
|
+
write_defs = defs - {EntryIndex}
|
|
167
|
+
if write_defs:
|
|
168
|
+
# logger.debug(f"Add exit defs for var {var} from op {index}: {defs}")
|
|
169
|
+
exit_defs.setdefault(var, set()).update(write_defs)
|
|
170
|
+
if op.output:
|
|
171
|
+
var = wrap(op.output)
|
|
172
|
+
if var.kind != VarKinds.Unique:
|
|
173
|
+
exit_defs[var] = {index}
|
|
174
|
+
elif op.opcode == OpCode.STORE:
|
|
175
|
+
# Do not kill other memory defs
|
|
176
|
+
exit_defs.setdefault(MEMORY, set()).add(index)
|
|
177
|
+
return exit_defs
|
|
178
|
+
|
|
179
|
+
def iter_inputs(self, index: OpIndex) -> Iterator[Var]:
|
|
180
|
+
"""Iterates operation inputs, skipping **special** ones (see P-Code documentation)."""
|
|
181
|
+
|
|
182
|
+
op = self.operations[index]
|
|
183
|
+
if op.opcode == OpCode.LOAD:
|
|
184
|
+
yield MEMORY
|
|
185
|
+
|
|
186
|
+
for i, var in enumerate(op.inputs):
|
|
187
|
+
if i != 0 or op.opcode not in {
|
|
188
|
+
OpCode.IMARK,
|
|
189
|
+
OpCode.CBRANCH,
|
|
190
|
+
OpCode.BRANCH,
|
|
191
|
+
OpCode.CALL,
|
|
192
|
+
OpCode.LOAD,
|
|
193
|
+
OpCode.STORE,
|
|
194
|
+
}:
|
|
195
|
+
yield wrap(var)
|
|
196
|
+
|
|
197
|
+
def iter_input_defs(self, index: OpIndex) -> Iterator[tuple[Var, set[OpIndex]]]:
|
|
198
|
+
"""For each operand, returns the written variable and the operation(s) defining it."""
|
|
199
|
+
|
|
200
|
+
for var in self.iter_inputs(index):
|
|
201
|
+
yield var, self.get_defs(index, var)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class PassBase(PcodeVisitor):
|
|
205
|
+
|
|
206
|
+
def __init__(self, program: RichPcodeList) -> None:
|
|
207
|
+
super().__init__()
|
|
208
|
+
self.program = program
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class InstructionAddressCollector(PassBase):
|
|
212
|
+
"""Collects instruction addresses removing IMARK operations"""
|
|
213
|
+
|
|
214
|
+
def __call__(self):
|
|
215
|
+
for i, op in enumerate(self.program.operations):
|
|
216
|
+
if op.opcode == OpCode.IMARK:
|
|
217
|
+
self.program.addresses[op.inputs[0].offset] = i
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class CFGBuilder(PassBase):
|
|
221
|
+
"""List the CFG edges"""
|
|
222
|
+
|
|
223
|
+
def __call__(self):
|
|
224
|
+
|
|
225
|
+
# Collected src/dst pairs
|
|
226
|
+
self._edges: list[tuple[OpIndex, OpIndex]] = []
|
|
227
|
+
|
|
228
|
+
for o, op in enumerate(self.program.operations):
|
|
229
|
+
opcode = from_native_opcode(op.opcode)
|
|
230
|
+
self.visit(opcode, o, op)
|
|
231
|
+
|
|
232
|
+
for src, dst in self._edges:
|
|
233
|
+
assert src >= 0 and src < len(self.program.operations)
|
|
234
|
+
self.program.successors.setdefault(src, []).append(dst)
|
|
235
|
+
if dst not in {UnknownCodeIndex, ExitIndex}:
|
|
236
|
+
self.program.predecessors.setdefault(dst, set()).add(src)
|
|
237
|
+
|
|
238
|
+
self.program.predecessors.setdefault(0, set()).add(EntryIndex)
|
|
239
|
+
|
|
240
|
+
@override
|
|
241
|
+
def visit_default(self, index: int, op: PcodeOp):
|
|
242
|
+
# Edge to next operation
|
|
243
|
+
next_index = index + 1
|
|
244
|
+
if next_index == len(self.program.operations):
|
|
245
|
+
next_index = ExitIndex
|
|
246
|
+
self._edges.append((index, next_index))
|
|
247
|
+
|
|
248
|
+
def _get_jump_edge(self, index: int, op: PcodeOp):
|
|
249
|
+
# Edge to branched operation
|
|
250
|
+
address_op = op.inputs[0]
|
|
251
|
+
match address_op.space.name:
|
|
252
|
+
case "ram":
|
|
253
|
+
target_index = self.program.addresses.get(
|
|
254
|
+
address_op.offset, UnknownCodeIndex
|
|
255
|
+
)
|
|
256
|
+
self._edges.append((index, target_index))
|
|
257
|
+
case "const":
|
|
258
|
+
# Detect negative offsets
|
|
259
|
+
bits = address_op.size * 8
|
|
260
|
+
sign_bit = 1 << (bits - 1)
|
|
261
|
+
if (address_op.offset & sign_bit) != 0:
|
|
262
|
+
offset = address_op.offset - (1 << bits)
|
|
263
|
+
else:
|
|
264
|
+
offset = address_op.offset
|
|
265
|
+
self._edges.append((index, index + offset))
|
|
266
|
+
case "register" | "unique":
|
|
267
|
+
self._edges.append((index, UnknownCodeIndex))
|
|
268
|
+
case _ as name:
|
|
269
|
+
raise ValueError(f"Unexpected address space for jump address: {name}")
|
|
270
|
+
|
|
271
|
+
@override
|
|
272
|
+
def visit_branch(self, index: int, op: PcodeOp):
|
|
273
|
+
# Unconditional branching without return
|
|
274
|
+
self._get_jump_edge(index, op)
|
|
275
|
+
|
|
276
|
+
@override
|
|
277
|
+
def visit_branchind(self, index: int, op: PcodeOp):
|
|
278
|
+
# Unconditional branching without return
|
|
279
|
+
self._get_jump_edge(index, op)
|
|
280
|
+
|
|
281
|
+
@override
|
|
282
|
+
def visit_return(self, index: int, op: PcodeOp):
|
|
283
|
+
# Unconditional branching without return
|
|
284
|
+
self._get_jump_edge(index, op)
|
|
285
|
+
|
|
286
|
+
@override
|
|
287
|
+
def visit_call(self, index: int, op: PcodeOp):
|
|
288
|
+
self._get_jump_edge(index, op)
|
|
289
|
+
if self._edges[-1][-1] is UnknownCodeIndex:
|
|
290
|
+
# Calling an unseen function: we suppose it returns here after
|
|
291
|
+
self.visit_default(index, op)
|
|
292
|
+
|
|
293
|
+
@override
|
|
294
|
+
def visit_callind(self, index: int, op: PcodeOp):
|
|
295
|
+
self._get_jump_edge(index, op)
|
|
296
|
+
if self._edges[-1][-1] is UnknownCodeIndex:
|
|
297
|
+
# Calling an unseen function: we suppose it returns here after
|
|
298
|
+
self.visit_default(index, op)
|
|
299
|
+
|
|
300
|
+
@override
|
|
301
|
+
def visit_cbranch(self, index: int, op: PcodeOp):
|
|
302
|
+
# Conditional branching
|
|
303
|
+
self._get_jump_edge(index, op)
|
|
304
|
+
self.visit_default(index, op)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class UnreachableCodeFinder(PassBase):
|
|
308
|
+
def __call__(self):
|
|
309
|
+
self.program.reachable = [False] * len(self.program.operations)
|
|
310
|
+
active: set[int] = {0}
|
|
311
|
+
while active:
|
|
312
|
+
index = active.pop()
|
|
313
|
+
self.program.reachable[index] = True
|
|
314
|
+
for s in self.program.successors.get(index, []):
|
|
315
|
+
if s >= 0 and not self.program.reachable[s]:
|
|
316
|
+
active.add(s)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class ReachingDefBuilder(PassBase):
|
|
320
|
+
def __call__(self):
|
|
321
|
+
|
|
322
|
+
# logger.debug("Initialize defs")
|
|
323
|
+
# Initialize empty defs for each operation
|
|
324
|
+
self.program.reaching_defs = [{} for i in range(len(self.program.operations))]
|
|
325
|
+
|
|
326
|
+
# To detect the end of propagation
|
|
327
|
+
changed: bool = True
|
|
328
|
+
|
|
329
|
+
def add_def(var: Var, to_op: OpIndex, from_op: OpIndex):
|
|
330
|
+
nonlocal changed
|
|
331
|
+
assert to_op >= 0
|
|
332
|
+
|
|
333
|
+
defs = self.program.reaching_defs[to_op].setdefault(var, set())
|
|
334
|
+
if from_op not in defs:
|
|
335
|
+
# logger.debug(f" {var} from {from_op} reaching {to_op}")
|
|
336
|
+
defs.add(from_op)
|
|
337
|
+
changed = True
|
|
338
|
+
|
|
339
|
+
# Initialize with start defs
|
|
340
|
+
for index, op in enumerate(self.program.operations):
|
|
341
|
+
|
|
342
|
+
if not self.program.reachable[index]:
|
|
343
|
+
continue
|
|
344
|
+
|
|
345
|
+
if op.output:
|
|
346
|
+
var = wrap(op.output)
|
|
347
|
+
for succ in self.program.successors.get(index, []):
|
|
348
|
+
if succ >= 0:
|
|
349
|
+
add_def(var, succ, index)
|
|
350
|
+
|
|
351
|
+
elif op.opcode == OpCode.STORE:
|
|
352
|
+
for succ in self.program.successors.get(index, []):
|
|
353
|
+
if succ >= 0:
|
|
354
|
+
add_def(MEMORY, succ, index)
|
|
355
|
+
|
|
356
|
+
for var in self.program.iter_inputs(index):
|
|
357
|
+
if var.kind in {
|
|
358
|
+
VarKinds.IndirectAccess,
|
|
359
|
+
VarKinds.Ram,
|
|
360
|
+
VarKinds.Register,
|
|
361
|
+
}:
|
|
362
|
+
add_def(var, 0, EntryIndex)
|
|
363
|
+
|
|
364
|
+
# Propagate defs to predecessors
|
|
365
|
+
while changed:
|
|
366
|
+
changed = False
|
|
367
|
+
|
|
368
|
+
# logger.debug("Now reaching defs are:")
|
|
369
|
+
# for index, op in enumerate(self.program.operations):
|
|
370
|
+
# for var, defs in self.program.reaching_defs[index].items():
|
|
371
|
+
# logger.debug(
|
|
372
|
+
# f"{index:>5}: {dump_operation(op)}: {var} from {', '.join(dump_index(d) for d in defs)}"
|
|
373
|
+
# )
|
|
374
|
+
|
|
375
|
+
for index, op in enumerate(self.program.operations):
|
|
376
|
+
|
|
377
|
+
if not self.program.reachable[index]:
|
|
378
|
+
continue
|
|
379
|
+
|
|
380
|
+
for p in self.program.predecessors.get(index, set()):
|
|
381
|
+
|
|
382
|
+
if p < 0:
|
|
383
|
+
continue
|
|
384
|
+
|
|
385
|
+
pred_op = self.program.operations[p]
|
|
386
|
+
for var, defs in self.program.reaching_defs[p].items():
|
|
387
|
+
|
|
388
|
+
if pred_op.output and wrap(pred_op.output) == var:
|
|
389
|
+
# Kill previous defs of output variable.
|
|
390
|
+
# As we do not analyze which part of memory is read or written,
|
|
391
|
+
# a write to memory won't kill previous definitions of MEMORY.
|
|
392
|
+
continue
|
|
393
|
+
|
|
394
|
+
for writer_index in defs:
|
|
395
|
+
add_def(var, index, writer_index)
|
pcode_graph/arch.py
ADDED
pcode_graph/asm.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import keystone
|
|
2
|
+
|
|
3
|
+
from pcode_graph.arch import Arch
|
|
4
|
+
|
|
5
|
+
KEYSTONE_MODES = {
|
|
6
|
+
Arch.arm_32: (
|
|
7
|
+
keystone.KS_ARCH_ARM,
|
|
8
|
+
keystone.KS_MODE_ARM | keystone.KS_MODE_V8 | keystone.KS_MODE_LITTLE_ENDIAN,
|
|
9
|
+
),
|
|
10
|
+
Arch.arm_64: (keystone.KS_ARCH_ARM64, keystone.KS_MODE_LITTLE_ENDIAN),
|
|
11
|
+
Arch.x86_32: (keystone.KS_ARCH_X86, keystone.KS_MODE_32),
|
|
12
|
+
Arch.x86_64: (keystone.KS_ARCH_X86, keystone.KS_MODE_64),
|
|
13
|
+
Arch.mips_32: (
|
|
14
|
+
keystone.KS_ARCH_MIPS,
|
|
15
|
+
keystone.KS_MODE_MIPS32 | keystone.KS_MODE_BIG_ENDIAN,
|
|
16
|
+
),
|
|
17
|
+
Arch.mips_64: (
|
|
18
|
+
keystone.KS_ARCH_MIPS,
|
|
19
|
+
keystone.KS_MODE_MIPS64 | keystone.KS_MODE_BIG_ENDIAN,
|
|
20
|
+
),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# To ease diagnostic when writing assembly by hand...
|
|
25
|
+
def split_instructions(asm: str):
|
|
26
|
+
|
|
27
|
+
asm = asm.replace("\t", " ")
|
|
28
|
+
instructions: list[str] = []
|
|
29
|
+
|
|
30
|
+
for line in asm.splitlines():
|
|
31
|
+
for ins in line.split(";"):
|
|
32
|
+
ins = ins.strip()
|
|
33
|
+
if ins:
|
|
34
|
+
instructions.append(ins)
|
|
35
|
+
|
|
36
|
+
return instructions
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Assembler:
|
|
40
|
+
|
|
41
|
+
def __init__(self, arch: Arch) -> None:
|
|
42
|
+
self.arch = arch
|
|
43
|
+
self._keystone = keystone.Ks(*KEYSTONE_MODES[arch])
|
|
44
|
+
|
|
45
|
+
def assemble(self, instructions: str, base_address: int) -> bytes:
|
|
46
|
+
try:
|
|
47
|
+
data, count = self._keystone.asm(
|
|
48
|
+
instructions, addr=base_address, as_bytes=True
|
|
49
|
+
)
|
|
50
|
+
except keystone.KsError:
|
|
51
|
+
for i, ins in enumerate(split_instructions(instructions)):
|
|
52
|
+
try:
|
|
53
|
+
self._keystone.asm(ins)
|
|
54
|
+
except keystone.KsError as e:
|
|
55
|
+
raise ValueError(f"Error in instruction {i}: {ins}: {e}")
|
|
56
|
+
raise
|
|
57
|
+
|
|
58
|
+
assert count > 0
|
|
59
|
+
assert isinstance(data, bytes)
|
|
60
|
+
return data
|
pcode_graph/charac.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from enum import IntFlag, auto
|
|
2
|
+
from typing import NamedTuple
|
|
3
|
+
import networkx as nx
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from pcode_graph.nx_exporter import export_to_networkx
|
|
7
|
+
from pcode_graph.graph import CDG, EdgeKinds, NodeKinds
|
|
8
|
+
from pcode_graph.pcode import OpCodes
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
from collections import deque
|
|
12
|
+
from itertools import chain
|
|
13
|
+
|
|
14
|
+
class Diameters(NamedTuple):
|
|
15
|
+
data_only: int
|
|
16
|
+
control_only: int
|
|
17
|
+
both: int
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_graph_diameters(cdg: CDG) -> Diameters:
|
|
21
|
+
n = len(cdg.nodes)
|
|
22
|
+
|
|
23
|
+
def build_adjacency(edges):
|
|
24
|
+
adj = [[] for _ in range(n)]
|
|
25
|
+
for e in edges:
|
|
26
|
+
adj[e.source_node].append(e.destination_node)
|
|
27
|
+
return adj
|
|
28
|
+
|
|
29
|
+
data_edges = []
|
|
30
|
+
ctrl_edges = []
|
|
31
|
+
for e in cdg.edges:
|
|
32
|
+
if e.kind == EdgeKinds.Data:
|
|
33
|
+
data_edges.append(e)
|
|
34
|
+
else:
|
|
35
|
+
ctrl_edges.append(e)
|
|
36
|
+
|
|
37
|
+
def diameter(adjacency) -> int:
|
|
38
|
+
# Compute graph diameter with BFS
|
|
39
|
+
d = 0
|
|
40
|
+
for src in range(n):
|
|
41
|
+
dist = [-1] * n
|
|
42
|
+
dist[src] = 0
|
|
43
|
+
front = deque((src,))
|
|
44
|
+
while front:
|
|
45
|
+
current = front.popleft()
|
|
46
|
+
for neigbor in adjacency[current]:
|
|
47
|
+
if dist[neigbor] == -1:
|
|
48
|
+
dist[neigbor] = dist[current] + 1
|
|
49
|
+
front.append(neigbor)
|
|
50
|
+
d = max(d, max(dist))
|
|
51
|
+
return d
|
|
52
|
+
|
|
53
|
+
return Diameters(
|
|
54
|
+
diameter(build_adjacency(data_edges)),
|
|
55
|
+
diameter(build_adjacency(ctrl_edges)),
|
|
56
|
+
diameter(build_adjacency(chain(data_edges, ctrl_edges))),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
class FunctionCharacteristics(IntFlag):
|
|
60
|
+
"""Helps evaluating the level of difficulty of this function."""
|
|
61
|
+
|
|
62
|
+
data = auto()
|
|
63
|
+
condition = auto()
|
|
64
|
+
memory = auto()
|
|
65
|
+
phi = auto()
|
|
66
|
+
call = auto()
|
|
67
|
+
loop = auto()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def get_function_characteristics(graph: CDG) -> FunctionCharacteristics:
|
|
71
|
+
|
|
72
|
+
# Every function contains data-flow easiest level
|
|
73
|
+
function_characteristics = FunctionCharacteristics.data
|
|
74
|
+
|
|
75
|
+
ng = export_to_networkx(graph, False)
|
|
76
|
+
try:
|
|
77
|
+
nx.find_cycle(ng)
|
|
78
|
+
except nx.NetworkXNoCycle:
|
|
79
|
+
pass
|
|
80
|
+
else:
|
|
81
|
+
function_characteristics |= FunctionCharacteristics.loop
|
|
82
|
+
|
|
83
|
+
for n in graph.nodes:
|
|
84
|
+
match n.kind:
|
|
85
|
+
case NodeKinds.Operation:
|
|
86
|
+
if n.opcode in {OpCodes.CALL, OpCodes.CALLIND}:
|
|
87
|
+
function_characteristics |= FunctionCharacteristics.call
|
|
88
|
+
elif n.opcode == OpCodes.CBRANCH:
|
|
89
|
+
function_characteristics |= FunctionCharacteristics.condition
|
|
90
|
+
|
|
91
|
+
case NodeKinds.Phi:
|
|
92
|
+
function_characteristics |= FunctionCharacteristics.phi
|
|
93
|
+
|
|
94
|
+
case NodeKinds.ReadMemory | NodeKinds.WrittenMemory:
|
|
95
|
+
function_characteristics |= FunctionCharacteristics.memory
|
|
96
|
+
|
|
97
|
+
return function_characteristics
|
pcode_graph/checker.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from pcode_graph.graph import CDG, EdgeKinds, NodeKinds
|
|
2
|
+
|
|
3
|
+
from pcode_graph.pcode import NUM_OPERANDS, OpCodes
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def check_graph(graph: CDG):
|
|
7
|
+
"""Checks the coherency of this graph.
|
|
8
|
+
|
|
9
|
+
Can be used to verify that the graph remains valid after a set of transformations
|
|
10
|
+
made by a neural network for instance.
|
|
11
|
+
|
|
12
|
+
Raises AssertionError if invalid.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
# Check edges are valid
|
|
16
|
+
has_cfg = False
|
|
17
|
+
for edge in graph.edges:
|
|
18
|
+
# assert isinstance(edge.source_node, int)
|
|
19
|
+
# assert isinstance(edge.destination_node, int)
|
|
20
|
+
has_cfg |= edge.kind == EdgeKinds.Control
|
|
21
|
+
assert 0 <= edge.source_node < len(graph.nodes)
|
|
22
|
+
assert 0 <= edge.destination_node < len(graph.nodes)
|
|
23
|
+
|
|
24
|
+
# Check nodes attributes are coherent with node kind
|
|
25
|
+
proxy_dict = graph.compute_edge_proxy()
|
|
26
|
+
|
|
27
|
+
for index, node in enumerate(graph.nodes):
|
|
28
|
+
|
|
29
|
+
if node.value:
|
|
30
|
+
assert node.kind == NodeKinds.Constant
|
|
31
|
+
|
|
32
|
+
assert (not node.register_name) ^ (
|
|
33
|
+
node.kind in {NodeKinds.InputRegister, NodeKinds.OutputRegister}
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
proxy = proxy_dict[index]
|
|
37
|
+
control_succs = proxy.get_control_successors()
|
|
38
|
+
data_preds = proxy.get_data_predecessors()
|
|
39
|
+
|
|
40
|
+
if node.kind != NodeKinds.Operation:
|
|
41
|
+
assert node.opcode is None
|
|
42
|
+
else:
|
|
43
|
+
assert node.opcode is not None
|
|
44
|
+
# Check operands numbering
|
|
45
|
+
assert NUM_OPERANDS[node.opcode] in {-1, len(data_preds)}
|
|
46
|
+
|
|
47
|
+
data_numbers: list[int] = []
|
|
48
|
+
control_numbers: list[int] = []
|
|
49
|
+
for edge in proxy.in_edges:
|
|
50
|
+
if edge.kind == EdgeKinds.Data:
|
|
51
|
+
assert edge.operand_number is not None
|
|
52
|
+
data_numbers.append(edge.operand_number)
|
|
53
|
+
for edge in proxy.out_edges:
|
|
54
|
+
if edge.kind == EdgeKinds.Control:
|
|
55
|
+
assert edge.operand_number is not None
|
|
56
|
+
control_numbers.append(edge.operand_number)
|
|
57
|
+
assert sorted(data_numbers) == list(range(len(data_preds)))
|
|
58
|
+
assert sorted(control_numbers) == list(range(len(control_succs)))
|
|
59
|
+
|
|
60
|
+
if has_cfg:
|
|
61
|
+
# Check CFG coherency
|
|
62
|
+
match len(control_succs):
|
|
63
|
+
case 0:
|
|
64
|
+
assert node.kind in {
|
|
65
|
+
NodeKinds.External,
|
|
66
|
+
NodeKinds.End,
|
|
67
|
+
NodeKinds.InputRegister,
|
|
68
|
+
NodeKinds.OutputRegister,
|
|
69
|
+
NodeKinds.Phi,
|
|
70
|
+
NodeKinds.Constant,
|
|
71
|
+
NodeKinds.ReadMemory,
|
|
72
|
+
NodeKinds.WrittenMemory,
|
|
73
|
+
}, f"Node {index} {node} without successor"
|
|
74
|
+
case 1:
|
|
75
|
+
assert node.kind in {NodeKinds.Begin, NodeKinds.Operation}
|
|
76
|
+
if node.kind == NodeKinds.Operation:
|
|
77
|
+
assert node.opcode not in {
|
|
78
|
+
OpCodes.CBRANCH,
|
|
79
|
+
OpCodes.CALLIND,
|
|
80
|
+
OpCodes.CALL,
|
|
81
|
+
}, f"Node {index} {node} with only 1 successors"
|
|
82
|
+
case 2:
|
|
83
|
+
assert node.kind == NodeKinds.Operation and node.opcode in {
|
|
84
|
+
OpCodes.CBRANCH,
|
|
85
|
+
OpCodes.CALLIND,
|
|
86
|
+
OpCodes.CALL,
|
|
87
|
+
}, f"Node {index} {node} with 2 successors"
|
|
88
|
+
case _:
|
|
89
|
+
raise AssertionError("Unexpected number of successors")
|
|
90
|
+
|
|
91
|
+
# Check dataflow coeherency
|
|
92
|
+
match len(data_preds):
|
|
93
|
+
case 0:
|
|
94
|
+
assert node.kind in {
|
|
95
|
+
NodeKinds.Begin,
|
|
96
|
+
NodeKinds.InputRegister,
|
|
97
|
+
NodeKinds.OutputRegister,
|
|
98
|
+
NodeKinds.Constant,
|
|
99
|
+
NodeKinds.ReadMemory,
|
|
100
|
+
NodeKinds.External,
|
|
101
|
+
NodeKinds.End,
|
|
102
|
+
NodeKinds.Operation, # For example the CALL operation
|
|
103
|
+
}, f"Node {index} {node} without data predecessors"
|
|
104
|
+
case 1 | 2:
|
|
105
|
+
assert node.kind in {
|
|
106
|
+
NodeKinds.Operation,
|
|
107
|
+
NodeKinds.Phi,
|
|
108
|
+
NodeKinds.OutputRegister,
|
|
109
|
+
NodeKinds.WrittenMemory,
|
|
110
|
+
}, f"Node {index} {node} with {len(data_preds)} data predecessors"
|
|
111
|
+
case _:
|
|
112
|
+
if node.kind not in {
|
|
113
|
+
NodeKinds.Phi,
|
|
114
|
+
NodeKinds.OutputRegister,
|
|
115
|
+
NodeKinds.WrittenMemory,
|
|
116
|
+
}:
|
|
117
|
+
assert (
|
|
118
|
+
node.kind == NodeKinds.Operation
|
|
119
|
+
and node.opcode == OpCodes.CALLOTHER
|
|
120
|
+
), f"Node {index} {node} with {len(data_preds)} data predecessors"
|