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 c248078 commit a6a00be
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 149 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())
1 change: 0 additions & 1 deletion capa/features/extractors/ida/extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from typing import List, Tuple, Iterator

import idaapi
import ida_nalt

import capa.ida.helpers
import capa.features.extractors.elf
Expand Down
2 changes: 1 addition & 1 deletion capa/ida/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
import idc
import idaapi
import ida_ida
import idautils
import ida_nalt
import idautils
import ida_bytes
import ida_loader
from netnode import netnode
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
Loading

0 comments on commit a6a00be

Please sign in to comment.