Skip to content

Commit f10129d

Browse files
committed
final draft of auto-unstrip
1 parent f16c67c commit f10129d

14 files changed

Lines changed: 357 additions & 57 deletions

features/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from .configuration import ConfigurationFeature
22
from .upload import UploadFeature
3+
from .autounstrip import AutoUnstripFeature
34

4-
__all__ = ['ConfigurationFeature', 'UploadFeature']
5+
__all__ = ['ConfigurationFeature', 'UploadFeature', 'AutoUnstripFeature']

features/autounstrip/__init__.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from binaryninja import PluginCommand, log_info, BinaryView
2+
from .autounstrip import AutoUnstrip
3+
from .autounstrip_dialog import AutoUnstripDialog
4+
from revengai_bn.utils import BaseAuthFeature
5+
6+
class AutoUnstripFeature(BaseAuthFeature):
7+
def __init__(self, config=None):
8+
super().__init__(config)
9+
self.autounstrip = AutoUnstrip(config)
10+
log_info("RevEng.AI | AutoUnstrip Feature initialized")
11+
12+
def register(self):
13+
PluginCommand.register(
14+
"RevEng.AI\\AutoUnstrip",
15+
"Attempt to recover stripped function names",
16+
self.show_autounstrip_dialog,
17+
self.is_valid
18+
)
19+
log_info("RevEng.AI | AutoUnstrip Feature registered")
20+
21+
def show_autounstrip_dialog(self, bv: BinaryView):
22+
log_info("RevEng.AI | Opening AutoUnstrip dialog")
23+
dialog = AutoUnstripDialog(self.config, self.autounstrip, bv)
24+
dialog.exec_()
25+
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
from binaryninja import BinaryView, log_info, log_error, Symbol, SymbolType
2+
from reait.api import RE_authentication, RE_search, RE_nearest_symbols_batch, RE_analyze_functions
3+
from concurrent.futures import ThreadPoolExecutor, as_completed
4+
from typing import List, Dict, Tuple
5+
import math
6+
7+
class AutoUnstrip:
8+
def __init__(self, config):
9+
self.config = config
10+
self.auto_unstrip_distance = 0.09999999999999998
11+
self.base_addr = None
12+
self.path = None
13+
self._analysed_functions = {}
14+
self._functions = []
15+
self.function_ids = []
16+
self.max_workers = 4 # Number of parallel threads
17+
18+
def _get_all_functions(self):
19+
return list(self.bv.functions)
20+
21+
def _get_analysed_functions(self):
22+
return self._analysed_functions
23+
24+
def _get_sync_analysed_ids_local(self):
25+
return self.function_ids
26+
27+
def _rename_function(self, bv: BinaryView, addr: int, new_name: str, new_name_mangled: str) -> bool:
28+
try:
29+
func = bv.get_function_at(addr)
30+
if not func:
31+
log_error(f"RevEng.AI | No function found at address {hex(addr)}")
32+
return False
33+
34+
if func.name == new_name or func.name == new_name_mangled:
35+
log_info(f"RevEng.AI | Function at {hex(addr)} already has name {func.name}")
36+
return False
37+
38+
new_symbol = Symbol(SymbolType.FunctionSymbol, addr, new_name_mangled)
39+
bv.define_user_symbol(new_symbol)
40+
41+
log_info(f"RevEng.AI | Renamed function at {hex(addr)} to {new_name}")
42+
return True
43+
44+
except Exception as e:
45+
log_error(f"RevEng.AI | Error renaming function at {hex(addr)}: {str(e)}")
46+
return False
47+
48+
def _process_batch(self, function_ids: List[int], id_to_addr: Dict[int, int], bv: BinaryView) -> Tuple[int, List[str]]:
49+
"""Process a batch of function IDs and return the number of renamed functions"""
50+
try:
51+
ret = RE_nearest_symbols_batch(
52+
function_ids=function_ids,
53+
distance=self.auto_unstrip_distance,
54+
debug_enabled=True,
55+
nns=1
56+
).json()["function_matches"]
57+
58+
renamed_count = 0
59+
errors = []
60+
for result in ret:
61+
try:
62+
func_id = result['origin_function_id']
63+
func_addr = id_to_addr.get(func_id)
64+
if not func_addr:
65+
continue
66+
67+
new_name = result['nearest_neighbor_function_name']
68+
if not new_name or new_name.startswith(("sub_", "FUN_")):
69+
continue
70+
71+
new_name_mangled = result['nearest_neighbor_function_name_mangled']
72+
if not new_name_mangled or new_name_mangled.startswith(("sub_", "FUN_")):
73+
continue
74+
75+
if self._rename_function(bv, func_addr, new_name, new_name_mangled):
76+
renamed_count += 1
77+
except Exception as e:
78+
errors.append(str(e))
79+
80+
return renamed_count, errors
81+
82+
except Exception as e:
83+
return 0, [str(e)]
84+
85+
def auto_unstrip(self, bv: BinaryView):
86+
try:
87+
log_info("RevEng.AI | Auto Unstripping binary")
88+
89+
self.base_addr = bv.image_base
90+
self.path = bv.file.filename
91+
log_info(f"RevEng.AI | Path: {self.path}")
92+
log_info(f"RevEng.AI | Binary ID: {self.config.binary_id}")
93+
self._analysed_functions = {}
94+
95+
results = RE_search(fpath=self.path).json()["query_results"]
96+
log_info(f"RevEng.AI | Search Results: {results}")
97+
98+
if not len(results):
99+
raise Exception("Binary not found in RevEng.AI, try uploading again.")
100+
101+
analyzed_functions = RE_analyze_functions(self.path, self.config.binary_id).json()["functions"]
102+
function_ids = [func["function_id"] for func in analyzed_functions]
103+
104+
id_to_addr = {
105+
func["function_id"]: func["function_vaddr"] + self.base_addr
106+
for func in analyzed_functions
107+
}
108+
109+
# Use fixed chunk size of 50
110+
chunk_size = 50
111+
chunks = [function_ids[i:i + chunk_size] for i in range(0, len(function_ids), chunk_size)]
112+
113+
log_info(f"RevEng.AI | Processing {len(function_ids)} functions in {len(chunks)} chunks of size {chunk_size}")
114+
115+
# Process chunks in parallel
116+
total_renamed = 0
117+
all_errors = []
118+
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
119+
# Submit all tasks
120+
future_to_chunk = {
121+
executor.submit(self._process_batch, chunk, id_to_addr, bv): i
122+
for i, chunk in enumerate(chunks)
123+
}
124+
125+
# Wait for all tasks to complete and collect results
126+
for future in as_completed(future_to_chunk):
127+
chunk_index = future_to_chunk[future]
128+
try:
129+
renamed_count, errors = future.result()
130+
total_renamed += renamed_count
131+
all_errors.extend(errors)
132+
log_info(f"RevEng.AI | Chunk {chunk_index} completed: renamed {renamed_count} functions")
133+
except Exception as e:
134+
log_error(f"RevEng.AI | Error processing chunk {chunk_index}: {str(e)}")
135+
136+
if total_renamed > 0:
137+
message = f"Successfully renamed {total_renamed} functions!"
138+
else:
139+
message = "After analyzing the binary, no functions were found to be renamed."
140+
141+
if all_errors:
142+
message += f"\nEncountered {len(all_errors)} errors during processing."
143+
144+
log_info(f"RevEng.AI | {message}")
145+
return True, message
146+
147+
except Exception as e:
148+
log_error(f"RevEng.AI | Error: {str(e)}")
149+
return False, str(e)
150+
"""
151+
self._functions = self._get_all_functions()
152+
self._get_analysed_functions()
153+
self.function_ids = self._get_sync_analysed_ids_local()
154+
155+
log_info(f"RevEng.AI | {bv.file.original_filename}")
156+
"""
157+
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
from binaryninja import BinaryView, PluginCommand, log_info, log_error
2+
from PySide6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout,
3+
QPushButton, QLabel, QCheckBox,
4+
QGroupBox, QRadioButton, QSpacerItem,
5+
QSizePolicy)
6+
from PySide6.QtCore import Qt
7+
from PySide6.QtGui import QPixmap, QIcon
8+
from PySide6.QtCore import QCoreApplication
9+
from PySide6.QtWidgets import QMessageBox
10+
from revengai_bn.utils import create_progress_dialog
11+
from .autounstrip_thread import AutoUnstripThread
12+
import os
13+
14+
class AutoUnstripDialog(QDialog):
15+
def __init__(self, config, autounstrip, bv):
16+
super().__init__()
17+
self.config = config
18+
self.autounstrip = autounstrip
19+
self.bv = bv
20+
self.init_ui()
21+
22+
def init_ui(self):
23+
self.setWindowTitle("Auto Unstrip Binary")
24+
self.setMinimumWidth(500)
25+
26+
layout = QVBoxLayout()
27+
28+
info_layout = QVBoxLayout()
29+
title_label = QLabel("Auto Unstrip Binary")
30+
title_label.setStyleSheet("font-size: 18px; font-weight: bold;")
31+
description_label = QLabel(
32+
"Using official RevEng.AI sources, function names will be recovered based on a low similarity threshold and limited to available debug symbols.\nFunctions will be renamed automatically for easier analysis.\n\nThis process may take several minutes depending on the binary size."
33+
)
34+
description_label.setWordWrap(True)
35+
info_layout.addWidget(title_label)
36+
info_layout.addWidget(description_label)
37+
38+
layout.addLayout(info_layout)
39+
layout.addSpacing(20)
40+
41+
# Buttons
42+
button_layout = QHBoxLayout()
43+
self.save_button = QPushButton("Auto Unstrip")
44+
self.save_button.setStyleSheet("""
45+
QPushButton {
46+
background-color: #007bff;
47+
color: white;
48+
padding: 8px 16px;
49+
border-radius: 4px;
50+
}
51+
QPushButton:hover {
52+
background-color: #4400ff;
53+
}
54+
""")
55+
self.save_button.clicked.connect(self.auto_unstrip)
56+
self.cancel_button = QPushButton("Cancel")
57+
self.cancel_button.setStyleSheet("""
58+
QPushButton {
59+
padding: 8px 16px;
60+
border-radius: 4px;
61+
}
62+
""")
63+
self.cancel_button.clicked.connect(self.reject)
64+
65+
button_layout.addWidget(self.save_button)
66+
button_layout.addWidget(self.cancel_button)
67+
layout.addLayout(button_layout)
68+
69+
self.setLayout(layout)
70+
71+
def auto_unstrip(self):
72+
log_info("RevEng.AI | Auto Unstripping binary")
73+
# Create and show progress dialog using utility function
74+
self.progress = create_progress_dialog(self, "RevEng.AI Auto Unstrip", "Auto Unstripping binary...")
75+
76+
# Create and start upload thread
77+
self.auto_unstrip_thread = AutoUnstripThread(self.autounstrip, self.bv)
78+
self.auto_unstrip_thread.finished.connect(self._on_auto_unstrip_finished)
79+
self.auto_unstrip_thread.start()
80+
81+
self.progress.show()
82+
QCoreApplication.processEvents()
83+
84+
85+
def _on_auto_unstrip_finished(self, success, message):
86+
"""Handle auto unstrip completion"""
87+
self.progress.close()
88+
89+
if success:
90+
QMessageBox.information(
91+
self,
92+
"RevEng.AI Auto Unstrip",
93+
f"Binary auto unstripped successfully!\n{message}",
94+
QMessageBox.Ok
95+
)
96+
self.accept()
97+
else:
98+
log_error(f"RevEng.AI | Failed to auto unstrip binary: {message}")
99+
QMessageBox.critical(
100+
self,
101+
"RevEng.AI Auto Unstrip Error",
102+
f"Failed to auto unstrip binary: {message}",
103+
QMessageBox.Ok
104+
)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from PySide6.QtCore import QThread, Signal
2+
3+
class AutoUnstripThread(QThread):
4+
finished = Signal(bool, str) # Signal for success/failure and error message
5+
6+
def __init__(self, autounstrip, bv):
7+
super().__init__()
8+
self.autounstrip = autounstrip
9+
self.bv = bv
10+
11+
def run(self):
12+
try:
13+
success, message = self.autounstrip.auto_unstrip(self.bv)
14+
if success:
15+
self.finished.emit(True, message)
16+
else:
17+
self.finished.emit(False, message)
18+
except Exception as e:
19+
self.finished.emit(False, str(e))

