mirror of
https://github.com/cyber-defence-campus/mole
synced 2026-06-20 13:19:21 +00:00
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:
co-authored by
wizche
parent
7c99f099e9
commit
9891992ec6
@@ -1,8 +1,9 @@
|
||||
[](https://github.com/pdamian/mole/actions/workflows/release.yml)
|
||||
[](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*.
|
||||
|
||||
@@ -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()
|
||||
@@ -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
@@ -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
@@ -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
@@ -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]
|
||||
|
||||
@@ -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 |
Reference in New Issue
Block a user