126 prepare to release on community plugin repository (#132)

* Add dependency extraction and plugin JSON update functionality

* Update minimum binary ninja version to 5336

* Update minimum binary ninja version to 5747

* Add dependency extraction and plugin JSON update functionality

* Clean some resources

* Fixed some dependencies

* Update to new description

* Prepare version for new release

* Add release badge

* Added installinstructions to pass validation

* Add link

* Add requirements.txt generation and remove dependencies from plugin.json

* Update README and plugin.json with new logo and improved long description

* Replaced other images too

* Minor cleanups

* Minor cleanups

* Refactor README processing to keep only the first section and remove others; update long description in plugin.json for clarity.

* Update networkx dependency to use the default extra in pyproject.toml and requirements.txt

---------

Co-authored-by: wizche <sergio.paganoni@gmail.com>
This commit is contained in:
Damian Pfammatter
2025-04-25 11:26:10 +02:00
committed by GitHub
co-authored by wizche
parent 7c99f099e9
commit 9891992ec6
9 changed files with 254 additions and 49 deletions
+3 -2
View File
@@ -1,8 +1,9 @@
[![Publish Release](https://github.com/pdamian/mole/actions/workflows/release.yml/badge.svg)](https://github.com/pdamian/mole/actions/workflows/release.yml)
[![Release](https://img.shields.io/github/v/release/cyber-defence-campus/mole)](https://img.shields.io/github/v/release/cyber-defence-campus/mole)
# Mole
<p align="center">
<img src="https://github.com/user-attachments/assets/e246bfe8-b871-42ff-88d7-a2ad9fbcf0ee" style="width: 256px; max-width: 100%; height: auto" alt="Mole Logo"/>
<img src="https://i.postimg.cc/mrcXH34C/image-1.png" alt="Mole Logo"/>
</p>
**_Mole_** is a *Binary Ninja* plugin designed to identify **interesting paths** in binaries. It performs **static backward slicing** on variables using *Binary Ninja*'s [*Medium Level Intermediate Language* (*MLIL*)](https://docs.binary.ninja/dev/bnil-mlil.html) in its *Static Single Assignment* (*SSA*) form.
@@ -12,7 +13,7 @@ In *Mole*, a **path** refers to the flow of data between a defined source and si
The following list highlights some of *Mole*'s current **features**:
- **Operation Mode**: *Mole* can be run either within *Binary Ninja*'s UI or in headless mode. Headless mode is particularly useful for scripted analysis across a large number of binaries. Conversely, using *Mole* within the UI is ideal for closely investigating detected paths.
- **Path Identification**:
- **Configuration**: *Mole* enables the definition of relevant source and sink functions in configuration files (see TODO). This provides flexibility in selecting sources and sinks based on the specific usage scenario.
- **Configuration**: *Mole* enables the definition of relevant source and sink functions in configuration files (see [Usage](./docs/02-Usage.md#definition-of-source-and-sink-functions)). This provides flexibility in selecting sources and sinks based on the specific usage scenario.
- **Exploration**: To better understand a path and examine its characteristics, all instructions along the path can be printed or visually highlighted within *Binary Ninja*. Additionally, a side-by-side comparison of two paths can be displayed to quickly identify differences. Similar to instructions, a path's sequence of function calls can be printed or even visualized as a graph.
- **Grouping**: To facilitate the identification of similar paths, *Mole* supports multiple grouping strategies. Currently, paths can be grouped based on matching source and sink functions, or by identical call sequences. New custom grouping strategies can easily be added to extend and customize this functionality (see [Customization](./docs/03-Customization.md#path-grouping-strategy)).
- **Persistence**: Discovered paths can be annotated for clarity or removed if deemed irrelevant. To preserve analysis progress, paths can be saved directly to the target binary's database (*Binary Ninja*'s `.bndb` format). Paths can also be exported - for example, when performing headless analysis across many binaries on a file system, allowing identified paths to be later imported for easier exploration within *Binary Ninja*.
+101
View File
@@ -0,0 +1,101 @@
from typing import List
import json
import os
import pathlib
import tomli
def extract_dependencies(pyproject_path: pathlib.Path) -> List[str]:
"""Extract pip dependencies from pyproject.toml file."""
with open(pyproject_path, "rb") as f:
try:
pyproject_data = tomli.load(f)
except Exception as e:
print(f"Error parsing pyproject.toml: {str(e):s}")
return []
# Check for dependencies in different possible locations
dependencies = []
# Check for project.dependencies (PEP 621 format)
if "project" in pyproject_data and "dependencies" in pyproject_data["project"]:
dependencies.extend(pyproject_data["project"]["dependencies"])
# Check for tool.poetry.dependencies (Poetry format)
elif "tool" in pyproject_data and "poetry" in pyproject_data["tool"]:
poetry_deps = pyproject_data["tool"]["poetry"].get("dependencies", {})
# Filter out python dependency and convert dict to requirements format
for pkg, version in poetry_deps.items():
if pkg != "python":
if isinstance(version, str):
dependencies.append(f"{pkg:s}=={version:s}")
elif isinstance(version, dict) and "version" in version:
dependencies.append(f"{pkg:s}=={version['version']:s}")
else:
dependencies.append(pkg)
# Check for tool.flit.metadata.requires (Flit format)
elif "tool" in pyproject_data and "flit" in pyproject_data["tool"]:
if "metadata" in pyproject_data["tool"]["flit"]:
flit_deps = pyproject_data["tool"]["flit"]["metadata"].get("requires", [])
dependencies.extend(flit_deps)
return sorted(dependencies)
def update_plugin_json(plugin_json_path: pathlib.Path, dependencies: List[str]) -> None:
"""Update the dependencies field in plugin.json"""
try:
with open(plugin_json_path, "r") as f:
plugin_data = json.load(f)
# Update the dependencies field
if "dependencies" not in plugin_data:
plugin_data["dependencies"] = {}
plugin_data["dependencies"]["pip"] = dependencies
# Write back to the file
with open(plugin_json_path, "w") as f:
json.dump(plugin_data, f, indent=2)
print(f"Updated dependencies in '{str(plugin_json_path):s}'")
except Exception as e:
print(f"Error updating plugin.json: {str(e):s}")
return
def create_requirements_txt(
requirements_path: pathlib.Path, dependencies: List[str]
) -> None:
"""Create a requirements.txt file from the dependencies"""
try:
with open(requirements_path, "w") as f:
for dep in dependencies:
f.write(f"{dep:s}\n")
print(f"Created requirements.txt at '{str(requirements_path):s}'")
except Exception as e:
print(f"Error creating requirements.txt: {str(e):s}")
return
def main() -> None:
# Get the directory of the current script
script_dir = pathlib.Path(os.path.dirname(os.path.abspath(__file__)))
# pyproject.toml and plugin.json are in the parent folder of the script
pyproject_path = script_dir.parent / "pyproject.toml"
requirements_path = script_dir.parent / "requirements.txt"
if not pyproject_path.exists():
print("Error: pyproject.toml not found")
return
dependencies = extract_dependencies(pyproject_path)
# Create requirements.txt
create_requirements_txt(requirements_path, dependencies)
return
if __name__ == "__main__":
main()
+127
View File
@@ -0,0 +1,127 @@
from typing import Optional
import json
import os
import re
def readme_to_json_string(
readme_filename="README.md", save_test_file=True
) -> Optional[str]:
"""
Reads the README file and returns its content as a JSON-escaped string.
Only keeps the first section content and removes all other sections.
Args:
readme_filename (str): The name of the README file.
save_test_file (bool): Whether to save a test file with processed
content.
Returns:
str: The JSON-escaped string content of the README file (including
quotes), or None if the file cannot be read.
"""
script_dir = os.path.dirname(os.path.abspath(__file__))
parent_dir = os.path.dirname(script_dir)
readme_path = os.path.join(parent_dir, readme_filename)
if not os.path.exists(readme_path):
print(f"Error: File '{readme_path:s}' not found")
return None
try:
with open(readme_path, "r", encoding="utf-8") as f:
content = f.read()
# Find the first occurrence of '#' indicating a heading (the root heading)
start_index = content.find("#")
if start_index != -1:
# Find the end of the first heading (next newline)
end_of_first_heading = content.find("\n", start_index)
if end_of_first_heading != -1:
# Skip the root heading and start from the next line
filtered_content = content[end_of_first_heading + 1 :].lstrip()
else:
# If no newline after heading (unlikely), use original content
filtered_content = content
else:
# If no heading found, use the original content
filtered_content = content
# Find the second heading (which marks the end of first section)
second_heading_index = filtered_content.find("\n#")
if second_heading_index != -1:
# Only keep content up to the second heading
processed_content = filtered_content[:second_heading_index].strip()
else:
# If no second heading, keep all content
processed_content = filtered_content
# Replace markdown links [text](url) with just the text
processed_content = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", processed_content)
# Save the processed content to a test file if requested
if save_test_file:
test_file_path = os.path.join("/tmp", "processed_readme.md")
with open(test_file_path, "w", encoding="utf-8") as test_file:
test_file.write(processed_content)
print(f"Saved processed markdown to '{test_file_path:s}'")
# Use json.dumps to correctly escape the string for JSON embedding
json_string = json.dumps(processed_content)
return json_string
except Exception as e:
print(f"Error reading or processing file '{readme_path:s}': {str(e):s}")
return None
def update_plugin_json(readme_content: str) -> bool:
"""
Updates the longdescription attribute in the plugin.json file.
Args:
readme_content (str): The README content to use for longdescription
Returns:
bool: True if successful, False otherwise
"""
script_dir = os.path.dirname(os.path.abspath(__file__))
parent_dir = os.path.dirname(script_dir)
plugin_json_path = os.path.join(parent_dir, "plugin.json")
if not os.path.exists(plugin_json_path):
print(f"Error: plugin.json not found at '{plugin_json_path:s}'")
return False
try:
# Read the existing plugin.json
with open(plugin_json_path, "r", encoding="utf-8") as f:
plugin_data = json.load(f)
# Update the longdescription attribute
plugin_data["longdescription"] = readme_content
# Write back to the file with pretty formatting
with open(plugin_json_path, "w", encoding="utf-8") as f:
json.dump(plugin_data, f, indent=2)
print(f"Successfully updated longdescription in '{plugin_json_path:s}'")
return True
except Exception as e:
print(f"Error updating plugin.json: {str(e):s}")
return False
if __name__ == "__main__":
json_escaped_readme_with_quotes = readme_to_json_string()
if json_escaped_readme_with_quotes:
# We need the raw content *without* the extra quotes added by the first
# json.dumps because we are embedding it into another JSON structure.
# json.loads will remove the outer quotes and unescape the content.
readme_content = json.loads(json_escaped_readme_with_quotes)
# Update the plugin.json file instead of printing
update_plugin_json(readme_content)
else:
# Error message already printed by readme_to_json_string
pass
+2 -2
View File
@@ -4,7 +4,7 @@ This section provides some guidance on how to use *Mole*.
*Mole* is implemented as a *Binary Ninja* sidebar, with a dedicated **_Configure_** tab that contains all plugin settings. Within this tab, the *Sources* and *Sinks* sub-tabs allow you to enable or disable available source and sink functions, respectively. General settings can be configured in the *Settings* sub-tab.
<p align="center">
<img src="https://github.com/user-attachments/assets/b79e089d-fc3f-4f75-bc13-59410e17c437" style="width: auto; max-width: 100%; height: auto" alt="Mole Configure Tab"/>
<img src="https://i.postimg.cc/65ZC1MJW/configure-tab.png" alt="Mole Configure Tab"/>
</p>
Clicking the *Save* button stores the current configuration and writes it to the file `conf/000-mole.yml` (see the table below). These saved values are also applied when *Mole* is run in [headless mode](02-Usage.md#headless-mode), unless they are overwritten by command-line arguments. The *Reset* button restores all configuration options to their default values.
@@ -90,7 +90,7 @@ Beyond the textual log output, *Mole* also summarizes identified paths in the *R
These features help users better inspect and validate identified paths during analysis.
<p align="center">
<img src="https://github.com/user-attachments/assets/53ab2e81-91ce-42f7-ac5e-fb09eac9a1cc" style="width: auto; max-width: 100%; height: auto" alt="Mole UI Interesting Path"/>
<img src="https://i.postimg.cc/7YLLQVCC/interesting-paths.png" alt="Mole UI Paths"/>
</p>
----------------------------------------------------------------------------------------------------
+11 -32
View File
@@ -2,20 +2,17 @@
"pluginmetadataversion": 2,
"name": "Mole",
"type": [
// "core",
"ui",
// "architecture",
// "binaryview",
"helper"
],
"api": [
"python3"
],
"description": "This is a short description meant to fit on one line.",
"longdescription": "",
"description": "Uncover interesting paths using static backward slicing",
"longdescription": "<p align=\"center\">\n <img src=\"https://i.postimg.cc/mrcXH34C/image-1.png\" alt=\"Mole Logo\"/>\n</p>\n\n**_Mole_** is a *Binary Ninja* plugin designed to identify **interesting paths** in binaries. It performs **static backward slicing** on variables using *Binary Ninja*'s *Medium Level Intermediate Language* (*MLIL*) in its *Static Single Assignment* (*SSA*) form.\n\nIn *Mole*, a **path** refers to the flow of data between a defined source and sink. What constitutes an \"interesting\" path depends on the analysis goals. For instance, when searching for **vulnerabilities**, one might look for paths where untrusted inputs (sources) influence sensitive operations (sinks) in potentially dangerous ways.\n\nThe following list highlights some of *Mole*'s current **features**:\n- **Operation Mode**: *Mole* can be run either within *Binary Ninja*'s UI or in headless mode. Headless mode is particularly useful for scripted analysis across a large number of binaries. Conversely, using *Mole* within the UI is ideal for closely investigating detected paths.\n- **Path Identification**:\n - **Configuration**: *Mole* enables the definition of relevant source and sink functions in configuration files (see Usage). This provides flexibility in selecting sources and sinks based on the specific usage scenario.\n - **Exploration**: To better understand a path and examine its characteristics, all instructions along the path can be printed or visually highlighted within *Binary Ninja*. Additionally, a side-by-side comparison of two paths can be displayed to quickly identify differences. Similar to instructions, a path's sequence of function calls can be printed or even visualized as a graph.\n - **Grouping**: To facilitate the identification of similar paths, *Mole* supports multiple grouping strategies. Currently, paths can be grouped based on matching source and sink functions, or by identical call sequences. New custom grouping strategies can easily be added to extend and customize this functionality (see Customization).\n - **Persistence**: Discovered paths can be annotated for clarity or removed if deemed irrelevant. To preserve analysis progress, paths can be saved directly to the target binary's database (*Binary Ninja*'s `.bndb` format). Paths can also be exported - for example, when performing headless analysis across many binaries on a file system, allowing identified paths to be later imported for easier exploration within *Binary Ninja*.\n- **Inter-Procedural Variable Slicing**: *Mole* supports slicing *MLIL variables* across function boundaries - a task that presents several challenges. For instance, statically determining a function's effective caller(s) is often difficult or even impossible. As a result, the implemented approach is an approximation. While not perfect, it performs reasonably well across a wide range of practical scenarios.\n- **Basic Pointer Analysis**: *Mole* currently implements a simplified strategy for tracking pointer usage. Like inter-procedural slicing, this approach is a simplification with inherent limitations. Nevertheless, it performs well in many practical cases and is planned to be improved in future versions.",
"license": {
"name": "MIT",
"text": "Copyright (c) <year> <copyright holders>\n\nPermission 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:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE 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."
"name": "Apache-2.0",
"text": "Copyright (c) 2025 Damian Pfammatter and Sergio Paganoni\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License."
},
"platforms": [
"Darwin",
@@ -23,29 +20,11 @@
"Windows"
],
"installinstructions": {
"Darwin": "Install the following pip packages: ...\n\nInstall the following brew packages: ...",
"Linux": "Install the following pip packages: ...\n\nInstall the following apt packages: ...",
"Windows": "Install the following pip packages: ...\n\nInstall the following libraries: ..."
"Darwin": "",
"Linux": "",
"Windows": ""
},
"dependencies": {
"pip": [
// "array",
// "of",
// "pip",
// "dependencies"
],
"apt": [
// "apt",
// "packages"
],
"installers": [
// "https://bogus-domain/this-package.exe"
],
"other": [
// "The sample plugin requires [this random package](https://bogus-domain/this-package/) be installed."
]
},
"version": "0.0.1",
"author": "Damian Pfammatter",
"minimumbinaryninjaversion": 3164
}
"version": "0.1.0",
"author": "Damian Pfammatter and Sergio Paganoni",
"minimumbinaryninjaversion": 6455
}
+5 -5
View File
@@ -7,8 +7,8 @@ packages = ["mole"]
[project]
name = "mole"
version = "0.0.7"
description = "A Binary Ninja plugin for vulneraiblity discovery"
version = "0.1.0"
description = "A Binary Ninja plugin to identify interesting paths using static backward slicing"
authors = [
{name = "Damian Pfammatter"},
{name = "Sergio Paganoni"}
@@ -17,8 +17,7 @@ requires-python = ">=3.10"
dependencies = [
"ijson==3.3.0",
"lark==1.2.2",
"networkx==3.4.2",
"numpy==2.2.2",
"networkx[default]==3.4.2",
"PyYAML==6.0.2",
"termcolor==2.4.0",
]
@@ -27,7 +26,8 @@ dependencies = [
develop = [
"debugpy==1.8.1",
"pre_commit==4.2.0",
"ruff==0.9.9"
"ruff==0.9.9",
"tomli==2.2.1"
]
[project.scripts]
+5
View File
@@ -0,0 +1,5 @@
PyYAML==6.0.2
ijson==3.3.0
lark==1.2.2
networkx[default]==3.4.2
termcolor==2.4.0
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 32 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 44 KiB