# Copyright (c) 2015-2024 Vector 35 Inc
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
import atexit
import sys
import ctypes
from time import gmtime, struct_time
import os
from typing import Mapping, Optional
import functools
# Binary Ninja components
import binaryninja._binaryninjacore as core
__version__ = core.BNGetVersionString()
import binaryninja
from .enums import *
from .databuffer import *
from .filemetadata import *
from .fileaccessor import *
from .binaryview import *
from .transform import *
from .architecture import *
from .basicblock import *
from .function import *
from .lowlevelil import *
from .mediumlevelil import *
from .highlevelil import *
from .types import *
from .typelibrary import *
from .functionrecognizer import *
from .update import *
from .plugin import *
from .callingconvention import *
from .platform import *
from .demangle import *
from .mainthread import *
from .interaction import *
from .lineardisassembly import *
from .highlight import *
from .scriptingprovider import *
from .downloadprovider import *
from .pluginmanager import *
from .settings import *
from .metadata import *
from .flowgraph import *
from .datarender import *
from .variable import *
from .websocketprovider import *
from .workflow import *
from .commonil import *
from .database import *
from .secretsprovider import *
from .typeparser import *
from .typeprinter import *
from .component import *
from .typearchive import *
from .typecontainer import *
from .exceptions import *
from .project import *
from .basedetection import *
from .debuginfo import *
from .externallibrary import *
from .undo import *
from .fileaccessor import *
from .languagerepresentation import *
# We import each of these by name to prevent conflicts between
# log.py and the function 'log' which we don't import below
from .log import (
redirect_output_to_log, is_output_redirected_to_log, log_debug, log_info, log_warn, log_error, log_alert,
log_to_stdout, log_to_stderr, log_to_file, close_logs
)
from .log import log as log_at_level
from .deprecation import *
import warnings
# We must alter the filter settings for DeprecatedWarning. Otherwise, it will never show up.
# https://docs.python.org/3/library/warnings.html#default-warning-filter
warnings.filterwarnings('once', '', DeprecatedWarning)
# Only load Enterprise Client support on Ultimate builds
if core.BNGetProduct() == "Binary Ninja Enterprise Client" or core.BNGetProduct() == "Binary Ninja Ultimate":
from .enterprise import *
[docs]
def shutdown():
"""
``shutdown`` cleanly shuts down the core, stopping all workers and closing all log files.
.. note:: This will be called automatically on script exit if you import the binaryninja module.
"""
# Release license if we have one
global _enterprise_license_checkout
_enterprise_license_checkout = None
core.BNShutdown()
atexit.register(shutdown)
[docs]
@functools.total_ordering
@dataclass
class CoreVersionInfo:
"""
Structure representing the Binary Ninja Version.
Use :py:func:`core_version_info` to look up the current version of Binary Ninja loaded.
"""
major: int
"""Major version number, e.g. 4.0.5000-dev would be 4"""
minor: int
"""Minor version number, e.g. 4.0.5000-dev would be 0"""
build: int
"""Build version number, e.g. 4.0.5000-dev would be 5000"""
channel: str
"""Release channel name, e.g. "dev" or "Stable" """
def __init__(self, major, minor = None, build = None, channel = None):
self.major = 0
self.minor = 0
self.build = 0
self.channel = ""
if isinstance(major, str) and minor is None and build is None and channel is None:
core_version_info = core.BNParseVersionString(major)
self.major = core_version_info.major
self.minor = core_version_info.minor
self.build = core_version_info.build
if core_version_info.channel is not None:
self.channel = core_version_info.channel
core.BNFreeString(core_version_info.channel)
else:
self.major = major
if minor is not None:
self.minor = minor
if build is not None:
self.build = build
self.channel = channel
def __str__(self):
if self.channel == "":
return f"{self.major}.{self.minor}.{self.build}"
else:
return f"{self.major}.{self.minor}.{self.build}-{self.channel}"
def __eq__(self, other):
if isinstance(other, CoreVersionInfo):
return self.major == other.major and self.minor == other.minor and self.build == other.build # channel doesn't matter
if isinstance(other, str):
return self == CoreVersionInfo(other)
return False
def _versionCompare(self, other):
a = core.BNVersionInfo()
a.major = self.major
a.minor = self.minor
a.build = self.build
b = core.BNVersionInfo()
b.major = other.major
b.minor = other.minor
b.build = other.build
return core.BNVersionLessThan(a, b)
def __lt__(self, other):
if isinstance(other, CoreVersionInfo):
return self._versionCompare(other)
if isinstance(other, str):
return self._versionCompare(CoreVersionInfo(other))
return False
[docs]
def get_unique_identifier():
"""
Generate a GUID
:return: A GUID string
"""
return core.BNGetUniqueIdentifierString()
[docs]
def get_install_directory():
"""
``get_install_directory`` returns a string pointing to the installed binary currently running
.. warning:: ONLY for use within the Binary Ninja UI, behavior is undefined and unreliable if run headlessly
"""
return core.BNGetInstallDirectory()
class _DestructionCallbackHandler:
def __init__(self):
self._cb = core.BNObjectDestructionCallbacks()
self._cb.context = 0
self._cb.destructBinaryView = self._cb.destructBinaryView.__class__(self.destruct_binary_view)
self._cb.destructFileMetadata = self._cb.destructFileMetadata.__class__(self.destruct_file_metadata)
self._cb.destructFunction = self._cb.destructFunction.__class__(self.destruct_function)
core.BNRegisterObjectDestructionCallbacks(self._cb)
def destruct_binary_view(self, ctxt, view):
binaryninja.binaryview.BinaryView._unregister(view)
def destruct_file_metadata(self, ctxt, f):
binaryninja.filemetadata.FileMetadata._unregister(f)
def destruct_function(self, ctxt, func):
binaryninja.function.Function._unregister(func)
_enable_default_log = True
_plugin_init = False
_enterprise_license_checkout = None
def _init_plugins():
global _enable_default_log
global _plugin_init
global _enterprise_license_checkout
if not _plugin_init:
if not core_ui_enabled() and (core.BNGetProduct() == "Binary Ninja Enterprise Client" or core.BNGetProduct() == "Binary Ninja Ultimate"):
# Enterprise client needs to reserve a license or else BNInitPlugins will fail
_enterprise_license_checkout = enterprise.LicenseCheckout()
_enterprise_license_checkout.acquire()
# The first call to BNInitCorePlugins returns True for successful initialization and True in this context indicates headless operation.
# The result is pulled from BNInitPlugins as that now wraps BNInitCorePlugins.
is_headless_init_once = core.BNInitPlugins(not os.environ.get('BN_DISABLE_USER_PLUGINS'))
min_level = Settings().get_string("python.log.minLevel")
if _enable_default_log and is_headless_init_once and min_level in LogLevel.__members__ and not core_ui_enabled(
) and sys.stderr.isatty():
log_to_stderr(LogLevel[min_level])
core.BNInitRepoPlugins()
if core.BNIsLicenseValidated():
_plugin_init = True
else:
raise RuntimeError("License is not valid. Please supply a valid license.")
_destruct_callbacks = _DestructionCallbackHandler()
[docs]
def disable_default_log() -> None:
'''Disable default logging in headless mode for the current session. By default, logging in headless operation is controlled by the 'python.log.minLevel' settings.'''
global _enable_default_log
_enable_default_log = False
close_logs()
[docs]
def bundled_plugin_path() -> Optional[str]:
"""
``bundled_plugin_path`` returns a string containing the current plugin path inside the `install path <https://docs.binary.ninja/guide/#binary-path>`_
:return: current bundled plugin path
:rtype: str, or None on failure
"""
return core.BNGetBundledPluginDirectory()
[docs]
def user_plugin_path() -> Optional[str]:
"""
``user_plugin_path`` returns a string containing the current plugin path inside the `user directory <https://docs.binary.ninja/guide/#user-folder>`_
:return: current user plugin path
:rtype: str, or None on failure
"""
return core.BNGetUserPluginDirectory()
[docs]
def user_directory() -> Optional[str]:
"""
``user_directory`` returns a string containing the path to the `user directory <https://docs.binary.ninja/guide/#user-folder>`_
:return: current user path
:rtype: str, or None on failure
"""
return core.BNGetUserDirectory()
[docs]
def core_version() -> Optional[str]:
"""
``core_version`` returns a string containing the current version
:return: current version
:rtype: str, or None on failure
"""
return core.BNGetVersionString()
[docs]
def core_version_info() -> CoreVersionInfo:
"""
``core_version_info`` returns a CoreVersionInfo containing the current version information
:return: current version information
:rtype: CoreVersionInfo
"""
handle = core.BNGetVersionInfo()
return CoreVersionInfo(major=handle.major, minor=handle.minor, build=handle.build, channel=handle.channel)
[docs]
def core_build_id() -> int:
"""
``core_build_id`` returns a integer containing the current build id
:return: current build id
:rtype: int
"""
return core.BNGetBuildId()
[docs]
def core_serial() -> Optional[str]:
"""
``core_serial`` returns a string containing the current serial number
:return: current serial
:rtype: str, or None on failure
"""
return core.BNGetSerialNumber()
[docs]
def core_expires() -> struct_time:
'''License Expiration'''
return gmtime(core.BNGetLicenseExpirationTime())
[docs]
def core_product() -> Optional[str]:
'''Product string from the license file'''
return core.BNGetProduct()
[docs]
def core_product_type() -> Optional[str]:
'''Product type from the license file'''
return core.BNGetProductType()
[docs]
def core_license_count() -> int:
'''License count from the license file'''
return core.BNGetLicenseCount()
[docs]
def core_ui_enabled() -> bool:
'''Indicates that a UI exists and the UI has invoked BNInitUI'''
return core.BNIsUIEnabled()
[docs]
def core_set_license(licenseData: str) -> None:
'''
``core_set_license`` is used to initialize the core with a license file that doesn't necessarily reside on a file system. This is especially useful for headless environments such as docker where loading the license file via an environment variable allows for greater security of the license file itself.
:param str licenseData: string containing the full contents of a license file
:rtype: None
:Example:
>>> import os
>>> core_set_license(os.environ['BNLICENSE']) #Do this before creating any BinaryViews
>>> with load("/bin/ls") as bv:
... print(len(list(bv.functions)))
128
'''
core.BNSetLicense(licenseData)
[docs]
def get_memory_usage_info() -> Mapping[str, int]:
"""
Get counts of various Binary Ninja objects in memory.
:return: Dictionary of {class name: count} for objects in memory
"""
count = ctypes.c_ulonglong()
info = core.BNGetMemoryUsageInfo(count)
assert info is not None, "core.BNGetMemoryUsageInfo returned None"
result = {}
for i in range(0, count.value):
result[info[i].name] = info[i].value
core.BNFreeMemoryUsageInfo(info, count.value)
return result
[docs]
def load(*args, **kwargs) -> BinaryView:
"""
Opens a BinaryView object.
:param Union[str, bytes, bytearray, 'databuffer.DataBuffer', 'os.PathLike'] source: a file or byte stream to load into a virtual memory space
:param bool update_analysis: whether or not to run :func:`update_analysis_and_wait` after opening a :py:class:`BinaryView`, defaults to ``True``
:param callback progress_func: optional function to be called with the current progress and total count for BNDB files only
:param dict options: a dictionary in the form {setting identifier string : object value}
:return: returns a :py:class:`BinaryView` object for the given filename
:rtype: :py:class:`BinaryView`
:raises Exception: When a BinaryView could not be created
.. note:: The progress_func callback **must** return True to continue the load operation, False will abort the load operation.
.. warning:: The progress_func will **only** be called for BNDB files, not for any other file format due to a `design limitation <https://docs.binary.ninja/guide/debugger/index.html#navigating-the-binary>`_.
:Example:
>>> from binaryninja import *
>>> with load("/bin/ls") as bv:
... print(len(list(bv.functions)))
...
134
>>> with load(bytes.fromhex('5054ebfe'), options={'loader.platform' : 'x86'}) as bv:
... print(len(list(bv.functions)))
...
1
"""
bv = BinaryView.load(*args, **kwargs)
if bv is None:
raise Exception("Unable to create new BinaryView")
return bv
[docs]
def connect_pycharm_debugger(port=5678):
"""
Connect to PyCharm (Professional Edition) for debugging.
.. note:: See the `user documentation <https://docs.binary.ninja/dev/plugins.html#remote-debugging-with-intellij-pycharm>`_ for step-by-step instructions on how to set up Python debugging.
:param port: Port number for connecting to the debugger.
"""
# Get pip install string from PyCharm's Python Debug Server Configuration
# e.g. for PyCharm 2021.1.1 #PY-7142.13:
# pip install --user pydevd-pycharm~=211.7142.13
import pydevd_pycharm # type: ignore
pydevd_pycharm.settrace('localhost', port=port, stdoutToServer=True, stderrToServer=True, suspend=False)
[docs]
def connect_vscode_debugger(port=5678):
"""
Connect to Visual Studio Code for debugging. This function blocks until the debugger
is connected! Not recommended for use in startup.py
.. note:: See the `user documentation <https://docs.binary.ninja/dev/plugins.html#remote-debugging-with-vscode>`_ for step-by-step instructions on how to set up Python debugging.
:param port: Port number for connecting to the debugger.
"""
# pip install --user debugpy
import debugpy # type: ignore
import sys
if sys.platform == "win32":
debugpy.configure(python=f"{sys.base_exec_prefix}/python", qt="pyside2")
else:
debugpy.configure(python=f"{sys.base_exec_prefix}/bin/python3", qt="pyside2")
debugpy.listen(("127.0.0.1", port))
debugpy.wait_for_client()
execute_on_main_thread(lambda: debugpy.debug_this_thread())
[docs]
class UIPluginInHeadlessError(Exception):
"""
Error thrown when trying to load a UI plugin in a headless Binary Ninja installation.
"""
def __init__(self, *args, **kwargs):
Exception.__init__(self, *args, **kwargs)
[docs]
def fuzzy_match_single(target, query) -> Optional[int]:
"""
Fuzzy match a string against a query string. Returns a score that is higher for
a more confident match, or None if the query does not match the target string.
:param target: Target (larger) string
:param query: Query (smaller) string
:return: Confidence of match, or None if the string doesn't match
"""
result = core.BNFuzzyMatchSingle(target, query)
if result == 0:
return None
return result
# Load Collaboration scripts from Ultimate (they are bundled in shipping builds)
try:
from . import collaboration
except ImportError:
pass