features/configuration/__init__.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
from binaryninja import PluginCommand, log_info
1+
from binaryninja import PluginCommand, log_info, BinaryViewType, log_error
22
from .config import Config
33
from .config_dialog import ConfigDialog
4-
from revengai_bn.utils import BaseAuthFeature
54

65
class ConfigurationFeature():
76
def __init__(self):
87
self.config = Config()
8+
self._register_binary_view_event()
99
log_info("RevEng.AI | Configuration Feature initialized")
1010

1111
def register(self):
@@ -14,11 +14,23 @@ def register(self):
1414
"Configure RevEng.AI settings",
1515
self.show_configuration
1616
)
17+
log_info("RevEng.AI | Configuration Feature registered")
1718

1819
def show_configuration(self, bv):
1920
log_info("RevEng.AI | Opening configuration wizard")
2021
wizard = ConfigDialog(self.config)
2122
wizard.exec_()
2223

2324
def get_config(self):
24-
return self.config
25+
return self.config
26+
27+
def _register_binary_view_event(self):
28+
BinaryViewType.add_binaryview_finalized_event(self._add_binaryview_finalized_event)
29+
log_info("RevEng.AI | Registered binary view event handler")
30+
31+
def _add_binaryview_finalized_event(self, bv):
32+
try:
33+
log_info(f"RevEng.AI | Binary view finalized: {bv.file.original_filename}")
34+
self.config.init_config()
35+
except Exception as e:
36+
log_error(f"RevEng.AI | Error in binary view event handler: {str(e)}")

0 commit comments

Comments
 (0)