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
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any, Iterator
|
|
4
|
+
from loguru import logger
|
|
5
|
+
from pypcode import OpCode
|
|
6
|
+
from pcode_graph.arch import Arch
|
|
7
|
+
import lief
|
|
8
|
+
from pcode_graph.translator import ChunkOps, Translator
|
|
9
|
+
|
|
10
|
+
lief.disable_leak_warning()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class ChunkCode:
|
|
15
|
+
address: int
|
|
16
|
+
content: bytes
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class FunctionCode:
|
|
21
|
+
name: str
|
|
22
|
+
address: int
|
|
23
|
+
content: bytes
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class FunctionOps(ChunkOps):
|
|
28
|
+
function_name: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def parse_binary(binary_path: Path) -> lief.Binary:
|
|
32
|
+
assert binary_path.exists(), f"{binary_path} not found"
|
|
33
|
+
logger.debug(f"Parse {binary_path}")
|
|
34
|
+
b = lief.parse(binary_path.as_posix())
|
|
35
|
+
assert isinstance(b, lief.Binary)
|
|
36
|
+
return b
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_architecture(binary: Path | lief.Binary) -> Arch:
|
|
40
|
+
"""Guesses the architecture of given binary."""
|
|
41
|
+
|
|
42
|
+
if isinstance(binary, Path):
|
|
43
|
+
binary = parse_binary(binary)
|
|
44
|
+
|
|
45
|
+
# ELF
|
|
46
|
+
if isinstance(binary, lief.ELF.Binary):
|
|
47
|
+
|
|
48
|
+
match binary.header.machine_type:
|
|
49
|
+
case lief.ELF.ARCH.ARM:
|
|
50
|
+
arch = Arch.arm_32
|
|
51
|
+
case lief.ELF.ARCH.AARCH64:
|
|
52
|
+
arch = Arch.arm_64
|
|
53
|
+
case lief.ELF.ARCH.X86_64:
|
|
54
|
+
arch = Arch.x86_64
|
|
55
|
+
case lief.ELF.ARCH.I386:
|
|
56
|
+
arch = Arch.x86_32
|
|
57
|
+
case lief.ELF.ARCH.MIPS:
|
|
58
|
+
if binary.header.identity_data != lief.ELF.Header.ELF_DATA.MSB:
|
|
59
|
+
raise ValueError("Unsupported edianness")
|
|
60
|
+
if binary.header.identity_class == lief.ELF.Header.CLASS.ELF64:
|
|
61
|
+
arch = Arch.mips_64
|
|
62
|
+
else:
|
|
63
|
+
flags = set(binary.header.flags_list)
|
|
64
|
+
_P = lief.ELF.PROCESSOR_FLAGS
|
|
65
|
+
if _P.MIPS_ABI2 in flags or bool(
|
|
66
|
+
flags
|
|
67
|
+
& {
|
|
68
|
+
_P.MIPS_ARCH_3, _P.MIPS_ARCH_4, _P.MIPS_ARCH_5, _P.MIPS_ARCH_64, _P.MIPS_ARCH_64R2, _P.MIPS_ARCH_64R6, # type: ignore # fmt: skip
|
|
69
|
+
}
|
|
70
|
+
):
|
|
71
|
+
arch = Arch.mips_64
|
|
72
|
+
else:
|
|
73
|
+
arch = Arch.mips_32
|
|
74
|
+
case _ as v:
|
|
75
|
+
raise ValueError(f"Unsupported CPU type {v}")
|
|
76
|
+
|
|
77
|
+
# Mach-O
|
|
78
|
+
elif isinstance(binary, lief.MachO.FatBinary):
|
|
79
|
+
arch = None
|
|
80
|
+
for sub_binary in binary:
|
|
81
|
+
match sub_binary.header.cpu_type:
|
|
82
|
+
case lief.MachO.Header.CPU_TYPE.ARM64:
|
|
83
|
+
arch = Arch.arm_64
|
|
84
|
+
case lief.MachO.Header.CPU_TYPE.X86_64:
|
|
85
|
+
arch = Arch.x86_64
|
|
86
|
+
case _:
|
|
87
|
+
continue
|
|
88
|
+
break
|
|
89
|
+
else:
|
|
90
|
+
raise ValueError(f"No sub binary found with supported architecture")
|
|
91
|
+
|
|
92
|
+
elif isinstance(binary, lief.MachO.Binary):
|
|
93
|
+
match binary.header.cpu_type:
|
|
94
|
+
case lief.MachO.Header.CPU_TYPE.ARM64:
|
|
95
|
+
arch = Arch.arm_64
|
|
96
|
+
case lief.MachO.Header.CPU_TYPE.X86_64:
|
|
97
|
+
arch = Arch.x86_64
|
|
98
|
+
case _ as v:
|
|
99
|
+
raise ValueError(f"Unsupported CPU type {v}")
|
|
100
|
+
|
|
101
|
+
# PE
|
|
102
|
+
elif isinstance(binary, lief.PE.Binary):
|
|
103
|
+
match binary.header.machine:
|
|
104
|
+
case lief.PE.Header.MACHINE_TYPES.ARM64:
|
|
105
|
+
arch = Arch.arm_64
|
|
106
|
+
case lief.PE.Header.MACHINE_TYPES.AMD64:
|
|
107
|
+
arch = Arch.x86_64
|
|
108
|
+
case _ as v:
|
|
109
|
+
raise ValueError(f"Unsupported CPU type {v}")
|
|
110
|
+
|
|
111
|
+
else:
|
|
112
|
+
raise ValueError("Unsupported binary format")
|
|
113
|
+
|
|
114
|
+
return arch
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def iter_code_sections(binary: Path | lief.Binary) -> Iterator[ChunkCode]:
|
|
118
|
+
|
|
119
|
+
if isinstance(binary, Path):
|
|
120
|
+
binary = parse_binary(binary)
|
|
121
|
+
|
|
122
|
+
if binary.format == lief.Binary.FORMATS.MACHO:
|
|
123
|
+
flags = {
|
|
124
|
+
lief.MachO.Section.FLAGS.PURE_INSTRUCTIONS, # type: ignore
|
|
125
|
+
lief.MachO.Section.FLAGS.SOME_INSTRUCTIONS, # type: ignore
|
|
126
|
+
}
|
|
127
|
+
for section in binary.sections:
|
|
128
|
+
if flags.intersection(section.flags_list): # type: ignore
|
|
129
|
+
yield ChunkCode(section.virtual_address, section.content.tobytes())
|
|
130
|
+
|
|
131
|
+
elif binary.format == lief.Binary.FORMATS.PE:
|
|
132
|
+
for section in binary.sections:
|
|
133
|
+
if section.has_characteristic( # type: ignore
|
|
134
|
+
lief.PE.Section.CHARACTERISTICS.CNT_CODE
|
|
135
|
+
) and section.has_characteristic( # type: ignore
|
|
136
|
+
lief.PE.Section.CHARACTERISTICS.MEM_EXECUTE
|
|
137
|
+
):
|
|
138
|
+
yield ChunkCode(section.virtual_address, section.content.tobytes())
|
|
139
|
+
|
|
140
|
+
elif binary.format == lief.Binary.FORMATS.ELF:
|
|
141
|
+
for section in binary.sections:
|
|
142
|
+
if section.has(lief.ELF.Section.FLAGS.EXECINSTR): # type: ignore
|
|
143
|
+
yield ChunkCode(section.virtual_address, section.content.tobytes())
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def get_function(binary: Path | lief.Binary, name: str) -> FunctionCode:
|
|
147
|
+
|
|
148
|
+
if isinstance(binary, Path):
|
|
149
|
+
binary = parse_binary(binary)
|
|
150
|
+
|
|
151
|
+
for f in iter_functions(binary):
|
|
152
|
+
if f.name == name:
|
|
153
|
+
return f
|
|
154
|
+
|
|
155
|
+
raise LookupError(f"Function {name} not found in {binary}")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def iter_functions(binary: Path | lief.Binary) -> Iterator[FunctionCode]:
|
|
159
|
+
|
|
160
|
+
if isinstance(binary, Path):
|
|
161
|
+
binary = parse_binary(binary)
|
|
162
|
+
|
|
163
|
+
cache: dict[Any, tuple[bytes, int]] = {}
|
|
164
|
+
sym: lief.ELF.Symbol
|
|
165
|
+
|
|
166
|
+
for sym in binary.symtab_symbols: # type: ignore
|
|
167
|
+
if sym.type != lief.ELF.Symbol.TYPE.FUNC or sym.size == 0:
|
|
168
|
+
continue
|
|
169
|
+
section: lief.ELF.Section | None = sym.section
|
|
170
|
+
if section is None:
|
|
171
|
+
continue
|
|
172
|
+
|
|
173
|
+
section_id = section.name
|
|
174
|
+
if section_id not in cache:
|
|
175
|
+
cache[section_id] = (bytes(section.content), section.virtual_address)
|
|
176
|
+
|
|
177
|
+
section_content, section_address = cache[section_id]
|
|
178
|
+
|
|
179
|
+
assert isinstance(sym.name, str)
|
|
180
|
+
yield FunctionCode(
|
|
181
|
+
sym.name,
|
|
182
|
+
section_address + sym.value,
|
|
183
|
+
section_content[sym.value : sym.value + sym.size],
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def lookup_chunk(
|
|
188
|
+
binary: Path | lief.Binary,
|
|
189
|
+
start_address: int,
|
|
190
|
+
end_address: int,
|
|
191
|
+
) -> bytes:
|
|
192
|
+
"""Returns the code at given address from the given binary.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
binary (Path | lief.Binary): Binary to read.
|
|
196
|
+
start_address (int): Address of the first instruction to return.
|
|
197
|
+
end_address (int): Address of the first instruction after the returned chunk.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
ChunkCode: A piece of binary code.
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
for section in iter_code_sections(binary):
|
|
204
|
+
if (
|
|
205
|
+
section.address <= start_address
|
|
206
|
+
and section.address + len(section.content) >= end_address
|
|
207
|
+
):
|
|
208
|
+
offset = start_address - section.address
|
|
209
|
+
size = end_address - start_address
|
|
210
|
+
return section.content[offset : offset + size]
|
|
211
|
+
|
|
212
|
+
raise LookupError("Cannot find a section containing given range")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def collect_functions(
|
|
216
|
+
binary: Path | lief.Binary,
|
|
217
|
+
translator: Translator,
|
|
218
|
+
) -> Iterator[FunctionOps]:
|
|
219
|
+
|
|
220
|
+
logger.debug(f"Extract functions")
|
|
221
|
+
|
|
222
|
+
for function in iter_functions(binary):
|
|
223
|
+
|
|
224
|
+
instructions = list(translator.disassemble(function.content, function.address))
|
|
225
|
+
operations = translator.translate(function.content, function.address)
|
|
226
|
+
yield FunctionOps(
|
|
227
|
+
len(instructions),
|
|
228
|
+
function.address,
|
|
229
|
+
operations,
|
|
230
|
+
function.name,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def collect_basic_blocks(
|
|
235
|
+
binary: Path | lief.Binary,
|
|
236
|
+
translator: Translator,
|
|
237
|
+
min_instructions: int,
|
|
238
|
+
max_instructions: int,
|
|
239
|
+
) -> Iterator[ChunkOps]:
|
|
240
|
+
"""Extracts the P-Code operations of basic blocks found in a binary, using LIEF library."""
|
|
241
|
+
|
|
242
|
+
logger.debug(f"Extract basic blocks")
|
|
243
|
+
|
|
244
|
+
# Extract P-Code operations of executable sections
|
|
245
|
+
for section in iter_code_sections(binary):
|
|
246
|
+
|
|
247
|
+
# Slice into basic blocks
|
|
248
|
+
num_instructions = 0
|
|
249
|
+
operations = []
|
|
250
|
+
bb_start_offset = 0
|
|
251
|
+
|
|
252
|
+
# First disassemble to translate into P-Code one instruction at a time,
|
|
253
|
+
# in order to avoid consuming too much RAM.
|
|
254
|
+
for instr in translator.disassemble(section.content, section.address):
|
|
255
|
+
|
|
256
|
+
instr_start_offset = instr.address - section.address
|
|
257
|
+
instr_end_offset = instr_start_offset + instr.length
|
|
258
|
+
instr_code = section.content[instr_start_offset:instr_end_offset]
|
|
259
|
+
|
|
260
|
+
instr_ops = translator.translate(instr_code, instr.address)
|
|
261
|
+
|
|
262
|
+
num_instructions += 1
|
|
263
|
+
|
|
264
|
+
if num_instructions <= max_instructions:
|
|
265
|
+
operations += instr_ops
|
|
266
|
+
|
|
267
|
+
last_op = instr_ops[-1]
|
|
268
|
+
if last_op.opcode not in {
|
|
269
|
+
OpCode.BRANCH,
|
|
270
|
+
OpCode.BRANCHIND,
|
|
271
|
+
OpCode.CBRANCH,
|
|
272
|
+
OpCode.RETURN,
|
|
273
|
+
}:
|
|
274
|
+
# Not at the end of a basic block
|
|
275
|
+
continue
|
|
276
|
+
|
|
277
|
+
if min_instructions <= num_instructions <= max_instructions:
|
|
278
|
+
yield ChunkOps(
|
|
279
|
+
num_instructions,
|
|
280
|
+
section.address + bb_start_offset,
|
|
281
|
+
# section.content[bb_start_offset:instr_end_offset],
|
|
282
|
+
operations,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
bb_start_offset = instr_end_offset
|
|
286
|
+
num_instructions = 0
|
|
287
|
+
operations = []
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def collect_chunks(
|
|
291
|
+
binary: Path | lief.Binary,
|
|
292
|
+
translator: Translator,
|
|
293
|
+
|
|
294
|
+
) -> Iterator[ChunkOps]:
|
|
295
|
+
"""Extracts the P-Code operations of the sections found in a binary, using LIEF library."""
|
|
296
|
+
|
|
297
|
+
for section in iter_code_sections(binary):
|
|
298
|
+
yield from translator.iter_operations(section.content, section.address)
|
|
299
|
+
|
pcode_graph/log.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import sys
|
|
3
|
+
from loguru import logger
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def setup_logger(
|
|
7
|
+
log_path: Path | None = None,
|
|
8
|
+
stderr_verbosity="INFO",
|
|
9
|
+
file_verbosity="DEBUG",
|
|
10
|
+
) -> int | None:
|
|
11
|
+
"""Setups the global logger with two sinks: a file and stderr.
|
|
12
|
+
Returns a handle to the file sink.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
logger.remove()
|
|
16
|
+
logger.add(
|
|
17
|
+
sys.stderr,
|
|
18
|
+
level=stderr_verbosity,
|
|
19
|
+
format="<level>{message}</level>",
|
|
20
|
+
backtrace=False,
|
|
21
|
+
diagnose=False,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
if log_path is not None:
|
|
25
|
+
logger.info(f"Logging in {log_path}")
|
|
26
|
+
log_path.parent.mkdir(exist_ok=True, parents=True)
|
|
27
|
+
if log_path.exists():
|
|
28
|
+
logger.warning(f"Overwriting logfile: {log_path}")
|
|
29
|
+
log_path.unlink()
|
|
30
|
+
return logger.add(
|
|
31
|
+
log_path,
|
|
32
|
+
level=file_verbosity,
|
|
33
|
+
format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | "
|
|
34
|
+
"<level>{level: <8}</level> | "
|
|
35
|
+
"<cyan>{name}:{line}: in {function}:</cyan> <level>{message}</level>",
|
|
36
|
+
)
|
pcode_graph/maker.py
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
|
|
3
|
+
from pcode_graph.analysis import (
|
|
4
|
+
EntryIndex,
|
|
5
|
+
OpIndex,
|
|
6
|
+
RichPcodeList,
|
|
7
|
+
UnknownCodeIndex,
|
|
8
|
+
ExitIndex,
|
|
9
|
+
Var,
|
|
10
|
+
VarKinds,
|
|
11
|
+
)
|
|
12
|
+
from pcode_graph.asm import Assembler
|
|
13
|
+
from pcode_graph.checker import check_graph
|
|
14
|
+
from pcode_graph.graph import CDG, Edge, EdgeKinds, NodeIndex, NodeKinds, Node
|
|
15
|
+
from pcode_graph.pcode import from_native_opcode
|
|
16
|
+
from pcode_graph.registers import SPECIAL_REGISTERS
|
|
17
|
+
from pcode_graph.simplifier import GraphSimplifier
|
|
18
|
+
from pcode_graph.translator import Translator
|
|
19
|
+
|
|
20
|
+
from pypcode import OpCode
|
|
21
|
+
from loguru import logger
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class MakerFlags:
|
|
26
|
+
build_cfg: bool = True
|
|
27
|
+
"""Add control flow links in addition to the data flow. Defaults to True."""
|
|
28
|
+
|
|
29
|
+
ignore_reg_outputs: set[str] | None = None
|
|
30
|
+
"""Do not consider the given registers as graph outputs to simplify the graphs.
|
|
31
|
+
Arch-dependent register names.
|
|
32
|
+
If None, the result of pcode.get_special_register_names() is used.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def make_graph(prg: RichPcodeList, flags: MakerFlags | None = None) -> CDG:
|
|
37
|
+
"""Makes a graph from a list of P-Code operations.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
prg (RichPcodeList): the input list of operations, enriched by analyses.
|
|
41
|
+
flags (MakerFlags): determines what to put in the graph.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
CDG: the graph built.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
if flags is None:
|
|
48
|
+
flags = MakerFlags()
|
|
49
|
+
ignore_reg_outputs = flags.ignore_reg_outputs
|
|
50
|
+
if ignore_reg_outputs is None:
|
|
51
|
+
ignore_reg_outputs = SPECIAL_REGISTERS[prg.arch]
|
|
52
|
+
|
|
53
|
+
# Already created input nodes, by related input variable
|
|
54
|
+
input_node_indexes: dict[Var, NodeIndex] = {}
|
|
55
|
+
|
|
56
|
+
# Already created operation nodes, by related P-Code operation
|
|
57
|
+
operation_node_indexes: dict[OpIndex, NodeIndex] = {}
|
|
58
|
+
|
|
59
|
+
# Already created phi nodes, by (sorted) possible alternatives
|
|
60
|
+
# to avoid duplicate phi nodes
|
|
61
|
+
phi_node_indexes: dict[tuple[NodeIndex, NodeIndex], NodeIndex] = {}
|
|
62
|
+
|
|
63
|
+
# Created nodes of all kinds
|
|
64
|
+
nodes: list[Node] = []
|
|
65
|
+
|
|
66
|
+
# Created edges
|
|
67
|
+
edges: list[Edge] = []
|
|
68
|
+
|
|
69
|
+
# To build the data graph recursively
|
|
70
|
+
newly_taken_ops: list[tuple[OpIndex, NodeIndex]] = []
|
|
71
|
+
|
|
72
|
+
# External code node if created
|
|
73
|
+
external_node_index: NodeIndex | None = None
|
|
74
|
+
|
|
75
|
+
# End node if created
|
|
76
|
+
end_node_index: NodeIndex | None = None
|
|
77
|
+
|
|
78
|
+
# begin node has zero index
|
|
79
|
+
if flags.build_cfg:
|
|
80
|
+
nodes.append(Node(NodeKinds.Begin))
|
|
81
|
+
begin_index = 0
|
|
82
|
+
else:
|
|
83
|
+
begin_index = -1
|
|
84
|
+
|
|
85
|
+
def get_or_create_terminal(op_index: OpIndex) -> NodeIndex:
|
|
86
|
+
nonlocal external_node_index, end_node_index
|
|
87
|
+
|
|
88
|
+
if op_index == ExitIndex:
|
|
89
|
+
if end_node_index is None:
|
|
90
|
+
end_node_index = len(nodes)
|
|
91
|
+
nodes.append(Node(NodeKinds.End))
|
|
92
|
+
return end_node_index
|
|
93
|
+
assert op_index == UnknownCodeIndex
|
|
94
|
+
if external_node_index is None:
|
|
95
|
+
external_node_index = len(nodes)
|
|
96
|
+
nodes.append(Node(NodeKinds.External))
|
|
97
|
+
return external_node_index
|
|
98
|
+
|
|
99
|
+
def get_or_create_operation(op_index: OpIndex) -> NodeIndex:
|
|
100
|
+
node_index = operation_node_indexes.get(op_index)
|
|
101
|
+
if node_index is None:
|
|
102
|
+
op = prg.operations[op_index]
|
|
103
|
+
node_index = len(nodes)
|
|
104
|
+
newly_taken_ops.append((op_index, node_index))
|
|
105
|
+
nodes.append(
|
|
106
|
+
Node(
|
|
107
|
+
NodeKinds.Operation,
|
|
108
|
+
op.output.size if op.output is not None else 0,
|
|
109
|
+
opcode=from_native_opcode(op.opcode),
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
operation_node_indexes[op_index] = node_index
|
|
113
|
+
|
|
114
|
+
# logger.debug(f"Create node {node_index}: {nodes[-1]} from operation {op_index}: {dump_operation(op)}")
|
|
115
|
+
return node_index
|
|
116
|
+
|
|
117
|
+
def get_or_create_input(var: Var) -> NodeIndex:
|
|
118
|
+
node_index = input_node_indexes.get(var)
|
|
119
|
+
if node_index is None:
|
|
120
|
+
match var.kind:
|
|
121
|
+
case VarKinds.Constant:
|
|
122
|
+
node = Node(NodeKinds.Constant, var.size, var.value)
|
|
123
|
+
case VarKinds.Register:
|
|
124
|
+
node = Node(
|
|
125
|
+
NodeKinds.InputRegister,
|
|
126
|
+
var.size,
|
|
127
|
+
register_id=var.value,
|
|
128
|
+
register_name=var.pretty_name,
|
|
129
|
+
)
|
|
130
|
+
case VarKinds.Ram:
|
|
131
|
+
node = Node(NodeKinds.ReadMemory, var.size)
|
|
132
|
+
case VarKinds.IndirectAccess:
|
|
133
|
+
node = Node(NodeKinds.ReadMemory, var.size)
|
|
134
|
+
case _:
|
|
135
|
+
raise ValueError(f"Unexpected kind for input {var.kind}")
|
|
136
|
+
node_index = len(nodes)
|
|
137
|
+
nodes.append(node)
|
|
138
|
+
input_node_indexes[var] = node_index
|
|
139
|
+
return node_index
|
|
140
|
+
|
|
141
|
+
def add_control_edge(
|
|
142
|
+
from_node: NodeIndex, to_node: NodeIndex, operand_number: int | None
|
|
143
|
+
):
|
|
144
|
+
edges.append(Edge(from_node, to_node, EdgeKinds.Control, operand_number))
|
|
145
|
+
|
|
146
|
+
def add_data_edge(
|
|
147
|
+
from_node: NodeIndex, to_node: NodeIndex, operand_number: int | None
|
|
148
|
+
):
|
|
149
|
+
edges.append(Edge(from_node, to_node, EdgeKinds.Data, operand_number))
|
|
150
|
+
|
|
151
|
+
def link_defs(
|
|
152
|
+
var: Var,
|
|
153
|
+
def_op_indexes: set[OpIndex],
|
|
154
|
+
user_node_index: NodeIndex,
|
|
155
|
+
operand_number: int | None,
|
|
156
|
+
):
|
|
157
|
+
|
|
158
|
+
# Collect data preds to see if we need a phi node
|
|
159
|
+
# Sort defs to remain determinist
|
|
160
|
+
pred_node_indexes = []
|
|
161
|
+
for def_op_index in sorted(list(def_op_indexes)):
|
|
162
|
+
if def_op_index == EntryIndex:
|
|
163
|
+
# Input variable
|
|
164
|
+
pred_node_indexes.append(get_or_create_input(var))
|
|
165
|
+
else:
|
|
166
|
+
pred_node_indexes.append(get_or_create_operation(def_op_index))
|
|
167
|
+
|
|
168
|
+
if len(pred_node_indexes) == 1:
|
|
169
|
+
# Simple def, direct link
|
|
170
|
+
add_data_edge(pred_node_indexes[0], user_node_index, operand_number)
|
|
171
|
+
else:
|
|
172
|
+
# Create phi node
|
|
173
|
+
assert pred_node_indexes
|
|
174
|
+
pred_node_indexes.sort()
|
|
175
|
+
key = tuple(pred_node_indexes)
|
|
176
|
+
phi_node_index = phi_node_indexes.get(key)
|
|
177
|
+
if phi_node_index is None:
|
|
178
|
+
phi_node = Node(NodeKinds.Phi, var.size)
|
|
179
|
+
phi_node_index = len(nodes)
|
|
180
|
+
nodes.append(phi_node)
|
|
181
|
+
phi_node_indexes[key] = phi_node_index
|
|
182
|
+
for pred_node_index in pred_node_indexes:
|
|
183
|
+
add_data_edge(pred_node_index, phi_node_index, None)
|
|
184
|
+
add_data_edge(phi_node_index, user_node_index, operand_number)
|
|
185
|
+
|
|
186
|
+
# Build the data flow
|
|
187
|
+
|
|
188
|
+
# Start from writes to outputs registers and memory
|
|
189
|
+
for var, def_op_indexes in prg.get_exit_defs().items():
|
|
190
|
+
assert def_op_indexes
|
|
191
|
+
match var.kind:
|
|
192
|
+
case VarKinds.Register:
|
|
193
|
+
if var.pretty_name.lower().startswith("tmp"):
|
|
194
|
+
# Ignore writes to temporaries
|
|
195
|
+
continue
|
|
196
|
+
if var.pretty_name in ignore_reg_outputs:
|
|
197
|
+
# Ignore writes to special registers
|
|
198
|
+
continue
|
|
199
|
+
output_node = Node(
|
|
200
|
+
NodeKinds.OutputRegister,
|
|
201
|
+
var.size,
|
|
202
|
+
register_id=var.value,
|
|
203
|
+
register_name=var.pretty_name,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
case VarKinds.IndirectAccess | VarKinds.Ram:
|
|
207
|
+
output_node = Node(NodeKinds.WrittenMemory, var.size)
|
|
208
|
+
|
|
209
|
+
case VarKinds.Unique:
|
|
210
|
+
# Ignore writes to temporaries
|
|
211
|
+
continue
|
|
212
|
+
|
|
213
|
+
case VarKinds.Constant:
|
|
214
|
+
raise ValueError(f"Unexpected output kind {var.kind}")
|
|
215
|
+
|
|
216
|
+
output_node_index = len(nodes)
|
|
217
|
+
nodes.append(output_node)
|
|
218
|
+
# logger.debug(f"Start from output {output_node_index} {output_node}")
|
|
219
|
+
link_defs(var, def_op_indexes, output_node_index, None)
|
|
220
|
+
|
|
221
|
+
if flags.build_cfg:
|
|
222
|
+
# Also add conditional branching or branching to unknown address
|
|
223
|
+
for op_index, op in enumerate(prg.operations):
|
|
224
|
+
if not prg.reachable[op_index]:
|
|
225
|
+
continue
|
|
226
|
+
if op.opcode == OpCode.CBRANCH or (
|
|
227
|
+
UnknownCodeIndex in prg.successors.get(op_index, set())
|
|
228
|
+
and op.opcode != OpCode.IMARK
|
|
229
|
+
):
|
|
230
|
+
get_or_create_operation(op_index)
|
|
231
|
+
|
|
232
|
+
# Follow predecessors recursively until the whole dataflow graph is built
|
|
233
|
+
while newly_taken_ops:
|
|
234
|
+
op_index, node_index = newly_taken_ops.pop()
|
|
235
|
+
|
|
236
|
+
for number, (var, defs) in enumerate(prg.iter_input_defs(op_index)):
|
|
237
|
+
|
|
238
|
+
if not defs:
|
|
239
|
+
# No definition means this is a constant
|
|
240
|
+
pred_node_index = get_or_create_input(var)
|
|
241
|
+
add_data_edge(pred_node_index, node_index, number)
|
|
242
|
+
continue
|
|
243
|
+
|
|
244
|
+
link_defs(var, defs, node_index, number)
|
|
245
|
+
|
|
246
|
+
if flags.build_cfg:
|
|
247
|
+
# Build the control flow graph, jumping over nodes which
|
|
248
|
+
# do not already belong to the data flow graph.
|
|
249
|
+
|
|
250
|
+
for op_index, node_index in operation_node_indexes.items():
|
|
251
|
+
|
|
252
|
+
if not prg.reachable[op_index]:
|
|
253
|
+
continue
|
|
254
|
+
|
|
255
|
+
successors = prg.successors.get(op_index, [])
|
|
256
|
+
|
|
257
|
+
for number, successor_op_index in enumerate(successors):
|
|
258
|
+
if successor_op_index < 0: # ie. in {UnknownCodeIndex, ExitIndex}
|
|
259
|
+
add_control_edge(
|
|
260
|
+
node_index, get_or_create_terminal(successor_op_index), number
|
|
261
|
+
)
|
|
262
|
+
continue
|
|
263
|
+
|
|
264
|
+
successor_node_index = operation_node_indexes.get(successor_op_index)
|
|
265
|
+
seen_ops: set[OpIndex] = set()
|
|
266
|
+
|
|
267
|
+
while successor_node_index is None:
|
|
268
|
+
|
|
269
|
+
# This successor is not part of the dataflow graph, skip it.
|
|
270
|
+
|
|
271
|
+
# Having more than one successor would imply being a CBRANCH,
|
|
272
|
+
# but CBRANCH are always put in the dataflow, see above.
|
|
273
|
+
(successor_op_index,) = prg.successors[successor_op_index]
|
|
274
|
+
|
|
275
|
+
if successor_op_index in seen_ops:
|
|
276
|
+
logger.warning("Looping CFG without side effects")
|
|
277
|
+
add_control_edge(node_index, node_index, number)
|
|
278
|
+
break
|
|
279
|
+
|
|
280
|
+
seen_ops.add(successor_op_index)
|
|
281
|
+
|
|
282
|
+
if successor_op_index < 0: # ie. in {UnknownCodeIndex, ExitIndex}
|
|
283
|
+
successor_node_index = get_or_create_terminal(
|
|
284
|
+
successor_op_index
|
|
285
|
+
)
|
|
286
|
+
continue
|
|
287
|
+
|
|
288
|
+
successor_node_index = operation_node_indexes.get(
|
|
289
|
+
successor_op_index
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
else:
|
|
293
|
+
assert successor_node_index >= 0
|
|
294
|
+
add_control_edge(node_index, successor_node_index, number)
|
|
295
|
+
|
|
296
|
+
if len(nodes) == 1:
|
|
297
|
+
# Empty graph
|
|
298
|
+
add_control_edge(begin_index, get_or_create_terminal(ExitIndex), None)
|
|
299
|
+
else:
|
|
300
|
+
# Link the entry node to the first taken operation
|
|
301
|
+
# TODO: specify entry operation
|
|
302
|
+
entry_op_index = 0
|
|
303
|
+
while entry_op_index not in operation_node_indexes:
|
|
304
|
+
successor_op_indexes = tuple(prg.successors.get(entry_op_index, ()))
|
|
305
|
+
assert len(successor_op_indexes) == 1
|
|
306
|
+
# Advance to next operation
|
|
307
|
+
entry_op_index = successor_op_indexes[0]
|
|
308
|
+
add_control_edge(begin_index, operation_node_indexes[entry_op_index], None)
|
|
309
|
+
|
|
310
|
+
graph = CDG(nodes, edges)
|
|
311
|
+
|
|
312
|
+
# Uncomment to locate a graph structure bug
|
|
313
|
+
# Comment to speed up
|
|
314
|
+
# check_graph(graph)
|
|
315
|
+
|
|
316
|
+
GraphSimplifier(graph).process()
|
|
317
|
+
check_graph(graph)
|
|
318
|
+
|
|
319
|
+
return graph
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def make_graph_from_binary(
|
|
323
|
+
translator: Translator,
|
|
324
|
+
code: bytes,
|
|
325
|
+
base_address: int,
|
|
326
|
+
flags: MakerFlags | None = None,
|
|
327
|
+
) -> CDG:
|
|
328
|
+
"""Makes a CDG graph from a piece of binary code.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
translator (Translator): The P-Code translator context.
|
|
332
|
+
code (bytes): The binary code to translate.
|
|
333
|
+
base_address (int): The address of the first instruction of the given code.
|
|
334
|
+
flags (MakerFlags): determines what to put in the graph.
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
CDG: the graph built.
|
|
338
|
+
"""
|
|
339
|
+
|
|
340
|
+
operations = translator.translate(code, base_address)
|
|
341
|
+
prg = RichPcodeList(translator.arch, operations=operations)
|
|
342
|
+
return make_graph(prg, flags)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def make_graph_from_asm(
|
|
346
|
+
assembler: Assembler,
|
|
347
|
+
translator: Translator,
|
|
348
|
+
asm: str,
|
|
349
|
+
base_address: int = 0,
|
|
350
|
+
flags: MakerFlags | None = None,
|
|
351
|
+
) -> CDG:
|
|
352
|
+
"""Makes a CDG graph from a piece of binary code. Mainly for testing purpose.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
translator (Translator): The P-Code translator context.
|
|
356
|
+
asm (str): The assembly code to translate.
|
|
357
|
+
base_address (int): The address of the first instruction of the given code.
|
|
358
|
+
flags (MakerFlags): determines what to put in the graph.
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
CDG: the graph built.
|
|
362
|
+
"""
|
|
363
|
+
|
|
364
|
+
return make_graph_from_binary(
|
|
365
|
+
code=assembler.assemble(asm, base_address),
|
|
366
|
+
base_address=base_address,
|
|
367
|
+
translator=translator,
|
|
368
|
+
flags=flags,
|
|
369
|
+
)
|