diff --git a/docs/source/tutorial.rst b/docs/source/tutorial.rst index 61e54fba..ca7653ad 100644 --- a/docs/source/tutorial.rst +++ b/docs/source/tutorial.rst @@ -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 -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 ``. +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 `_. +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 `_. ******************************************************** 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. diff --git a/mythril/interfaces/cli.py b/mythril/interfaces/cli.py index 7c5650e2..9f848601 100644 --- a/mythril/interfaces/cli.py +++ b/mythril/interfaces/cli.py @@ -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 ) diff --git a/mythril/laser/ethereum/call.py b/mythril/laser/ethereum/call.py index d2b4d8d9..5d09ca3b 100644 --- a/mythril/laser/ethereum/call.py +++ b/mythril/laser/ethereum/call.py @@ -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 diff --git a/mythril/mythril/mythril_disassembler.py b/mythril/mythril/mythril_disassembler.py index 088531dd..96fc269f 100644 --- a/mythril/mythril/mythril_disassembler.py +++ b/mythril/mythril/mythril_disassembler.py @@ -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]]: diff --git a/mythril/solidity/soliditycontract.py b/mythril/solidity/soliditycontract.py index 4922c528..eb568300 100644 --- a/mythril/solidity/soliditycontract.py +++ b/mythril/solidity/soliditycontract.py @@ -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, ): - data = get_solc_json( - input_file, solc_settings_json=solc_settings_json, solc_binary=solc_binary - ) + + if solc_data is None: + data = get_solc_json( + 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: diff --git a/mythril/support/signatures.py b/mythril/support/signatures.py index c6cded33..216a3d4c 100644 --- a/mythril/support/signatures.py +++ b/mythril/support/signatures.py @@ -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: