diff --git a/docs/source/index.rst b/docs/source/index.rst index e3f9df42..77ff628d 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -9,6 +9,7 @@ Welcome to Mythril's documentation! installation security-analysis analysis-modules + mythx-analysis mythril diff --git a/docs/source/mythx-analysis.rst b/docs/source/mythx-analysis.rst new file mode 100644 index 00000000..eddb0db9 --- /dev/null +++ b/docs/source/mythx-analysis.rst @@ -0,0 +1,63 @@ +MythX Analysis +================= + +Run :code:`myth pro` with one of the input options described below will run a `MythX analysis `_ on the desired input. This includes a run of Mythril, the fuzzer Harvey, and the static analysis engine Maru and has some false-positive filtering only possible by combining the tool capabilities. + +************** +Authentication +************** + +In order to authenticate with the MythX API, set the environment variables ``MYTHX_PASSWORD`` and ``MYTHX_ETH_ADDRESS``. + +.. code-block:: bash + + $ export MYTHX_ETH_ADDRESS='0x0000000000000000000000000000000000000000' + $ export MYTHX_PASSWORD='password' + +*********************** +Analyzing Solidity Code +*********************** + +The input format is the same as a regular Mythril analysis. + +.. code-block:: bash + + $ myth pro ether_send.sol + ==== Unprotected Ether Withdrawal ==== + SWC ID: 105 + Severity: High + Contract: Crowdfunding + Function name: withdrawfunds() + PC address: 730 + Anyone can withdraw ETH from the contract account. + Arbitrary senders other than the contract creator can withdraw ETH from the contract account without previously having sent an equivalent amount of ETH to it. This is likely to be a vulnerability. + -------------------- + In file: tests/testdata/input_contracts/ether_send.sol:21 + + msg.sender.transfer(address(this).balance) + + -------------------- + +If an input file contains multiple contract definitions, Mythril analyzes the *last* bytecode output produced by solc. You can override this by specifying the contract name explicitly: + +.. code-block:: bash + + myth pro OmiseGo.sol:OMGToken + +To specify a contract address, use :code:`-a
` + +**************************** +Analyzing On-Chain Contracts +**************************** + +Analyzing a mainnet contract via INFURA: + +.. code-block:: bash + + myth pro -a 0x5c436ff914c458983414019195e0f4ecbef9e6dd + +Adding the :code:`-l` flag will cause mythril to automatically retrieve dependencies, such as dynamically linked library contracts: + +.. code-block:: bash + + myth -v4 pro -l -a 0xEbFD99838cb0c132016B9E117563CB41f2B02264 diff --git a/docs/source/security-analysis.rst b/docs/source/security-analysis.rst index 2e276f70..26166d97 100644 --- a/docs/source/security-analysis.rst +++ b/docs/source/security-analysis.rst @@ -1,7 +1,7 @@ Security Analysis ================= -Run :code:`myth -x` with one of the input options described below will run the analysis modules in the `/analysis/modules `_ directory. +Run :code:`myth analyze` with one of the input options described below will run the analysis modules in the `/analysis/modules `_ directory. *********************** Analyzing Solidity Code diff --git a/mythril/analysis/modules/base.py b/mythril/analysis/modules/base.py index d45072fd..bcecf6a7 100644 --- a/mythril/analysis/modules/base.py +++ b/mythril/analysis/modules/base.py @@ -3,6 +3,7 @@ modules.""" import logging from typing import List, Set + from mythril.analysis.report import Issue log = logging.getLogger(__name__) @@ -34,21 +35,14 @@ class DetectionModule: self.name, ) self.entrypoint = entrypoint - self._issues = [] # type: List[Issue] - self._cache = set() # type: Set[int] - - @property - def issues(self): - """ - Returns the issues - """ - return self._issues + self.issues = [] # type: List[Issue] + self.cache = set() # type: Set[int] def reset_module(self): """ Resets issues """ - self._issues = [] + self.issues = [] def execute(self, statespace) -> None: """The entry point for execution, which is being called by Mythril. diff --git a/mythril/analysis/modules/delegatecall.py b/mythril/analysis/modules/delegatecall.py index b0e0435d..4b97228b 100644 --- a/mythril/analysis/modules/delegatecall.py +++ b/mythril/analysis/modules/delegatecall.py @@ -20,60 +20,6 @@ from mythril.laser.smt import symbol_factory, UGT log = logging.getLogger(__name__) -class DelegateCallAnnotation(StateAnnotation): - def __init__(self, call_state: GlobalState, constraints: List) -> None: - """ - Initialize DelegateCall Annotation - :param call_state: Call state - """ - self.call_state = call_state - self.constraints = constraints - self.return_value = call_state.new_bitvec( - "retval_{}".format(call_state.get_current_instruction()["address"]), 256 - ) - - def _copy__(self): - return DelegateCallAnnotation(self.call_state, copy(self.constraints)) - - def get_issue(self, global_state: GlobalState, transaction_sequence: Dict) -> Issue: - """ - Returns Issue for the annotation - :param global_state: Global State - :param transaction_sequence: Transaction sequence - :return: Issue - """ - - address = self.call_state.get_current_instruction()["address"] - logging.debug( - "[DELEGATECALL] Detected delegatecall to a user-supplied address : {}".format( - address - ) - ) - description_head = "The contract delegates execution to another contract with a user-supplied address." - description_tail = ( - "The smart contract delegates execution to a user-supplied address. Note that callers " - "can execute arbitrary contracts and that the callee contract " - "can access the storage of the calling contract. " - ) - - return Issue( - contract=self.call_state.environment.active_account.contract_name, - function_name=self.call_state.environment.active_function_name, - address=address, - swc_id=DELEGATECALL_TO_UNTRUSTED_CONTRACT, - title="Delegatecall Proxy To User-Supplied Address", - bytecode=global_state.environment.code.bytecode, - severity="Medium", - description_head=description_head, - description_tail=description_tail, - transaction_sequence=transaction_sequence, - gas_used=( - global_state.mstate.min_gas_used, - global_state.mstate.max_gas_used, - ), - ) - - class DelegateCallModule(DetectionModule): """This module detects calldata being forwarded using DELEGATECALL.""" @@ -84,7 +30,7 @@ class DelegateCallModule(DetectionModule): swc_id=DELEGATECALL_TO_UNTRUSTED_CONTRACT, description="Check for invocations of delegatecall(msg.data) in the fallback function.", entrypoint="callback", - pre_hooks=["DELEGATECALL", "RETURN", "STOP"], + pre_hooks=["DELEGATECALL"], ) def _execute(self, state: GlobalState) -> None: @@ -93,12 +39,12 @@ class DelegateCallModule(DetectionModule): :param state: :return: """ - if state.get_current_instruction()["address"] in self._cache: + if state.get_current_instruction()["address"] in self.cache: return issues = self._analyze_state(state) for issue in issues: - self._cache.add(issue.address) - self._issues.extend(issues) + self.cache.add(issue.address) + self.issues.extend(issues) @staticmethod def _analyze_state(state: GlobalState) -> List[Issue]: @@ -106,50 +52,56 @@ class DelegateCallModule(DetectionModule): :param state: the current state :return: returns the issues for that corresponding state """ - issues = [] op_code = state.get_current_instruction()["opcode"] - annotations = cast( - List[DelegateCallAnnotation], - list(state.get_annotations(DelegateCallAnnotation)), - ) - if len(annotations) == 0 and op_code in ("RETURN", "STOP"): - return [] + gas = state.mstate.stack[-1] + to = state.mstate.stack[-2] - if op_code == "DELEGATECALL": - gas = state.mstate.stack[-1] - to = state.mstate.stack[-2] + constraints = [ + to == ATTACKER_ADDRESS, + UGT(gas, symbol_factory.BitVecVal(2300, 256)), + ] - constraints = [ - to == ATTACKER_ADDRESS, - UGT(gas, symbol_factory.BitVecVal(2300, 256)), - ] + for tx in state.world_state.transaction_sequence: + if not isinstance(tx, ContractCreationTransaction): + constraints.append(tx.caller == ATTACKER_ADDRESS) + + try: + transaction_sequence = solver.get_transaction_sequence( + state, state.mstate.constraints + constraints + ) - for tx in state.world_state.transaction_sequence: - if not isinstance(tx, ContractCreationTransaction): - constraints.append(tx.caller == ATTACKER_ADDRESS) + address = state.get_current_instruction()["address"] + logging.debug( + "[DELEGATECALL] Detected delegatecall to a user-supplied address : {}".format( + address + ) + ) + description_head = "The contract delegates execution to another contract with a user-supplied address." + description_tail = ( + "The smart contract delegates execution to a user-supplied address. Note that callers " + "can execute arbitrary contracts and that the callee contract " + "can access the storage of the calling contract. " + ) - state.annotate(DelegateCallAnnotation(state, constraints)) + return [ + Issue( + contract=state.environment.active_account.contract_name, + function_name=state.environment.active_function_name, + address=address, + swc_id=DELEGATECALL_TO_UNTRUSTED_CONTRACT, + bytecode=state.environment.code.bytecode, + title="Delegatecall Proxy To User-Supplied Address", + severity="Medium", + description_head=description_head, + description_tail=description_tail, + transaction_sequence=transaction_sequence, + gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used), + ) + ] + except UnsatError: return [] - else: - for annotation in annotations: - try: - transaction_sequence = solver.get_transaction_sequence( - state, - state.mstate.constraints - + annotation.constraints - + [annotation.return_value == 1], - ) - issues.append( - annotation.get_issue( - state, transaction_sequence=transaction_sequence - ) - ) - except UnsatError: - continue - - return issues detector = DelegateCallModule() diff --git a/mythril/analysis/modules/dependence_on_predictable_vars.py b/mythril/analysis/modules/dependence_on_predictable_vars.py index 2ff7a454..a9c730fa 100644 --- a/mythril/analysis/modules/dependence_on_predictable_vars.py +++ b/mythril/analysis/modules/dependence_on_predictable_vars.py @@ -74,12 +74,12 @@ class PredictableDependenceModule(DetectionModule): :param state: :return: """ - if state.get_current_instruction()["address"] in self._cache: + if state.get_current_instruction()["address"] in self.cache: return issues = self._analyze_state(state) for issue in issues: - self._cache.add(issue.address) - self._issues.extend(issues) + self.cache.add(issue.address) + self.issues.extend(issues) @staticmethod def _analyze_state(state: GlobalState) -> list: diff --git a/mythril/analysis/modules/deprecated_ops.py b/mythril/analysis/modules/deprecated_ops.py index 7e495b5b..85766537 100644 --- a/mythril/analysis/modules/deprecated_ops.py +++ b/mythril/analysis/modules/deprecated_ops.py @@ -1,6 +1,8 @@ """This module contains the detection code for deprecated op codes.""" -from mythril.analysis.report import Issue -from mythril.analysis.solver import get_transaction_sequence, UnsatError +from mythril.analysis.potential_issues import ( + PotentialIssue, + get_potential_issues_annotation, +) from mythril.analysis.swc_data import DEPRECATED_FUNCTIONS_USAGE from mythril.analysis.modules.base import DetectionModule from mythril.laser.ethereum.state.global_state import GlobalState @@ -32,16 +34,14 @@ class DeprecatedOperationsModule(DetectionModule): :param state: :return: """ - if state.get_current_instruction()["address"] in self._cache: + if state.get_current_instruction()["address"] in self.cache: return issues = self._analyze_state(state) - for issue in issues: - self._cache.add(issue.address) - self._issues.extend(issues) + annotation = get_potential_issues_annotation(state) + annotation.potential_issues.extend(issues) - @staticmethod - def _analyze_state(state): + def _analyze_state(self, state): """ :param state: @@ -76,26 +76,21 @@ class DeprecatedOperationsModule(DetectionModule): swc_id = DEPRECATED_FUNCTIONS_USAGE else: return [] - try: - transaction_sequence = get_transaction_sequence( - state, state.mstate.constraints - ) - except UnsatError: - return [] - issue = Issue( + + potential_issue = PotentialIssue( contract=state.environment.active_account.contract_name, function_name=state.environment.active_function_name, address=instruction["address"], title=title, bytecode=state.environment.code.bytecode, + detector=self, swc_id=swc_id, severity="Medium", description_head=description_head, description_tail=description_tail, - gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used), - transaction_sequence=transaction_sequence, + constraints=[], ) - return [issue] + return [potential_issue] detector = DeprecatedOperationsModule() diff --git a/mythril/analysis/modules/dos.py b/mythril/analysis/modules/dos.py index 2ee08abe..cb9ac2f3 100644 --- a/mythril/analysis/modules/dos.py +++ b/mythril/analysis/modules/dos.py @@ -56,7 +56,7 @@ class DosModule(DetectionModule): :return: """ issues = self._analyze_state(state) - self._issues.extend(issues) + self.issues.extend(issues) def _analyze_state(self, state: GlobalState) -> List[Issue]: """ diff --git a/mythril/analysis/modules/ether_thief.py b/mythril/analysis/modules/ether_thief.py index 65e5c8e8..13066795 100644 --- a/mythril/analysis/modules/ether_thief.py +++ b/mythril/analysis/modules/ether_thief.py @@ -3,15 +3,16 @@ withdrawal.""" import logging from copy import copy -from mythril.analysis import solver from mythril.analysis.modules.base import DetectionModule -from mythril.analysis.report import Issue +from mythril.analysis.potential_issues import ( + get_potential_issues_annotation, + PotentialIssue, +) from mythril.laser.ethereum.transaction.symbolic import ( ATTACKER_ADDRESS, CREATOR_ADDRESS, ) from mythril.analysis.swc_data import UNPROTECTED_ETHER_WITHDRAWAL -from mythril.exceptions import UnsatError from mythril.laser.ethereum.state.global_state import GlobalState from mythril.laser.ethereum.transaction import ContractCreationTransaction @@ -60,15 +61,14 @@ class EtherThief(DetectionModule): :param state: :return: """ - if state.get_current_instruction()["address"] in self._cache: + if state.get_current_instruction()["address"] in self.cache: return - issues = self._analyze_state(state) - for issue in issues: - self._cache.add(issue.address) - self._issues.extend(issues) + potential_issues = self._analyze_state(state) + + annotation = get_potential_issues_annotation(state) + annotation.potential_issues.extend(potential_issues) - @staticmethod - def _analyze_state(state): + def _analyze_state(self, state): """ :param state: @@ -115,29 +115,23 @@ class EtherThief(DetectionModule): state.current_transaction.caller == ATTACKER_ADDRESS, ] - try: - transaction_sequence = solver.get_transaction_sequence(state, constraints) - - issue = Issue( - contract=state.environment.active_account.contract_name, - function_name=state.environment.active_function_name, - address=instruction["address"], - swc_id=UNPROTECTED_ETHER_WITHDRAWAL, - title="Unprotected Ether Withdrawal", - severity="High", - bytecode=state.environment.code.bytecode, - description_head="Anyone can withdraw ETH from the contract account.", - description_tail="Arbitrary senders other than the contract creator can withdraw ETH from the contract" - + " account without previously having sent an equivalent amount of ETH to it. This is likely to be" - + " a vulnerability.", - transaction_sequence=transaction_sequence, - gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used), - ) - except UnsatError: - log.debug("No model found") - return [] + potential_issue = PotentialIssue( + contract=state.environment.active_account.contract_name, + function_name=state.environment.active_function_name, + address=instruction["address"], + swc_id=UNPROTECTED_ETHER_WITHDRAWAL, + title="Unprotected Ether Withdrawal", + severity="High", + bytecode=state.environment.code.bytecode, + description_head="Anyone can withdraw ETH from the contract account.", + description_tail="Arbitrary senders other than the contract creator can withdraw ETH from the contract" + + " account without previously having sent an equivalent amount of ETH to it. This is likely to be" + + " a vulnerability.", + detector=self, + constraints=constraints, + ) - return [issue] + return [potential_issue] detector = EtherThief() diff --git a/mythril/analysis/modules/exceptions.py b/mythril/analysis/modules/exceptions.py index fe593e8d..804df576 100644 --- a/mythril/analysis/modules/exceptions.py +++ b/mythril/analysis/modules/exceptions.py @@ -33,8 +33,8 @@ class ReachableExceptionsModule(DetectionModule): """ issues = self._analyze_state(state) for issue in issues: - self._cache.add(issue.address) - self._issues.extend(issues) + self.cache.add(issue.address) + self.issues.extend(issues) @staticmethod def _analyze_state(state) -> list: diff --git a/mythril/analysis/modules/external_calls.py b/mythril/analysis/modules/external_calls.py index 81cd6bd2..8a249a74 100644 --- a/mythril/analysis/modules/external_calls.py +++ b/mythril/analysis/modules/external_calls.py @@ -2,20 +2,23 @@ calls.""" from mythril.analysis import solver +from mythril.analysis.potential_issues import ( + PotentialIssue, + get_potential_issues_annotation, +) from mythril.analysis.swc_data import REENTRANCY +from mythril.laser.ethereum.state.constraints import Constraints from mythril.laser.ethereum.transaction.symbolic import ATTACKER_ADDRESS from mythril.laser.ethereum.transaction.transaction_models import ( ContractCreationTransaction, ) from mythril.analysis.modules.base import DetectionModule -from mythril.analysis.report import Issue from mythril.laser.smt import UGT, symbol_factory, Or, BitVec from mythril.laser.ethereum.natives import PRECOMPILE_COUNT from mythril.laser.ethereum.state.global_state import GlobalState from mythril.exceptions import UnsatError from copy import copy import logging -import json log = logging.getLogger(__name__) @@ -65,13 +68,12 @@ class ExternalCalls(DetectionModule): :param state: :return: """ - issues = self._analyze_state(state) - for issue in issues: - self._cache.add(issue.address) - self._issues.extend(issues) + potential_issues = self._analyze_state(state) + + annotation = get_potential_issues_annotation(state) + annotation.potential_issues.extend(potential_issues) - @staticmethod - def _analyze_state(state): + def _analyze_state(self, state: GlobalState): """ :param state: @@ -83,10 +85,10 @@ class ExternalCalls(DetectionModule): address = state.get_current_instruction()["address"] try: - constraints = copy(state.mstate.constraints) + constraints = Constraints([UGT(gas, symbol_factory.BitVecVal(2300, 256))]) transaction_sequence = solver.get_transaction_sequence( - state, constraints + [UGT(gas, symbol_factory.BitVecVal(2300, 256))] + state, constraints + state.mstate.constraints ) # Check whether we can also set the callee address @@ -99,7 +101,7 @@ class ExternalCalls(DetectionModule): constraints.append(tx.caller == ATTACKER_ADDRESS) transaction_sequence = solver.get_transaction_sequence( - state, constraints + state, constraints + state.mstate.constraints ) description_head = "A call to a user-supplied address is executed." @@ -110,7 +112,7 @@ class ExternalCalls(DetectionModule): "contract state." ) - issue = Issue( + issue = PotentialIssue( contract=state.environment.active_account.contract_name, function_name=state.environment.active_function_name, address=address, @@ -120,8 +122,8 @@ class ExternalCalls(DetectionModule): severity="Medium", description_head=description_head, description_tail=description_tail, - transaction_sequence=transaction_sequence, - gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used), + constraints=constraints, + detector=self, ) except UnsatError: @@ -138,7 +140,7 @@ class ExternalCalls(DetectionModule): "that the callee contract has been reviewed carefully." ) - issue = Issue( + issue = PotentialIssue( contract=state.environment.active_account.contract_name, function_name=state.environment.active_function_name, address=address, @@ -148,8 +150,8 @@ class ExternalCalls(DetectionModule): severity="Low", description_head=description_head, description_tail=description_tail, - transaction_sequence=transaction_sequence, - gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used), + constraints=constraints, + detector=self, ) except UnsatError: diff --git a/mythril/analysis/modules/integer.py b/mythril/analysis/modules/integer.py index c7c7c24a..78d93f31 100644 --- a/mythril/analysis/modules/integer.py +++ b/mythril/analysis/modules/integer.py @@ -113,7 +113,7 @@ class IntegerOverflowUnderflowModule(DetectionModule): address = _get_address_from_state(state) - if address in self._cache: + if address in self.cache: return opcode = state.get_current_instruction()["opcode"] @@ -331,8 +331,8 @@ class IntegerOverflowUnderflowModule(DetectionModule): ) address = _get_address_from_state(ostate) - self._cache.add(address) - self._issues.append(issue) + self.cache.add(address) + self.issues.append(issue) detector = IntegerOverflowUnderflowModule() diff --git a/mythril/analysis/modules/multiple_sends.py b/mythril/analysis/modules/multiple_sends.py index be3b7474..e6b95085 100644 --- a/mythril/analysis/modules/multiple_sends.py +++ b/mythril/analysis/modules/multiple_sends.py @@ -45,12 +45,12 @@ class MultipleSendsModule(DetectionModule): ) def _execute(self, state: GlobalState) -> None: - if state.get_current_instruction()["address"] in self._cache: + if state.get_current_instruction()["address"] in self.cache: return issues = self._analyze_state(state) for issue in issues: - self._cache.add(issue.address) - self._issues.extend(issues) + self.cache.add(issue.address) + self.issues.extend(issues) @staticmethod def _analyze_state(state: GlobalState): diff --git a/mythril/analysis/modules/state_change_external_calls.py b/mythril/analysis/modules/state_change_external_calls.py index c410b4c0..a05f4ffd 100644 --- a/mythril/analysis/modules/state_change_external_calls.py +++ b/mythril/analysis/modules/state_change_external_calls.py @@ -1,6 +1,10 @@ +from mythril.analysis.potential_issues import ( + PotentialIssue, + get_potential_issues_annotation, +) from mythril.analysis.swc_data import REENTRANCY from mythril.analysis.modules.base import DetectionModule -from mythril.analysis.report import Issue +from mythril.laser.ethereum.state.constraints import Constraints from mythril.laser.smt import symbol_factory, UGT, BitVec, Or from mythril.laser.ethereum.state.global_state import GlobalState from mythril.laser.ethereum.state.annotation import StateAnnotation @@ -32,10 +36,12 @@ class StateChangeCallsAnnotation(StateAnnotation): new_annotation.state_change_states = self.state_change_states[:] return new_annotation - def get_issue(self, global_state: GlobalState) -> Optional[Issue]: + def get_issue( + self, global_state: GlobalState, detector: DetectionModule + ) -> Optional[PotentialIssue]: if not self.state_change_states: return None - constraints = copy(global_state.mstate.constraints) + constraints = Constraints() gas = self.call_state.mstate.stack[-1] to = self.call_state.mstate.stack[-2] constraints += [ @@ -50,10 +56,11 @@ class StateChangeCallsAnnotation(StateAnnotation): try: transaction_sequence = solver.get_transaction_sequence( - global_state, constraints + global_state, constraints + global_state.mstate.constraints ) except UnsatError: return None + severity = "Medium" if self.user_defined_address else "Low" address = global_state.get_current_instruction()["address"] logging.debug( @@ -67,7 +74,7 @@ class StateChangeCallsAnnotation(StateAnnotation): "state change takes place. This can lead to business logic vulnerabilities." ) - return Issue( + return PotentialIssue( contract=global_state.environment.active_account.contract_name, function_name=global_state.environment.active_function_name, address=address, @@ -77,7 +84,8 @@ class StateChangeCallsAnnotation(StateAnnotation): description_tail=description_tail, swc_id=REENTRANCY, bytecode=global_state.environment.code.bytecode, - transaction_sequence=transaction_sequence, + constraints=constraints, + detector=detector, ) @@ -104,12 +112,12 @@ class StateChange(DetectionModule): ) def _execute(self, state: GlobalState) -> None: - if state.get_current_instruction()["address"] in self._cache: + if state.get_current_instruction()["address"] in self.cache: return issues = self._analyze_state(state) - for issue in issues: - self._cache.add(issue.address) - self._issues.extend(issues) + + annotation = get_potential_issues_annotation(state) + annotation.potential_issues.extend(issues) @staticmethod def _add_external_call(global_state: GlobalState) -> None: @@ -139,8 +147,7 @@ class StateChange(DetectionModule): except UnsatError: pass - @staticmethod - def _analyze_state(global_state: GlobalState) -> List[Issue]: + def _analyze_state(self, global_state: GlobalState) -> List[PotentialIssue]: annotations = cast( List[StateChangeCallsAnnotation], @@ -171,7 +178,7 @@ class StateChange(DetectionModule): for annotation in annotations: if not annotation.state_change_states: continue - issue = annotation.get_issue(global_state) + issue = annotation.get_issue(global_state, self) if issue: vulnerabilities.append(issue) return vulnerabilities diff --git a/mythril/analysis/modules/suicide.py b/mythril/analysis/modules/suicide.py index b5a9b988..b55fcd3f 100644 --- a/mythril/analysis/modules/suicide.py +++ b/mythril/analysis/modules/suicide.py @@ -46,12 +46,12 @@ class SuicideModule(DetectionModule): :param state: :return: """ - if state.get_current_instruction()["address"] in self._cache: + if state.get_current_instruction()["address"] in self.cache: return issues = self._analyze_state(state) for issue in issues: - self._cache.add(issue.address) - self._issues.extend(issues) + self.cache.add(issue.address) + self.issues.extend(issues) @staticmethod def _analyze_state(state): diff --git a/mythril/analysis/modules/unchecked_retval.py b/mythril/analysis/modules/unchecked_retval.py index e5e99e3a..678f329c 100644 --- a/mythril/analysis/modules/unchecked_retval.py +++ b/mythril/analysis/modules/unchecked_retval.py @@ -55,12 +55,12 @@ class UncheckedRetvalModule(DetectionModule): :param state: :return: """ - if state.get_current_instruction()["address"] in self._cache: + if state.get_current_instruction()["address"] in self.cache: return issues = self._analyze_state(state) for issue in issues: - self._cache.add(issue.address) - self._issues.extend(issues) + self.cache.add(issue.address) + self.issues.extend(issues) def _analyze_state(self, state: GlobalState) -> list: instruction = state.get_current_instruction() diff --git a/mythril/analysis/potential_issues.py b/mythril/analysis/potential_issues.py new file mode 100644 index 00000000..99340330 --- /dev/null +++ b/mythril/analysis/potential_issues.py @@ -0,0 +1,108 @@ +from mythril.analysis.report import Issue +from mythril.analysis.solver import get_transaction_sequence +from mythril.exceptions import UnsatError +from mythril.laser.ethereum.state.annotation import StateAnnotation +from mythril.laser.ethereum.state.global_state import GlobalState + + +class PotentialIssue: + """Representation of a potential issue""" + + def __init__( + self, + contract, + function_name, + address, + swc_id, + title, + bytecode, + detector, + severity=None, + description_head="", + description_tail="", + constraints=None, + ): + """ + + :param contract: The contract + :param function_name: Function name where the issue is detected + :param address: The address of the issue + :param swc_id: Issue's corresponding swc-id + :param title: Title + :param bytecode: bytecode of the issue + :param detector: The detector the potential issue belongs to + :param gas_used: amount of gas used + :param severity: The severity of the issue + :param description_head: The top part of description + :param description_tail: The bottom part of the description + :param constraints: The non-path related constraints for the potential issue + """ + self.title = title + self.contract = contract + self.function_name = function_name + self.address = address + self.description_head = description_head + self.description_tail = description_tail + self.severity = severity + self.swc_id = swc_id + self.bytecode = bytecode + self.constraints = constraints or [] + self.detector = detector + + +class PotentialIssuesAnnotation(StateAnnotation): + def __init__(self): + self.potential_issues = [] + + +def get_potential_issues_annotation(state: GlobalState) -> PotentialIssuesAnnotation: + """ + Returns the potential issues annotation of the given global state, and creates one if + one does not already exist. + + :param state: The global state + :return: + """ + for annotation in state.annotations: + if isinstance(annotation, PotentialIssuesAnnotation): + return annotation + + annotation = PotentialIssuesAnnotation() + state.annotate(annotation) + return annotation + + +def check_potential_issues(state: GlobalState) -> None: + """ + Called at the end of a transaction, checks potential issues, and + adds valid issues to the detector. + + :param state: The final global state of a transaction + :return: + """ + annotation = get_potential_issues_annotation(state) + for potential_issue in annotation.potential_issues: + try: + transaction_sequence = get_transaction_sequence( + state, state.mstate.constraints + potential_issue.constraints + ) + except UnsatError: + continue + + annotation.potential_issues.remove(potential_issue) + potential_issue.detector.cache.add(potential_issue.address) + potential_issue.detector.issues.append( + Issue( + contract=potential_issue.contract, + function_name=potential_issue.function_name, + address=potential_issue.address, + title=potential_issue.title, + bytecode=potential_issue.bytecode, + swc_id=potential_issue.swc_id, + gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used), + severity=potential_issue.severity, + description_head=potential_issue.description_head, + description_tail=potential_issue.description_tail, + transaction_sequence=transaction_sequence, + ) + ) diff --git a/mythril/analysis/templates/report_as_markdown.jinja2 b/mythril/analysis/templates/report_as_markdown.jinja2 index 78b0b7be..360648ea 100644 --- a/mythril/analysis/templates/report_as_markdown.jinja2 +++ b/mythril/analysis/templates/report_as_markdown.jinja2 @@ -6,15 +6,21 @@ - SWC ID: {{ issue['swc-id'] }} - Severity: {{ issue.severity }} - Contract: {{ issue.contract | default("Unknown") }} +{% if issue.function %} - Function name: `{{ issue.function }}` +{% endif %} - PC address: {{ issue.address }} +{% if issue.min_gas_used or issue.max_gas_used %} - Estimated Gas Usage: {{ issue.min_gas_used }} - {{ issue.max_gas_used }} +{% endif %} ### Description {{ issue.description.rstrip() }} {% if issue.filename and issue.lineno %} In file: {{ issue.filename }}:{{ issue.lineno }} +{% elif issue.filename %} +In file: {{ issue.filename }} {% endif %} {% if issue.code %} diff --git a/mythril/analysis/templates/report_as_text.jinja2 b/mythril/analysis/templates/report_as_text.jinja2 index 2201dbcf..1488807c 100644 --- a/mythril/analysis/templates/report_as_text.jinja2 +++ b/mythril/analysis/templates/report_as_text.jinja2 @@ -4,9 +4,13 @@ SWC ID: {{ issue['swc-id'] }} Severity: {{ issue.severity }} Contract: {{ issue.contract | default("Unknown") }} +{% if issue.function %} Function name: {{ issue.function }} +{% endif %} PC address: {{ issue.address }} +{% if issue.min_gas_used or issue.max_gas_used %} Estimated Gas Usage: {{ issue.min_gas_used }} - {{ issue.max_gas_used }} +{% endif %} {{ issue.description }} -------------------- {% if issue.filename and issue.lineno %} diff --git a/mythril/ethereum/util.py b/mythril/ethereum/util.py index 2b6c7771..7c9020f5 100644 --- a/mythril/ethereum/util.py +++ b/mythril/ethereum/util.py @@ -24,39 +24,44 @@ def safe_decode(hex_encoded_string): return bytes.fromhex(hex_encoded_string) -def get_solc_json(file, solc_binary="solc", solc_args=None): +def get_solc_json(file, solc_binary="solc", solc_settings_json=None): """ :param file: :param solc_binary: - :param solc_args: + :param solc_settings_json: :return: """ - - cmd = [solc_binary, "--combined-json", "bin,bin-runtime,srcmap,srcmap-runtime,ast"] - - if solc_args: - cmd.extend(solc_args.split()) - if not "--allow-paths" in cmd: - cmd.extend(["--allow-paths", "."]) - else: - for i, arg in enumerate(cmd): - if arg == "--allow-paths": - cmd[i + 1] += ",." - - cmd.append(file) + cmd = [solc_binary, "--standard-json", "--allow-paths", "."] + + settings = json.loads(solc_settings_json) if solc_settings_json else {} + settings.update( + { + "outputSelection": { + "*": { + "": ["ast"], + "*": [ + "metadata", + "evm.bytecode", + "evm.deployedBytecode", + "evm.methodIdentifiers", + ], + } + } + } + ) + input_json = json.dumps( + { + "language": "Solidity", + "sources": {file: {"urls": [file]}}, + "settings": settings, + } + ) try: - p = Popen(cmd, stdout=PIPE, stderr=PIPE) - - stdout, stderr = p.communicate() - ret = p.returncode + p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) + stdout, stderr = p.communicate(bytes(input_json, "utf8")) - if ret != 0: - raise CompilerError( - "Solc experienced a fatal error (code %d).\n\n%s" - % (ret, stderr.decode("UTF-8")) - ) except FileNotFoundError: raise CompilerError( "Compiler not found. Make sure that solc is installed and in PATH, or set the SOLC environment variable." @@ -64,10 +69,15 @@ def get_solc_json(file, solc_binary="solc", solc_args=None): out = stdout.decode("UTF-8") - if not len(out): - raise CompilerError("Compilation failed.") + result = json.loads(out) + + for error in result.get("errors", []): + if error["severity"] == "error": + raise CompilerError( + "Solc experienced a fatal error.\n\n%s" % error["formattedMessage"] + ) - return json.loads(out) + return result def encode_calldata(func_name, arg_types, args): diff --git a/mythril/interfaces/cli.py b/mythril/interfaces/cli.py index 5b78ba38..1501120f 100644 --- a/mythril/interfaces/cli.py +++ b/mythril/interfaces/cli.py @@ -16,6 +16,7 @@ import traceback import mythril.support.signatures as sigs from argparse import ArgumentParser, Namespace, RawTextHelpFormatter +from mythril import mythx from mythril.exceptions import AddressNotFoundError, CriticalError from mythril.mythril import ( MythrilAnalyzer, @@ -27,12 +28,14 @@ from mythril.__version__ import __version__ as VERSION ANALYZE_LIST = ("analyze", "a") DISASSEMBLE_LIST = ("disassemble", "d") +PRO_LIST = ("pro", "p") log = logging.getLogger(__name__) COMMAND_LIST = ( ANALYZE_LIST + DISASSEMBLE_LIST + + PRO_LIST + ( "read-storage", "leveldb-search", @@ -41,6 +44,7 @@ COMMAND_LIST = ( "version", "truffle", "help", + "pro", ) ) @@ -70,7 +74,27 @@ def exit_with_error(format_, message): sys.exit() -def get_input_parser() -> ArgumentParser: +def get_runtime_input_parser() -> ArgumentParser: + """ + Returns Parser which handles input + :return: Parser which handles input + """ + parser = ArgumentParser(add_help=False) + parser.add_argument( + "-a", + "--address", + help="pull contract from the blockchain", + metavar="CONTRACT_ADDRESS", + ) + parser.add_argument( + "--bin-runtime", + action="store_true", + help="Only when -c or -f is used. Consider the input bytecode as binary runtime code, default being the contract creation bytecode.", + ) + return parser + + +def get_creation_input_parser() -> ArgumentParser: """ Returns Parser which handles input :return: Parser which handles input @@ -89,17 +113,6 @@ def get_input_parser() -> ArgumentParser: metavar="BYTECODEFILE", type=argparse.FileType("r"), ) - parser.add_argument( - "-a", - "--address", - help="pull contract from the blockchain", - metavar="CONTRACT_ADDRESS", - ) - parser.add_argument( - "--bin-runtime", - action="store_true", - help="Only when -c or -f is used. Consider the input bytecode as binary runtime code, default being the contract creation bytecode.", - ) return parser @@ -144,7 +157,10 @@ def get_utilities_parser() -> ArgumentParser: :return: Parser which handles utility flags """ parser = argparse.ArgumentParser(add_help=False) - parser.add_argument("--solc-args", help="Extra arguments for solc") + parser.add_argument( + "--solc-json", + help="Json for the optional 'settings' parameter of solc's standard-json input", + ) parser.add_argument( "--solv", help="specify solidity compiler version. If not present, will try to install it (Experimental)", @@ -158,7 +174,8 @@ def main() -> None: rpc_parser = get_rpc_parser() utilities_parser = get_utilities_parser() - input_parser = get_input_parser() + runtime_input_parser = get_runtime_input_parser() + creation_input_parser = get_creation_input_parser() output_parser = get_output_parser() parser = argparse.ArgumentParser( description="Security analysis of Ethereum smart contracts" @@ -172,7 +189,13 @@ def main() -> None: analyzer_parser = subparsers.add_parser( ANALYZE_LIST[0], help="Triggers the analysis of the smart contract", - parents=[rpc_parser, utilities_parser, input_parser, output_parser], + parents=[ + rpc_parser, + utilities_parser, + creation_input_parser, + runtime_input_parser, + output_parser, + ], aliases=ANALYZE_LIST[1:], formatter_class=RawTextHelpFormatter, ) @@ -182,11 +205,25 @@ def main() -> None: DISASSEMBLE_LIST[0], help="Disassembles the smart contract", aliases=DISASSEMBLE_LIST[1:], - parents=[rpc_parser, utilities_parser, input_parser], + parents=[ + rpc_parser, + utilities_parser, + creation_input_parser, + runtime_input_parser, + ], formatter_class=RawTextHelpFormatter, ) create_disassemble_parser(disassemble_parser) + pro_parser = subparsers.add_parser( + PRO_LIST[0], + help="Analyzes input with the MythX API (https://mythx.io)", + aliases=PRO_LIST[1:], + parents=[utilities_parser, creation_input_parser, output_parser], + formatter_class=RawTextHelpFormatter, + ) + create_pro_parser(pro_parser) + read_storage_parser = subparsers.add_parser( "read-storage", help="Retrieves storage slots from a given address through rpc", @@ -234,6 +271,25 @@ def create_disassemble_parser(parser: ArgumentParser): ) +def create_pro_parser(parser: ArgumentParser): + """ + Modify parser to handle mythx analysis + :param parser: + :return: + """ + parser.add_argument( + "solidity_files", + nargs="*", + help="Inputs file name and contract name. \n" + "usage: file1.sol:OptionalContractName file2.sol file3.sol:OptionalContractName", + ) + parser.add_argument( + "--full", + help="Run a full analysis. Default: quick analysis", + action="store_true", + ) + + def create_read_storage_parser(read_storage_parser: ArgumentParser): """ Modify parser to handle storage slots @@ -564,6 +620,17 @@ def execute_command( ) print(storage) + elif args.command in PRO_LIST: + mode = "full" if args.full else "quick" + report = mythx.analyze(disassembler.contracts, mode) + outputs = { + "json": report.as_json(), + "jsonv2": report.as_swc_standard_format(), + "text": report.as_text(), + "markdown": report.as_markdown(), + } + print(outputs[args.outform]) + elif args.command in DISASSEMBLE_LIST: if disassembler.contracts[0].code: print("Runtime Disassembly: \n" + disassembler.contracts[0].get_easm()) @@ -694,12 +761,12 @@ def parse_args_and_execute(parser: ArgumentParser, args: Namespace) -> None: config = set_config(args) leveldb_search(config, args) query_signature = args.__dict__.get("query_signature", None) - solc_args = args.__dict__.get("solc_args", None) + solc_json = args.__dict__.get("solc_json", None) solv = args.__dict__.get("solv", None) disassembler = MythrilDisassembler( eth=config.eth, solc_version=solv, - solc_args=solc_args, + solc_settings_json=solc_json, enable_online_lookup=query_signature, ) if args.command == "truffle": diff --git a/mythril/interfaces/old_cli.py b/mythril/interfaces/old_cli.py index 211d1c8e..27bde387 100644 --- a/mythril/interfaces/old_cli.py +++ b/mythril/interfaces/old_cli.py @@ -229,7 +229,10 @@ def create_parser(parser: argparse.ArgumentParser) -> None: default=10, help="The amount of seconds to spend on " "the initial contract creation", ) - options.add_argument("--solc-args", help="Extra arguments for solc") + options.add_argument( + "--solc-json", + help="Json for the optional 'settings' parameter of solc's standard-json input", + ) options.add_argument( "--phrack", action="store_true", help="Phrack-style call graph" ) @@ -522,7 +525,7 @@ def parse_args(parser: argparse.ArgumentParser, args: argparse.Namespace) -> Non disassembler = MythrilDisassembler( eth=config.eth, solc_version=args.solv, - solc_args=args.solc_args, + solc_settings_json=args.solc_json, enable_online_lookup=args.query_signature, ) if args.truffle: diff --git a/mythril/laser/ethereum/svm.py b/mythril/laser/ethereum/svm.py index f3ab1d5e..fa78c840 100644 --- a/mythril/laser/ethereum/svm.py +++ b/mythril/laser/ethereum/svm.py @@ -5,6 +5,7 @@ from copy import copy from datetime import datetime, timedelta from typing import Callable, Dict, DefaultDict, List, Tuple, Optional +from mythril.analysis.potential_issues import check_potential_issues from mythril.laser.ethereum.cfg import NodeFlags, Node, Edge, JumpType from mythril.laser.ethereum.evm_exceptions import StackUnderflowException from mythril.laser.ethereum.evm_exceptions import VmException @@ -344,6 +345,8 @@ class LaserEVM: not isinstance(transaction, ContractCreationTransaction) or transaction.return_data ) and not end_signal.revert: + check_potential_issues(global_state) + end_signal.global_state.world_state.node = global_state.node self._add_world_state(end_signal.global_state) new_global_states = [] diff --git a/mythril/laser/ethereum/util.py b/mythril/laser/ethereum/util.py index 4191173d..ff544559 100644 --- a/mythril/laser/ethereum/util.py +++ b/mythril/laser/ethereum/util.py @@ -45,7 +45,7 @@ def get_instruction_index( """ index = 0 for instr in instruction_list: - if instr["address"] == address: + if instr["address"] >= address: return index index += 1 return None diff --git a/mythril/mythril/mythril_disassembler.py b/mythril/mythril/mythril_disassembler.py index bfd7e23c..28fa01eb 100644 --- a/mythril/mythril/mythril_disassembler.py +++ b/mythril/mythril/mythril_disassembler.py @@ -30,11 +30,11 @@ class MythrilDisassembler: self, eth: Optional[EthJsonRpc] = None, solc_version: str = None, - solc_args: str = None, + solc_settings_json: str = None, enable_online_lookup: bool = False, ) -> None: self.solc_binary = self._init_solc_binary(solc_version) - self.solc_args = solc_args + self.solc_settings_json = solc_settings_json self.eth = eth self.enable_online_lookup = enable_online_lookup self.sigs = signatures.SignatureDB(enable_online_lookup=enable_online_lookup) @@ -163,13 +163,15 @@ class MythrilDisassembler: try: # import signatures from solidity source self.sigs.import_solidity_file( - file, solc_binary=self.solc_binary, solc_args=self.solc_args + file, + solc_binary=self.solc_binary, + solc_settings_json=self.solc_settings_json, ) if contract_name is not None: contract = SolidityContract( input_file=file, name=contract_name, - solc_args=self.solc_args, + solc_settings_json=self.solc_settings_json, solc_binary=self.solc_binary, ) self.contracts.append(contract) @@ -177,7 +179,7 @@ class MythrilDisassembler: else: for contract in get_contracts_from_file( input_file=file, - solc_args=self.solc_args, + solc_settings_json=self.solc_settings_json, solc_binary=self.solc_binary, ): self.contracts.append(contract) diff --git a/mythril/mythx/__init__.py b/mythril/mythx/__init__.py new file mode 100644 index 00000000..f1ebede0 --- /dev/null +++ b/mythril/mythx/__init__.py @@ -0,0 +1,111 @@ +import sys + +import os +import time + +from mythx_models.exceptions import MythXAPIError +from typing import List, Dict, Any + +from mythril.analysis.report import Issue, Report +from mythril.solidity.soliditycontract import SolidityContract + +from pythx import Client + +import logging + +log = logging.getLogger(__name__) + +TRIAL_ETH_ADDRESS = "0x0000000000000000000000000000000000000000" +TRIAL_PASSWORD = "trial" + + +def analyze(contracts: List[SolidityContract], analysis_mode: str = "quick") -> Report: + """ + Analyze contracts via the MythX API. + + :param contracts: List of solidity contracts to analyze + :param analysis_mode: The mode to submit the analysis request with. "quick" or "full" (default: "quick") + :return: Report with analyzed contracts + """ + assert analysis_mode in ("quick", "full"), "analysis_mode must be 'quick' or 'full'" + + c = Client( + eth_address=os.environ.get("MYTHX_ETH_ADDRESS", TRIAL_ETH_ADDRESS), + password=os.environ.get("MYTHX_PASSWORD", TRIAL_PASSWORD), + ) + + if c.eth_address == TRIAL_ETH_ADDRESS: + print( + "You are currently running MythX in Trial mode. This mode reports only a partial analysis of your smart contracts, limited to three vulnerabilities. To get a more complete analysis, sign up for a free account at https://mythx.io." + ) + + issues = [] # type: List[Issue] + + # TODO: Analyze multiple contracts asynchronously. + for contract in contracts: + source_codes = {} + source_list = [] + sources = {} # type: Dict[str, Any] + main_source = None + + try: + main_source = contract.input_file + for solidity_file in contract.solidity_files: + source_codes[solidity_file.filename] = solidity_file.data + for filename in contract.solc_json["sources"].keys(): + sources[filename] = {} + if source_codes[filename]: + sources[filename]["source"] = source_codes[filename] + sources[filename]["ast"] = contract.solc_json["sources"][filename][ + "ast" + ] + + source_list.append(filename) + + source_list.sort( + key=lambda fname: contract.solc_json["sources"][fname]["id"] + ) + except AttributeError: + # No solidity file + pass + + assert contract.creation_code, "Creation bytecode must exist." + try: + resp = c.analyze( + contract_name=contract.name, + analysis_mode=analysis_mode, + bytecode=contract.creation_code or None, + deployed_bytecode=contract.code or None, + sources=sources or None, + main_source=main_source, + source_list=source_list or None, + ) + except MythXAPIError as e: + log.critical(e) + + while not c.analysis_ready(resp.uuid): + log.info(c.status(resp.uuid).analysis) + time.sleep(5) + + for issue in c.report(resp.uuid): + issue = Issue( + contract=contract.name, + function_name=None, + address=issue.locations[0].source_map.components[0].offset + if issue.locations + else -1, + swc_id=issue.swc_id[4:] or "None", # remove 'SWC-' prefix + title=issue.swc_title, + bytecode=contract.creation_code, + severity=issue.severity.capitalize(), + description_head=issue.description_short, + description_tail=issue.description_long, + ) + issue.add_code_info(contract) + issues.append(issue) + + report = Report(contracts=contracts) + for issue in issues: + report.append_issue(issue) + + return report diff --git a/mythril/solidity/soliditycontract.py b/mythril/solidity/soliditycontract.py index f8594701..772141c8 100644 --- a/mythril/solidity/soliditycontract.py +++ b/mythril/solidity/soliditycontract.py @@ -44,23 +44,28 @@ class SourceCodeInfo: self.solc_mapping = mapping -def get_contracts_from_file(input_file, solc_args=None, solc_binary="solc"): +def get_contracts_from_file(input_file, solc_settings_json=None, solc_binary="solc"): """ :param input_file: - :param solc_args: + :param solc_settings_json: :param solc_binary: """ - data = get_solc_json(input_file, solc_args=solc_args, solc_binary=solc_binary) + data = get_solc_json( + input_file, solc_settings_json=solc_settings_json, solc_binary=solc_binary + ) try: - for key, contract in data["contracts"].items(): - filename, name = key.split(":") - if filename == input_file and len(contract["bin-runtime"]): + for contract_name in data["contracts"][input_file].keys(): + if len( + data["contracts"][input_file][contract_name]["evm"]["deployedBytecode"][ + "object" + ] + ): yield SolidityContract( input_file=input_file, - name=name, - solc_args=solc_args, + name=contract_name, + solc_settings_json=solc_settings_json, solc_binary=solc_binary, ) except KeyError: @@ -70,16 +75,22 @@ def get_contracts_from_file(input_file, solc_args=None, solc_binary="solc"): class SolidityContract(EVMContract): """Representation of a Solidity contract.""" - def __init__(self, input_file, name=None, solc_args=None, solc_binary="solc"): - data = get_solc_json(input_file, solc_args=solc_args, solc_binary=solc_binary) + def __init__( + self, input_file, name=None, solc_settings_json=None, solc_binary="solc" + ): + data = get_solc_json( + input_file, solc_settings_json=solc_settings_json, solc_binary=solc_binary + ) self.solidity_files = [] + self.solc_json = data + self.input_file = input_file - for filename in data["sourceList"]: + for filename, contract in data["sources"].items(): with open(filename, "r", encoding="utf-8") as file: code = file.read() full_contract_src_maps = self.get_full_contract_src_maps( - data["sources"][filename]["AST"] + contract["ast"] ) self.solidity_files.append( SolidityFile(filename, code, full_contract_src_maps) @@ -91,32 +102,28 @@ class SolidityContract(EVMContract): srcmap_constructor = [] srcmap = [] if name: - for key, contract in sorted(data["contracts"].items()): - filename, _name = key.split(":") - - if ( - filename == input_file - and name == _name - and len(contract["bin-runtime"]) - ): - code = contract["bin-runtime"] - creation_code = contract["bin"] - srcmap = contract["srcmap-runtime"].split(";") - srcmap_constructor = contract["srcmap"].split(";") - has_contract = True - break + contract = data["contracts"][input_file][name] + if len(contract["evm"]["deployedBytecode"]["object"]): + code = contract["evm"]["deployedBytecode"]["object"] + creation_code = contract["evm"]["bytecode"]["object"] + srcmap = contract["evm"]["deployedBytecode"]["sourceMap"].split(";") + srcmap_constructor = contract["evm"]["bytecode"]["sourceMap"].split(";") + has_contract = True # If no contract name is specified, get the last bytecode entry for the input file else: - for key, contract in sorted(data["contracts"].items()): - filename, name = key.split(":") - - if filename == input_file and len(contract["bin-runtime"]): - code = contract["bin-runtime"] - creation_code = contract["bin"] - srcmap = contract["srcmap-runtime"].split(";") - srcmap_constructor = contract["srcmap"].split(";") + for contract_name, contract in sorted( + data["contracts"][input_file].items() + ): + if len(contract["evm"]["deployedBytecode"]["object"]): + name = contract_name + code = contract["evm"]["deployedBytecode"]["object"] + creation_code = contract["evm"]["bytecode"]["object"] + srcmap = contract["evm"]["deployedBytecode"]["sourceMap"].split(";") + srcmap_constructor = contract["evm"]["bytecode"]["sourceMap"].split( + ";" + ) has_contract = True if not has_contract: @@ -139,8 +146,8 @@ class SolidityContract(EVMContract): :return: The source maps """ source_maps = set() - for child in ast["children"]: - if "contractKind" in child["attributes"]: + for child in ast["nodes"]: + if child.get("contractKind"): source_maps.add(child["src"]) return source_maps diff --git a/mythril/support/signatures.py b/mythril/support/signatures.py index e0deb9ea..67a1bf1d 100644 --- a/mythril/support/signatures.py +++ b/mythril/support/signatures.py @@ -1,5 +1,6 @@ """The Mythril function signature database.""" import functools +import json import logging import multiprocessing import os @@ -9,6 +10,7 @@ from collections import defaultdict from subprocess import PIPE, Popen from typing import List, Set, DefaultDict, Dict +from mythril.ethereum.util import get_solc_json from mythril.exceptions import CompilerError log = logging.getLogger(__name__) @@ -231,53 +233,20 @@ class SignatureDB(object, metaclass=Singleton): return [] def import_solidity_file( - self, file_path: str, solc_binary: str = "solc", solc_args: str = None + self, file_path: str, solc_binary: str = "solc", solc_settings_json: str = None ): """Import Function Signatures from solidity source files. :param solc_binary: - :param solc_args: + :param solc_settings_json: :param file_path: solidity source code file path :return: """ - cmd = [solc_binary, "--hashes", file_path] - if solc_args: - cmd.extend(solc_args.split()) + solc_json = get_solc_json(file_path, solc_binary, solc_settings_json) - try: - p = Popen(cmd, stdout=PIPE, stderr=PIPE) - stdout, stderr = p.communicate() - ret = p.returncode - - if ret != 0: - raise CompilerError( - "Solc has experienced a fatal error (code {}).\n\n{}".format( - ret, stderr.decode("utf-8") - ) - ) - except FileNotFoundError: - raise CompilerError( - ( - "Compiler not found. Make sure that solc is installed and in PATH, " - "or the SOLC environment variable is set." - ) - ) - - stdout = stdout.decode("unicode_escape").split("\n") - for line in stdout: - # the ':' need not be checked but just to be sure - if all(map(lambda x: x in line, ["(", ")", ":"])): - solc_bytes = "0x" + line.split(":")[0] - solc_text = line.split(":")[1].strip() - self.solidity_sigs[solc_bytes].append(solc_text) - log.debug( - "Signatures: found %d signatures after parsing" % len(self.solidity_sigs) - ) - - # update DB with what we've found - for byte_sig, text_sigs in self.solidity_sigs.items(): - for text_sig in text_sigs: - self.add(byte_sig, text_sig) + for contract in solc_json["contracts"][file_path].values(): + for name, hash in contract["evm"]["methodIdentifiers"].items(): + self.add("0x" + hash, name) @staticmethod def lookup_online(byte_sig: str, timeout: int, proxies=None) -> List[str]: diff --git a/requirements.txt b/requirements.txt index 6901873e..1a3662a0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,9 +21,10 @@ py-solc pytest>=3.6.0 pytest-cov pytest_mock -requests +requests>=2.22.0 rlp>=1.0.1 transaction>=2.2.1 z3-solver>=4.8.5.0 pysha3 matplotlib +pythx diff --git a/setup.py b/setup.py index bee87da0..dd673b04 100755 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ REQUIRED = [ "py_ecc==1.6.0", "ethereum>=2.3.2", "z3-solver>=4.8.5.0", - "requests", + "requests>=2.22.0", "py-solc", "plyvel", "eth_abi==1.3.0", @@ -49,6 +49,7 @@ REQUIRED = [ "persistent>=4.2.0", "ethereum-input-decoder>=0.2.2", "matplotlib", + "pythx", ] TESTS_REQUIRE = ["mypy", "pytest>=3.6.0", "pytest_mock", "pytest-cov"] diff --git a/tests/disassembler_test.py b/tests/disassembler_test.py index d3955d82..ffdc5f08 100644 --- a/tests/disassembler_test.py +++ b/tests/disassembler_test.py @@ -3,12 +3,6 @@ from mythril.ethereum import util from tests import * -def _compile_to_code(input_file): - compiled = util.get_solc_json(str(input_file)) - code = list(compiled["contracts"].values())[0]["bin-runtime"] - return code - - class DisassemblerTestCase(BaseTestCase): def test_instruction_list(self): code = "0x606060405236156100ca5763ffffffff60e060020a600035041663054f7d9c81146100d3578063095c21e3146100f45780630ba50baa146101165780631a3719321461012857806366529e3f14610153578063682789a81461017257806389f21efc146101915780638da5cb5b146101ac5780638f4ffcb1146101d55780639a1f2a5714610240578063b5f522f71461025b578063bd94b005146102b6578063c5ab5a13146102c8578063cc424839146102f1578063deb077b914610303578063f3fef3a314610322575b6100d15b5b565b005b34610000576100e0610340565b604080519115158252519081900360200190f35b3461000057610104600435610361565b60408051918252519081900360200190f35b34610000576100d1600435610382565b005b3461000057610104600160a060020a03600435166103b0565b60408051918252519081900360200190f35b346100005761010461041e565b60408051918252519081900360200190f35b3461000057610104610424565b60408051918252519081900360200190f35b34610000576100d1600160a060020a036004351661042b565b005b34610000576101b961046f565b60408051600160a060020a039092168252519081900360200190f35b3461000057604080516020600460643581810135601f81018490048402850184019095528484526100d1948235600160a060020a039081169560248035966044359093169594608494929391019190819084018382808284375094965061048595505050505050565b005b34610000576100d1600160a060020a03600435166106e7565b005b346100005761026b60043561072b565b60408051600160a060020a0390991689526020890197909752878701959095526060870193909352608086019190915260a085015260c084015260e083015251908190036101000190f35b34610000576100d160043561077a565b005b34610000576101b9610830565b60408051600160a060020a039092168252519081900360200190f35b34610000576100d160043561083f565b005b34610000576101046108a1565b60408051918252519081900360200190f35b34610000576100d1600160a060020a03600435166024356108a7565b005b60015474010000000000000000000000000000000000000000900460ff1681565b600681815481101561000057906000526020600020900160005b5054905081565b600054600160a060020a036301000000909104811690331681146103a557610000565b60038290555b5b5050565b6005546040805160006020918201819052825160e260020a631d010437028152600160a060020a03868116600483015293519194939093169263740410dc92602480830193919282900301818787803b156100005760325a03f115610000575050604051519150505b919050565b60035481565b6006545b90565b600054600160a060020a0363010000009091048116903316811461044e57610000565b60018054600160a060020a031916600160a060020a0384161790555b5b5050565b60005463010000009004600160a060020a031681565b6000600060006000600060006000600160149054906101000a900460ff16156104ad57610000565b87600081518110156100005760209101015160005460f860020a918290048202975002600160f860020a031990811690871614156105405760009450600196505b600587101561053057878781518110156100005790602001015160f860020a900460f860020a0260f860020a900485610100020194505b6001909601956104ee565b61053b8b868c610955565b6106d6565b600054610100900460f860020a02600160f860020a0319908116908716141561069e57506001955060009250829150819050805b60058710156105b657878781518110156100005790602001015160f860020a900460f860020a0260f860020a900481610100020190505b600190960195610574565b600596505b60098710156105fd57878781518110156100005790602001015160f860020a900460f860020a0260f860020a900484610100020193505b6001909601956105bb565b600996505b600d87101561064457878781518110156100005790602001015160f860020a900460f860020a0260f860020a900483610100020192505b600190960195610602565b600d96505b601187101561068b57878781518110156100005790602001015160f860020a900460f860020a0260f860020a900482610100020191505b600190960195610649565b61053b8b828c878787610bc4565b6106d6565b60005462010000900460f860020a02600160f860020a031990811690871614156106d15761053b8b8b610e8e565b6106d6565b610000565b5b5b5b5b5050505050505050505050565b600054600160a060020a0363010000009091048116903316811461070a57610000565b60058054600160a060020a031916600160a060020a0384161790555b5b5050565b600760208190526000918252604090912080546001820154600283015460038401546004850154600586015460068701549690970154600160a060020a03909516969395929491939092909188565b600081815260076020526040812054600160a060020a0390811690331681146107a257610000565b600083815260076020526040902080546004820154600583015460038401549395506107dc93600160a060020a039093169291029061105e565b50600060058301556107ed83611151565b6040805184815290517fb5dc9baf0cb4e7e4759fa12eadebddf9316e26147d5a9ae150c4228d5a1dd23f9181900360200190a161082933611244565b5b5b505050565b600154600160a060020a031681565b600054600160a060020a0363010000009091048116903316811461086257610000565b600080546040516301000000909104600160a060020a0316916108fc851502918591818181858888f1935050505015156103ab57610000565b5b5b5050565b60025481565b600054600160a060020a036301000000909104811690331681146108ca57610000565b6000805460408051602090810184905281517fa9059cbb0000000000000000000000000000000000000000000000000000000081526301000000909304600160a060020a0390811660048501526024840187905291519187169363a9059cbb9360448082019492918390030190829087803b156100005760325a03f115610000575050505b5b505050565b610100604051908101604052806000600160a060020a03168152602001600081526020016000815260200160008152602001600081526020016000815260200160008152602001600081525060006007600085815260200190815260200160002061010060405190810160405290816000820160009054906101000a9004600160a060020a0316600160a060020a0316600160a060020a0316815260200160018201548152602001600282015481526020016003820154815260200160048201548152602001600582015481526020016006820154815260200160078201548152505091508260001415610a4857610000565b8160400151838115610000570615610a5f57610000565b6002548410610a6d57610000565b8160400151838115610000570490508160a00151811115610a8d57610000565b610a9c8584846020015161128e565b1515610aa757610000565b60a082018051829003815260008581526007602081815260409283902086518154600160a060020a031916600160a060020a038216178255918701516001820181905593870151600282015560608701516003820155608087015160048201559351600585015560c0860151600685015560e08601519390910192909255610b319190859061105e565b1515610b3c57610000565b610b518582846080015102846060015161105e565b1515610b5c57610000565b60a0820151158015610b71575060c082015115155b15610b7f57610b7f84611151565b5b6040805185815290517fb5dc9baf0cb4e7e4759fa12eadebddf9316e26147d5a9ae150c4228d5a1dd23f9181900360200190a1610bbc85611244565b5b5050505050565b831515610bd057610000565b82851415610bdd57610000565b801580610be8575081155b15610bf257610000565b80848115610000570615610c0557610000565b6005546040805160006020918201819052825160e260020a631d010437028152600160a060020a038b8116600483015293518695949094169363740410dc9360248084019491938390030190829087803b156100005760325a03f11561000057505050604051805190501015610c7a57610000565b610c8586858761128e565b1515610c9057610000565b600554604080517fbe0140a6000000000000000000000000000000000000000000000000000000008152600160a060020a03898116600483015260006024830181905260448301869052925193169263be0140a69260648084019391929182900301818387803b156100005760325a03f115610000575050506101006040519081016040528087600160a060020a03168152602001848152602001838152602001868152602001828681156100005704815260200182815260200160068054905081526020014281525060076000600254815260200190815260200160002060008201518160000160006101000a815481600160a060020a030219169083600160a060020a031602179055506020820151816001015560408201518160020155606082015181600301556080820151816004015560a0820151816005015560c0820151816006015560e0820151816007015590505060068054806001018281815481835581811511610e2757600083815260209020610e279181019083015b80821115610e235760008155600101610e0f565b5090565b5b505050916000526020600020900160005b50600280549182905560018201905560408051918252517fb5dc9baf0cb4e7e4759fa12eadebddf9316e26147d5a9ae150c4228d5a1dd23f92509081900360200190a1610e8586611244565b5b505050505050565b600354818115610000570615610ea357610000565b600160009054906101000a9004600160a060020a0316600160a060020a031663cf35bdd060016000604051602001526040518263ffffffff1660e060020a02815260040180828152602001915050602060405180830381600087803b156100005760325a03f115610000575050604080518051600080546020938401829052845160e060020a6323b872dd028152600160a060020a038981166004830152630100000090920482166024820152604481018890529451921694506323b872dd936064808201949392918390030190829087803b156100005760325a03f1156100005750506040515115159050610f9857610000565b600554600354600160a060020a039091169063be0140a6908490600190858115610000576040805160e060020a63ffffffff8816028152600160a060020a039095166004860152921515602485015204604483015251606480830192600092919082900301818387803b156100005760325a03f1156100005750505061101d82611244565b60408051600160a060020a038416815290517f30a29a0aa75376a69254bb98dbd11db423b7e8c3473fb5bf0fcba60bcbc42c4b9181900360200190a15b5050565b600081151561106c57610000565b6001546040805160006020918201819052825160e460020a630cf35bdd028152600481018790529251600160a060020a039094169363cf35bdd09360248082019493918390030190829087803b156100005760325a03f1156100005750505060405180519050600160a060020a031663a9059cbb85856000604051602001526040518363ffffffff1660e060020a0281526004018083600160a060020a0316600160a060020a0316815260200182815260200192505050602060405180830381600087803b156100005760325a03f115610000575050604051519150505b9392505050565b6000818152600760205260409020600690810154815490919060001981019081101561000057906000526020600020900160005b5054600682815481101561000057906000526020600020900160005b50556006805460001981018083559091908280158290116111e7576000838152602090206111e79181019083015b80821115610e235760008155600101610e0f565b5090565b5b50506006548314915061122d9050578060076000600684815481101561000057906000526020600020900160005b505481526020810191909152604001600020600601555b6000828152600760205260408120600601555b5050565b60045481600160a060020a031631101561128957600454604051600160a060020a0383169180156108fc02916000818181858888f19350505050151561128957610000565b5b5b50565b600081151561129c57610000565b6001546040805160006020918201819052825160e460020a630cf35bdd028152600481018790529251600160a060020a039094169363cf35bdd09360248082019493918390030190829087803b156100005760325a03f11561000057505060408051805160006020928301819052835160e060020a6323b872dd028152600160a060020a038a811660048301523081166024830152604482018a905294519490921694506323b872dd93606480840194939192918390030190829087803b156100005760325a03f115610000575050604051519150505b93925050505600a165627a7a723058204dee0e1bf170a9d122508f3e876c4a84893b12a7345591521af4ca737bb765000029"