Skip to content

Commit

Permalink
Add EVM support (#26)
Browse files Browse the repository at this point in the history
* add basic evm support

* add evm run settings

* add evm run dialog

* refactor settings + evm run dialog improvements

* run dialog refactoring

* change deletion method

* add allow_repeats option

* add is_file_path

* make solc path a settings option

* update readme

* Update README.rst

Co-authored-by: Eric Kilmer <[email protected]>

* Update mui/dockwidgets/list_widget.py

Co-authored-by: Eric Kilmer <[email protected]>

* Update README.rst

Co-authored-by: Sonya <[email protected]>

* remove unnecessary print statements

* add comment for CryticCompile workaround

* Update README.rst to fix URL link

* reduce image size

Co-authored-by: Eric Kilmer <[email protected]>
Co-authored-by: Sonya <[email protected]>
  • Loading branch information
3 people authored Sep 15, 2021
1 parent 5097fef commit 18bf4f5
Show file tree
Hide file tree
Showing 12 changed files with 1,135 additions and 420 deletions.
32 changes: 30 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ MUI

With the Manticore User Interface (MUI) project, we provide a graphical user interface plugin for `Binary Ninja <https://binary.ninja/>`_ to allow users to easily interact with and view progress of the `Manticore symbolic execution engine <https://github.com/trailofbits/manticore>`_ for analysis of smart contracts and native binaries.

❗ATTENTION❗ This project is under active development and may be unstable or unusable. Notably, EVM is yet to be supported.
❗ATTENTION❗ This project is under active development and may be unstable or unusable. Please open an issue if you have any difficulties using the existing features. New feature development will be considered on a case by case basis.

Requirements
------------
Expand All @@ -24,7 +24,7 @@ Installation

MUI requires a copy of Binary Ninja with a GUI. Currently we are testing against the latest ``dev`` release(s) (``2.4.2901-dev`` at time of writing).

Manticore only operates on native binaries within a Linux environment. EVM support has only been tested on Mac and Linux.
Manticore only operates on native binaries within a Linux environment. EVM support has only been tested on Mac and Linux, and it requires the installation of `ethersplay <https://github.com/crytic/ethersplay>`_.

Python dependencies are currently managed using ``requirements.txt`` and ``requirements-dev.txt``. You can run ``make init`` to set up a development environment.

Expand Down Expand Up @@ -115,3 +115,31 @@ And the following widgets are available:
:align: center
:alt: Custom Hook Dialog

Usage (EVM)
--------------

EVM support is currently a bit limited. MUI EVM only supports the same feature set as the `Manticore CLI tool <https://github.com/trailofbits/manticore>`_. Available commands include:

- Load Ethereum Contract
- Solve With Manticore / Stop Manticore

And the following views are implemented:

- EVM Run Dialog

The run dialog is shown when you invoke the ``Solve with Manticore`` command. It allows you to configure the various manticore options, and the changes will be saved to the ``bndb`` file.

.. image:: ./screenshots/evm_run_dialog.png
:align: center
:alt: EVM Run Dialog



- Run Report

The report page shows the result of a manticore execution. It displays all the files produced using the Binary Ninja UI.

.. image:: ./screenshots/evm_run_report.png
:align: center
:alt: Run Report

3 changes: 2 additions & 1 deletion mui/constants.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import Final

BINJA_SETTINGS_GROUP: Final[str] = "mui"
BINJA_RUN_SETTINGS_PREFIX: Final[str] = f"{BINJA_SETTINGS_GROUP}.run."
BINJA_NATIVE_RUN_SETTINGS_PREFIX: Final[str] = f"{BINJA_SETTINGS_GROUP}.native_run."
BINJA_HOOK_SETTINGS_PREFIX: Final[str] = f"{BINJA_SETTINGS_GROUP}.hook."
BINJA_EVM_RUN_SETTINGS_PREFIX: Final[str] = f"{BINJA_SETTINGS_GROUP}.evm_run."
142 changes: 142 additions & 0 deletions mui/dockwidgets/list_widget.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
from typing import Callable, List, Optional

from PySide6.QtCore import QObject
from PySide6.QtWidgets import QVBoxLayout, QWidget, QPushButton, QHBoxLayout, QLineEdit, QComboBox


class ListWidget(QWidget):
def __init__(
self,
parent: QWidget = None,
possible_values: Optional[List[str]] = None,
allow_repeats: bool = True,
initial_row_count: int = 0,
validate_fun: Callable[[], None] = lambda: None,
):
QWidget.__init__(self, parent)
self.validate_fun = validate_fun
self.possible_values = possible_values
self.allow_repeats = allow_repeats

self.row_layout = QVBoxLayout()

for i in range(initial_row_count):
self.add_row()

add_btn = QPushButton("+")
add_btn.clicked.connect(lambda: self.add_row())

layout = QVBoxLayout()
layout.addLayout(self.row_layout)
layout.addWidget(add_btn)

layout.setContentsMargins(0, 0, 0, 0)

self.setLayout(layout)

def add_row(self):
"""Adds a new row to the current widget"""

if self.possible_values is None:
input = QLineEdit()
input.editingFinished.connect(lambda: self.validate_fun())
else:
input = QComboBox()

if self.allow_repeats:
input.addItems(self.possible_values)
else:
input.addItems(sorted(set(self.possible_values) - set(self.get_results())))

input.currentIndexChanged.connect(self._on_index_change)

btn = QPushButton("-")
btn.setMaximumWidth(20)

row = QHBoxLayout()
row.addWidget(input)
row.addWidget(btn)

btn.clicked.connect(lambda: self._on_remove_row([input, btn, row]))

self.row_layout.addLayout(row)

if not self.allow_repeats:
self._remove_repeats()

def _on_remove_row(self, elements_to_remove: List[QObject]):
"""Remove a row from the list"""

for x in elements_to_remove:
x.setParent(None)
x.deleteLater()

if not self.allow_repeats:
self._remove_repeats()

def _on_index_change(self):
"""Handle index change"""

self.validate_fun()

if not self.allow_repeats:
self._remove_repeats()

def _remove_repeats(self):
"""Update the available options for each row to prevent repeats"""

assert self.possible_values is not None
assert not self.allow_repeats

not_selected = set(self.possible_values) - set(self.get_results())
for row in self.row_layout.children():
combobox: QComboBox = row.itemAt(0).widget()
curr_text = combobox.currentText()

# need to disable the event listener temporarily in order to
# prevent the same method being called recursively
combobox.currentIndexChanged.disconnect(self._on_index_change)

combobox.clear()
combobox.addItems(sorted(not_selected | {curr_text}))
combobox.setCurrentIndex(combobox.findText(curr_text))

combobox.currentIndexChanged.connect(self._on_index_change)

def set_rows(self, values: List[str]):
"""Sets the rows to given values. Adds and removes rows when needed"""

values_len = len(values)
curr_row_count = len(self.row_layout.children())

# adjust row count
if values_len > curr_row_count:
for _ in range(values_len - curr_row_count):
self.add_row()
elif values_len < curr_row_count:
for _ in range(curr_row_count - values_len):
last_row = self.row_layout.children()[-1]
last_row.itemAt(1).widget().click()

# set values
idx = 0
for row in self.row_layout.children():
if self.possible_values is None:
row.itemAt(0).widget().setText(values[idx])
else:
if values[idx] in self.possible_values:
row.itemAt(0).widget().setCurrentIndex(self.possible_values.index(values[idx]))
idx += 1

def get_results(self) -> List[str]:
"""Get all non-empty row inputs as a string array"""
output = []
for row in self.row_layout.children():
if self.possible_values is None:
text = row.itemAt(0).widget().text()
else:
text = row.itemAt(0).widget().currentText()

if text != "":
output.append(text)
return output
Loading

0 comments on commit 18bf4f5

Please sign in to comment.