Add support to foundry (#1744)

* Add foundry command support

* Add foundry support
pull/1750/head
Nikhil Parasaram 2 years ago committed by GitHub
parent 34aa49c2e5
commit f7e5debeac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 71
      docs/source/tutorial.rst
  2. 30
      mythril/interfaces/cli.py
  3. 1
      mythril/laser/ethereum/call.py
  4. 88
      mythril/mythril/mythril_disassembler.py
  5. 45
      mythril/solidity/soliditycontract.py
  6. 4
      mythril/support/signatures.py

@ -1,11 +1,21 @@
Tutorial Tutorial
====================== ======================
******************************************
Introduction
******************************************
Mythril is a popular security analysis tool for smart contracts. It is an open-source tool that can analyze Ethereum smart contracts and report potential security vulnerabilities in them. By analyzing the bytecode of a smart contract, Mythril can identify and report on possible security vulnerabilities, such as reentrancy attacks, integer overflows, and other common smart contract vulnerabilities.
This tutorial explains how to use Mythril to analyze simple Solidity contracts for security vulnerabilities. A simple contract is one that does not have any imports.
****************************************** ******************************************
Executing Mythril on Simple Contracts Executing Mythril on Simple Contracts
****************************************** ******************************************
We consider a contract simple if it does not have any imports, like the following contract: To start, we consider this simple contract, ``Exceptions``, which has a number of functions, including ``assert1()``, ``assert2()``, and ``assert3()``, that contain Solidity ``assert()`` statements. We will use Mythril to analyze this contract and report any potential vulnerabilities.
.. code-block:: solidity .. code-block:: solidity
@ -51,13 +61,14 @@ We consider a contract simple if it does not have any imports, like the followin
} }
We can execute such a contract by directly using the following command: The sample contract has several functions, some of which contain vulnerabilities. For instance, the ``assert1()`` function contains an assertion violation. To analyze the contract using Mythril, the following command can be used:
.. code-block:: bash .. code-block:: bash
$ myth analyze <file_path> $ myth analyze <file_path>
This execution can give the following output: The output will show the vulnerabilities in the contract. In the case of the "Exceptions" contract, Mythril detected two instances of assertion violations.
.. code-block:: none .. code-block:: none
@ -112,10 +123,8 @@ This execution can give the following output:
Caller: [SOMEGUY], function: assert3(uint256), txdata: 0x546455b50000000000000000000000000000000000000000000000000000000000000017, value: 0x0 Caller: [SOMEGUY], function: assert3(uint256), txdata: 0x546455b50000000000000000000000000000000000000000000000000000000000000017, value: 0x0
We can observe that the function ``assert5(uint256)`` should have an assertion failure One of the functions, ``assert5(uint256)``, should also have an assertion failure, but it is not detected because Mythril's default configuration is to run three transactions.
with the assertion ``assert(input_x > 10)`` which is missing from our output. This can be attributed to To detect this vulnerability, the transaction count can be increased to four using the ``-t`` option, as shown below:
Mythril's default configuration of running three transactions. We can increase the transaction count to 4
using the ``-t <tx_count>``.
.. code-block:: bash .. code-block:: bash
@ -204,6 +213,8 @@ This gives the following execution output:
Caller: [ATTACKER], function: assert5(uint256), txdata: 0x1d5d53dd0000000000000000000000000000000000000000000000000000000000000003, value: 0x0 Caller: [ATTACKER], function: assert5(uint256), txdata: 0x1d5d53dd0000000000000000000000000000000000000000000000000000000000000003, value: 0x0
For the violation in the 4th transaction, the input value should be less than 10. The transaction data generated by Mythril for the For the violation in the 4th transaction, the input value should be less than 10. The transaction data generated by Mythril for the
4th transaction is ``0x1d5d53dd0000000000000000000000000000000000000000000000000000000000000003``, the first 4 bytes ``1d5d53dd`` 4th transaction is ``0x1d5d53dd0000000000000000000000000000000000000000000000000000000000000003``, the first 4 bytes ``1d5d53dd``
correspond to the function signature hence the input generated by Mythril is ``0000000000000000000000000000000000000000000000000000000000000003`` correspond to the function signature hence the input generated by Mythril is ``0000000000000000000000000000000000000000000000000000000000000003``
@ -407,7 +418,10 @@ which can be increased for better results. The default execution-timeout and sol
Executing Mythril on Contracts with Imports Executing Mythril on Contracts with Imports
******************************************************** ********************************************************
Consider the following contract: When using Mythril to analyze a Solidity contract, you may encounter issues related to import statements. Solidity does not have access to the import locations, which can result in errors when compiling the program. In order to provide import information to Solidity, you can use the remappings option in Mythril.
Consider the following Solidity contract, which imports the PRC20 contract from the ``@openzeppelin/contracts/token/PRC20/PRC20.sol`` file:
.. code-block:: solidity .. code-block:: solidity
@ -483,9 +497,13 @@ We encounter the following error:
1 | import "@openzeppelin/contracts/token/PRC20/PRC20.sol"; 1 | import "@openzeppelin/contracts/token/PRC20/PRC20.sol";
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This is because Mythril uses Solidity to compile the program. Solidity does not have access to the import locations.
This import information has to be explicitly provided to Solidity through Mythril. This error occurs because Solidity cannot locate the ``PRC20.sol`` file.
We can do this by providing the remapping information to Mythril as follows: To solve this issue, you need to provide remapping information to Mythril, which will relay it to the Solidity compiler.
Remapping involves mapping an import statement to the path that contains the corresponding file.
In this example, we can map the import statement ``@openzeppelin/contracts/token/PRC20/`` to the path that contains ``PRC20.sol``. Let's assume that the file is located at ``node_modules/PRC20/PRC20.sol``. We can provide the remapping information to Mythril in a JSON file like this:
.. code-block:: json .. code-block:: json
@ -493,32 +511,40 @@ We can do this by providing the remapping information to Mythril as follows:
"remappings": [ "@openzeppelin/contracts/token/PRC20/=node_modules/PRC20/"] "remappings": [ "@openzeppelin/contracts/token/PRC20/=node_modules/PRC20/"]
} }
Here we are mapping the import ``@openzeppelin/contracts/token/PRC20/`` to the path which contains ``PRC20.sol``, which this example This JSON file maps the prefix ``@openzeppelin/contracts/token/PRC20/`` to the path ``node_modules/PRC20/`` in the file system.
assumes as ``node_modules/PRC20``. This instructs the compiler to search for anything with the prefix ``@openzeppelin/contracts/token/PRC20/` ` When you run Mythril, you can use the ``--solc-json`` option to provide the remapping file:
in the path ``node_modules/PRC20`` in our file system. We feed this file to Mythril using ``--solc-json`` argument, which
relays it to the solc compiler.
.. code-block:: bash .. code-block:: bash
$ myth analyze {file_path} --solc-json {json_file_path} $ myth analyze {file_path} --solc-json {json_file_path}
This can effectively execute the file since the Solidity compiler can locate `PRC20.sol`. For more information on remappings, you can With this command, Mythril will be able to locate the ``PRC20.sol`` file, and the analysis should proceed without errors.
refer to `Solc docs <https://docs.soliditylang.org/en/v0.8.14/using-the-compiler.html#base-path-and-import-remapping>`_.
For more information on remappings, you can refer to the `Solidity documentation <https://docs.soliditylang.org/en/v0.8.14/using-the-compiler.html#base-path-and-import-remapping>`_.
******************************************************** ********************************************************
Executing Mythril by Restricting Transaction Sequences Executing Mythril by Restricting Transaction Sequences
******************************************************** ********************************************************
Mythril is a security analysis tool that can be used to search certain transaction sequences.
The `--transaction-sequences` argument can be used to direct the search.
You should provide a list of transactions that are sequenced in the same order that they will be executed in the contract.
For example, suppose you want to find vulnerabilities in a contract that executes three transactions, where the first transaction is constrained with ``func_hash1`` and ``func_hash2``,
the second transaction is constrained with ``func_hash2`` and ``func_hash3``, and the final transaction is unconstrained on any function. You would provide ``--transaction-sequences [[func_hash1,func_hash2], [func_hash2,func_hash3],[]]`` as an argument to Mythril.
You can use ``-1`` as a proxy for the hash of the `fallback()` function and ``-2`` as a proxy for the hash of the ``receive()`` function.
Here is an example contract that demonstrates how to use Mythril with ``--transaction-sequences``.
Consider the following contract:
You can use Mythril to search certain transaction sequences. You can use ``--transaction-sequences`` argument to direct the search.
An example usage is ``[[func_hash1,func_hash2], [func_hash2,func_hash3],[]]`` where the first transaction is constrained with ``func_hash1`` and ``func_hash2``, the second tx is constrained with ``func_hash2`` and ``func_hash3`` and the final transaction is unconstrained on any function.
Use ``-1`` as a proxy for the hash of ``fallback()`` function and ``-2`` as a proxy for the hash of ``receive()`` function.
Consider the following contract
.. code-block:: solidity .. code-block:: solidity
pragma solidity 0.5.0; pragma solidity ^0.5.0;
contract Rubixi { contract Rubixi {
@ -671,6 +697,7 @@ Consider the following contract
} }
} }
Since this contract has ``16`` functions, it is infeasible to execute uninteresting functions such as ``feesSeperateFromBalanceApproximately()``. Since this contract has ``16`` functions, it is infeasible to execute uninteresting functions such as ``feesSeperateFromBalanceApproximately()``.
To successfully explore useful transaction sequences we can use Mythril's ``--transaction-sequences`` argument. To successfully explore useful transaction sequences we can use Mythril's ``--transaction-sequences`` argument.

