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 +14 -0
- r2bagpipe/_bin2ml.py +261 -0
- r2bagpipe/_parsers.py +100 -0
- r2bagpipe/_r2types.py +349 -0
- r2bagpipe/loader.py +567 -0
- r2bagpipe/parsers.py +457 -0
- r2bagpipe/r2types.py +267 -0
- r2bagpipe/trackers.py +310 -0
- r2bagpipe-0.0.1.dist-info/METADATA +15 -0
- r2bagpipe-0.0.1.dist-info/RECORD +13 -0
- r2bagpipe-0.0.1.dist-info/WHEEL +5 -0
- r2bagpipe-0.0.1.dist-info/licenses/LICENCE +674 -0
- r2bagpipe-0.0.1.dist-info/top_level.txt +1 -0
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
|
+
}
|