Skip to content

Commit

Permalink
binary ninja: search for API using XDG desktop entry
Browse files Browse the repository at this point in the history
ref #2376
  • Loading branch information
williballenthin committed Sep 23, 2024
1 parent ac94f87 commit 666693e
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 148 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### New Features

- add IDA v9.0 backend via idalib #2376 @williballenthin
- locate Binary Ninja API using XDG Desktop Entries #2376 @williballenthin

### Breaking Changes

Expand Down
152 changes: 147 additions & 5 deletions capa/features/extractors/binja/find_binja_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,24 @@
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
import os
import sys
import logging
import subprocess
import importlib.util
from typing import Optional
from pathlib import Path

logger = logging.getLogger(__name__)


# When the script gets executed as a standalone executable (via PyInstaller), `import binaryninja` does not work because
# we have excluded the binaryninja module in `pyinstaller.spec`. The trick here is to call the system Python and try
# to find out the path of the binaryninja module that has been installed.
# Note, including the binaryninja module in the `pyinstaller.spec` would not work, since the binaryninja module tries to
# find the binaryninja core e.g., `libbinaryninjacore.dylib`, using a relative path. And this does not work when the
# binaryninja module is extracted by the PyInstaller.
code = r"""
CODE = r"""
from pathlib import Path
from importlib import util
spec = util.find_spec('binaryninja')
Expand All @@ -26,10 +34,144 @@
"""


def find_binja_path() -> Path:
raw_output = subprocess.check_output(["python", "-c", code]).decode("ascii").strip()
return Path(bytes.fromhex(raw_output).decode("utf8"))
def find_binaryninja_path_via_subprocess() -> Optional[Path]:
raw_output = subprocess.check_output(["python", "-c", CODE]).decode("ascii").strip()
output = bytes.fromhex(raw_output).decode("utf8")
if not output.strip():
return None
return Path(output)


def get_desktop_entry(name: str) -> Optional[Path]:
"""
Find the path for the given XDG Desktop Entry name.
Like:
>> get_desktop_entry("com.vector35.binaryninja.desktop")
Path("~/.local/share/applications/com.vector35.binaryninja.desktop")
"""
assert sys.platform in ("linux", "linux2")
assert name.endswith(".desktop")

default_data_dirs = f"/usr/share/applications:{Path.home()}/.local/share"
data_dirs = os.environ.get("XDG_DATA_DIRS", default_data_dirs)
for data_dir in data_dirs.split(":"):
applications = Path(data_dir) / "applications"
for application in applications.glob("*.desktop"):
if application.name == name:
return application

return None


def get_binaryninja_path(desktop_entry: Path) -> Optional[Path]:
# from: Exec=/home/wballenthin/software/binaryninja/binaryninja %u
# to: /home/wballenthin/software/binaryninja/
for line in desktop_entry.read_text(encoding="utf-8").splitlines():
if not line.startswith("Exec="):
continue

if not line.endswith("binaryninja %u"):
continue

binaryninja_path = Path(line[len("Exec=") : -len("binaryninja %u")])
if not binaryninja_path.exists():
return None

return binaryninja_path

return None


def validate_binaryninja_path(binaryninja_path: Path) -> bool:
if not binaryninja_path:
return False

module_path = binaryninja_path / "python"
if not module_path.is_dir():
return False

if not (module_path / "binaryninja" / "__init__.py").is_file():
return False

return True


def find_binaryninja() -> Optional[Path]:
binaryninja_path = find_binaryninja_path_via_subprocess()
if not binaryninja_path or not validate_binaryninja_path(binaryninja_path):
if sys.platform == "linux" or sys.platform == "linux2":
# ok
logger.debug("detected OS: linux")
elif sys.platform == "darwin":
logger.warning("unsupported platform to find Binary Ninja: %s", sys.platform)
return False
elif sys.platform == "win32":
logger.warning("unsupported platform to find Binary Ninja: %s", sys.platform)
return False
else:
logger.warning("unsupported platform to find Binary Ninja: %s", sys.platform)
return False

desktop_entry = get_desktop_entry("com.vector35.binaryninja.desktop")
if not desktop_entry:
return None
logger.debug("found Binary Ninja application: %s", desktop_entry)

binaryninja_path = get_binaryninja_path(desktop_entry)
if not binaryninja_path:
return None

if not validate_binaryninja_path(binaryninja_path):
return None

logger.debug("found Binary Ninja installation: %s", binaryninja_path)

return binaryninja_path / "python"


def is_binaryninja_installed() -> bool:
"""Is the binaryninja module ready to import?"""
try:
return importlib.util.find_spec("binaryninja") is not None
except ModuleNotFoundError:
return False


def has_binaryninja() -> bool:
if is_binaryninja_installed():
logger.debug("found installed Binary Ninja API")
return True

logger.debug("Binary Ninja API not installed, searching...")

binaryninja_path = find_binaryninja()
if not binaryninja_path:
logger.debug("failed to find Binary Ninja installation")

logger.debug("found Binary Ninja API: %s", binaryninja_path)
return binaryninja_path is not None


def load_binaryninja() -> bool:
try:
import binaryninja

return True
except ImportError:
binaryninja_path = find_binaryninja()
if not binaryninja_path:
return False

sys.path.append(binaryninja_path.absolute().as_posix())
try:
import binaryninja # noqa: F401 unused import

return True
except ImportError:
return False


if __name__ == "__main__":
print(find_binja_path())
print(find_binaryninja_path_via_subprocess())
2 changes: 1 addition & 1 deletion capa/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def stdout_redirector(stream):
*But*, this only works on Linux! Otherwise will silently still write to stdout.
So, try to upstream the fix when possible.
Via: https://eli.thegreenplace.net/2015/redirecting-all-kinds-of-stdout-in-python/
"""
if sys.platform not in ("linux", "linux2"):
Expand Down
34 changes: 11 additions & 23 deletions capa/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
# See the License for the specific language governing permissions and limitations under the License.
import io
import os
import sys
import logging
import datetime
import contextlib
Expand Down Expand Up @@ -239,24 +238,15 @@ def get_extractor(
return capa.features.extractors.dnfile.extractor.DnfileFeatureExtractor(input_path)

elif backend == BACKEND_BINJA:
import capa.helpers
from capa.features.extractors.binja.find_binja_api import find_binja_path

# When we are running as a standalone executable, we cannot directly import binaryninja
# We need to fist find the binja API installation path and add it into sys.path
if capa.helpers.is_running_standalone():
bn_api = find_binja_path()
if bn_api.exists():
sys.path.append(str(bn_api))

try:
import binaryninja
from binaryninja import BinaryView
except ImportError:
raise RuntimeError(
"Cannot import binaryninja module. Please install the Binary Ninja Python API first: "
+ "https://docs.binary.ninja/dev/batch.html#install-the-api)."
)
import capa.features.extractors.binja.find_binja_api as finder

if not finder.has_binaryninja():
raise RuntimeError("cannot find Binary Ninja API module.")

if not finder.load_binaryninja():
raise RuntimeError("failed to load Binary Ninja API module.")

import binaryninja

import capa.features.extractors.binja.extractor

Expand All @@ -271,7 +261,7 @@ def get_extractor(
raise UnsupportedOSError()

with console.status("analyzing program...", spinner="dots"):
bv: BinaryView = binaryninja.load(str(input_path))
bv: binaryninja.BinaryView = binaryninja.load(str(input_path))
if bv is None:
raise RuntimeError(f"Binary Ninja cannot open file {input_path}")

Expand Down Expand Up @@ -327,9 +317,7 @@ def get_extractor(
import capa.features.extractors.ida.idalib as idalib

if not idalib.has_idalib():
raise RuntimeError(
"cannot find IDA idalib module."
)
raise RuntimeError("cannot find IDA idalib module.")

if not idalib.load_idalib():
raise RuntimeError("failed to load IDA idalib module.")
Expand Down
120 changes: 1 addition & 119 deletions scripts/detect-backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,136 +6,18 @@
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.

import os
import sys
import logging
import importlib.util
from typing import Optional
from pathlib import Path

import rich
import rich.table

from capa.features.extractors.ida.idalib import find_idalib, load_idalib, is_idalib_installed
from capa.features.extractors.binja.find_binja_api import find_binaryninja, load_binaryninja, is_binaryninja_installed

logger = logging.getLogger(__name__)


def get_desktop_entry(name: str) -> Optional[Path]:
"""
Find the path for the given XDG Desktop Entry name.
Like:
>> get_desktop_entry("com.vector35.binaryninja.desktop")
Path("~/.local/share/applications/com.vector35.binaryninja.desktop")
"""
assert sys.platform in ("linux", "linux2")
assert name.endswith(".desktop")

default_data_dirs = f"/usr/share/applications:{Path.home()}/.local/share"
data_dirs = os.environ.get("XDG_DATA_DIRS", default_data_dirs)
for data_dir in data_dirs.split(":"):
applications = Path(data_dir) / "applications"
for application in applications.glob("*.desktop"):
if application.name == name:
return application

return None


def get_binaryninja_path(desktop_entry: Path) -> Optional[Path]:
# from: Exec=/home/wballenthin/software/binaryninja/binaryninja %u
# to: /home/wballenthin/software/binaryninja/
for line in desktop_entry.read_text(encoding="utf-8").splitlines():
if not line.startswith("Exec="):
continue

if not line.endswith("binaryninja %u"):
continue

binaryninja_path = Path(line[len("Exec=") : -len("binaryninja %u")])
if not binaryninja_path.exists():
return None

return binaryninja_path

return None


def find_binaryninja() -> Optional[Path]:
if sys.platform == "linux" or sys.platform == "linux2":
# ok
logger.debug("detected OS: linux")
elif sys.platform == "darwin":
raise NotImplementedError(f"unsupported platform: {sys.platform}")
elif sys.platform == "win32":
raise NotImplementedError(f"unsupported platform: {sys.platform}")
else:
raise NotImplementedError(f"unsupported platform: {sys.platform}")

desktop_entry = get_desktop_entry("com.vector35.binaryninja.desktop")
if not desktop_entry:
return None
logger.debug("found Binary Ninja application: %s", desktop_entry)

binaryninja_path = get_binaryninja_path(desktop_entry)
if not binaryninja_path:
return None
logger.debug("found Binary Ninja installation: %s", binaryninja_path)

module_path = binaryninja_path / "python"
if not module_path.exists():
return None

if not (module_path / "binaryninja" / "__init__.py").exists():
return None

return module_path


def is_binaryninja_installed() -> bool:
"""Is the binaryninja module ready to import?"""
try:
return importlib.util.find_spec("binaryninja") is not None
except ModuleNotFoundError:
return False


def has_binaryninja() -> bool:
if is_binaryninja_installed():
logger.debug("found installed Binary Ninja API")
return True

logger.debug("Binary Ninja API not installed, searching...")

binaryninja_path = find_binaryninja()
if not binaryninja_path:
logger.debug("failed to find Binary Ninja installation")

logger.debug("found Binary Ninja API: %s", binaryninja_path)
return binaryninja_path is not None


def load_binaryninja() -> bool:
try:
import binaryninja

return True
except ImportError:
binaryninja_path = find_binaryninja()
if not binaryninja_path:
return False

sys.path.append(binaryninja_path.absolute().as_posix())
try:
import binaryninja # noqa: F401 unused import

return True
except ImportError:
return False


def is_vivisect_installed() -> bool:
try:
return importlib.util.find_spec("vivisect") is not None
Expand Down

0 comments on commit 666693e

Please sign in to comment.