declib 3.8.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.
- declib/__init__.py +9 -0
- declib/__main__.py +190 -0
- declib/api/__init__.py +13 -0
- declib/api/artifact_dict.py +153 -0
- declib/api/artifact_lifter.py +161 -0
- declib/api/decompiler_client.py +1219 -0
- declib/api/decompiler_interface.py +1261 -0
- declib/api/decompiler_server.py +782 -0
- declib/api/server_registry.py +171 -0
- declib/api/type_definition_parser.py +201 -0
- declib/api/type_parser.py +409 -0
- declib/api/utils.py +31 -0
- declib/artifacts/__init__.py +93 -0
- declib/artifacts/artifact.py +311 -0
- declib/artifacts/comment.py +49 -0
- declib/artifacts/context.py +61 -0
- declib/artifacts/decompilation.py +35 -0
- declib/artifacts/enum.py +53 -0
- declib/artifacts/formatting.py +27 -0
- declib/artifacts/func.py +433 -0
- declib/artifacts/global_variable.py +31 -0
- declib/artifacts/patch.py +49 -0
- declib/artifacts/segment.py +37 -0
- declib/artifacts/stack_variable.py +50 -0
- declib/artifacts/struct.py +184 -0
- declib/artifacts/typedef.py +59 -0
- declib/cli/__init__.py +3 -0
- declib/cli/decompiler_cli.py +1487 -0
- declib/configuration.py +184 -0
- declib/decompiler_stubs/__init__.py +0 -0
- declib/decompiler_stubs/angr_declib/__init__.py +4 -0
- declib/decompiler_stubs/binja_declib/__init__.py +4 -0
- declib/decompiler_stubs/ida_declib.py +8 -0
- declib/decompilers/__init__.py +8 -0
- declib/decompilers/angr/__init__.py +11 -0
- declib/decompilers/angr/artifact_lifter.py +46 -0
- declib/decompilers/angr/compat.py +262 -0
- declib/decompilers/angr/interface.py +949 -0
- declib/decompilers/binja/__init__.py +0 -0
- declib/decompilers/binja/artifact_lifter.py +32 -0
- declib/decompilers/binja/hooks.py +201 -0
- declib/decompilers/binja/interface.py +795 -0
- declib/decompilers/ghidra/__init__.py +0 -0
- declib/decompilers/ghidra/artifact_lifter.py +60 -0
- declib/decompilers/ghidra/compat/__init__.py +0 -0
- declib/decompilers/ghidra/compat/headless.py +156 -0
- declib/decompilers/ghidra/compat/imports.py +78 -0
- declib/decompilers/ghidra/compat/state.py +54 -0
- declib/decompilers/ghidra/compat/transaction.py +30 -0
- declib/decompilers/ghidra/hooks.py +242 -0
- declib/decompilers/ghidra/interface.py +1433 -0
- declib/decompilers/ida/__init__.py +0 -0
- declib/decompilers/ida/artifact_lifter.py +51 -0
- declib/decompilers/ida/compat.py +2054 -0
- declib/decompilers/ida/hooks.py +700 -0
- declib/decompilers/ida/ida_ui.py +80 -0
- declib/decompilers/ida/interface.py +659 -0
- declib/logger.py +101 -0
- declib/plugin_installer.py +259 -0
- declib/skills/__init__.py +24 -0
- declib/skills/decompiler/SKILL.md +316 -0
- declib/ui/__init__.py +33 -0
- declib/ui/qt_objects.py +146 -0
- declib/ui/utils.py +115 -0
- declib/ui/version.py +14 -0
- declib-3.8.0.dist-info/METADATA +138 -0
- declib-3.8.0.dist-info/RECORD +71 -0
- declib-3.8.0.dist-info/WHEEL +5 -0
- declib-3.8.0.dist-info/entry_points.txt +3 -0
- declib-3.8.0.dist-info/licenses/LICENSE +24 -0
- declib-3.8.0.dist-info/top_level.txt +1 -0
declib/logger.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import logging.config
|
|
3
|
+
import tempfile
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
7
|
+
_, tempfilename = tempfile.mkstemp(prefix=timestamp + '.declib.', suffix='.log')
|
|
8
|
+
string_format = "%(levelname)s | %(asctime)s | %(name)-8s | %(message)s"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
default_config = {
|
|
12
|
+
"version": 1,
|
|
13
|
+
"disable_existing_loggers": False,
|
|
14
|
+
"formatters": {
|
|
15
|
+
"console": {
|
|
16
|
+
"format": string_format
|
|
17
|
+
},
|
|
18
|
+
"logfile": {
|
|
19
|
+
"format": string_format
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
"handlers": {
|
|
24
|
+
"console": {
|
|
25
|
+
"class": "logging.StreamHandler",
|
|
26
|
+
"level": "DEBUG",
|
|
27
|
+
"formatter": "console",
|
|
28
|
+
"stream": "ext://sys.stdout"
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
"local_file_handler": {
|
|
32
|
+
"class": "logging.handlers.RotatingFileHandler",
|
|
33
|
+
"level": "DEBUG",
|
|
34
|
+
"formatter": "logfile",
|
|
35
|
+
"filename": tempfilename,
|
|
36
|
+
"maxBytes": 1000000,
|
|
37
|
+
"backupCount": 20,
|
|
38
|
+
"encoding": "utf8",
|
|
39
|
+
"delay": True
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
'loggers': {
|
|
43
|
+
'declib': {
|
|
44
|
+
'handlers': ["console", "local_file_handler"],
|
|
45
|
+
'level': 'INFO',
|
|
46
|
+
'propagate': False
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Loggers:
|
|
53
|
+
"""
|
|
54
|
+
Logger Manager.
|
|
55
|
+
"""
|
|
56
|
+
IN_SCOPE_LOGGERS = ('declib', )
|
|
57
|
+
|
|
58
|
+
def __init__(self):
|
|
59
|
+
self._loggers = {}
|
|
60
|
+
self.load_all_loggers()
|
|
61
|
+
self.profiling_enabled = False
|
|
62
|
+
|
|
63
|
+
# disable filelock info logs
|
|
64
|
+
logging.getLogger("filelock").setLevel(logging.WARNING)
|
|
65
|
+
|
|
66
|
+
self.config_dict = None
|
|
67
|
+
if default_config is not None:
|
|
68
|
+
self.config_dict = default_config
|
|
69
|
+
if self.config_dict is not None:
|
|
70
|
+
logging.config.dictConfig(self.config_dict)
|
|
71
|
+
self.handler = logging.StreamHandler()
|
|
72
|
+
self.handler.setFormatter(logging.Formatter('%(levelname)-7s | %(asctime)-23s | %(name)-8s | %(message)s'))
|
|
73
|
+
|
|
74
|
+
def load_all_loggers(self):
|
|
75
|
+
for name, logger in logging.Logger.manager.loggerDict.items():
|
|
76
|
+
if any(name.startswith(x + '.') or name == x for x in self.IN_SCOPE_LOGGERS):
|
|
77
|
+
self._loggers[name] = logger
|
|
78
|
+
|
|
79
|
+
def __getattr__(self, k):
|
|
80
|
+
real_k = k.replace('_', '.')
|
|
81
|
+
if real_k in self._loggers:
|
|
82
|
+
return self._loggers[real_k]
|
|
83
|
+
else:
|
|
84
|
+
raise AttributeError(k)
|
|
85
|
+
|
|
86
|
+
def __dir__(self):
|
|
87
|
+
return list(super(Loggers, self).__dir__()) + list(self._loggers.keys())
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def is_enabled_for(logger, level):
|
|
91
|
+
if level == 1:
|
|
92
|
+
from .. import loggers
|
|
93
|
+
return loggers.profiling_enabled
|
|
94
|
+
return originalIsEnabledFor(logger, level)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
originalIsEnabledFor = logging.Logger.isEnabledFor
|
|
98
|
+
|
|
99
|
+
# Override isEnabledFor() for Logger class
|
|
100
|
+
logging.Logger.isEnabledFor = is_enabled_for
|
|
101
|
+
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import platform
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import textwrap
|
|
5
|
+
import sys
|
|
6
|
+
import shutil
|
|
7
|
+
from typing import Optional, Union, Tuple
|
|
8
|
+
|
|
9
|
+
from prompt_toolkit import prompt
|
|
10
|
+
from prompt_toolkit.completion.filesystem import PathCompleter
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Color:
|
|
14
|
+
"""
|
|
15
|
+
Used to colorify terminal output.
|
|
16
|
+
Taken from: https://github.com/hugsy/gef/blob/dev/tests/utils.py
|
|
17
|
+
"""
|
|
18
|
+
NORMAL = "\x1b[0m"
|
|
19
|
+
GRAY = "\x1b[1;38;5;240m"
|
|
20
|
+
LIGHT_GRAY = "\x1b[0;37m"
|
|
21
|
+
RED = "\x1b[31m"
|
|
22
|
+
GREEN = "\x1b[32m"
|
|
23
|
+
YELLOW = "\x1b[33m"
|
|
24
|
+
BLUE = "\x1b[34m"
|
|
25
|
+
PINK = "\x1b[35m"
|
|
26
|
+
CYAN = "\x1b[36m"
|
|
27
|
+
BOLD = "\x1b[1m"
|
|
28
|
+
UNDERLINE = "\x1b[4m"
|
|
29
|
+
UNDERLINE_OFF = "\x1b[24m"
|
|
30
|
+
HIGHLIGHT = "\x1b[3m"
|
|
31
|
+
HIGHLIGHT_OFF = "\x1b[23m"
|
|
32
|
+
BLINK = "\x1b[5m"
|
|
33
|
+
BLINK_OFF = "\x1b[25m"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class PluginInstaller:
|
|
37
|
+
DECOMPILERS = (
|
|
38
|
+
"ida",
|
|
39
|
+
"binja",
|
|
40
|
+
"ghidra",
|
|
41
|
+
"angr"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
DEBUGGERS = (
|
|
45
|
+
"gdb",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def __init__(self, targets=None, target_install_paths=None):
|
|
49
|
+
self.targets = targets if targets is not None else self.DECOMPILERS+self.DEBUGGERS
|
|
50
|
+
self._home = Path(os.getenv("HOME") or "~/").expanduser().absolute()
|
|
51
|
+
self.target_install_paths = target_install_paths or {} #or self._populate_installs_from_config()
|
|
52
|
+
self._successful_installs = {}
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def find_pkg_files(pkg_name):
|
|
56
|
+
if sys.version_info >= (3, 9):
|
|
57
|
+
import importlib.resources
|
|
58
|
+
path = str(importlib.resources.files(pkg_name))
|
|
59
|
+
else:
|
|
60
|
+
import pkg_resources
|
|
61
|
+
path = pkg_resources.resource_filename(pkg_name, "")
|
|
62
|
+
|
|
63
|
+
return Path(path).absolute()
|
|
64
|
+
|
|
65
|
+
def install(self, interactive=True, paths_by_target=None):
|
|
66
|
+
self.target_install_paths.update(paths_by_target or {})
|
|
67
|
+
self.display_prologue()
|
|
68
|
+
|
|
69
|
+
if interactive:
|
|
70
|
+
self.display_install_instructions()
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
self.install_targets(interactive=interactive)
|
|
74
|
+
except Exception as e:
|
|
75
|
+
print(f"Stopping Install... because: {e}")
|
|
76
|
+
except KeyboardInterrupt:
|
|
77
|
+
print("Goodbye...")
|
|
78
|
+
|
|
79
|
+
self.display_epilogue()
|
|
80
|
+
|
|
81
|
+
def display_prologue(self):
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
def display_install_instructions(self):
|
|
85
|
+
print(textwrap.dedent("""
|
|
86
|
+
Each decompiler/debugger will be prompted for install below. If you would like to skip install for something
|
|
87
|
+
you can enter 'n' or just hit enter. Each path prompt has tab path completion.
|
|
88
|
+
Enter nothing in each path prompt to get the default listed.
|
|
89
|
+
"""))
|
|
90
|
+
|
|
91
|
+
def display_epilogue(self):
|
|
92
|
+
self.good("Plugin install completed! If anything was skipped by mistake, please manually install it.")
|
|
93
|
+
|
|
94
|
+
@staticmethod
|
|
95
|
+
def info(msg):
|
|
96
|
+
print(f"{Color.BLUE}{msg}{Color.NORMAL}")
|
|
97
|
+
|
|
98
|
+
@staticmethod
|
|
99
|
+
def good(msg):
|
|
100
|
+
print(f"{Color.GREEN}[+] {msg}{Color.NORMAL}")
|
|
101
|
+
|
|
102
|
+
@staticmethod
|
|
103
|
+
def warn(msg):
|
|
104
|
+
print(f"{Color.YELLOW}[!] {msg}{Color.NORMAL}")
|
|
105
|
+
|
|
106
|
+
@staticmethod
|
|
107
|
+
def ask_path(target, location, default=None) -> Optional[Union[bool, Path]]:
|
|
108
|
+
"""
|
|
109
|
+
Possible return values:
|
|
110
|
+
- None: install failed or skipped
|
|
111
|
+
- Path: install succeeded
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
PluginInstaller.info(f"Install for {target}? [y/n]")
|
|
115
|
+
res = prompt("")
|
|
116
|
+
if res.lower() != "y":
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
PluginInstaller.info(location + f" [default = {default}] (enter nothing to use default): ")
|
|
120
|
+
filepath = prompt("", completer=PathCompleter(expanduser=True))
|
|
121
|
+
if not filepath and default:
|
|
122
|
+
return default
|
|
123
|
+
|
|
124
|
+
filepath = Path(filepath).expanduser().absolute()
|
|
125
|
+
if not filepath.exists():
|
|
126
|
+
PluginInstaller.warn(f"Provided filepath {filepath} does not exist. {'Using default.' if default else 'Skipping.'}")
|
|
127
|
+
return default if default else None
|
|
128
|
+
|
|
129
|
+
return filepath
|
|
130
|
+
|
|
131
|
+
@staticmethod
|
|
132
|
+
def link_or_copy(src, dst, is_dir=False, symlink=False):
|
|
133
|
+
if platform.platform().startswith("Windows"):
|
|
134
|
+
# you can't symlink on windows, so just copy
|
|
135
|
+
symlink = False
|
|
136
|
+
|
|
137
|
+
# clean the install location
|
|
138
|
+
shutil.rmtree(dst, ignore_errors=True)
|
|
139
|
+
try:
|
|
140
|
+
os.unlink(dst)
|
|
141
|
+
except:
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
if not symlink:
|
|
145
|
+
# copy if symlinking is not available on target system
|
|
146
|
+
if is_dir:
|
|
147
|
+
shutil.copytree(src, dst)
|
|
148
|
+
else:
|
|
149
|
+
shutil.copy(src, dst)
|
|
150
|
+
else:
|
|
151
|
+
# first attempt a symlink, if it works, exit early
|
|
152
|
+
try:
|
|
153
|
+
os.symlink(src, dst, target_is_directory=is_dir)
|
|
154
|
+
return
|
|
155
|
+
except:
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def _get_path_without_ask(path, default_path=None, interactive=True) -> Tuple[Path, bool]:
|
|
160
|
+
path = Path(path) if path else None
|
|
161
|
+
if not interactive and path.exists():
|
|
162
|
+
return path, True
|
|
163
|
+
|
|
164
|
+
if path and path.exists():
|
|
165
|
+
default_path = path
|
|
166
|
+
else:
|
|
167
|
+
default_path = Path(default_path) if default_path else None
|
|
168
|
+
if not default_path or not default_path.exists():
|
|
169
|
+
default_path = None
|
|
170
|
+
|
|
171
|
+
return default_path, (not interactive and default_path and default_path.exists())
|
|
172
|
+
|
|
173
|
+
def install_targets(self, interactive=True):
|
|
174
|
+
for target in self.targets:
|
|
175
|
+
try:
|
|
176
|
+
target_installer = getattr(self, f"install_{target}")
|
|
177
|
+
except AttributeError:
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
path = self.target_install_paths.get(f"{target}", None)
|
|
181
|
+
if path:
|
|
182
|
+
path = Path(path).expanduser().absolute()
|
|
183
|
+
|
|
184
|
+
if not path and not interactive:
|
|
185
|
+
continue
|
|
186
|
+
|
|
187
|
+
res = target_installer(path=path, interactive=interactive)
|
|
188
|
+
if res is None:
|
|
189
|
+
self.warn(f"Skipping or failed install for {target}... {Color.NORMAL}\n")
|
|
190
|
+
else:
|
|
191
|
+
self.good(f"Installed {target} to {res}\n")
|
|
192
|
+
self._successful_installs[target] = res
|
|
193
|
+
#GlobalConfig.update_or_make(self._home, **{f"{target}_path": res.parent})
|
|
194
|
+
|
|
195
|
+
def install_ida(self, path=None, interactive=True):
|
|
196
|
+
default_path, skip_ask = self._get_path_without_ask(
|
|
197
|
+
path, default_path=self._home.joinpath(".idapro").joinpath("plugins").expanduser(), interactive=interactive
|
|
198
|
+
)
|
|
199
|
+
return self.ask_path("IDA Pro", "Plugins Path", default=default_path) if not skip_ask \
|
|
200
|
+
else default_path
|
|
201
|
+
|
|
202
|
+
def install_ghidra(self, path=None, interactive=True):
|
|
203
|
+
potential_path = self._home.joinpath('ghidra_scripts').expanduser()
|
|
204
|
+
if self._home.exists() and not potential_path.exists():
|
|
205
|
+
self.info(f"Creating Ghidra Scripts directory at {potential_path}...")
|
|
206
|
+
potential_path.mkdir()
|
|
207
|
+
|
|
208
|
+
default_path, skip_ask = self._get_path_without_ask(
|
|
209
|
+
path, default_path=potential_path, interactive=interactive
|
|
210
|
+
)
|
|
211
|
+
return self.ask_path("Ghidra", "Ghidra Scripts Path", default=default_path) if not skip_ask \
|
|
212
|
+
else default_path
|
|
213
|
+
|
|
214
|
+
def install_binja(self, path=None, interactive=True):
|
|
215
|
+
os_name = platform.system()
|
|
216
|
+
if os_name == "Windows":
|
|
217
|
+
default_path = Path(os.environ.get("APPDATA", str(self._home))) / "Binary Ninja" / "plugins"
|
|
218
|
+
elif os_name == "Darwin":
|
|
219
|
+
default_path = (self._home / "Library" / "Application Support" / "Binary Ninja" / "plugins").expanduser()
|
|
220
|
+
else:
|
|
221
|
+
default_path = (self._home / ".binaryninja" / "plugins").expanduser()
|
|
222
|
+
default_path, skip_ask = self._get_path_without_ask(
|
|
223
|
+
path, default_path=default_path,
|
|
224
|
+
interactive=interactive
|
|
225
|
+
)
|
|
226
|
+
return self.ask_path("Binary Ninja", "Plugins Path", default=default_path) if not skip_ask \
|
|
227
|
+
else default_path
|
|
228
|
+
|
|
229
|
+
def install_angr(self, path=None, interactive=True):
|
|
230
|
+
# attempt to find the plugins folder for angr-management which is installed via pip
|
|
231
|
+
angr_resolved = True
|
|
232
|
+
try:
|
|
233
|
+
import angrmanagement
|
|
234
|
+
except ImportError:
|
|
235
|
+
angr_resolved = False
|
|
236
|
+
default_path = Path(angrmanagement.__file__).parent / "plugins" if angr_resolved else None
|
|
237
|
+
|
|
238
|
+
default_path, skip_ask = self._get_path_without_ask(path, default_path=default_path, interactive=interactive)
|
|
239
|
+
return self.ask_path("angr-management", "angr-management Plugins Path", default=default_path) if not skip_ask \
|
|
240
|
+
else default_path
|
|
241
|
+
|
|
242
|
+
def install_gdb(self, path=None, interactive=True):
|
|
243
|
+
default_path, skip_ask = self._get_path_without_ask(
|
|
244
|
+
path, default_path=self._home.joinpath(".gdbinit").expanduser(),
|
|
245
|
+
interactive=interactive
|
|
246
|
+
)
|
|
247
|
+
return self.ask_path("GDB", "gdbinit Path", default=default_path) if not skip_ask \
|
|
248
|
+
else default_path
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class DecLibPluginInstaller(PluginInstaller):
|
|
252
|
+
def __init__(self, targets=None, target_install_paths=None):
|
|
253
|
+
targets = targets or PluginInstaller.DECOMPILERS
|
|
254
|
+
super().__init__(targets=targets, target_install_paths=target_install_paths)
|
|
255
|
+
self._declib_plugins_path = self.find_pkg_files("declib").joinpath("decompiler_stubs")
|
|
256
|
+
|
|
257
|
+
def display_prologue(self):
|
|
258
|
+
print(textwrap.dedent("""
|
|
259
|
+
Now installing DecLib plugins for all supported decompilers..."""))
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Bundled Agent Skills for declib.
|
|
2
|
+
|
|
3
|
+
Each subdirectory holds a SKILL.md (and any optional resources) that an LLM can
|
|
4
|
+
load to learn how to drive declib via the `decompiler` CLI. Use
|
|
5
|
+
`decompiler install-skill` to copy a skill into Claude Code or Codex.
|
|
6
|
+
"""
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
SKILLS_DIR = Path(__file__).parent
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def available_skills() -> list[str]:
|
|
13
|
+
return sorted(
|
|
14
|
+
p.name
|
|
15
|
+
for p in SKILLS_DIR.iterdir()
|
|
16
|
+
if p.is_dir() and (p / "SKILL.md").is_file()
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def skill_path(name: str) -> Path:
|
|
21
|
+
path = SKILLS_DIR / name
|
|
22
|
+
if not (path / "SKILL.md").is_file():
|
|
23
|
+
raise FileNotFoundError(f"Unknown bundled skill: {name!r}")
|
|
24
|
+
return path
|