r2bagpipe 0.0.1__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.
r2bagpipe/__init__.py ADDED
@@ -0,0 +1,14 @@
1
+ """Expose data loader and radare2 types."""
2
+
3
+ # Export loader
4
+ from .loader import BinaryDataLoader as BinaryDataLoader
5
+ from .loader import get_data_loader as get_data_loader
6
+
7
+ # Export types
8
+ from .r2types import Bin2MlCfg as Bin2MlCfg
9
+ from .r2types import HasAddr as HasAddr
10
+ from .r2types import R2BinInfo as R2BinInfo
11
+ from .r2types import R2CallGraph as R2CallGraph
12
+ from .r2types import R2CodeInfo as R2CodeInfo
13
+ from .r2types import R2FunctionInfo as R2FunctionInfo
14
+ from .r2types import R2Zignature as R2Zignature
r2bagpipe/_bin2ml.py ADDED
@@ -0,0 +1,261 @@
1
+ """Python module to interact with bin2ml utility
2
+ https://github.com/br0kej/bin2ml
3
+ """
4
+ import subprocess
5
+ import re
6
+ import logging
7
+
8
+ from pathlib import Path
9
+ from typing import Iterable, Literal, Any, Optional
10
+
11
+
12
+ # Compatible function file names templates
13
+ FuncFileTemplate = Literal["symbol", "address"] | str
14
+
15
+ # Compatible data extraction modes
16
+ ExtractMode = Literal[
17
+ "finfo", "reg", "cfg", "func-cfg", "func-xrefs", "cg", "decomp",
18
+ "pcode-func", "pcode-bb", "localvar-xrefs", "strings", "bytes",
19
+ "bytes-masked", "fvars", "zigs", "bininfo"]
20
+ ExtractModeInput = ExtractMode | Iterable[ExtractMode]
21
+
22
+ # Compatible graphs generation modes
23
+ GraphData = Literal["cfg", "cg", "onehopcg", "cgcallers", "onehopcgcallers",
24
+ "globalcg"]
25
+ GraphFeature = Literal["gemini", "discovre", "dgis", "tiknib", "disasm", "esil",
26
+ "pcode", "pseudo"]
27
+
28
+ # Compatible NLP generation modes
29
+ NLPData = Literal["esil", "disasm", "pcode"]
30
+ NLPFormat = Literal["single", "funcstring"]
31
+ PCodeFormat = Literal["pcode-func", "pcode-bb"]
32
+
33
+ # Catch-all features
34
+ FeatureType = GraphFeature | NLPFormat
35
+
36
+
37
+ class Bin2MLError(Exception):
38
+ pass
39
+
40
+
41
+ class BinaryDataTracker:
42
+
43
+ LOGGER = logging.getLogger("BinaryDataTracker")
44
+ EXTRACT_FORMAT = "{binary_name}_{data_type}{maybe_json_ext}"
45
+ GENERATE_FORMAT = "{binary_name}_{data_type}-{feature_type}"
46
+ FUNCTION_FORMAT = "{binary_name}_{data_type}-{function_name}{file_ext}"
47
+ ERROR_FORMAT = "{binary_name}_{data_type}.error.log"
48
+
49
+ def __init__(self, binary_path: Path | str, output_dir: Path | str):
50
+ self.binary_path = Path(binary_path)
51
+ self.output_dir = Path(output_dir)
52
+
53
+ @property
54
+ def binary_name(self) -> str:
55
+ return self.binary_path.name
56
+
57
+ @staticmethod
58
+ def sanitize_filename(filename: str, max_length: int=100) -> str:
59
+ # Replace non-valid characters with '_'
60
+ # Valid characters: letters, digits, '_', '-', and '.'
61
+ sanitized_name = re.sub(r"[^\w.-]", "_", filename)
62
+
63
+ half_length = max_length // 2
64
+ if len(sanitized_name) > max_length:
65
+ sanitized_name = (sanitized_name[:half_length] +
66
+ sanitized_name[-half_length:])
67
+
68
+ return sanitized_name
69
+
70
+ @staticmethod
71
+ def get_file_extensions(data_type: ExtractMode
72
+ ) -> tuple[str, Optional[str]]:
73
+ # If it's a folder with data files then we have None, file_ext
74
+ extensions = {
75
+ "bytes": ("", ".bin"),
76
+ "bytes-masked": ("", ".masked.bin"),
77
+ "func-cfg": ("", ".json"),
78
+ # If it's a single file on extraction, but folder on generation
79
+ # we specify two extensions
80
+ "cfg": (".json", ".json"),
81
+ }
82
+ # If it's anything else then we have file_ext, None
83
+ default = (".json", None)
84
+ return extensions.get(data_type, default)
85
+
86
+ @staticmethod
87
+ def apply_function_file_template(template: FuncFileTemplate="symbol",
88
+ function_name: Optional[str]=None,
89
+ function_address: Optional[str]=None,
90
+ func_file_ext: Optional[str]=None
91
+ ) -> str:
92
+ file_name = None
93
+ if template == "symbol":
94
+ if function_name is None:
95
+ raise ValueError(
96
+ "Function name is required when template is 'symbol'")
97
+ file_name = (f"{function_name}{func_file_ext}"
98
+ if func_file_ext is not None else function_name)
99
+
100
+ elif template == "address":
101
+ if function_address is None:
102
+ raise ValueError(
103
+ "Function address is required when template is 'address'")
104
+ file_name = (f"{function_address}{func_file_ext}"
105
+ if func_file_ext is not None else function_address)
106
+
107
+ else:
108
+ # Template will look like "{address}-{symbol}.{ext}"
109
+ # Check any of these are in the template and make sure the
110
+ # corresponding arguments are not None
111
+ if not ("{address}" in template or "{symbol}" in template):
112
+ raise ValueError("Template must contain '{address}' "
113
+ "or '{symbol}'")
114
+ elif "{address}" in template and function_address is None:
115
+ raise ValueError("Function address is required when template "
116
+ "contains '{address}'")
117
+ elif "{symbol}" in template and function_name is None:
118
+ raise ValueError("Function name is required when template "
119
+ "contains '{symbol}'")
120
+
121
+ file_name = template.format(address=function_address,
122
+ symbol=function_name,
123
+ ext=func_file_ext)
124
+
125
+ return file_name
126
+
127
+ def get_function_file_name(self, data_type: ExtractMode,
128
+ template: FuncFileTemplate="symbol",
129
+ function_name: Optional[str] = None,
130
+ function_address: Optional[str] = None) -> str:
131
+ _, func_file_ext = self.get_file_extensions(data_type)
132
+ if func_file_ext is None:
133
+ self.LOGGER.debug("Using default function file extension for "
134
+ f"{data_type} data type")
135
+ func_file_ext = ".json"
136
+
137
+ file_name = self.apply_function_file_template(template, function_name,
138
+ function_address,
139
+ func_file_ext)
140
+ return self.sanitize_filename(file_name)
141
+
142
+ def get_extract_path(self, data_type: ExtractMode,
143
+ template: FuncFileTemplate="symbol",
144
+ function_name: Optional[str] = None,
145
+ function_address: Optional[str] = None) -> Path:
146
+ bin_ext, _ = self.get_file_extensions(data_type)
147
+ if bin_ext is None:
148
+ # Replace empty bin_ext with empty string
149
+ bin_ext = ""
150
+
151
+ extract_name = self.EXTRACT_FORMAT.format(
152
+ binary_name=self.binary_name, data_type=data_type,
153
+ maybe_json_ext=bin_ext)
154
+ extract_path = self.output_dir.joinpath(extract_name)
155
+
156
+ if function_name is None and function_address is None:
157
+ return extract_path
158
+ else:
159
+ file_name = self.get_function_file_name(data_type, template,
160
+ function_name,
161
+ function_address)
162
+ return extract_path.joinpath(file_name)
163
+
164
+ def get_extract_paths(self, modes: ExtractModeInput
165
+ ) -> dict[ExtractMode, Path]:
166
+ return {mode: self.get_extract_path(mode) for mode in
167
+ Bin2MLPipe.get_modes_list(modes)}
168
+
169
+ def get_error_path(self, data_type: ExtractMode,
170
+ function_name: Optional[str] = None) -> Path:
171
+ error_name = self.ERROR_FORMAT.format(
172
+ binary_name=self.binary_name, data_type=data_type)
173
+
174
+ return self.output_dir.joinpath(error_name)
175
+
176
+ def get_error_paths(self, modes: ExtractModeInput
177
+ ) -> dict[ExtractMode, Path]:
178
+ return {mode: self.get_error_path(mode) for mode in
179
+ Bin2MLPipe.get_modes_list(modes)}
180
+
181
+ def get_generate_path(self, data_type: ExtractMode,
182
+ feature_type: FeatureType,
183
+ function_name: Optional[str] = None) -> Path:
184
+ generate_dir_name = self.GENERATE_FORMAT.format(
185
+ binary_name=self.binary_name, data_type=data_type,
186
+ feature_type=feature_type)
187
+ generate_dir_path = self.output_dir.joinpath(generate_dir_name)
188
+ if function_name is None:
189
+ return generate_dir_path
190
+ else:
191
+ # This will need more flexible handling in the future
192
+ func_file_ext = ".json"
193
+ file_name = self.FUNCTION_FORMAT.format(
194
+ binary_name=self.binary_name, data_type=data_type,
195
+ function_name=function_name, file_ext=func_file_ext)
196
+ return generate_dir_path.joinpath(file_name)
197
+
198
+
199
+ class Bin2MLPipe:
200
+
201
+ def __init__(self, exe_path: str | Path, num_threads=2, capture_output: bool = True):
202
+ self.validate_bin2ml_path(Path(exe_path))
203
+ self.exe_path = str(exe_path)
204
+ self._num_threads = num_threads
205
+ self._capture_output = capture_output
206
+
207
+ @staticmethod
208
+ def get_modes_list(modes: ExtractModeInput) -> list[ExtractMode]:
209
+ if isinstance(modes, str):
210
+ return [modes]
211
+ else:
212
+ return list(modes)
213
+
214
+ def extract(self, input_path: Path | str, output_dir: Path | str,
215
+ modes: ExtractModeInput, *options: Any
216
+ ) -> subprocess.CompletedProcess:
217
+ modes = self.get_modes_list(modes)
218
+ return self.cmd("extract", *options, "-f", input_path,
219
+ "-o", output_dir, "-m", *modes, "-n", self._num_threads)
220
+
221
+ def generate_graphs(self, data_path: Path | str, data_type: GraphData,
222
+ output_dir: Path | str, feature_type: GraphFeature,
223
+ *options: Any) -> subprocess.CompletedProcess:
224
+ return self.cmd(
225
+ "generate", "graphs", *options, "-p", data_path,
226
+ "-d", data_type, "-o", output_dir, "-f", feature_type,
227
+ "-n", self._num_threads)
228
+
229
+ def generate_nlp(self, data_path: Path | str, data_type: NLPData,
230
+ output_dir: Path | str, feature_type: NLPFormat,
231
+ *options: Any, pcode_format: Optional[PCodeFormat]=None
232
+ ) -> subprocess.CompletedProcess:
233
+ _options = list(options)
234
+ if pcode_format is not None:
235
+ _options.extend(["--pcode-file-format", pcode_format])
236
+ return self.cmd("generate", "nlp", *_options, "-p", data_path,
237
+ "-i", data_type, "-d", output_dir, "-o", feature_type)
238
+
239
+ @staticmethod
240
+ def validate_bin2ml_path(bin2ml_path: Path):
241
+ if not bin2ml_path.exists():
242
+ raise FileNotFoundError(
243
+ f"The provided Bin2ML path {bin2ml_path} does not exist.")
244
+ elif not bin2ml_path.is_file():
245
+ raise FileNotFoundError(
246
+ f"The provided Bin2ML path {bin2ml_path} is not a file.")
247
+
248
+ def cmd(self, *args, capture_output: bool | None = None) -> subprocess.CompletedProcess:
249
+ if capture_output is None:
250
+ capture_output = self._capture_output
251
+ command = list(map(str, (self.exe_path, *args)))
252
+ result = subprocess.run(command, capture_output=capture_output)
253
+ if result.returncode != 0:
254
+ command_str = " ".join(command)
255
+ stderr = result.stderr.decode() if result.stderr else "(output not captured)"
256
+ raise Bin2MLError("Encountered Bin2ML error when executing:\n"
257
+ f"{command_str!r}:\n{stderr}")
258
+ return result
259
+
260
+ def get_version(self):
261
+ return self.cmd("--version", capture_output=True).stdout.decode().split(" ")[-1]
r2bagpipe/_parsers.py ADDED
@@ -0,0 +1,100 @@
1
+ """Binary data loading.
2
+
3
+ Discovers, validates, and streams per-function analysis data from disk.
4
+ """
5
+
6
+ import csv
7
+ import json
8
+ import logging
9
+ import re
10
+ import sys
11
+ import tarfile
12
+ from abc import ABC, abstractmethod
13
+ from collections.abc import Iterator
14
+ from dataclasses import dataclass, field
15
+ from enum import Enum, auto
16
+ from pathlib import Path, PurePosixPath
17
+ from typing import Any, Self
18
+
19
+ from hexio import HexStr
20
+
21
+ # from .bin2ml import ExtractMode
22
+
23
+
24
+ # Increase CSV field size limit to avoid checkpoint read failures
25
+ # on large fields
26
+ csv.field_size_limit(sys.maxsize)
27
+
28
+
29
+ # region ADDRESS HANDLING
30
+ ADDR_SENTINEL = 0xFFFFFFFFFFFFFFFF
31
+
32
+
33
+ def is_address_placeholder(address: int | None) -> bool:
34
+ """Return True if address is None or the sentinel value."""
35
+ return address is None or address == ADDR_SENTINEL
36
+
37
+
38
+ def get_address(data: _HasOffsetOrAddr) -> int:
39
+ """Return address from radare2 dictionary.
40
+
41
+ Radare2 < v6.0.0 specifies this value in the "offset" key,
42
+ while >= v6.0.0 has "address".
43
+ This function maintains compatibility with both schemas.
44
+ """
45
+ address = None
46
+ if "offset" in data:
47
+ address = data["offset"] # type: ignore
48
+ elif "addr" in data:
49
+ address = data["addr"] # type: ignore
50
+ else:
51
+ raise ValueError(f"Data does not have an offset or address: {data}")
52
+
53
+ if is_address_placeholder(address):
54
+ # This shoould never happen, we log it to be safe
55
+ # but we don't want to raise an exception here
56
+ logger.error(f"Address is placeholder: {address}")
57
+ logger.debug(f"Data: {data}")
58
+ return address
59
+
60
+
61
+ def set_address(data: _HasOffsetOrAddr, address: int) -> None:
62
+ if "offset" in data:
63
+ data["offset"] = address
64
+ elif "addr" in data:
65
+ data["addr"] = address
66
+ else:
67
+ raise ValueError(f"Data does not have an offset or address: {data}")
68
+
69
+
70
+ def is_address_set(data: _HasOffsetOrAddr) -> bool:
71
+ try:
72
+ addr = get_address(data)
73
+ return not is_address_placeholder(addr)
74
+ except ValueError:
75
+ # No address/offset key found, so address is definitely not set
76
+ return False
77
+
78
+
79
+ # endregion ADDRESS HANDLING
80
+
81
+
82
+ REQUIRED_DATA_TYPES = {
83
+ "ccbhash": (
84
+ DataType.FUNCTION_INFO,
85
+ DataType.ZIGNATURES,
86
+ DataType.CFG,
87
+ DataType.CALL_GRAPH,
88
+ ),
89
+ "ccbhash_cfe": (
90
+ DataType.FUNCTION_INFO,
91
+ DataType.ZIGNATURES,
92
+ DataType.CFG,
93
+ DataType.CALL_GRAPH,
94
+ ),
95
+ "cfehash": (DataType.CFG,),
96
+ "fss": (DataType.CFG,),
97
+ "machoke": (DataType.CFG,),
98
+ "catalog1": (DataType.BYTES,),
99
+ "nibble_masked_catalog1": (DataType.ZIGNATURES,),
100
+ }