@ -38,6 +38,7 @@ _ = MythrilPluginLoader()
ANALYZE_LIST = ("analyze", "a") ANALYZE_LIST = ("analyze", "a")
DISASSEMBLE_LIST = ("disassemble", "d") DISASSEMBLE_LIST = ("disassemble", "d")
FOUNDRY_LIST = ("foundry", "f")
CONCOLIC_LIST = ("concolic", "c") CONCOLIC_LIST = ("concolic", "c")
SAFE_FUNCTIONS_COMMAND = "safe-functions" SAFE_FUNCTIONS_COMMAND = "safe-functions"
@ -53,6 +54,7 @@ log = logging.getLogger(__name__)
COMMAND_LIST = ( COMMAND_LIST = (
ANALYZE_LIST ANALYZE_LIST
+ DISASSEMBLE_LIST + DISASSEMBLE_LIST
+ FOUNDRY_LIST
+ CONCOLIC_LIST + CONCOLIC_LIST
+ ( + (
READ_STORAGE_COMNAND, READ_STORAGE_COMNAND,
@ -308,7 +310,19 @@ def main() -> None:
) )
create_concolic_parser(concolic_parser) create_concolic_parser(concolic_parser)
subparsers.add_parser( foundry_parser = subparsers.add_parser(
FOUNDRY_LIST[0],
help="Triggers the analysis of the smart contract",
parents=[
rpc_parser,
utilities_parser,
output_parser,
],
aliases=FOUNDRY_LIST[1:],
formatter_class=RawTextHelpFormatter,
)
list_detectors_parser = subparsers.add_parser(
LIST_DETECTORS_COMMAND, LIST_DETECTORS_COMMAND,
parents=[output_parser], parents=[output_parser],
help="Lists available detection modules", help="Lists available detection modules",
@ -328,10 +342,11 @@ def main() -> None:
subparsers.add_parser( subparsers.add_parser(
VERSION_COMMAND, parents=[output_parser], help="Outputs the version" VERSION_COMMAND, parents=[output_parser], help="Outputs the version"
) )
create_read_storage_parser(read_storage_parser) create_read_storage_parser(read_storage_parser)
create_hash_to_addr_parser(contract_hash_to_addr) create_hash_to_addr_parser(contract_hash_to_addr)
create_func_to_hash_parser(contract_func_to_hash) create_func_to_hash_parser(contract_func_to_hash)
create_foundry_parser(foundry_parser)
subparsers.add_parser(HELP_COMMAND, add_help=False) subparsers.add_parser(HELP_COMMAND, add_help=False)
# Get config values # Get config values
@ -586,6 +601,12 @@ def create_analyzer_parser(analyzer_parser: ArgumentParser):
add_analysis_args(options) add_analysis_args(options)
def create_foundry_parser(foundry_parser: ArgumentParser):
add_graph_commands(foundry_parser)
options = foundry_parser.add_argument_group("options")
add_analysis_args(options)
def validate_args(args: Namespace): def validate_args(args: Namespace):
""" """
Validate cli args Validate cli args
@ -698,6 +719,9 @@ def load_code(disassembler: MythrilDisassembler, args: Namespace):
address, _ = disassembler.load_from_solidity( address, _ = disassembler.load_from_solidity(
args.solidity_files args.solidity_files
) # list of files ) # list of files
elif args.command in FOUNDRY_LIST:
address, _ = disassembler.load_from_foundry()
else: else:
exit_with_error( exit_with_error(
args.__dict__.get("outform", "text"), args.__dict__.get("outform", "text"),
@ -782,7 +806,7 @@ def execute_command(
except CriticalError as e: except CriticalError as e:
exit_with_error("text", "Analysis error encountered: " + format(e)) exit_with_error("text", "Analysis error encountered: " + format(e))
elif args.command in ANALYZE_LIST: elif args.command in ANALYZE_LIST + FOUNDRY_LIST:
analyzer = MythrilAnalyzer( analyzer = MythrilAnalyzer(
strategy=strategy, disassembler=disassembler, address=address, cmd_args=args strategy=strategy, disassembler=disassembler, address=address, cmd_args=args
) )

@ -37,6 +37,7 @@ def get_call_parameters(
"""Gets call parameters from global state Pops the values from the stack """Gets call parameters from global state Pops the values from the stack
and determines output parameters. and determines output parameters.
:param global_state: state to look in :param global_state: state to look in
:param dynamic_loader: dynamic loader to use :param dynamic_loader: dynamic loader to use
:param with_value: whether to pop the value argument from the stack :param with_value: whether to pop the value argument from the stack

@ -1,11 +1,17 @@
import json
import logging import logging
import os
from pathlib import Path
import re import re
import shutil
import solc import solc
import subprocess
import sys import sys
import os import warnings
from eth_utils import int_to_big_endian
from semantic_version import Version, NpmSpec from semantic_version import Version, NpmSpec
from typing import List, Tuple, Optional from typing import List, Tuple, Optional, TYPE_CHECKING
from mythril.support.support_utils import sha3, zpad from mythril.support.support_utils import sha3, zpad
from mythril.ethereum import util from mythril.ethereum import util
@ -16,10 +22,20 @@ from mythril.support.support_utils import rzpad
from mythril.support.support_args import args from mythril.support.support_args import args
from mythril.ethereum.evmcontract import EVMContract from mythril.ethereum.evmcontract import EVMContract
from mythril.ethereum.interface.rpc.exceptions import ConnectionError from mythril.ethereum.interface.rpc.exceptions import ConnectionError
from mythril.solidity.soliditycontract import SolidityContract, get_contracts_from_file from mythril.solidity.soliditycontract import (
SolidityContract,
get_contracts_from_file,
get_contracts_from_foundry,
)
from mythril.support.support_args import args from mythril.support.support_args import args
from eth_utils import int_to_big_endian
def format_Warning(message, category, filename, lineno, line=""):
return "{}: {}\n\n".format(str(filename), str(message))
warnings.formatwarning = format_Warning
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -152,6 +168,70 @@ class MythrilDisassembler:
) )
return address, self.contracts[-1] # return address and contract object return address, self.contracts[-1] # return address and contract object
def load_from_foundry(self):
project_root = os.getcwd()
cmd = ["forge", "build", "--build-info", "--force"]
with subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
cwd=project_root,
executable=shutil.which(cmd[0]),
) as p:
stdout, stderr = p.communicate()
stdout, stderr = (stdout.decode(), stderr.decode())
if stderr:
log.error(stderr)
build_dir = Path(project_root, "artifacts", "contracts", "build-info")
build_dir = os.path.join(project_root, "artifacts", "contracts", "build-info")
files = os.listdir(build_dir)
address = util.get_indexed_address(0)
files = sorted(
os.listdir(build_dir), key=lambda x: os.path.getmtime(Path(build_dir, x))
)
files = [str(f) for f in files if str(f).endswith(".json")]
if not files:
txt = f"`compile` failed. Can you run it?\n{build_dir} is empty"
raise Exception(txt)
contracts = []
for file in files:
build_info = Path(build_dir, file)
uniq_id = file if ".json" not in file else file[0:-5]
with open(build_info, encoding="utf8") as file_desc:
loaded_json = json.load(file_desc)
targets_json = loaded_json["output"]
version_from_config = loaded_json["solcVersion"]
input_json = loaded_json["input"]
compiler = "solc" if input_json["language"] == "Solidity" else "vyper"
optimizer = input_json["settings"]["optimizer"]["enabled"]
if compiler == "vyper":
raise NotImplementedError("Support for Vyper is not implemented.")
if "contracts" in targets_json:
for original_filename, contracts_info in targets_json[
"contracts"
].items():
for contract in get_contracts_from_foundry(
original_filename, targets_json
):
self.contracts.append(contract)
contracts.append(contract)
self.sigs.add_sigs(original_filename, targets_json)
return address, contracts
def load_from_solidity( def load_from_solidity(
self, solidity_files: List[str] self, solidity_files: List[str]
) -> Tuple[str, List[SolidityContract]]: ) -> Tuple[str, List[SolidityContract]]:

@ -136,15 +136,55 @@ def get_contracts_from_file(input_file, solc_settings_json=None, solc_binary="so
) )
def get_contracts_from_foundry(input_file, foundry_json):
"""
:param input_file:
:param solc_settings_json:
:param solc_binary:
"""
try:
contract_names = foundry_json["contracts"][input_file].keys()
except KeyError:
raise NoContractFoundError
for contract_name in contract_names:
if len(
foundry_json["contracts"][input_file][contract_name]["evm"][
"deployedBytecode"
]["object"]
):
yield SolidityContract(
input_file=input_file,
name=contract_name,
solc_settings_json=None,
solc_binary=None,
solc_data=foundry_json,
)
class SolidityContract(EVMContract): class SolidityContract(EVMContract):
"""Representation of a Solidity contract.""" """Representation of a Solidity contract."""
def __init__( def __init__(
self, input_file, name=None, solc_settings_json=None, solc_binary="solc" self,
input_file,
name=None,
solc_settings_json=None,
solc_binary="solc",
solc_data=None,
): ):
if solc_data is None:
data = get_solc_json( data = get_solc_json(
input_file, solc_settings_json=solc_settings_json, solc_binary=solc_binary input_file,
solc_settings_json=solc_settings_json,
solc_binary=solc_binary,
) )
else:
data = solc_data
self.solc_indices = self.get_solc_indices(input_file, data) self.solc_indices = self.get_solc_indices(input_file, data)
self.solc_json = data self.solc_json = data
@ -268,6 +308,7 @@ class SolidityContract(EVMContract):
disassembly = self.creation_disassembly if constructor else self.disassembly disassembly = self.creation_disassembly if constructor else self.disassembly
mappings = self.constructor_mappings if constructor else self.mappings mappings = self.constructor_mappings if constructor else self.mappings
index = helper.get_instruction_index(disassembly.instruction_list, address) index = helper.get_instruction_index(disassembly.instruction_list, address)
file_index = mappings[index].solidity_file_idx file_index = mappings[index].solidity_file_idx
if file_index == -1: if file_index == -1:

@ -243,8 +243,12 @@ class SignatureDB(object, metaclass=Singleton):
:return: :return:
""" """
solc_json = get_solc_json(file_path, solc_binary, solc_settings_json) solc_json = get_solc_json(file_path, solc_binary, solc_settings_json)
self.add_sigs(file_path, solc_json)
def add_sigs(self, file_path: str, solc_json):
for contract in solc_json["contracts"][file_path].values(): for contract in solc_json["contracts"][file_path].values():
if "methodIdentifiers" not in contract["evm"]:
continue
for name, hash_ in contract["evm"]["methodIdentifiers"].items(): for name, hash_ in contract["evm"]["methodIdentifiers"].items():
sig = "0x{}".format(hash_) sig = "0x{}".format(hash_)
if sig in self.solidity_sigs: if sig in self.solidity_sigs:

Loading…
Cancel
Save