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/cli.py ADDED
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Dumps Control and Data Graph (CDG) and various data from a binary or assembly file.
4
+
5
+ Usage:
6
+ cdg ( asm | functions | html | md | pcode | table ) <input_path> [options]
7
+
8
+ Dump commands:
9
+ asm Assembly dump
10
+ functions Functions from symbol table (using LIEF, binary must not be stripped)
11
+ html CDG in HTML format
12
+ md CDG in markdown/mermaid format
13
+ pcode P-Code operations
14
+ table Markdown table of the P-Code enriched by static analysis results
15
+
16
+ Options:
17
+ -f, --function <name> Take only the function with given symbol name.
18
+ -c, --chunk <begin:end> Pass "0xbeaf:0xdead" to lift only the chunk started at 0xbeaf and ending before 0xdead.
19
+ -a, --arch <arch> Use the given architecture (x86|arm|mips)_(64|32) or auto [Default: auto].
20
+ -b, --base-address <int> Consider the given assembly starts at given address [Default: 0].
21
+ -D, --dataflow-only Only extract dataflow graph (and no control flow).
22
+ -o, --output <path> Where to write the output.
23
+ -h, --help This help.
24
+ -d, --debug Debug mode: do no catch exceptions.
25
+ """
26
+
27
+ from pathlib import Path
28
+ import sys
29
+ from docopt import docopt
30
+ from lief import Binary
31
+ import lief
32
+ from loguru import logger
33
+ import pypcode
34
+ from pcode_graph.arch import Arch
35
+ from pcode_graph.asm import Assembler
36
+ from pcode_graph.lief_importer import (
37
+ get_function,
38
+ iter_code_sections,
39
+ iter_functions,
40
+ get_architecture,
41
+ lookup_chunk,
42
+ parse_binary,
43
+ )
44
+ from pcode_graph.log import setup_logger
45
+ from pcode_graph.translator import Translator
46
+ from pcode_graph.pcode import dump_operation
47
+ from pcode_graph.analysis import RichPcodeList
48
+ from pcode_graph.maker import MakerFlags, make_graph
49
+ from pcode_graph.view import draw_graph
50
+ from magika import Magika
51
+
52
+
53
+ def main():
54
+ args = docopt(__doc__)
55
+
56
+ try:
57
+ if args["--debug"]:
58
+ setup_logger(stderr_verbosity="DEBUG")
59
+ else:
60
+ lief.logging.disable()
61
+ setup_logger()
62
+
63
+ input_path = Path(args["<input_path>"])
64
+ assert input_path.exists()
65
+
66
+ binary: Binary | None = None
67
+ assert input_path.stat().st_size > 0, "Input file is empty"
68
+ if not Magika().identify_path(input_path).output.is_text:
69
+ binary = parse_binary(input_path)
70
+
71
+ if args["--arch"] != "auto":
72
+ arch: Arch = Arch(args["--arch"])
73
+ else:
74
+ assert binary, "Assembly file requires --arch"
75
+ arch = get_architecture(binary)
76
+
77
+ output_path = args["--output"]
78
+ if output_path:
79
+ Path(output_path).parent.mkdir(parents=True, exist_ok=True)
80
+ output = open(output_path, "w")
81
+ else:
82
+ output = sys.stdout
83
+
84
+ if args["functions"]:
85
+ assert binary, "Command 'functions' requires a binary file"
86
+ for f in iter_functions(binary):
87
+ print(
88
+ f"{hex(f.address)}: {f.name}: {len(f.content)} bytes", file=output
89
+ )
90
+ sys.exit(0)
91
+
92
+ translator = Translator(arch)
93
+
94
+ function_name = args["--function"]
95
+ chunk_begin_end = args["--chunk"]
96
+ operations: list[pypcode.PcodeOp] = []
97
+
98
+ if binary:
99
+ if function_name or chunk_begin_end:
100
+ if function_name:
101
+ # Lift function
102
+ function = get_function(binary, function_name)
103
+ start_address = function.address
104
+ code = function.content
105
+ else:
106
+ # Lift piece of code
107
+ start_address, end_address = [
108
+ int(a, 16) for a in chunk_begin_end.split(":")
109
+ ]
110
+ code = lookup_chunk(binary, start_address, end_address)
111
+ if args["asm"]:
112
+ print(translator.dump_asm(code, start_address, False), file=output)
113
+ sys.exit(0)
114
+ operations = translator.translate(code, start_address)
115
+ else:
116
+ # Lift everything
117
+ for section in iter_code_sections(binary):
118
+ if args["asm"]:
119
+ print(f"; Section at {hex(section.address)}", file=output)
120
+ print(
121
+ translator.dump_asm(
122
+ section.content, section.address, False
123
+ ),
124
+ file=output,
125
+ )
126
+ else:
127
+ operations += translator.translate(
128
+ section.content, section.address
129
+ )
130
+ if args["asm"]:
131
+ sys.exit(0)
132
+ else:
133
+ assert (
134
+ not function_name
135
+ ), "Option --function not supported for assembly files."
136
+ assert (
137
+ not chunk_begin_end
138
+ ), "Option --chunk not supported for assembly files."
139
+ assembly = input_path.read_text()
140
+ assembler = Assembler(arch)
141
+ start_address = int(args["--base-address"])
142
+ code = assembler.assemble(assembly, start_address)
143
+ if args["asm"]:
144
+ print(translator.dump_asm(code, start_address, False), file=output)
145
+ sys.exit(0)
146
+ operations = translator.translate(code, start_address)
147
+
148
+ if args["pcode"]:
149
+ for op in operations:
150
+ print(dump_operation(op), file=output)
151
+ sys.exit(0)
152
+
153
+ logger.info(f"Analyze {len(operations)} P-Code operations")
154
+ rpl = RichPcodeList(arch, operations)
155
+
156
+ if args["table"]:
157
+ print(rpl.dump())
158
+ sys.exit(0)
159
+
160
+ flags = MakerFlags(build_cfg=not args["--dataflow-only"])
161
+ cdg = make_graph(rpl, flags)
162
+
163
+ if args["md"]:
164
+ print(str(cdg), file=output)
165
+ sys.exit(0)
166
+
167
+ assert output_path, "HTML output file is mandatory"
168
+ output.close()
169
+ draw_graph(cdg, Path(output_path))
170
+
171
+ except Exception as e:
172
+ if args["--debug"]:
173
+ raise
174
+ print(str(e), file=sys.stderr)
175
+ sys.exit(1)
@@ -0,0 +1,419 @@
1
+ from typing import override
2
+ from pcode_graph.graph import (
3
+ CDG,
4
+ EdgeKinds,
5
+ Node,
6
+ NodeIndex,
7
+ NodeKinds,
8
+ OperandNumber,
9
+ RegisterId,
10
+ Value,
11
+ )
12
+ from pcode_graph.utils import signed_to_unsigned, unsigned_to_signed
13
+ from pcode_graph.visitor import PcodeVisitor
14
+
15
+
16
+ class Emulator(PcodeVisitor):
17
+ """Emulate the execution of an homogeneous graph"""
18
+
19
+ def __init__(self, graph: CDG) -> None:
20
+ super().__init__()
21
+ self.graph = graph
22
+
23
+ # Known value of every node
24
+ self.values: dict[NodeIndex, int] = {}
25
+
26
+ # Inputs and outputs node indexes sorted by register id
27
+ self.inputs: dict[RegisterId, NodeIndex] = {}
28
+ self.outputs: dict[RegisterId, NodeIndex] = {}
29
+
30
+ for index, node in enumerate(self.graph.nodes):
31
+ if node.kind == NodeKinds.InputRegister:
32
+ self.inputs[node.register_id] = index
33
+ elif node.kind == NodeKinds.OutputRegister:
34
+ self.outputs[node.register_id] = index
35
+ elif node.kind == NodeKinds.Constant:
36
+ self.values[index] = node.value
37
+
38
+ def clear(self):
39
+ self.values.clear()
40
+
41
+ def set_input(self, index: NodeIndex, value: Value):
42
+ self.values[index] = value
43
+
44
+ def set_input_register(self, register_id: RegisterId, value: Value):
45
+ index = self.inputs[register_id]
46
+ self.set_input(index, value)
47
+
48
+ def set_input_registers(self, values_by_reg_id: dict[RegisterId, Value]):
49
+ for rid, value in values_by_reg_id.items():
50
+ self.set_input_register(rid, value)
51
+
52
+ def emulate(self):
53
+
54
+ # TODO: support CFG and Phi nodes
55
+ proxy = self.graph.compute_edge_proxy()
56
+ propagated = True
57
+ while propagated:
58
+ propagated = False
59
+
60
+ for index, node in enumerate(self.graph.nodes):
61
+ if index in self.values:
62
+ # Already computed
63
+ continue
64
+ p = proxy[index]
65
+ match node.kind:
66
+
67
+ case NodeKinds.Operation:
68
+ operands: dict[
69
+ OperandNumber | None, tuple[NodeIndex, Value]
70
+ ] = {}
71
+
72
+ for in_edge in p.in_edges:
73
+ if in_edge.kind != EdgeKinds.Data:
74
+ continue
75
+
76
+ value = self.values.get(in_edge.source_node)
77
+ if value is None:
78
+ break
79
+
80
+ assert in_edge.operand_number not in operands
81
+ operands[in_edge.operand_number] = (
82
+ in_edge.source_node,
83
+ value,
84
+ )
85
+ else:
86
+ # We can evaluate this node
87
+ self.values[index] = self.visit(node.opcode, node, operands)
88
+ propagated = True
89
+
90
+ case NodeKinds.OutputRegister:
91
+ (in_edge,) = p.get_inputs_edges()
92
+ value = self.values.get(in_edge.source_node)
93
+ if value is not None:
94
+ self.values[index] = value
95
+ # Do not set propagated as OutputRegister nodes are never used as inputs
96
+
97
+ def get_output(self, register_id) -> Value:
98
+ index = self.outputs[register_id]
99
+ return self.values[index]
100
+
101
+ def get_outputs(self) -> dict[RegisterId, Value]:
102
+ out_values = {}
103
+ for register_id, index in self.outputs.items():
104
+ out_values[register_id] = self.values[index]
105
+ return out_values
106
+
107
+ @override
108
+ def visit_bool_and(
109
+ self, node: Node, values: dict[OperandNumber, tuple[NodeIndex, Value]]
110
+ ) -> int:
111
+ i1, i2 = values[0][1], values[1][1]
112
+ return int((i1 != 0) and (i2 != 0))
113
+
114
+ @override
115
+ def visit_bool_negate(
116
+ self, node: Node, values: dict[OperandNumber, tuple[NodeIndex, Value]]
117
+ ) -> int:
118
+ return int(values[0] == 0)
119
+
120
+ @override
121
+ def visit_bool_or(
122
+ self, node: Node, values: dict[OperandNumber, tuple[NodeIndex, Value]]
123
+ ) -> int:
124
+ i1, i2 = values[0][1], values[1][1]
125
+ return int((i1 != 0) or (i2 != 0))
126
+
127
+ @override
128
+ def visit_bool_xor(
129
+ self, node: Node, values: dict[OperandNumber, tuple[NodeIndex, Value]]
130
+ ) -> int:
131
+ i1, i2 = values[0][1], values[1][1]
132
+ return int((i1 != 0) ^ (i2 != 0))
133
+
134
+ @override
135
+ def visit_int_2comp(
136
+ self, node: Node, values: dict[OperandNumber, tuple[NodeIndex, Value]]
137
+ ) -> int:
138
+ i = values[0][1]
139
+ mask = 2**node.size_bits - 1
140
+ return (mask ^ i) + 1
141
+
142
+ @override
143
+ def visit_int_add(
144
+ self, node: Node, values: dict[OperandNumber, tuple[NodeIndex, Value]]
145
+ ) -> int:
146
+ i1, i2 = values[0][1], values[1][1]
147
+ return i1 + i2
148
+
149
+ @override
150
+ def visit_int_and(
151
+ self, node: Node, values: dict[OperandNumber, tuple[NodeIndex, Value]]
152
+ ) -> int:
153
+ i1, i2 = values[0][1], values[1][1]
154
+ return i1 & i2
155
+
156
+ @override
157
+ def visit_int_carry(
158
+ self, node: Node, values: dict[OperandNumber, tuple[NodeIndex, Value]]
159
+ ) -> int:
160
+ i1, i2 = values[0][1], values[1][1]
161
+ return int((i1 + i2) >= 2**node.size_bits)
162
+
163
+ @override
164
+ def visit_int_div(
165
+ self, node: Node, values: dict[OperandNumber, tuple[NodeIndex, Value]]
166
+ ) -> int:
167
+ i1, i2 = values[0][1], values[1][1]
168
+ if i2 == 0:
169
+ return 0
170
+ return i1 // i2
171
+
172
+ @override
173
+ def visit_int_equal(
174
+ self, node: Node, values: dict[OperandNumber, tuple[NodeIndex, Value]]
175
+ ) -> int:
176
+ i1, i2 = values[0][1], values[1][1]
177
+ return int(i1 == i2)
178
+
179
+ @override
180
+ def visit_int_left(
181
+ self, node: Node, values: dict[OperandNumber, tuple[NodeIndex, Value]]
182
+ ) -> int:
183
+ i1, i2 = values[0][1], values[1][1]
184
+ return i1 << i2
185
+
186
+ @override
187
+ def visit_int_less(
188
+ self, node: Node, values: dict[OperandNumber, tuple[NodeIndex, Value]]
189
+ ) -> int:
190
+ i1, i2 = values[0][1], values[1][1]
191
+ return int(i1 < i2)
192
+
193
+ @override
194
+ def visit_int_lessequal(
195
+ self, node: Node, values: dict[OperandNumber, tuple[NodeIndex, Value]]
196
+ ) -> int:
197
+ i1, i2 = values[0][1], values[1][1]
198
+ return int(i1 <= i2)
199
+
200
+ @override
201
+ def visit_int_mult(
202
+ self, node: Node, values: dict[OperandNumber, tuple[NodeIndex, Value]]
203
+ ) -> int:
204
+ i1, i2 = values[0][1], values[1][1]
205
+ return i1 * i2
206
+
207
+ @override
208
+ def visit_int_negate(
209
+ self, node: Node, values: dict[OperandNumber, tuple[NodeIndex, Value]]
210
+ ) -> int:
211
+ i = values[0][1]
212
+ mask = 2 ** (node.size * 8) - 1
213
+ return mask ^ i
214
+
215
+ @override
216
+ def visit_int_notequal(
217
+ self, node: Node, values: dict[OperandNumber, tuple[NodeIndex, Value]]
218
+ ) -> int:
219
+ i1, i2 = values[0][1], values[1][1]
220
+ return int(i1 != i2)
221
+
222
+ @override
223
+ def visit_int_or(
224
+ self, node: Node, values: dict[OperandNumber, tuple[NodeIndex, Value]]
225
+ ) -> int:
226
+ i1, i2 = values[0][1], values[1][1]
227
+ return i1 | i2
228
+
229
+ @override
230
+ def visit_int_rem(
231
+ self, node: Node, values: dict[OperandNumber, tuple[NodeIndex, Value]]
232
+ ) -> int:
233
+ i1, i2 = values[0][1], values[1][1]
234
+ if i2 == 0:
235
+ return 0
236
+ q = i1 // i2
237
+ return i1 - q * i2
238
+
239
+ @override
240
+ def visit_int_right(
241
+ self, node: Node, values: dict[OperandNumber, tuple[NodeIndex, Value]]
242
+ ) -> int:
243
+ i1, i2 = values[0][1], values[1][1]
244
+ return i1 >> i2
245
+
246
+ @override
247
+ def visit_int_sborrow(
248
+ self, node: Node, values: dict[OperandNumber, tuple[NodeIndex, Value]]
249
+ ) -> int:
250
+ i1, i2 = values[0][1], values[1][1]
251
+ input_size = self.graph.nodes[values[0][0]].size
252
+ max = 2 ** (input_size * 8 - 1)
253
+ return int(
254
+ not (
255
+ -max
256
+ <= (
257
+ unsigned_to_signed(i1, input_size)
258
+ - unsigned_to_signed(i2, input_size)
259
+ < max
260
+ )
261
+ )
262
+ )
263
+
264
+ @override
265
+ def visit_int_scarry(
266
+ self, node: Node, values: dict[OperandNumber, tuple[NodeIndex, Value]]
267
+ ) -> int:
268
+ i1, i2 = values[0][1], values[1][1]
269
+ input_size = self.graph.nodes[values[0][0]].size
270
+ max = 2 ** (input_size * 8 - 1)
271
+ return int(
272
+ not (
273
+ -max
274
+ <= (
275
+ unsigned_to_signed(i1, input_size)
276
+ + unsigned_to_signed(i2, input_size)
277
+ < max
278
+ )
279
+ )
280
+ )
281
+
282
+ @override
283
+ def visit_int_sdiv(
284
+ self, node: Node, values: dict[OperandNumber, tuple[NodeIndex, Value]]
285
+ ) -> int:
286
+ i1, i2 = values[0][1], values[1][1]
287
+ if i2 == 0:
288
+ return 0
289
+ s1, s2 = unsigned_to_signed(i1, node.size), unsigned_to_signed(i2, node.size)
290
+ return signed_to_unsigned(int(s1 / s2), node.size)
291
+
292
+ @override
293
+ def visit_int_sext(
294
+ self, node: Node, values: dict[OperandNumber, tuple[NodeIndex, Value]]
295
+ ) -> int:
296
+ i = values[0][1]
297
+ input_size = self.graph.nodes[values[0][0]].size
298
+ s = unsigned_to_signed(i, input_size)
299
+ return signed_to_unsigned(s, node.size)
300
+
301
+ @override
302
+ def visit_int_sless(
303
+ self, node: Node, values: dict[OperandNumber, tuple[NodeIndex, Value]]
304
+ ) -> int:
305
+ i1, i2 = values[0][1], values[1][1]
306
+ input_size = self.graph.nodes[values[0][0]].size
307
+ return int(
308
+ unsigned_to_signed(i1, input_size) < unsigned_to_signed(i2, input_size)
309
+ )
310
+
311
+ @override
312
+ def visit_int_slessequal(
313
+ self, node: Node, values: dict[OperandNumber, tuple[NodeIndex, Value]]
314
+ ) -> int:
315
+ i1, i2 = values[0][1], values[1][1]
316
+ input_size = self.graph.nodes[values[0][0]].size
317
+ return int(
318
+ unsigned_to_signed(i1, input_size) <= unsigned_to_signed(i2, input_size)
319
+ )
320
+
321
+ @override
322
+ def visit_int_srem(
323
+ self, node: Node, values: dict[OperandNumber, tuple[NodeIndex, Value]]
324
+ ) -> int:
325
+ i1, i2 = values[0][1], values[1][1]
326
+ if i2 == 0:
327
+ return 0
328
+ s1, s2 = unsigned_to_signed(i1, node.size), unsigned_to_signed(i2, node.size)
329
+ q = int(s1 / s2)
330
+ return signed_to_unsigned(s1 - q * s2, node.size)
331
+
332
+ @override
333
+ def visit_int_sright(
334
+ self, node: Node, values: dict[OperandNumber, tuple[NodeIndex, Value]]
335
+ ) -> int:
336
+ i1, i2 = values[0][1], values[1][1]
337
+ s1, s2 = unsigned_to_signed(i1, node.size), unsigned_to_signed(i2, node.size)
338
+ return signed_to_unsigned(s1 >> s2, node.size)
339
+
340
+ @override
341
+ def visit_int_sub(
342
+ self, node: Node, values: dict[OperandNumber, tuple[NodeIndex, Value]]
343
+ ) -> int:
344
+ i1, i2 = values[0][1], values[1][1]
345
+ return i1 - i2
346
+
347
+ @override
348
+ def visit_int_xor(
349
+ self, node: Node, values: dict[OperandNumber, tuple[NodeIndex, Value]]
350
+ ) -> int:
351
+ i1, i2 = values[0][1], values[1][1]
352
+ return i1 ^ i2
353
+
354
+ @override
355
+ def visit_int_zext(
356
+ self, node: Node, values: dict[OperandNumber, tuple[NodeIndex, Value]]
357
+ ) -> int:
358
+ return values[0][1]
359
+
360
+ @override
361
+ def visit_load(
362
+ self, node: Node, values: dict[OperandNumber, tuple[NodeIndex, Value]]
363
+ ) -> None:
364
+ raise NotImplementedError
365
+
366
+ @override
367
+ def visit_lzcount(
368
+ self, node: Node, values: dict[OperandNumber, tuple[NodeIndex, Value]]
369
+ ) -> int:
370
+ value = values[0][1]
371
+ max_bits = node.size * 8
372
+ top_bit = 1 << (max_bits - 1)
373
+ count = 0
374
+ while not (value & top_bit):
375
+ count += 1
376
+ value <<= 1
377
+ return count
378
+
379
+ @override
380
+ def visit_piece(
381
+ self, node: Node, values: dict[OperandNumber, tuple[NodeIndex, Value]]
382
+ ) -> int:
383
+ # Consider the virtual machine as big endian
384
+ i1, i2 = values[0][1], values[1][1]
385
+ return (i1 << self.graph.nodes[values[1][0]].size_bits) | i2
386
+
387
+ @override
388
+ def visit_popcount(
389
+ self, node: Node, values: dict[OperandNumber, tuple[NodeIndex, Value]]
390
+ ) -> int:
391
+ i = values[0][1]
392
+ return i.bit_count()
393
+
394
+ @override
395
+ def visit_segmentop(
396
+ self, node: Node, values: dict[OperandNumber, tuple[NodeIndex, Value]]
397
+ ) -> int:
398
+ # TODO: opcode not documented in Pcode spec
399
+ raise NotImplementedError
400
+
401
+ @override
402
+ def visit_store(
403
+ self, node: Node, values: dict[OperandNumber, tuple[NodeIndex, Value]]
404
+ ) -> None:
405
+ raise NotImplementedError
406
+
407
+ @override
408
+ def visit_subpiece(
409
+ self, node: Node, values: dict[OperandNumber, tuple[NodeIndex, Value]]
410
+ ) -> int:
411
+ # Consider the machine as big endian (see piece)
412
+ i1, i2 = values[0][1], values[1][1]
413
+ return (i1 >> (i2 * 8)) & (2**node.size_bits - 1)
414
+
415
+ @override
416
+ def visit_copy(
417
+ self, node: Node, values: dict[OperandNumber, tuple[NodeIndex, Value]]
418
+ ) -> int:
419
+ return values[0][1]