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
======================
******************************************
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
******************************************
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
@ -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
$ 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
@ -112,10 +123,8 @@ This execution can give the following output:
Caller: [SOMEGUY], function: assert3(uint256), txdata: 0x546455b50000000000000000000000000000000000000000000000000000000000000017, value: 0x0
We can observe that the function ``assert5(uint256)`` should have an assertion failure
with the assertion ``assert(input_x > 10)`` which is missing from our output. This can be attributed to
Mythril's default configuration of running three transactions. We can increase the transaction count to 4
using the ``-t <tx_count>``.
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.
To detect this vulnerability, the transaction count can be increased to four using the ``-t`` option, as shown below:
.. code-block:: bash
@ -204,6 +213,8 @@ This gives the following execution output:
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
4th transaction is ``0x1d5d53dd0000000000000000000000000000000000000000000000000000000000000003``, the first 4 bytes ``1d5d53dd``
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
********************************************************
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
@ -483,9 +497,13 @@ We encounter the following error:
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.
We can do this by providing the remapping information to Mythril as follows:
This error occurs because Solidity cannot locate the ``PRC20.sol`` file.
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
@ -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/"]
}
Here we are mapping the import ``@openzeppelin/contracts/token/PRC20/`` to the path which contains ``PRC20.sol``, which this example
assumes as ``node_modules/PRC20``. This instructs the compiler to search for anything with the prefix ``@openzeppelin/contracts/token/PRC20/` `
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.
This JSON file maps the prefix ``@openzeppelin/contracts/token/PRC20/`` to the path ``node_modules/PRC20/`` in the file system.
When you run Mythril, you can use the ``--solc-json`` option to provide the remapping file:
.. code-block:: bash
$ 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
refer to `Solc docs <https://docs.soliditylang.org/en/v0.8.14/using-the-compiler.html#base-path-and-import-remapping>`_.
With this command, Mythril will be able to locate the ``PRC20.sol`` file, and the analysis should proceed without errors.
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
********************************************************
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
pragma solidity 0.5.0;
pragma solidity ^0.5.0;
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()``.
To successfully explore useful transaction sequences we can use Mythril's ``--transaction-sequences`` argument.

@ -38,6 +38,7 @@ _ = MythrilPluginLoader()
ANALYZE_LIST = ("analyze", "a")
DISASSEMBLE_LIST = ("disassemble", "d")
FOUNDRY_LIST = ("foundry", "f")
CONCOLIC_LIST = ("concolic", "c")
SAFE_FUNCTIONS_COMMAND = "safe-functions"
@ -53,6 +54,7 @@ log = logging.getLogger(__name__)
COMMAND_LIST = (
ANALYZE_LIST
+ DISASSEMBLE_LIST
+ FOUNDRY_LIST
+ CONCOLIC_LIST
+ (
READ_STORAGE_COMNAND,
@ -308,7 +310,19 @@ def main() -> None:
)
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,
parents=[output_parser],
help="Lists available detection modules",
@ -328,10 +342,11 @@ def main() -> None:
subparsers.add_parser(
VERSION_COMMAND, parents=[output_parser], help="Outputs the version"
)
create_read_storage_parser(read_storage_parser)
create_hash_to_addr_parser(contract_hash_to_addr)
create_func_to_hash_parser(contract_func_to_hash)
create_foundry_parser(foundry_parser)
subparsers.add_parser(HELP_COMMAND, add_help=False)
# Get config values
@ -586,6 +601,12 @@ def create_analyzer_parser(analyzer_parser: ArgumentParser):
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):
"""
Validate cli args
@ -698,6 +719,9 @@ def load_code(disassembler: MythrilDisassembler, args: Namespace):
address, _ = disassembler.load_from_solidity(
args.solidity_files
) # list of files
elif args.command in FOUNDRY_LIST:
address, _ = disassembler.load_from_foundry()
else:
exit_with_error(
args.__dict__.get("outform", "text"),
@ -782,7 +806,7 @@ def execute_command(
except CriticalError as 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(
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
and determines output parameters.
:param global_state: state to look in
:param dynamic_loader: dynamic loader to use
:param with_value: whether to pop the value argument from the stack

@ -1,11 +1,17 @@
import json
import logging
import os
from pathlib import Path
import re
import shutil
import solc
import subprocess
import sys
import os
import warnings
from eth_utils import int_to_big_endian
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.ethereum import util
@ -16,10 +22,20 @@ from mythril.support.support_utils import rzpad
from mythril.support.support_args import args
from mythril.ethereum.evmcontract import EVMContract
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 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__)
@ -152,6 +168,70 @@ class MythrilDisassembler:
)
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(
self, solidity_files: List[str]
) -> 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):
"""Representation of a Solidity contract."""
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(
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_json = data
@ -268,6 +308,7 @@ class SolidityContract(EVMContract):
disassembly = self.creation_disassembly if constructor else self.disassembly
mappings = self.constructor_mappings if constructor else self.mappings
index = helper.get_instruction_index(disassembly.instruction_list, address)
file_index = mappings[index].solidity_file_idx
if file_index == -1:

@ -243,8 +243,12 @@ class SignatureDB(object, metaclass=Singleton):
:return:
"""
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():
if "methodIdentifiers" not in contract["evm"]:
continue
for name, hash_ in contract["evm"]["methodIdentifiers"].items():
sig = "0x{}".format(hash_)
if sig in self.solidity_sigs:

Loading…
Cancel
Save