263 lines
7.8 KiB
Python
263 lines
7.8 KiB
Python
import importlib
|
|
import importlib.util
|
|
import inspect
|
|
import logging
|
|
import sys
|
|
import traceback
|
|
from collections.abc import ItemsView, KeysView
|
|
from importlib.machinery import ModuleSpec, SourceFileLoader
|
|
from pathlib import Path
|
|
from jpype import JClass, JImplementationFor
|
|
from typing import List
|
|
|
|
|
|
_NO_ATTRIBUTE = object()
|
|
|
|
_headless_interpreter = None
|
|
|
|
|
|
class _StaticMap(dict):
|
|
|
|
__slots__ = ('script',)
|
|
|
|
def __init__(self, script: "PyGhidraScript"):
|
|
super().__init__()
|
|
self.script = script
|
|
|
|
def __getitem__(self, key):
|
|
res = self.get(key, _NO_ATTRIBUTE)
|
|
if res is not _NO_ATTRIBUTE:
|
|
return res
|
|
raise KeyError(key)
|
|
|
|
def get(self, key, default=None):
|
|
res = self.script.get_static(key)
|
|
return res if res is not _NO_ATTRIBUTE else default
|
|
|
|
def __iter__(self):
|
|
yield from self.script
|
|
|
|
def keys(self):
|
|
return KeysView(self)
|
|
|
|
def items(self):
|
|
return ItemsView(self)
|
|
|
|
|
|
class _JavaProperty(property):
|
|
|
|
def __init__(self, field):
|
|
super().__init__()
|
|
self._field = field
|
|
|
|
def __get__(self, obj, cls):
|
|
return self._field.fget(obj)
|
|
|
|
def __set__(self, obj, value):
|
|
self._field.fset(obj, value)
|
|
|
|
|
|
#pylint: disable=too-few-public-methods
|
|
@JImplementationFor("dc3.pyhidra.plugin.PythonFieldExposer")
|
|
class _PythonFieldExposer:
|
|
|
|
#pylint: disable=no-member
|
|
def __jclass_init__(self):
|
|
exposer = JClass("dc3.pyhidra.plugin.PythonFieldExposer")
|
|
if self.class_ == exposer:
|
|
return
|
|
try:
|
|
for k, v in exposer.getProperties(self.class_).items():
|
|
self._customize(k, _JavaProperty(v))
|
|
# allowing any exception to escape here may cause the jvm to terminate
|
|
# pylint: disable=bare-except
|
|
except:
|
|
logger = logging.getLogger(__name__)
|
|
logger.error("Failed to add property customizations for %s", self, exc_info=1)
|
|
|
|
|
|
class _GhidraScriptModule:
|
|
|
|
def __init__(self, spec: ModuleSpec):
|
|
super().__setattr__("__dict__", spec.loader_state["script"])
|
|
|
|
def __setattr__(self, attr, value):
|
|
if hasattr(self, attr):
|
|
raise AttributeError(f"readonly attribute {attr}")
|
|
super().__setattr__(attr, value)
|
|
|
|
|
|
class _GhidraScriptLoader(SourceFileLoader):
|
|
|
|
def __init__(self, script: "PyGhidraScript", spec: ModuleSpec):
|
|
super().__init__(spec.name, spec.origin)
|
|
spec.loader_state = {"script": script}
|
|
|
|
def create_module(self, spec: ModuleSpec):
|
|
return _GhidraScriptModule(spec)
|
|
|
|
|
|
# pylint: disable=missing-function-docstring
|
|
class PyGhidraScript(dict):
|
|
"""
|
|
Python GhidraScript Wrapper
|
|
"""
|
|
|
|
def __init__(self, jobj=None):
|
|
super().__init__()
|
|
if jobj is None:
|
|
jobj = JClass("dc3.pyhidra.plugin.PyScriptProvider").PyhidraHeadlessScript()
|
|
self._script = jobj
|
|
|
|
global _headless_interpreter
|
|
|
|
from ghidra.util import SystemUtilities
|
|
|
|
if SystemUtilities.isInHeadlessMode() and _headless_interpreter is None:
|
|
_headless_interpreter = jobj
|
|
|
|
# ensure the builtin set takes precedence over GhidraScript.set
|
|
super().__setitem__("set", set)
|
|
|
|
super().__setitem__("__this__", self._script)
|
|
|
|
# this is injected since Ghidra commit e66e72577ded1aeae53bcc3f361dfce1ecf6e24a
|
|
super().__setitem__("this", self._script)
|
|
|
|
def __missing__(self, k):
|
|
attr = getattr(self._script, k, _NO_ATTRIBUTE)
|
|
if attr is not _NO_ATTRIBUTE:
|
|
return attr
|
|
raise KeyError(k)
|
|
|
|
def __getattr__(self, item):
|
|
return getattr(self._script, item)
|
|
|
|
def __setitem__(self, k, v):
|
|
attr = inspect.getattr_static(self._script, k, _NO_ATTRIBUTE)
|
|
if attr is not _NO_ATTRIBUTE and isinstance(attr, property):
|
|
setattr(self._script, k, v)
|
|
else:
|
|
super().__setitem__(k, v)
|
|
|
|
def __iter__(self):
|
|
yield from super().__iter__()
|
|
yield from dir(self._script)
|
|
|
|
def get_static(self, key):
|
|
res = self.get(key, _NO_ATTRIBUTE)
|
|
if res is not _NO_ATTRIBUTE:
|
|
return res
|
|
return inspect.getattr_static(self._script, key, _NO_ATTRIBUTE)
|
|
|
|
def get_static_view(self):
|
|
return _StaticMap(self)
|
|
|
|
def set(self, state, monitor, writer):
|
|
"""
|
|
see GhidraScript.set
|
|
"""
|
|
self._script.set(state, monitor, writer)
|
|
|
|
def run(self, script_path: str = None, script_args: List[str] = None):
|
|
"""
|
|
Run this GhidraScript
|
|
|
|
:param script_path: The path of the python script
|
|
:param script_args: The arguments for the python script
|
|
"""
|
|
sf = self._script.getSourceFile()
|
|
if sf is None and script_path is None:
|
|
return
|
|
if script_path is None:
|
|
script_path = sf.getAbsolutePath()
|
|
script_args = self._script.getScriptArgs()
|
|
|
|
if script_args is None:
|
|
script_args = []
|
|
else:
|
|
self._script.setScriptArgs(script_args)
|
|
|
|
orig_argv = sys.argv
|
|
script_root = str(Path(script_path).parent)
|
|
|
|
# honor the python safe_path flag introduced in 3.11
|
|
safe_path = bool(getattr(sys.flags, "safe_path", 0))
|
|
try:
|
|
# Temporarily set command line arguments.
|
|
sys.argv = [script_path] + list(script_args)
|
|
|
|
if not safe_path:
|
|
# add the directory containing the script to the start of the path
|
|
# this provides the same import behavior as if the script was run normally
|
|
sys.path.insert(0, script_root)
|
|
|
|
spec = importlib.util.spec_from_file_location('__main__', script_path)
|
|
spec.loader = _GhidraScriptLoader(self, spec)
|
|
m = importlib.util.module_from_spec(spec)
|
|
try:
|
|
spec.loader.exec_module(m)
|
|
# pylint: disable=bare-except
|
|
except:
|
|
# filter the traceback so that it stops at the script
|
|
exc_type, exc_value, exc_tb = sys.exc_info()
|
|
i = 0
|
|
tb = traceback.extract_tb(exc_tb)
|
|
for fs in tb:
|
|
if fs.filename == script_path:
|
|
break
|
|
i += 1
|
|
ss = traceback.StackSummary.from_list(tb[i:])
|
|
e = traceback.TracebackException(exc_type, exc_value, exc_tb)
|
|
e.stack = ss
|
|
self._script.printerr(''.join(e.format()))
|
|
finally:
|
|
sys.argv = orig_argv
|
|
|
|
if not safe_path:
|
|
sys.path.remove(script_root)
|
|
|
|
|
|
def get_current_interpreter():
|
|
"""
|
|
Gets the underlying GhidraScript for the focused Pyhidra InteractiveConsole.
|
|
This will always return None unless it is being access from a function
|
|
called from within the interactive console.
|
|
|
|
:return: The GhidraScript for the active interactive console.
|
|
"""
|
|
|
|
try:
|
|
from ghidra.util import SystemUtilities
|
|
from ghidra.framework.main import AppInfo
|
|
|
|
global _headless_interpreter
|
|
|
|
if SystemUtilities.isInHeadlessMode():
|
|
if _headless_interpreter is None:
|
|
# one hasn't been created yet so make one now
|
|
PyScriptProvider = JClass("dc3.pyhidra.plugin.PyScriptProvider")
|
|
_headless_interpreter = PyScriptProvider.PyhidraHeadlessScript()
|
|
return _headless_interpreter
|
|
|
|
project = AppInfo.getActiveProject()
|
|
if project is None:
|
|
return None
|
|
|
|
ts = project.getToolServices()
|
|
tool = None
|
|
for t in ts.getRunningTools():
|
|
if t.getActiveWindow().isFocused():
|
|
tool = t
|
|
break
|
|
|
|
if tool is None:
|
|
return None
|
|
|
|
for plugin in tool.getManagedPlugins():
|
|
if plugin.name == 'PyhidraPlugin':
|
|
return plugin.script
|
|
|
|
except ImportError:
|
|
return None
|