From e02a2612d0cbee2f750966cdf5866bb1891484dd Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Mon, 13 May 2019 19:29:14 +0530 Subject: [PATCH 001/164] Refactor cli --- mythril/interfaces/cli.py | 384 ++++++++++++++++++++------------------ tests/cmd_line_test.py | 20 +- tests/test_cli_opts.py | 4 +- 3 files changed, 214 insertions(+), 194 deletions(-) diff --git a/mythril/interfaces/cli.py b/mythril/interfaces/cli.py index adb0e8f2..4a7b6d98 100644 --- a/mythril/interfaces/cli.py +++ b/mythril/interfaces/cli.py @@ -57,10 +57,35 @@ def exit_with_error(format_, message): def main() -> None: """The main CLI interface entry point.""" + common_parser = argparse.ArgumentParser(add_help=False) + create_parser(common_parser) + parser = argparse.ArgumentParser( description="Security analysis of Ethereum smart contracts" ) - create_parser(parser) + + subparsers = parser.add_subparsers(dest="command", help="Commands") + analyzer_parser = subparsers.add_parser("analyze", parents=[common_parser]) + subparsers.add_parser("disassemble", parents=[common_parser]) + read_storage_parser = subparsers.add_parser("read-storage", parents=[common_parser]) + leveldb_search_parser = subparsers.add_parser( + "leveldb-search", parents=[common_parser] + ) + contract_func_to_hash = subparsers.add_parser( + "contract-function-to-hash", parents=[common_parser] + ) + contract_hash_to_addr = subparsers.add_parser( + "contract-hash-to-address", parents=[common_parser] + ) + subparsers.add_parser("version", parents=[common_parser]) + + create_analyzer_parser(analyzer_parser) + 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_leveldb_parser(leveldb_search_parser) + + subparsers.add_parser("truffle", parents=[analyzer_parser], add_help=False) # Get config values @@ -68,94 +93,15 @@ def main() -> None: parse_args(parser=parser, args=args) -def create_parser(parser: argparse.ArgumentParser) -> None: - """ - Creates the parser by setting all the possible arguments - :param parser: The parser - """ - parser.add_argument("solidity_file", nargs="*") - - commands = parser.add_argument_group("commands") - commands.add_argument("-g", "--graph", help="generate a control flow graph") - commands.add_argument( - "-V", - "--version", - action="store_true", - help="print the Mythril version number and exit", - ) - commands.add_argument( - "-x", - "--fire-lasers", - action="store_true", - help="detect vulnerabilities, use with -c, -a or solidity file(s)", - ) - commands.add_argument( - "--truffle", - action="store_true", - help="analyze a truffle project (run from project dir)", - ) - commands.add_argument( - "-d", "--disassemble", action="store_true", help="print disassembly" - ) - commands.add_argument( - "-j", - "--statespace-json", - help="dumps the statespace json", - metavar="OUTPUT_FILE", - ) - - inputs = parser.add_argument_group("input arguments") - inputs.add_argument( - "-c", - "--code", - help='hex-encoded bytecode string ("6060604052...")', - metavar="BYTECODE", - ) - inputs.add_argument( - "-f", - "--codefile", - help="file containing hex-encoded bytecode string", - metavar="BYTECODEFILE", - type=argparse.FileType("r"), - ) - inputs.add_argument( - "-a", - "--address", - help="pull contract from the blockchain", - metavar="CONTRACT_ADDRESS", - ) - inputs.add_argument( - "-l", - "--dynld", - action="store_true", - help="auto-load dependencies from the blockchain", - ) - inputs.add_argument( - "--no-onchain-storage-access", - action="store_true", - help="turns off getting the data from onchain contracts", - ) - inputs.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.", +def create_read_storage_parser(read_storage_parser: argparse.ArgumentParser): + read_storage_parser.add_argument( + "--storage-slots", + help="read state vasriables from storage index, use with -a", + metavar="INDEX,NUM_SLOTS,[array] / mapping,INDEX,[KEY1, KEY2...]", ) - outputs = parser.add_argument_group("output formats") - outputs.add_argument( - "-o", - "--outform", - choices=["text", "markdown", "json", "jsonv2"], - default="text", - help="report output format", - metavar="", - ) - outputs.add_argument( - "--verbose-report", - action="store_true", - help="Include debugging information in report", - ) +def create_leveldb_parser(parser: argparse.ArgumentParser): database = parser.add_argument_group("local contracts database") database.add_argument( "-s", "--search", help="search the contract database", metavar="EXPRESSION" @@ -166,27 +112,40 @@ def create_parser(parser: argparse.ArgumentParser) -> None: metavar="LEVELDB_PATH", ) - utilities = parser.add_argument_group("utilities") - utilities.add_argument( - "--hash", help="calculate function signature hash", metavar="SIGNATURE" - ) - utilities.add_argument( - "--storage", - help="read state variables from storage index, use with -a", - metavar="INDEX,NUM_SLOTS,[array] / mapping,INDEX,[KEY1, KEY2...]", + +def create_version_parser(hash_parser: argparse.ArgumentParser): + hash_parser.add_argument( + "version", help="print the Mythril version number and exit", metavar="VERSION" ) - utilities.add_argument( - "--solv", - help="specify solidity compiler version. If not present, will try to install it (Experimental)", - metavar="SOLV", + + +def create_func_to_hash_parser(hash_parser: argparse.ArgumentParser): + hash_parser.add_argument( + "func_name", help="calculate function signature hash", metavar="SIGNATURE" ) - utilities.add_argument( - "--contract-hash-to-address", - help="returns corresponding address for a contract address hash", - metavar="SHA3_TO_LOOK_FOR", + + +def create_hash_to_addr_parser(hash_parser: argparse.ArgumentParser): + hash_parser.add_argument( + "--hash", help="Find the address from hash", metavar="FUNCTION_NAME" ) - options = parser.add_argument_group("options") + +def create_analyzer_parser(analyzer_parser: argparse.ArgumentParser): + commands = analyzer_parser.add_argument_group("commands") + commands.add_argument("-g", "--graph", help="generate a control flow graph") + commands.add_argument( + "-j", + "--statespace-json", + help="dumps the statespace json", + metavar="OUTPUT_FILE", + ) + commands.add_argument( + "--truffle", + action="store_true", + help="analyze a truffle project (run from project dir)", + ) + options = analyzer_parser.add_argument_group("options") options.add_argument( "-m", "--modules", @@ -225,15 +184,23 @@ 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( - "--phrack", action="store_true", help="Phrack-style call graph" + "-l", + "--dynld", + action="store_true", + help="auto-load dependencies from the blockchain", ) options.add_argument( - "--enable-physics", action="store_true", help="enable graph physics simulation" + "--no-onchain-storage-access", + action="store_true", + help="turns off getting the data from onchain contracts", ) + options.add_argument( - "-v", type=int, help="log level (0-5)", metavar="LOG_LEVEL", default=2 + "--phrack", action="store_true", help="Phrack-style call graph" + ) + options.add_argument( + "--enable-physics", action="store_true", help="enable graph physics simulation" ) options.add_argument( "-q", @@ -245,6 +212,43 @@ def create_parser(parser: argparse.ArgumentParser) -> None: "--enable-iprof", action="store_true", help="enable the instruction profiler" ) + +def create_parser(parser: argparse.ArgumentParser) -> None: + parser.add_argument("solidity_file", nargs="*") + inputs = parser.add_argument_group("input arguments") + inputs.add_argument( + "-c", + "--code", + help='hex-encoded bytecode string ("6060604052...")', + metavar="BYTECODE", + ) + inputs.add_argument( + "-f", + "--codefile", + help="file containing hex-encoded bytecode string", + metavar="BYTECODEFILE", + type=argparse.FileType("r"), + ) + inputs.add_argument( + "-a", + "--address", + help="pull contract from the blockchain", + metavar="CONTRACT_ADDRESS", + ) + inputs.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.", + ) + utilities = parser.add_argument_group("utilities") + + utilities.add_argument("--solc-args", help="Extra arguments for solc") + utilities.add_argument( + "--solv", + help="specify solidity compiler version. If not present, will try to install it (Experimental)", + metavar="SOLV", + ) + rpc = parser.add_argument_group("RPC options") rpc.add_argument( @@ -258,22 +262,26 @@ def create_parser(parser: argparse.ArgumentParser) -> None: ) parser.add_argument("--epic", action="store_true", help=argparse.SUPPRESS) + outputs = parser.add_argument_group("output formats") + outputs.add_argument( + "-o", + "--outform", + choices=["text", "markdown", "json", "jsonv2"], + default="text", + help="report output format", + metavar="", + ) + outputs.add_argument( + "--verbose-report", + action="store_true", + help="Include debugging information in report", + ) + outputs.add_argument( + "-v", type=int, help="log level (0-5)", metavar="LOG_LEVEL", default=2 + ) -def validate_args(parser: argparse.ArgumentParser, args: argparse.Namespace): - if not ( - args.search - or args.hash - or args.disassemble - or args.graph - or args.fire_lasers - or args.storage - or args.truffle - or args.statespace_json - or args.contract_hash_to_address - ): - parser.print_help() - sys.exit() +def validate_args(args: argparse.Namespace): if args.v: if 0 <= args.v < 6: log_levels = [ @@ -293,59 +301,56 @@ def validate_args(parser: argparse.ArgumentParser, args: argparse.Namespace): args.outform, "Invalid -v value, you can find valid values in usage" ) - if args.query_signature: - if sigs.ethereum_input_decoder is None: + if args.command == "analyze": + if args.query_signature and sigs.ethereum_input_decoder is None: exit_with_error( args.outform, "The --query-signature function requires the python package ethereum-input-decoder", ) - if args.enable_iprof: - if args.v < 4: - exit_with_error( - args.outform, - "--enable-iprof must be used with -v LOG_LEVEL where LOG_LEVEL >= 4", - ) - elif not (args.graph or args.fire_lasers or args.statespace_json): - exit_with_error( - args.outform, - "--enable-iprof must be used with one of -g, --graph, -x, --fire-lasers, -j and --statespace-json", - ) - - -def quick_commands(args: argparse.Namespace): - if args.hash: - print(MythrilDisassembler.hash_for_function_signature(args.hash)) - sys.exit() + if args.enable_iprof: + if args.v < 4: + exit_with_error( + args.outform, + "--enable-iprof must be used with -v LOG_LEVEL where LOG_LEVEL >= 4", + ) + elif not (args.graph or args.fire_lasers or args.statespace_json): + exit_with_error( + args.outform, + "--enable-iprof must be used with one of -g, --graph, -x, --fire-lasers, -j and --statespace-json", + ) def set_config(args: argparse.Namespace): config = MythrilConfig() - if args.dynld or not args.no_onchain_storage_access and not (args.rpc or args.i): + if ( + args.command == "analyze" and (args.dynld or not args.no_onchain_storage_access) + ) and not (args.rpc or args.i): config.set_api_from_config_path() if args.address: # Establish RPC connection if necessary config.set_api_rpc(rpc=args.rpc, rpctls=args.rpctls) - elif args.search or args.contract_hash_to_address: + if args.command in ("contract-hash-to-address", "leveldb-search"): # Open LevelDB if necessary - config.set_api_leveldb( - config.leveldb_dir if not args.leveldb_dir else args.leveldb_dir + leveldb_dir = ( + args.leveldb_dir if "leveldb_dir" in args.__dict__ else config.leveldb_dir ) + config.set_api_leveldb(leveldb_dir) return config def leveldb_search(config: MythrilConfig, args: argparse.Namespace): - if args.search or args.contract_hash_to_address: + if args.command in ("contract-hash-to-address", "leveldb-search"): leveldb_searcher = MythrilLevelDB(config.eth_db) - if args.search: + if args.command == "leveldb-search": # Database search ops leveldb_searcher.search_db(args.search) else: # search corresponding address try: - leveldb_searcher.contract_hash_to_address(args.contract_hash_to_address) + leveldb_searcher.contract_hash_to_address(args.hash) except AddressNotFoundError: print("Address not found.") @@ -367,7 +372,7 @@ def get_code(disassembler: MythrilDisassembler, args: argparse.Namespace): address, _ = disassembler.load_from_address(args.address) elif args.solidity_file: # Compile Solidity source file(s) - if args.graph and len(args.solidity_file) > 1: + if args.command == "analyze" and args.graph and len(args.solidity_file) > 1: exit_with_error( args.outform, "Cannot generate call graphs from multiple input files. Please do it one at a time.", @@ -390,7 +395,7 @@ def execute_command( args: argparse.Namespace, ): - if args.storage: + if args.command == "read-storage": if not args.address: exit_with_error( args.outform, @@ -398,23 +403,13 @@ def execute_command( ) storage = disassembler.get_state_variable_from_storage( - address=address, params=[a.strip() for a in args.storage.strip().split(",")] + address=address, + params=[a.strip() for a in args.storage_slots.strip().split(",")], ) print(storage) return - analyzer = MythrilAnalyzer( - strategy=args.strategy, - disassembler=disassembler, - address=address, - max_depth=args.max_depth, - execution_timeout=args.execution_timeout, - create_timeout=args.create_timeout, - enable_iprof=args.enable_iprof, - onchain_storage_access=not args.no_onchain_storage_access, - ) - - if args.disassemble: + if args.command == "disassemble": # or mythril.disassemble(mythril.contracts[0]) if disassembler.contracts[0].code: @@ -422,7 +417,18 @@ def execute_command( if disassembler.contracts[0].creation_code: print("Disassembly: \n" + disassembler.contracts[0].get_creation_easm()) - elif args.graph or args.fire_lasers: + elif args.command == "analyze": + analyzer = MythrilAnalyzer( + strategy=args.strategy, + disassembler=disassembler, + address=address, + max_depth=args.max_depth, + execution_timeout=args.execution_timeout, + create_timeout=args.create_timeout, + enable_iprof=args.enable_iprof, + onchain_storage_access=not args.no_onchain_storage_access, + ) + if not disassembler.contracts: exit_with_error( args.outform, "input files do not contain any valid contracts" @@ -442,6 +448,21 @@ def execute_command( except Exception as e: exit_with_error(args.outform, "Error saving graph: " + str(e)) + elif args.statespace_json: + + if not analyzer.contracts: + exit_with_error( + args.outform, "input files do not contain any valid contracts" + ) + + statespace = analyzer.dump_statespace(contract=analyzer.contracts[0]) + + try: + with open(args.statespace_json, "w") as f: + json.dump(statespace, f) + except Exception as e: + exit_with_error(args.outform, "Error saving json: " + str(e)) + else: try: report = analyzer.fire_lasers( @@ -463,25 +484,15 @@ def execute_command( args.outform, "Error loading analyis modules: " + format(e) ) - elif args.statespace_json: - - if not analyzer.contracts: - exit_with_error( - args.outform, "input files do not contain any valid contracts" - ) - - statespace = analyzer.dump_statespace(contract=analyzer.contracts[0]) - - try: - with open(args.statespace_json, "w") as f: - json.dump(statespace, f) - except Exception as e: - exit_with_error(args.outform, "Error saving json: " + str(e)) - else: parser.print_help() +def contract_hash_to_address(args: argparse.Namespace): + print(MythrilDisassembler.hash_for_function_signature(args.func_name)) + sys.exit() + + def parse_args(parser: argparse.ArgumentParser, args: argparse.Namespace) -> None: """ Parses the arguments @@ -495,7 +506,7 @@ def parse_args(parser: argparse.ArgumentParser, args: argparse.Namespace) -> Non os.system(" ".join(sys.argv) + " | python3 " + path + "/epic.py") sys.exit() - if args.version: + if args.command == "version": if args.outform == "json": print(json.dumps({"version_str": VERSION})) else: @@ -503,23 +514,28 @@ def parse_args(parser: argparse.ArgumentParser, args: argparse.Namespace) -> Non sys.exit() # Parse cmdline args - validate_args(parser, args) + validate_args(args) try: - quick_commands(args) + if args.command == "contract-function-to-hash": + contract_hash_to_address(args) config = set_config(args) leveldb_search(config, args) + query_signature = ( + args.query_signature if "query_signature" in args.__dict__ else None + ) disassembler = MythrilDisassembler( eth=config.eth, solc_version=args.solv, solc_args=args.solc_args, - enable_online_lookup=args.query_signature, + enable_online_lookup=query_signature, ) - if args.truffle: + if args.command == "truffle": try: disassembler.analyze_truffle_project(args) except FileNotFoundError: print( - "Build directory not found. Make sure that you start the analysis from the project root, and that 'truffle compile' has executed successfully." + "Build directory not found. Make sure that you start the analysis from the project root, " + "and that 'truffle compile' has executed successfully." ) sys.exit() diff --git a/tests/cmd_line_test.py b/tests/cmd_line_test.py index 0862cf9e..dad51e5f 100644 --- a/tests/cmd_line_test.py +++ b/tests/cmd_line_test.py @@ -15,23 +15,27 @@ def output_of(command): class CommandLineToolTestCase(BaseTestCase): def test_disassemble_code_correctly(self): - command = "python3 {} MYTH -d --bin-runtime -c 0x5050 --solv 0.5.0".format(MYTH) + command = "python3 {} disassemble --bin-runtime -c 0x5050 --solv 0.5.0".format( + MYTH + ) self.assertIn("0 POP\n1 POP\n", output_of(command)) def test_disassemble_solidity_file_correctly(self): solidity_file = str(TESTDATA / "input_contracts" / "metacoin.sol") - command = "python3 {} -d {} --solv 0.5.0".format(MYTH, solidity_file) + command = "python3 {} disassemble {} --solv 0.5.0".format(MYTH, solidity_file) self.assertIn("2 PUSH1 0x40\n4 MSTORE", output_of(command)) def test_hash_a_function_correctly(self): - command = "python3 {} --solv 0.5.0 --hash 'setOwner(address)'".format(MYTH) + command = "python3 {} contract-function-to-hash --solv 0.5.0 'setOwner(address)'".format( + MYTH + ) self.assertIn("0x13af4035\n", output_of(command)) class TruffleTestCase(BaseTestCase): def test_analysis_truffle_project(self): truffle_project_root = str(TESTS_DIR / "truffle_project") - command = "cd {}; truffle compile; python3 {} --truffle -t 2".format( + command = "cd {}; truffle compile; python3 {} truffle -t 2".format( truffle_project_root, MYTH ) self.assertIn("=== Unprotected Ether Withdrawal ====", output_of(command)) @@ -39,7 +43,7 @@ class TruffleTestCase(BaseTestCase): class InfuraTestCase(BaseTestCase): def test_infura_mainnet(self): - command = "python3 {} --rpc infura-mainnet -d -a 0x2a0c0dbecc7e4d658f48e01e3fa353f44050c208".format( + command = "python3 {} disassemble --rpc infura-mainnet -a 0x2a0c0dbecc7e4d658f48e01e3fa353f44050c208".format( MYTH ) output = output_of(command) @@ -47,21 +51,21 @@ class InfuraTestCase(BaseTestCase): self.assertIn("7278 POP\n7279 POP\n7280 JUMP\n7281 STOP", output) def test_infura_rinkeby(self): - command = "python3 {} --rpc infura-rinkeby -d -a 0xB6f2bFED892a662bBF26258ceDD443f50Fa307F5".format( + command = "python3 {} disassemble --rpc infura-rinkeby -a 0xB6f2bFED892a662bBF26258ceDD443f50Fa307F5".format( MYTH ) output = output_of(command) self.assertIn("34 JUMPDEST\n35 CALLVALUE", output) def test_infura_kovan(self): - command = "python3 {} --rpc infura-kovan -d -a 0xE6bBF9B5A3451242F82f8cd458675092617a1235".format( + command = "python3 {} disassemble --rpc infura-kovan -a 0xE6bBF9B5A3451242F82f8cd458675092617a1235".format( MYTH ) output = output_of(command) self.assertIn("9999 PUSH1 0x00\n10001 NOT\n10002 AND\n10003 PUSH1 0x00", output) def test_infura_ropsten(self): - command = "python3 {} --rpc infura-ropsten -d -a 0x6e0E0e02377Bc1d90E8a7c21f12BA385C2C35f78".format( + command = "python3 {} disassemble --rpc infura-ropsten -a 0x6e0E0e02377Bc1d90E8a7c21f12BA385C2C35f78".format( MYTH ) output = output_of(command) diff --git a/tests/test_cli_opts.py b/tests/test_cli_opts.py index 5de6cdcd..1985e45e 100644 --- a/tests/test_cli_opts.py +++ b/tests/test_cli_opts.py @@ -8,7 +8,7 @@ import sys def test_version_opt(capsys): # Check that "myth --version" returns a string with the word # "version" in it - sys.argv = ["mythril", "--version"] + sys.argv = ["mythril", "version"] with pytest.raises(SystemExit) as pytest_wrapped_e: main() assert pytest_wrapped_e.type == SystemExit @@ -16,7 +16,7 @@ def test_version_opt(capsys): assert captured.out.find(" version ") >= 1 # Check that "myth --version -o json" returns a JSON object - sys.argv = ["mythril", "--version", "-o", "json"] + sys.argv = ["mythril", "version", "-o", "json"] with pytest.raises(SystemExit) as pytest_wrapped_e: main() assert pytest_wrapped_e.type == SystemExit From c14f6a32925ccb161bdd8e208f6a4dd6422eed4e Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Tue, 14 May 2019 13:15:12 +0530 Subject: [PATCH 002/164] enhance cli --- mythril/interfaces/cli.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/mythril/interfaces/cli.py b/mythril/interfaces/cli.py index 4a7b6d98..0ee241dd 100644 --- a/mythril/interfaces/cli.py +++ b/mythril/interfaces/cli.py @@ -66,7 +66,7 @@ def main() -> None: subparsers = parser.add_subparsers(dest="command", help="Commands") analyzer_parser = subparsers.add_parser("analyze", parents=[common_parser]) - subparsers.add_parser("disassemble", parents=[common_parser]) + disassemble_parser = subparsers.add_parser("disassemble", parents=[common_parser]) read_storage_parser = subparsers.add_parser("read-storage", parents=[common_parser]) leveldb_search_parser = subparsers.add_parser( "leveldb-search", parents=[common_parser] @@ -79,6 +79,7 @@ def main() -> None: ) subparsers.add_parser("version", parents=[common_parser]) + create_disassemble_parser(disassemble_parser) create_analyzer_parser(analyzer_parser) create_read_storage_parser(read_storage_parser) create_hash_to_addr_parser(contract_hash_to_addr) @@ -93,6 +94,10 @@ def main() -> None: parse_args(parser=parser, args=args) +def create_disassemble_parser(parser): + parser.add_argument("solidity_file", nargs="*") + + def create_read_storage_parser(read_storage_parser: argparse.ArgumentParser): read_storage_parser.add_argument( "--storage-slots", @@ -102,11 +107,8 @@ def create_read_storage_parser(read_storage_parser: argparse.ArgumentParser): def create_leveldb_parser(parser: argparse.ArgumentParser): - database = parser.add_argument_group("local contracts database") - database.add_argument( - "-s", "--search", help="search the contract database", metavar="EXPRESSION" - ) - database.add_argument( + parser.add_argument("search") + parser.add_argument( "--leveldb-dir", help="specify leveldb directory for search or direct access operations", metavar="LEVELDB_PATH", @@ -127,11 +129,12 @@ def create_func_to_hash_parser(hash_parser: argparse.ArgumentParser): def create_hash_to_addr_parser(hash_parser: argparse.ArgumentParser): hash_parser.add_argument( - "--hash", help="Find the address from hash", metavar="FUNCTION_NAME" + "hash", help="Find the address from hash", metavar="FUNCTION_NAME" ) def create_analyzer_parser(analyzer_parser: argparse.ArgumentParser): + analyzer_parser.add_argument("solidity_file", nargs="*") commands = analyzer_parser.add_argument_group("commands") commands.add_argument("-g", "--graph", help="generate a control flow graph") commands.add_argument( @@ -214,7 +217,6 @@ def create_analyzer_parser(analyzer_parser: argparse.ArgumentParser): def create_parser(parser: argparse.ArgumentParser) -> None: - parser.add_argument("solidity_file", nargs="*") inputs = parser.add_argument_group("input arguments") inputs.add_argument( "-c", @@ -333,9 +335,10 @@ def set_config(args: argparse.Namespace): config.set_api_rpc(rpc=args.rpc, rpctls=args.rpctls) if args.command in ("contract-hash-to-address", "leveldb-search"): # Open LevelDB if necessary - leveldb_dir = ( - args.leveldb_dir if "leveldb_dir" in args.__dict__ else config.leveldb_dir - ) + if "leveldb_dir" not in args.__dict__ or args.leveldb_dir is None: + leveldb_dir = config.leveldb_dir + else: + leveldb_dir = args.leveldb_dir config.set_api_leveldb(leveldb_dir) return config From 7611d2b01ad10c35a17516c37144c6bf41c49dd8 Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Thu, 16 May 2019 12:23:39 +0530 Subject: [PATCH 003/164] Re-Refactor the cli structure for more flexibility --- mythril/interfaces/cli.py | 235 +++++++++++++++---------- tests/{ => cli_tests}/cmd_line_test.py | 10 +- tests/{ => cli_tests}/test_cli_opts.py | 0 3 files changed, 143 insertions(+), 102 deletions(-) rename tests/{ => cli_tests}/cmd_line_test.py (90%) rename tests/{ => cli_tests}/test_cli_opts.py (100%) diff --git a/mythril/interfaces/cli.py b/mythril/interfaces/cli.py index 0ee241dd..1fd1f140 100644 --- a/mythril/interfaces/cli.py +++ b/mythril/interfaces/cli.py @@ -55,29 +55,125 @@ def exit_with_error(format_, message): sys.exit() +def get_input_parser(): + parser = argparse.ArgumentParser(add_help=False) + parser.add_argument( + "-c", + "--code", + help='hex-encoded bytecode string ("6060604052...")', + metavar="BYTECODE", + ) + parser.add_argument( + "-f", + "--codefile", + help="file containing hex-encoded bytecode string", + 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 + + +def get_output_parser(): + parser = argparse.ArgumentParser(add_help=False) + parser.add_argument("--epic", action="store_true", help=argparse.SUPPRESS) + parser.add_argument( + "-o", + "--outform", + choices=["text", "markdown", "json", "jsonv2"], + default="text", + help="report output format", + metavar="", + ) + parser.add_argument( + "--verbose-report", + action="store_true", + help="Include debugging information in report", + ) + parser.add_argument( + "-v", type=int, help="log level (0-5)", metavar="LOG_LEVEL", default=2 + ) + return parser + + +def get_rpc_parser(): + parser = argparse.ArgumentParser(add_help=False) + parser.add_argument( + "--rpc", + help="custom RPC settings", + metavar="HOST:PORT / ganache / infura-[network_name]", + default="infura-mainnet", + ) + parser.add_argument( + "--rpctls", type=bool, default=False, help="RPC connection over TLS" + ) + return parser + + +def get_utilities_parser(): + parser = argparse.ArgumentParser(add_help=False) + parser.add_argument("--solc-args", help="Extra arguments for solc") + parser.add_argument( + "--solv", + help="specify solidity compiler version. If not present, will try to install it (Experimental)", + metavar="SOLV", + ) + return parser + + def main() -> None: """The main CLI interface entry point.""" - common_parser = argparse.ArgumentParser(add_help=False) - create_parser(common_parser) + rpc_parser = get_rpc_parser() + utilities_parser = get_utilities_parser() + input_parser = get_input_parser() + output_parser = get_output_parser() parser = argparse.ArgumentParser( description="Security analysis of Ethereum smart contracts" ) subparsers = parser.add_subparsers(dest="command", help="Commands") - analyzer_parser = subparsers.add_parser("analyze", parents=[common_parser]) - disassemble_parser = subparsers.add_parser("disassemble", parents=[common_parser]) - read_storage_parser = subparsers.add_parser("read-storage", parents=[common_parser]) + analyzer_parser = subparsers.add_parser( + "analyze", + help="Triggers analysis of smart contract", + parents=[rpc_parser, utilities_parser, input_parser, output_parser], + ) + disassemble_parser = subparsers.add_parser( + "disassemble", + help="Disassembles smart contract", + parents=[rpc_parser, utilities_parser, input_parser, output_parser], + ) + read_storage_parser = subparsers.add_parser( + "read-storage", + help="Retrieves storage slots from rpc address", + parents=[rpc_parser, output_parser], + ) leveldb_search_parser = subparsers.add_parser( - "leveldb-search", parents=[common_parser] + "leveldb-search", parents=[output_parser], help="Search code in local leveldb" ) contract_func_to_hash = subparsers.add_parser( - "contract-function-to-hash", parents=[common_parser] + "function-to-hash", + parents=[output_parser], + help="Returns the hash signature of the function", ) contract_hash_to_addr = subparsers.add_parser( - "contract-hash-to-address", parents=[common_parser] + "hash-to-address", + parents=[output_parser], + help="converts the hashes in the blockchain to ethereum address", + ) + subparsers.add_parser( + "version", parents=[output_parser], help="Outputs the version" ) - subparsers.add_parser("version", parents=[common_parser]) create_disassemble_parser(disassemble_parser) create_analyzer_parser(analyzer_parser) @@ -99,11 +195,15 @@ def create_disassemble_parser(parser): def create_read_storage_parser(read_storage_parser: argparse.ArgumentParser): + read_storage_parser.add_argument( - "--storage-slots", - help="read state vasriables from storage index, use with -a", + "storage_slots", + help="read state variables from storage index, use with -a", metavar="INDEX,NUM_SLOTS,[array] / mapping,INDEX,[KEY1, KEY2...]", ) + read_storage_parser.add_argument( + "address", help="contract address", metavar="ADDRESS" + ) def create_leveldb_parser(parser: argparse.ArgumentParser): @@ -131,6 +231,11 @@ def create_hash_to_addr_parser(hash_parser: argparse.ArgumentParser): hash_parser.add_argument( "hash", help="Find the address from hash", metavar="FUNCTION_NAME" ) + hash_parser.add_argument( + "--leveldb-dir", + help="specify leveldb directory for search or direct access operations", + metavar="LEVELDB_PATH", + ) def create_analyzer_parser(analyzer_parser: argparse.ArgumentParser): @@ -216,75 +321,8 @@ def create_analyzer_parser(analyzer_parser: argparse.ArgumentParser): ) -def create_parser(parser: argparse.ArgumentParser) -> None: - inputs = parser.add_argument_group("input arguments") - inputs.add_argument( - "-c", - "--code", - help='hex-encoded bytecode string ("6060604052...")', - metavar="BYTECODE", - ) - inputs.add_argument( - "-f", - "--codefile", - help="file containing hex-encoded bytecode string", - metavar="BYTECODEFILE", - type=argparse.FileType("r"), - ) - inputs.add_argument( - "-a", - "--address", - help="pull contract from the blockchain", - metavar="CONTRACT_ADDRESS", - ) - inputs.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.", - ) - utilities = parser.add_argument_group("utilities") - - utilities.add_argument("--solc-args", help="Extra arguments for solc") - utilities.add_argument( - "--solv", - help="specify solidity compiler version. If not present, will try to install it (Experimental)", - metavar="SOLV", - ) - - rpc = parser.add_argument_group("RPC options") - - rpc.add_argument( - "--rpc", - help="custom RPC settings", - metavar="HOST:PORT / ganache / infura-[network_name]", - default="infura-mainnet", - ) - rpc.add_argument( - "--rpctls", type=bool, default=False, help="RPC connection over TLS" - ) - parser.add_argument("--epic", action="store_true", help=argparse.SUPPRESS) - - outputs = parser.add_argument_group("output formats") - outputs.add_argument( - "-o", - "--outform", - choices=["text", "markdown", "json", "jsonv2"], - default="text", - help="report output format", - metavar="", - ) - outputs.add_argument( - "--verbose-report", - action="store_true", - help="Include debugging information in report", - ) - outputs.add_argument( - "-v", type=int, help="log level (0-5)", metavar="LOG_LEVEL", default=2 - ) - - def validate_args(args: argparse.Namespace): - if args.v: + if args.__dict__.get("v", False): if 0 <= args.v < 6: log_levels = [ logging.NOTSET, @@ -330,12 +368,12 @@ def set_config(args: argparse.Namespace): ) and not (args.rpc or args.i): config.set_api_from_config_path() - if args.address: + if args.__dict__.get("address", None): # Establish RPC connection if necessary config.set_api_rpc(rpc=args.rpc, rpctls=args.rpctls) - if args.command in ("contract-hash-to-address", "leveldb-search"): + if args.command in ("hash-to-address", "leveldb-search"): # Open LevelDB if necessary - if "leveldb_dir" not in args.__dict__ or args.leveldb_dir is None: + if not args.__dict__.get("leveldb_dir", None): leveldb_dir = config.leveldb_dir else: leveldb_dir = args.leveldb_dir @@ -344,7 +382,7 @@ def set_config(args: argparse.Namespace): def leveldb_search(config: MythrilConfig, args: argparse.Namespace): - if args.command in ("contract-hash-to-address", "leveldb-search"): + if args.command in ("hash-to-address", "leveldb-search"): leveldb_searcher = MythrilLevelDB(config.eth_db) if args.command == "leveldb-search": # Database search ops @@ -360,20 +398,27 @@ def leveldb_search(config: MythrilConfig, args: argparse.Namespace): sys.exit() -def get_code(disassembler: MythrilDisassembler, args: argparse.Namespace): +def load_code(disassembler: MythrilDisassembler, args: argparse.Namespace): + """ + Loads code into disassembly and returns address + :param disassembler: + :param args: + :return: Address + """ + address = None - if args.code: + if args.__dict__.get("code", False): # Load from bytecode code = args.code[2:] if args.code.startswith("0x") else args.code address, _ = disassembler.load_from_bytecode(code, args.bin_runtime) - elif args.codefile: + elif args.__dict__.get("codefile", False): bytecode = "".join([l.strip() for l in args.codefile if len(l.strip()) > 0]) bytecode = bytecode[2:] if bytecode.startswith("0x") else bytecode address, _ = disassembler.load_from_bytecode(bytecode, args.bin_runtime) - elif args.address: + elif args.__dict__.get("address", False): # Get bytecode from a contract address address, _ = disassembler.load_from_address(args.address) - elif args.solidity_file: + elif args.__dict__.get("solidity_file", False): # Compile Solidity source file(s) if args.command == "analyze" and args.graph and len(args.solidity_file) > 1: exit_with_error( @@ -519,17 +564,17 @@ def parse_args(parser: argparse.ArgumentParser, args: argparse.Namespace) -> Non # Parse cmdline args validate_args(args) try: - if args.command == "contract-function-to-hash": + if args.command == "function-to-hash": contract_hash_to_address(args) config = set_config(args) leveldb_search(config, args) - query_signature = ( - args.query_signature if "query_signature" in args.__dict__ else None - ) + query_signature = args.__dict__.get("query_signature", None) + solc_args = args.__dict__.get("solc_args", None) + solv = args.__dict__.get("solv", None) disassembler = MythrilDisassembler( eth=config.eth, - solc_version=args.solv, - solc_args=args.solc_args, + solc_version=solv, + solc_args=solc_args, enable_online_lookup=query_signature, ) if args.command == "truffle": @@ -542,7 +587,7 @@ def parse_args(parser: argparse.ArgumentParser, args: argparse.Namespace) -> Non ) sys.exit() - address = get_code(disassembler, args) + address = load_code(disassembler, args) execute_command( disassembler=disassembler, address=address, parser=parser, args=args ) diff --git a/tests/cmd_line_test.py b/tests/cli_tests/cmd_line_test.py similarity index 90% rename from tests/cmd_line_test.py rename to tests/cli_tests/cmd_line_test.py index dad51e5f..d7ed5967 100644 --- a/tests/cmd_line_test.py +++ b/tests/cli_tests/cmd_line_test.py @@ -15,20 +15,16 @@ def output_of(command): class CommandLineToolTestCase(BaseTestCase): def test_disassemble_code_correctly(self): - command = "python3 {} disassemble --bin-runtime -c 0x5050 --solv 0.5.0".format( - MYTH - ) + command = "python3 {} disassemble --bin-runtime -c 0x5050".format(MYTH) self.assertIn("0 POP\n1 POP\n", output_of(command)) def test_disassemble_solidity_file_correctly(self): solidity_file = str(TESTDATA / "input_contracts" / "metacoin.sol") - command = "python3 {} disassemble {} --solv 0.5.0".format(MYTH, solidity_file) + command = "python3 {} disassemble {}".format(MYTH, solidity_file) self.assertIn("2 PUSH1 0x40\n4 MSTORE", output_of(command)) def test_hash_a_function_correctly(self): - command = "python3 {} contract-function-to-hash --solv 0.5.0 'setOwner(address)'".format( - MYTH - ) + command = "python3 {} function-to-hash 'setOwner(address)'".format(MYTH) self.assertIn("0x13af4035\n", output_of(command)) diff --git a/tests/test_cli_opts.py b/tests/cli_tests/test_cli_opts.py similarity index 100% rename from tests/test_cli_opts.py rename to tests/cli_tests/test_cli_opts.py From 2eb3d33b23bf20b60f8594f1fcb5b1a44493cade Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Thu, 16 May 2019 13:40:05 +0530 Subject: [PATCH 004/164] Add cli tests for error --- mythril/interfaces/cli.py | 6 ------ tests/cli_tests/cmd_line_test.py | 13 +++++++++++++ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/mythril/interfaces/cli.py b/mythril/interfaces/cli.py index 1fd1f140..6bbc4885 100644 --- a/mythril/interfaces/cli.py +++ b/mythril/interfaces/cli.py @@ -215,12 +215,6 @@ def create_leveldb_parser(parser: argparse.ArgumentParser): ) -def create_version_parser(hash_parser: argparse.ArgumentParser): - hash_parser.add_argument( - "version", help="print the Mythril version number and exit", metavar="VERSION" - ) - - def create_func_to_hash_parser(hash_parser: argparse.ArgumentParser): hash_parser.add_argument( "func_name", help="calculate function signature hash", metavar="SIGNATURE" diff --git a/tests/cli_tests/cmd_line_test.py b/tests/cli_tests/cmd_line_test.py index d7ed5967..765e4022 100644 --- a/tests/cli_tests/cmd_line_test.py +++ b/tests/cli_tests/cmd_line_test.py @@ -27,6 +27,19 @@ class CommandLineToolTestCase(BaseTestCase): command = "python3 {} function-to-hash 'setOwner(address)'".format(MYTH) self.assertIn("0x13af4035\n", output_of(command)) + def test_failure_json(self): + command = "python3 {} analyze doesnt_exist.sol -o json".format(MYTH) + print(output_of(command)) + self.assertIn(""""success": false""", output_of(command)) + + def test_failure_text(self): + command = "python3 {} analyze doesnt_exist.sol".format(MYTH) + assert output_of(command) == "" + + def test_failure_jsonv2(self): + command = "python3 {} analyze doesnt_exist.sol -o jsonv2".format(MYTH) + self.assertIn(""""level": "error""" "", output_of(command)) + class TruffleTestCase(BaseTestCase): def test_analysis_truffle_project(self): From ef55c1a0d5c2be13a543eb5b43c275da101af844 Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Thu, 16 May 2019 13:50:13 +0530 Subject: [PATCH 005/164] Move the cmd_line_test to the previous directory --- tests/{cli_tests => }/cmd_line_test.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{cli_tests => }/cmd_line_test.py (100%) diff --git a/tests/cli_tests/cmd_line_test.py b/tests/cmd_line_test.py similarity index 100% rename from tests/cli_tests/cmd_line_test.py rename to tests/cmd_line_test.py From 6c4fe8b291897f744ac01ff25616420002bb3a92 Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Thu, 16 May 2019 15:19:02 +0530 Subject: [PATCH 006/164] Add documentation --- mythril/interfaces/cli.py | 121 +++++++++++++++++++++++++++++++------- 1 file changed, 99 insertions(+), 22 deletions(-) diff --git a/mythril/interfaces/cli.py b/mythril/interfaces/cli.py index 6bbc4885..ddb1cb32 100644 --- a/mythril/interfaces/cli.py +++ b/mythril/interfaces/cli.py @@ -15,6 +15,7 @@ import coloredlogs import traceback import mythril.support.signatures as sigs +from argparse import ArgumentParser, Namespace from mythril.exceptions import AddressNotFoundError, CriticalError from mythril.mythril import ( MythrilAnalyzer, @@ -31,8 +32,9 @@ log = logging.getLogger(__name__) def exit_with_error(format_, message): """ - :param format_: - :param message: + Exits with error + :param format_: The format of the message + :param message: message """ if format_ == "text" or format_ == "markdown": log.error(message) @@ -55,8 +57,12 @@ def exit_with_error(format_, message): sys.exit() -def get_input_parser(): - parser = argparse.ArgumentParser(add_help=False) +def get_input_parser() -> ArgumentParser: + """ + Returns Parser which handles input + :return: Parser which handles input + """ + parser = ArgumentParser(add_help=False) parser.add_argument( "-c", "--code", @@ -84,7 +90,11 @@ def get_input_parser(): return parser -def get_output_parser(): +def get_output_parser() -> ArgumentParser: + """ + Get parser which handles output + :return: Parser which handles output + """ parser = argparse.ArgumentParser(add_help=False) parser.add_argument("--epic", action="store_true", help=argparse.SUPPRESS) parser.add_argument( @@ -106,7 +116,11 @@ def get_output_parser(): return parser -def get_rpc_parser(): +def get_rpc_parser() -> ArgumentParser: + """ + Get parser which handles RPC flags + :return: c + """ parser = argparse.ArgumentParser(add_help=False) parser.add_argument( "--rpc", @@ -120,7 +134,11 @@ def get_rpc_parser(): return parser -def get_utilities_parser(): +def get_utilities_parser() -> ArgumentParser: + """ + Get parser which handles utilities flags + :return: Get parser which handles utility flags + """ parser = argparse.ArgumentParser(add_help=False) parser.add_argument("--solc-args", help="Extra arguments for solc") parser.add_argument( @@ -190,11 +208,21 @@ def main() -> None: parse_args(parser=parser, args=args) -def create_disassemble_parser(parser): +def create_disassemble_parser(parser: ArgumentParser): + """ + Modify parser to handle disassembly + :param parser: + :return: + """ parser.add_argument("solidity_file", nargs="*") -def create_read_storage_parser(read_storage_parser: argparse.ArgumentParser): +def create_read_storage_parser(read_storage_parser: ArgumentParser): + """ + Modify parser to handle storage slots + :param read_storage_parser: + :return: + """ read_storage_parser.add_argument( "storage_slots", @@ -206,7 +234,12 @@ def create_read_storage_parser(read_storage_parser: argparse.ArgumentParser): ) -def create_leveldb_parser(parser: argparse.ArgumentParser): +def create_leveldb_parser(parser: ArgumentParser): + """ + Modify parser to handle leveldb-search + :param parser: + :return: + """ parser.add_argument("search") parser.add_argument( "--leveldb-dir", @@ -215,13 +248,23 @@ def create_leveldb_parser(parser: argparse.ArgumentParser): ) -def create_func_to_hash_parser(hash_parser: argparse.ArgumentParser): - hash_parser.add_argument( +def create_func_to_hash_parser(parser: ArgumentParser): + """ + Modify parser to handle func_to_hash command + :param parser: + :return: + """ + parser.add_argument( "func_name", help="calculate function signature hash", metavar="SIGNATURE" ) -def create_hash_to_addr_parser(hash_parser: argparse.ArgumentParser): +def create_hash_to_addr_parser(hash_parser: ArgumentParser): + """ + Modify parser to handle hash_to_addr command + :param hash_parser: + :return: + """ hash_parser.add_argument( "hash", help="Find the address from hash", metavar="FUNCTION_NAME" ) @@ -232,7 +275,12 @@ def create_hash_to_addr_parser(hash_parser: argparse.ArgumentParser): ) -def create_analyzer_parser(analyzer_parser: argparse.ArgumentParser): +def create_analyzer_parser(analyzer_parser: ArgumentParser): + """ + Modify parser to handle analyze command + :param analyzer_parser: + :return: + """ analyzer_parser.add_argument("solidity_file", nargs="*") commands = analyzer_parser.add_argument_group("commands") commands.add_argument("-g", "--graph", help="generate a control flow graph") @@ -315,7 +363,12 @@ def create_analyzer_parser(analyzer_parser: argparse.ArgumentParser): ) -def validate_args(args: argparse.Namespace): +def validate_args(args: Namespace): + """ + Validate cli args + :param args: + :return: + """ if args.__dict__.get("v", False): if 0 <= args.v < 6: log_levels = [ @@ -355,7 +408,12 @@ def validate_args(args: argparse.Namespace): ) -def set_config(args: argparse.Namespace): +def set_config(args: Namespace): + """ + Set config based on args + :param args: + :return: modified config + """ config = MythrilConfig() if ( args.command == "analyze" and (args.dynld or not args.no_onchain_storage_access) @@ -375,7 +433,13 @@ def set_config(args: argparse.Namespace): return config -def leveldb_search(config: MythrilConfig, args: argparse.Namespace): +def leveldb_search(config: MythrilConfig, args: Namespace): + """ + Handle leveldb search + :param config: + :param args: + :return: + """ if args.command in ("hash-to-address", "leveldb-search"): leveldb_searcher = MythrilLevelDB(config.eth_db) if args.command == "leveldb-search": @@ -392,7 +456,7 @@ def leveldb_search(config: MythrilConfig, args: argparse.Namespace): sys.exit() -def load_code(disassembler: MythrilDisassembler, args: argparse.Namespace): +def load_code(disassembler: MythrilDisassembler, args: Namespace): """ Loads code into disassembly and returns address :param disassembler: @@ -433,9 +497,17 @@ def load_code(disassembler: MythrilDisassembler, args: argparse.Namespace): def execute_command( disassembler: MythrilDisassembler, address: str, - parser: argparse.ArgumentParser, - args: argparse.Namespace, + parser: ArgumentParser, + args: Namespace, ): + """ + Execute command + :param disassembler: + :param address: + :param parser: + :param args: + :return: + """ if args.command == "read-storage": if not args.address: @@ -530,12 +602,17 @@ def execute_command( parser.print_help() -def contract_hash_to_address(args: argparse.Namespace): +def contract_hash_to_address(args: Namespace): + """ + prints the hash from function signature + :param args: + :return: + """ print(MythrilDisassembler.hash_for_function_signature(args.func_name)) sys.exit() -def parse_args(parser: argparse.ArgumentParser, args: argparse.Namespace) -> None: +def parse_args(parser: ArgumentParser, args: Namespace) -> None: """ Parses the arguments :param parser: The parser From cbe5a656f0fd151f70e72373032b187b754fa17f Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Fri, 17 May 2019 11:37:23 +0530 Subject: [PATCH 007/164] Add more tests and change docs --- mythril/interfaces/cli.py | 14 ++++++++------ tests/cmd_line_test.py | 6 ++++++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/mythril/interfaces/cli.py b/mythril/interfaces/cli.py index ddb1cb32..bfa5dd1d 100644 --- a/mythril/interfaces/cli.py +++ b/mythril/interfaces/cli.py @@ -119,7 +119,7 @@ def get_output_parser() -> ArgumentParser: def get_rpc_parser() -> ArgumentParser: """ Get parser which handles RPC flags - :return: c + :return: Parser which handles rpc inputs """ parser = argparse.ArgumentParser(add_help=False) parser.add_argument( @@ -137,7 +137,7 @@ def get_rpc_parser() -> ArgumentParser: def get_utilities_parser() -> ArgumentParser: """ Get parser which handles utilities flags - :return: Get parser which handles utility flags + :return: Parser which handles utility flags """ parser = argparse.ArgumentParser(add_help=False) parser.add_argument("--solc-args", help="Extra arguments for solc") @@ -163,21 +163,23 @@ def main() -> None: subparsers = parser.add_subparsers(dest="command", help="Commands") analyzer_parser = subparsers.add_parser( "analyze", - help="Triggers analysis of smart contract", + help="Triggers the analysis of the smart contract", parents=[rpc_parser, utilities_parser, input_parser, output_parser], ) disassemble_parser = subparsers.add_parser( "disassemble", - help="Disassembles smart contract", + help="Disassembles the smart contract", parents=[rpc_parser, utilities_parser, input_parser, output_parser], ) read_storage_parser = subparsers.add_parser( "read-storage", - help="Retrieves storage slots from rpc address", + help="Retrieves storage slots from a given address through rpc", parents=[rpc_parser, output_parser], ) leveldb_search_parser = subparsers.add_parser( - "leveldb-search", parents=[output_parser], help="Search code in local leveldb" + "leveldb-search", + parents=[output_parser], + help="Searches the code fragment in local leveldb", ) contract_func_to_hash = subparsers.add_parser( "function-to-hash", diff --git a/tests/cmd_line_test.py b/tests/cmd_line_test.py index 765e4022..528bbf9a 100644 --- a/tests/cmd_line_test.py +++ b/tests/cmd_line_test.py @@ -1,5 +1,6 @@ from subprocess import check_output from tests import BaseTestCase, TESTDATA, PROJECT_DIR, TESTS_DIR +from mock import patch MYTH = str(PROJECT_DIR / "myth") @@ -40,6 +41,11 @@ class CommandLineToolTestCase(BaseTestCase): command = "python3 {} analyze doesnt_exist.sol -o jsonv2".format(MYTH) self.assertIn(""""level": "error""" "", output_of(command)) + def test_analyze(self): + solidity_file = str(TESTDATA / "input_contracts" / "origin.sol") + command = "python3 {} analyze {}".format(MYTH, solidity_file) + self.assertIn("111", output_of(command)) + class TruffleTestCase(BaseTestCase): def test_analysis_truffle_project(self): From 46c1b29528447f2bdb3d0ea598ff51923251f495 Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Fri, 17 May 2019 13:25:48 +0530 Subject: [PATCH 008/164] Add more tests for storage slots and execution --- mythril/interfaces/cli.py | 22 +++++----------------- tests/cmd_line_test.py | 12 ++++++++++++ 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/mythril/interfaces/cli.py b/mythril/interfaces/cli.py index bfa5dd1d..e1759915 100644 --- a/mythril/interfaces/cli.py +++ b/mythril/interfaces/cli.py @@ -397,17 +397,11 @@ def validate_args(args: Namespace): "The --query-signature function requires the python package ethereum-input-decoder", ) - if args.enable_iprof: - if args.v < 4: - exit_with_error( - args.outform, - "--enable-iprof must be used with -v LOG_LEVEL where LOG_LEVEL >= 4", - ) - elif not (args.graph or args.fire_lasers or args.statespace_json): - exit_with_error( - args.outform, - "--enable-iprof must be used with one of -g, --graph, -x, --fire-lasers, -j and --statespace-json", - ) + if args.enable_iprof and args.v < 4: + exit_with_error( + args.outform, + "--enable-iprof must be used with -v LOG_LEVEL where LOG_LEVEL >= 4", + ) def set_config(args: Namespace): @@ -512,12 +506,6 @@ def execute_command( """ if args.command == "read-storage": - if not args.address: - exit_with_error( - args.outform, - "To read storage, provide the address of a deployed contract with the -a option.", - ) - storage = disassembler.get_state_variable_from_storage( address=address, params=[a.strip() for a in args.storage_slots.strip().split(",")], diff --git a/tests/cmd_line_test.py b/tests/cmd_line_test.py index 528bbf9a..a3ed1949 100644 --- a/tests/cmd_line_test.py +++ b/tests/cmd_line_test.py @@ -46,6 +46,18 @@ class CommandLineToolTestCase(BaseTestCase): command = "python3 {} analyze {}".format(MYTH, solidity_file) self.assertIn("111", output_of(command)) + def test_analyze_bytecode(self): + solidity_file = str(TESTDATA / "inputs" / "origin.sol.o") + command = "python3 {} analyze --bin-runtime -f {}".format(MYTH, solidity_file) + self.assertIn("111", output_of(command)) + + def test_invalid_args_iprof(self): + solidity_file = str(TESTDATA / "input_contracts" / "origin.sol") + command = "python3 {} analyze {} --enable-iprof -o json".format( + MYTH, solidity_file + ) + self.assertIn(""""success": false""", output_of(command)) + class TruffleTestCase(BaseTestCase): def test_analysis_truffle_project(self): From 70c036cc26c14da89f746e11b1f30ba5273c8238 Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Fri, 17 May 2019 13:55:29 +0530 Subject: [PATCH 009/164] support a for analyze and add more tests --- mythril/interfaces/cli.py | 43 +++++++++++++++++++++++---------------- tests/cmd_line_test.py | 7 +++++++ 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/mythril/interfaces/cli.py b/mythril/interfaces/cli.py index e1759915..0227acaf 100644 --- a/mythril/interfaces/cli.py +++ b/mythril/interfaces/cli.py @@ -27,6 +27,9 @@ from mythril.version import VERSION # logging.basicConfig(level=logging.DEBUG) +ANALYZE_LIST = ("a", "analyze") +DISASSEMBLE_LIST = ("d", "disassemble") + log = logging.getLogger(__name__) @@ -161,16 +164,22 @@ def main() -> None: ) subparsers = parser.add_subparsers(dest="command", help="Commands") - analyzer_parser = subparsers.add_parser( - "analyze", - help="Triggers the analysis of the smart contract", - parents=[rpc_parser, utilities_parser, input_parser, output_parser], - ) - disassemble_parser = subparsers.add_parser( - "disassemble", - help="Disassembles the smart contract", - parents=[rpc_parser, utilities_parser, input_parser, output_parser], - ) + for analyze_string in ANALYZE_LIST: + analyzer_parser = subparsers.add_parser( + analyze_string, + help="Triggers the analysis of the smart contract", + parents=[rpc_parser, utilities_parser, input_parser, output_parser], + ) + create_analyzer_parser(analyzer_parser) + + for disassemble_string in DISASSEMBLE_LIST: + disassemble_parser = subparsers.add_parser( + disassemble_string, + help="Disassembles the smart contract", + parents=[rpc_parser, utilities_parser, input_parser, output_parser], + ) + create_disassemble_parser(disassemble_parser) + read_storage_parser = subparsers.add_parser( "read-storage", help="Retrieves storage slots from a given address through rpc", @@ -195,8 +204,6 @@ def main() -> None: "version", parents=[output_parser], help="Outputs the version" ) - create_disassemble_parser(disassemble_parser) - create_analyzer_parser(analyzer_parser) 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) @@ -390,7 +397,7 @@ def validate_args(args: Namespace): args.outform, "Invalid -v value, you can find valid values in usage" ) - if args.command == "analyze": + if args.command in ANALYZE_LIST: if args.query_signature and sigs.ethereum_input_decoder is None: exit_with_error( args.outform, @@ -412,7 +419,8 @@ def set_config(args: Namespace): """ config = MythrilConfig() if ( - args.command == "analyze" and (args.dynld or not args.no_onchain_storage_access) + args.command in ANALYZE_LIST + and (args.dynld or not args.no_onchain_storage_access) ) and not (args.rpc or args.i): config.set_api_from_config_path() @@ -474,7 +482,7 @@ def load_code(disassembler: MythrilDisassembler, args: Namespace): address, _ = disassembler.load_from_address(args.address) elif args.__dict__.get("solidity_file", False): # Compile Solidity source file(s) - if args.command == "analyze" and args.graph and len(args.solidity_file) > 1: + if args.command in ANALYZE_LIST and args.graph and len(args.solidity_file) > 1: exit_with_error( args.outform, "Cannot generate call graphs from multiple input files. Please do it one at a time.", @@ -513,15 +521,14 @@ def execute_command( print(storage) return - if args.command == "disassemble": - # or mythril.disassemble(mythril.contracts[0]) + if args.command in DISASSEMBLE_LIST: if disassembler.contracts[0].code: print("Runtime Disassembly: \n" + disassembler.contracts[0].get_easm()) if disassembler.contracts[0].creation_code: print("Disassembly: \n" + disassembler.contracts[0].get_creation_easm()) - elif args.command == "analyze": + elif args.command in ANALYZE_LIST: analyzer = MythrilAnalyzer( strategy=args.strategy, disassembler=disassembler, diff --git a/tests/cmd_line_test.py b/tests/cmd_line_test.py index a3ed1949..5fa6e3bb 100644 --- a/tests/cmd_line_test.py +++ b/tests/cmd_line_test.py @@ -58,6 +58,13 @@ class CommandLineToolTestCase(BaseTestCase): ) self.assertIn(""""success": false""", output_of(command)) + def test_storage(self): + solidity_file = str(TESTDATA / "input_contracts" / "origin.sol") + command = """python3 {} read-storage "438767356, 3" 0x76799f77587738bfeef09452df215b63d2cfb08a """.format( + MYTH + ) + self.assertIn("0x1a270efc", output_of(command)) + class TruffleTestCase(BaseTestCase): def test_analysis_truffle_project(self): From 33c9e4b48459758e2b87a77dea8c6ca34b4047a3 Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Fri, 21 Jun 2019 00:05:26 +0530 Subject: [PATCH 010/164] Improve cli interface --- mythril/interfaces/cli.py | 61 ++++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 33 deletions(-) diff --git a/mythril/interfaces/cli.py b/mythril/interfaces/cli.py index d9dc5b15..a0d03204 100644 --- a/mythril/interfaces/cli.py +++ b/mythril/interfaces/cli.py @@ -97,7 +97,6 @@ def get_output_parser() -> ArgumentParser: :return: Parser which handles output """ parser = argparse.ArgumentParser(add_help=False) - parser.add_argument("--epic", action="store_true", help=argparse.SUPPRESS) parser.add_argument( "-o", "--outform", @@ -111,9 +110,6 @@ def get_output_parser() -> ArgumentParser: action="store_true", help="Include debugging information in report", ) - parser.add_argument( - "-v", type=int, help="log level (0-5)", metavar="LOG_LEVEL", default=2 - ) return parser @@ -160,42 +156,41 @@ def main() -> None: parser = argparse.ArgumentParser( description="Security analysis of Ethereum smart contracts" ) + parser.add_argument("--epic", action="store_true", help=argparse.SUPPRESS) + parser.add_argument( + "-v", type=int, help="log level (0-5)", metavar="LOG_LEVEL", default=2 + ) subparsers = parser.add_subparsers(dest="command", help="Commands") - for analyze_string in ANALYZE_LIST: - analyzer_parser = subparsers.add_parser( - analyze_string, - help="Triggers the analysis of the smart contract", - parents=[rpc_parser, utilities_parser, input_parser, output_parser], - ) - create_analyzer_parser(analyzer_parser) + analyzer_parser = subparsers.add_parser( + "analyze", + help="Triggers the analysis of the smart contract", + parents=[rpc_parser, utilities_parser, input_parser, output_parser], + aliases=["a"], + ) + create_analyzer_parser(analyzer_parser) - for disassemble_string in DISASSEMBLE_LIST: - disassemble_parser = subparsers.add_parser( - disassemble_string, - help="Disassembles the smart contract", - parents=[rpc_parser, utilities_parser, input_parser, output_parser], - ) - create_disassemble_parser(disassemble_parser) + disassemble_parser = subparsers.add_parser( + "disassemble", + help="Disassembles the smart contract", + aliases=["d"], + parents=[rpc_parser, utilities_parser, input_parser], + ) + create_disassemble_parser(disassemble_parser) read_storage_parser = subparsers.add_parser( "read-storage", help="Retrieves storage slots from a given address through rpc", - parents=[rpc_parser, output_parser], + parents=[rpc_parser], ) leveldb_search_parser = subparsers.add_parser( - "leveldb-search", - parents=[output_parser], - help="Searches the code fragment in local leveldb", + "leveldb-search", help="Searches the code fragment in local leveldb" ) contract_func_to_hash = subparsers.add_parser( - "function-to-hash", - parents=[output_parser], - help="Returns the hash signature of the function", + "function-to-hash", help="Returns the hash signature of the function" ) contract_hash_to_addr = subparsers.add_parser( "hash-to-address", - parents=[output_parser], help="converts the hashes in the blockchain to ethereum address", ) subparsers.add_parser( @@ -233,7 +228,7 @@ def create_read_storage_parser(read_storage_parser: ArgumentParser): read_storage_parser.add_argument( "storage_slots", - help="read state variables from storage index, use with -a", + help="read state variables from storage index", metavar="INDEX,NUM_SLOTS,[array] / mapping,INDEX,[KEY1, KEY2...]", ) read_storage_parser.add_argument( @@ -502,8 +497,8 @@ def load_code(disassembler: MythrilDisassembler, args: Namespace): ) # list of files else: exit_with_error( - args.outform, - "No input bytecode. Please provide EVM code via -c BYTECODE, -a ADDRESS, or -i SOLIDITY_FILES", + args.__dict__.get("outform", "text"), + "No input bytecode. Please provide EVM code via -c BYTECODE, -a ADDRESS, -f BYTECODE_FILE or ", ) return address @@ -602,7 +597,7 @@ def execute_command( print(outputs[args.outform]) except ModuleNotFoundError as e: exit_with_error( - args.outform, "Error loading analyis modules: " + format(e) + args.outform, "Error loading analysis modules: " + format(e) ) else: @@ -626,7 +621,7 @@ def parse_args(parser: ArgumentParser, args: Namespace) -> None: :param args: The args """ - if args.epic: + if args.__dict__.get("epic", None): path = os.path.dirname(os.path.realpath(__file__)) sys.argv.remove("--epic") os.system(" ".join(sys.argv) + " | python3 " + path + "/epic.py") @@ -670,9 +665,9 @@ def parse_args(parser: ArgumentParser, args: Namespace) -> None: disassembler=disassembler, address=address, parser=parser, args=args ) except CriticalError as ce: - exit_with_error(args.outform, str(ce)) + exit_with_error(args.__dict__.get("outform", "text"), str(ce)) except Exception: - exit_with_error(args.outform, traceback.format_exc()) + exit_with_error(args.__dict__.get("outform", "text"), traceback.format_exc()) if __name__ == "__main__": From 6172d55fc982ef63d011837108783e4ee252d990 Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Fri, 21 Jun 2019 00:24:37 +0530 Subject: [PATCH 011/164] Fix previous errors --- mythril/interfaces/cli.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/mythril/interfaces/cli.py b/mythril/interfaces/cli.py index a0d03204..45f8253b 100644 --- a/mythril/interfaces/cli.py +++ b/mythril/interfaces/cli.py @@ -25,11 +25,20 @@ from mythril.mythril import ( ) from mythril.__version__ import __version__ as VERSION -ANALYZE_LIST = ("a", "analyze") -DISASSEMBLE_LIST = ("d", "disassemble") +ANALYZE_LIST = ("analyze", "a") +DISASSEMBLE_LIST = ("disassemble", "d") log = logging.getLogger(__name__) +COMMAND_LIST = ANALYZE_LIST + DISASSEMBLE_LIST + ( + "read-storage", + "leveldb-search", + "function-to-hash", + "hash-to-address", + "version", + "truffle", +) + def exit_with_error(format_, message): """ @@ -163,17 +172,17 @@ def main() -> None: subparsers = parser.add_subparsers(dest="command", help="Commands") analyzer_parser = subparsers.add_parser( - "analyze", + ANALYZE_LIST[0], help="Triggers the analysis of the smart contract", parents=[rpc_parser, utilities_parser, input_parser, output_parser], - aliases=["a"], + aliases=ANALYZE_LIST[1:], ) create_analyzer_parser(analyzer_parser) disassemble_parser = subparsers.add_parser( - "disassemble", + DISASSEMBLE_LIST[0], help="Disassembles the smart contract", - aliases=["d"], + aliases=DISASSEMBLE_LIST[1:], parents=[rpc_parser, utilities_parser, input_parser], ) create_disassemble_parser(disassemble_parser) @@ -620,8 +629,10 @@ def parse_args(parser: ArgumentParser, args: Namespace) -> None: :param parser: The parser :param args: The args """ + if args.command not in COMMAND_LIST: + parser.print_usage() - if args.__dict__.get("epic", None): + if args.epic: path = os.path.dirname(os.path.realpath(__file__)) sys.argv.remove("--epic") os.system(" ".join(sys.argv) + " | python3 " + path + "/epic.py") From fc6c97c3e891ac43060a7bb679cefff21f024d22 Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Fri, 21 Jun 2019 00:27:31 +0530 Subject: [PATCH 012/164] Refactor with black --- mythril/interfaces/cli.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/mythril/interfaces/cli.py b/mythril/interfaces/cli.py index 45f8253b..a0f4e2f1 100644 --- a/mythril/interfaces/cli.py +++ b/mythril/interfaces/cli.py @@ -30,13 +30,17 @@ DISASSEMBLE_LIST = ("disassemble", "d") log = logging.getLogger(__name__) -COMMAND_LIST = ANALYZE_LIST + DISASSEMBLE_LIST + ( - "read-storage", - "leveldb-search", - "function-to-hash", - "hash-to-address", - "version", - "truffle", +COMMAND_LIST = ( + ANALYZE_LIST + + DISASSEMBLE_LIST + + ( + "read-storage", + "leveldb-search", + "function-to-hash", + "hash-to-address", + "version", + "truffle", + ) ) From 41c7ce03d8c7dfb5ecf23beee1e3d690fa88e3d3 Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Fri, 21 Jun 2019 00:34:26 +0530 Subject: [PATCH 013/164] Add help command --- mythril/interfaces/cli.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/mythril/interfaces/cli.py b/mythril/interfaces/cli.py index a0f4e2f1..97e5c57d 100644 --- a/mythril/interfaces/cli.py +++ b/mythril/interfaces/cli.py @@ -40,6 +40,7 @@ COMMAND_LIST = ( "hash-to-address", "version", "truffle", + "help", ) ) @@ -209,18 +210,18 @@ def main() -> None: subparsers.add_parser( "version", 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_leveldb_parser(leveldb_search_parser) subparsers.add_parser("truffle", parents=[analyzer_parser], add_help=False) + subparsers.add_parser("help", add_help=False) # Get config values args = parser.parse_args() - parse_args(parser=parser, args=args) + parse_args_and_execute(parser=parser, args=args) def create_disassemble_parser(parser: ArgumentParser): @@ -627,7 +628,7 @@ def contract_hash_to_address(args: Namespace): sys.exit() -def parse_args(parser: ArgumentParser, args: Namespace) -> None: +def parse_args_and_execute(parser: ArgumentParser, args: Namespace) -> None: """ Parses the arguments :param parser: The parser @@ -649,6 +650,10 @@ def parse_args(parser: ArgumentParser, args: Namespace) -> None: print("Mythril version {}".format(VERSION)) sys.exit() + if args.command == "help": + parser.print_help() + sys.exit() + # Parse cmdline args validate_args(args) try: From fd397ed2327e9afb0b692228c1c3a63f1eb307b2 Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Mon, 24 Jun 2019 18:48:31 +0530 Subject: [PATCH 014/164] Add new tests, fix an edge case and improve code and help messages --- mythril/interfaces/cli.py | 9 +++++---- tests/cmd_line_test.py | 4 ++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/mythril/interfaces/cli.py b/mythril/interfaces/cli.py index 97e5c57d..cf8a2e08 100644 --- a/mythril/interfaces/cli.py +++ b/mythril/interfaces/cli.py @@ -538,9 +538,8 @@ def execute_command( params=[a.strip() for a in args.storage_slots.strip().split(",")], ) print(storage) - return - if args.command in DISASSEMBLE_LIST: + elif args.command in DISASSEMBLE_LIST: if disassembler.contracts[0].code: print("Runtime Disassembly: \n" + disassembler.contracts[0].get_easm()) if disassembler.contracts[0].creation_code: @@ -634,8 +633,6 @@ def parse_args_and_execute(parser: ArgumentParser, args: Namespace) -> None: :param parser: The parser :param args: The args """ - if args.command not in COMMAND_LIST: - parser.print_usage() if args.epic: path = os.path.dirname(os.path.realpath(__file__)) @@ -643,6 +640,10 @@ def parse_args_and_execute(parser: ArgumentParser, args: Namespace) -> None: os.system(" ".join(sys.argv) + " | python3 " + path + "/epic.py") sys.exit() + if args.command not in COMMAND_LIST or args.command is None: + parser.print_help() + sys.exit() + if args.command == "version": if args.outform == "json": print(json.dumps({"version_str": VERSION})) diff --git a/tests/cmd_line_test.py b/tests/cmd_line_test.py index 5fa6e3bb..258a6882 100644 --- a/tests/cmd_line_test.py +++ b/tests/cmd_line_test.py @@ -58,6 +58,10 @@ class CommandLineToolTestCase(BaseTestCase): ) self.assertIn(""""success": false""", output_of(command)) + def test_only_epic(self): + command = "python3 {}".format(MYTH) + self.assertIn("usage: ", output_of(command)) + def test_storage(self): solidity_file = str(TESTDATA / "input_contracts" / "origin.sol") command = """python3 {} read-storage "438767356, 3" 0x76799f77587738bfeef09452df215b63d2cfb08a """.format( From 1080184b8a311633ab2d096b9a12362c9e68735a Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 25 Jun 2019 12:00:29 -0700 Subject: [PATCH 015/164] Turn mythril into a MythX client --- .../templates/report_as_markdown.jinja2 | 6 ++ .../analysis/templates/report_as_text.jinja2 | 4 + mythril/ethereum/util.py | 21 ++++- mythril/interfaces/cli.py | 89 +++++++++++++++--- mythril/mythx/__init__.py | 93 +++++++++++++++++++ mythril/solidity/soliditycontract.py | 58 ++++++------ requirements.txt | 1 + setup.py | 1 + tests/disassembler_test.py | 6 -- 9 files changed, 225 insertions(+), 54 deletions(-) create mode 100644 mythril/mythx/__init__.py diff --git a/mythril/analysis/templates/report_as_markdown.jinja2 b/mythril/analysis/templates/report_as_markdown.jinja2 index 289d1871..60579705 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 da962583..cfc32d2e 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..3e608b3a 100644 --- a/mythril/ethereum/util.py +++ b/mythril/ethereum/util.py @@ -34,6 +34,7 @@ def get_solc_json(file, solc_binary="solc", solc_args=None): """ cmd = [solc_binary, "--combined-json", "bin,bin-runtime,srcmap,srcmap-runtime,ast"] + cmd = [solc_binary, "--standard-json", "bin,bin-runtime,srcmap,srcmap-runtime,ast"] if solc_args: cmd.extend(solc_args.split()) @@ -46,10 +47,24 @@ def get_solc_json(file, solc_binary="solc", solc_args=None): cmd.append(file) - try: - p = Popen(cmd, stdout=PIPE, stderr=PIPE) + input_json = json.dumps( + { + "language": "Solidity", + "sources": {file: {"urls": [file]}}, + "settings": { + "outputSelection": { + "*": { + "": ["ast"], + "*": ["metadata", "evm.bytecode", "evm.deployedBytecode"], + } + } + }, + } + ) - stdout, stderr = p.communicate() + try: + p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) + stdout, stderr = p.communicate(bytes(input_json, "utf8")) ret = p.returncode if ret != 0: diff --git a/mythril/interfaces/cli.py b/mythril/interfaces/cli.py index cf8a2e08..b94b360d 100644 --- a/mythril/interfaces/cli.py +++ b/mythril/interfaces/cli.py @@ -16,6 +16,8 @@ import traceback import mythril.support.signatures as sigs from argparse import ArgumentParser, Namespace + +from mythril import mythx from mythril.exceptions import AddressNotFoundError, CriticalError from mythril.mythril import ( MythrilAnalyzer, @@ -27,12 +29,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 +45,7 @@ COMMAND_LIST = ( "version", "truffle", "help", + "pro", ) ) @@ -72,7 +77,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 @@ -91,17 +116,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 @@ -165,7 +179,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" @@ -179,7 +194,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:], ) create_analyzer_parser(analyzer_parser) @@ -188,10 +209,23 @@ 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, + ], ) 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], + ) + create_pro_parser(pro_parser) + read_storage_parser = subparsers.add_parser( "read-storage", help="Retrieves storage slots from a given address through rpc", @@ -233,6 +267,20 @@ def create_disassemble_parser(parser: ArgumentParser): parser.add_argument("solidity_file", nargs="*") +def create_pro_parser(parser: ArgumentParser): + """ + Modify parser to handle mythx analysis + :param parser: + :return: + """ + parser.add_argument( + "--full", + help="Run a full analysis. Default: quick analysis", + action="store_true", + ) + parser.add_argument("solidity_file", nargs="*") + + def create_read_storage_parser(read_storage_parser: ArgumentParser): """ Modify parser to handle storage slots @@ -539,6 +587,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()) diff --git a/mythril/mythx/__init__.py b/mythril/mythx/__init__.py new file mode 100644 index 00000000..631e8ba8 --- /dev/null +++ b/mythril/mythx/__init__.py @@ -0,0 +1,93 @@ +import os +import time + +from typing import List, Dict, Any + +from mythril.analysis.report import Issue, Report +from mythril.solidity.soliditycontract import SolidityContract + +from pythx import Client + + +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", "0x0000000000000000000000000000000000000000" + ), + password=os.environ.get("MYTHX_PASSWORD", "trial"), + ) + + 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." + 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, + ) + + while not c.analysis_ready(resp.uuid): + print(c.status(resp.uuid).analysis) + time.sleep(5) + + for issue in c.report(resp.uuid): + issue = Issue( + contract=contract.name, + function_name=None, + address=int(issue.locations[0].source_map.split(":")[0]), + swc_id=issue.swc_id[4:], # 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..513916a7 100644 --- a/mythril/solidity/soliditycontract.py +++ b/mythril/solidity/soliditycontract.py @@ -54,12 +54,15 @@ def get_contracts_from_file(input_file, solc_args=None, solc_binary="solc"): data = get_solc_json(input_file, solc_args=solc_args, 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 contractName in data["contracts"][input_file].keys(): + if len( + data["contracts"][input_file][contractName]["evm"]["deployedBytecode"][ + "object" + ] + ): yield SolidityContract( input_file=input_file, - name=name, + name=contractName, solc_args=solc_args, solc_binary=solc_binary, ) @@ -74,12 +77,14 @@ class SolidityContract(EVMContract): data = get_solc_json(input_file, solc_args=solc_args, 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 +96,25 @@ 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 filename, contract in sorted(data["contracts"][input_file].items()): + 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 not has_contract: @@ -139,8 +137,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/requirements.txt b/requirements.txt index 1b49350c..6cfdffb2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,3 +28,4 @@ transaction>=2.2.1 z3-solver>=4.8.5.0 pysha3 matplotlib +pythx diff --git a/setup.py b/setup.py index d76f546e..e88646e8 100755 --- a/setup.py +++ b/setup.py @@ -50,6 +50,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" From ebfe26cbb076b25d847271c1cd63738c4a2ca573 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 25 Jun 2019 15:30:44 -0700 Subject: [PATCH 016/164] Fix merge mistake --- myth | 20 +- mythril/interfaces/old_cli.py | 549 ++++++++++++++++++ tests/report_test.py | 200 ------- .../outputs_expected/calls.sol.o.json | 123 ---- .../outputs_expected/calls.sol.o.jsonv2 | 174 ------ .../outputs_expected/calls.sol.o.markdown | 117 ---- .../outputs_expected/calls.sol.o.text | 98 ---- .../outputs_expected/ether_send.sol.o.json | 32 - .../outputs_expected/ether_send.sol.o.jsonv2 | 48 -- .../ether_send.sol.o.markdown | 27 - .../outputs_expected/ether_send.sol.o.text | 22 - .../outputs_expected/exceptions.sol.o.json | 58 -- .../outputs_expected/exceptions.sol.o.jsonv2 | 84 --- .../exceptions.sol.o.markdown | 53 -- .../outputs_expected/exceptions.sol.o.text | 44 -- .../kinds_of_calls.sol.o.json | 84 --- .../kinds_of_calls.sol.o.jsonv2 | 120 ---- .../kinds_of_calls.sol.o.markdown | 79 --- .../kinds_of_calls.sol.o.text | 66 --- .../outputs_expected/metacoin.sol.o.json | 5 - .../outputs_expected/metacoin.sol.o.jsonv2 | 11 - .../outputs_expected/metacoin.sol.o.markdown | 3 - .../outputs_expected/metacoin.sol.o.text | 1 - .../multi_contracts.sol.o.json | 19 - .../multi_contracts.sol.o.jsonv2 | 30 - .../multi_contracts.sol.o.markdown | 14 - .../multi_contracts.sol.o.text | 11 - .../outputs_expected/nonascii.sol.o.json | 5 - .../outputs_expected/nonascii.sol.o.jsonv2 | 11 - .../outputs_expected/nonascii.sol.o.markdown | 3 - .../outputs_expected/nonascii.sol.o.text | 1 - .../outputs_expected/origin.sol.o.json | 19 - .../outputs_expected/origin.sol.o.jsonv2 | 30 - .../outputs_expected/origin.sol.o.markdown | 15 - .../outputs_expected/origin.sol.o.text | 12 - .../outputs_expected/overflow.sol.o.json | 45 -- .../outputs_expected/overflow.sol.o.jsonv2 | 66 --- .../outputs_expected/overflow.sol.o.markdown | 40 -- .../outputs_expected/overflow.sol.o.text | 33 -- .../outputs_expected/returnvalue.sol.o.json | 45 -- .../outputs_expected/returnvalue.sol.o.jsonv2 | 66 --- .../returnvalue.sol.o.markdown | 40 -- .../outputs_expected/returnvalue.sol.o.text | 33 -- .../outputs_expected/suicide.sol.o.json | 19 - .../outputs_expected/suicide.sol.o.jsonv2 | 30 - .../outputs_expected/suicide.sol.o.markdown | 14 - .../outputs_expected/suicide.sol.o.text | 11 - .../outputs_expected/underflow.sol.o.json | 45 -- .../outputs_expected/underflow.sol.o.jsonv2 | 66 --- .../outputs_expected/underflow.sol.o.markdown | 40 -- .../outputs_expected/underflow.sol.o.text | 33 -- 51 files changed, 568 insertions(+), 2246 deletions(-) create mode 100644 mythril/interfaces/old_cli.py delete mode 100644 tests/report_test.py delete mode 100644 tests/testdata/outputs_expected/calls.sol.o.json delete mode 100644 tests/testdata/outputs_expected/calls.sol.o.jsonv2 delete mode 100644 tests/testdata/outputs_expected/calls.sol.o.markdown delete mode 100644 tests/testdata/outputs_expected/calls.sol.o.text delete mode 100644 tests/testdata/outputs_expected/ether_send.sol.o.json delete mode 100644 tests/testdata/outputs_expected/ether_send.sol.o.jsonv2 delete mode 100644 tests/testdata/outputs_expected/ether_send.sol.o.markdown delete mode 100644 tests/testdata/outputs_expected/ether_send.sol.o.text delete mode 100644 tests/testdata/outputs_expected/exceptions.sol.o.json delete mode 100644 tests/testdata/outputs_expected/exceptions.sol.o.jsonv2 delete mode 100644 tests/testdata/outputs_expected/exceptions.sol.o.markdown delete mode 100644 tests/testdata/outputs_expected/exceptions.sol.o.text delete mode 100644 tests/testdata/outputs_expected/kinds_of_calls.sol.o.json delete mode 100644 tests/testdata/outputs_expected/kinds_of_calls.sol.o.jsonv2 delete mode 100644 tests/testdata/outputs_expected/kinds_of_calls.sol.o.markdown delete mode 100644 tests/testdata/outputs_expected/kinds_of_calls.sol.o.text delete mode 100644 tests/testdata/outputs_expected/metacoin.sol.o.json delete mode 100644 tests/testdata/outputs_expected/metacoin.sol.o.jsonv2 delete mode 100644 tests/testdata/outputs_expected/metacoin.sol.o.markdown delete mode 100644 tests/testdata/outputs_expected/metacoin.sol.o.text delete mode 100644 tests/testdata/outputs_expected/multi_contracts.sol.o.json delete mode 100644 tests/testdata/outputs_expected/multi_contracts.sol.o.jsonv2 delete mode 100644 tests/testdata/outputs_expected/multi_contracts.sol.o.markdown delete mode 100644 tests/testdata/outputs_expected/multi_contracts.sol.o.text delete mode 100644 tests/testdata/outputs_expected/nonascii.sol.o.json delete mode 100644 tests/testdata/outputs_expected/nonascii.sol.o.jsonv2 delete mode 100644 tests/testdata/outputs_expected/nonascii.sol.o.markdown delete mode 100644 tests/testdata/outputs_expected/nonascii.sol.o.text delete mode 100644 tests/testdata/outputs_expected/origin.sol.o.json delete mode 100644 tests/testdata/outputs_expected/origin.sol.o.jsonv2 delete mode 100644 tests/testdata/outputs_expected/origin.sol.o.markdown delete mode 100644 tests/testdata/outputs_expected/origin.sol.o.text delete mode 100644 tests/testdata/outputs_expected/overflow.sol.o.json delete mode 100644 tests/testdata/outputs_expected/overflow.sol.o.jsonv2 delete mode 100644 tests/testdata/outputs_expected/overflow.sol.o.markdown delete mode 100644 tests/testdata/outputs_expected/overflow.sol.o.text delete mode 100644 tests/testdata/outputs_expected/returnvalue.sol.o.json delete mode 100644 tests/testdata/outputs_expected/returnvalue.sol.o.jsonv2 delete mode 100644 tests/testdata/outputs_expected/returnvalue.sol.o.markdown delete mode 100644 tests/testdata/outputs_expected/returnvalue.sol.o.text delete mode 100644 tests/testdata/outputs_expected/suicide.sol.o.json delete mode 100644 tests/testdata/outputs_expected/suicide.sol.o.jsonv2 delete mode 100644 tests/testdata/outputs_expected/suicide.sol.o.markdown delete mode 100644 tests/testdata/outputs_expected/suicide.sol.o.text delete mode 100644 tests/testdata/outputs_expected/underflow.sol.o.json delete mode 100644 tests/testdata/outputs_expected/underflow.sol.o.jsonv2 delete mode 100644 tests/testdata/outputs_expected/underflow.sol.o.markdown delete mode 100644 tests/testdata/outputs_expected/underflow.sol.o.text diff --git a/myth b/myth index a45d431f..f378aba7 100755 --- a/myth +++ b/myth @@ -3,7 +3,25 @@ """mythril.py: Bug hunting on the Ethereum blockchain http://www.github.com/b-mueller/mythril """ +from sys import argv, exit +from mythril.interfaces.cli import COMMAND_LIST import mythril.interfaces.cli +import mythril.interfaces.old_cli +import warnings + + +def format_Warning(message, category, filename, lineno, line=""): + return "Deprecated Warning: {}\n\n".format(str(message)) + + +warnings.formatwarning = format_Warning if __name__ == "__main__": - mythril.interfaces.cli.main() + for arg in argv: + if arg in COMMAND_LIST: + mythril.interfaces.cli.main() + exit() + warnings.warn("The old cli arguments are deprecated, Please use 'myth -h' to view the new command line interface") + mythril.interfaces.old_cli.main() + + diff --git a/mythril/interfaces/old_cli.py b/mythril/interfaces/old_cli.py new file mode 100644 index 00000000..4c9d4222 --- /dev/null +++ b/mythril/interfaces/old_cli.py @@ -0,0 +1,549 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""mythril.py: Bug hunting on the Ethereum blockchain + + http://www.github.com/ConsenSys/mythril +""" + +import argparse +import json +import logging +import os +import sys + +import coloredlogs +import traceback + +import mythril.support.signatures as sigs +from mythril.exceptions import AddressNotFoundError, CriticalError +from mythril.mythril import ( + MythrilAnalyzer, + MythrilDisassembler, + MythrilConfig, + MythrilLevelDB, +) +from mythril.__version__ import __version__ as VERSION + +log = logging.getLogger(__name__) + + +def exit_with_error(format_, message): + """ + :param format_: + :param message: + """ + if format_ == "text" or format_ == "markdown": + log.error(message) + elif format_ == "json": + result = {"success": False, "error": str(message), "issues": []} + print(json.dumps(result)) + else: + result = [ + { + "issues": [], + "sourceType": "", + "sourceFormat": "", + "sourceList": [], + "meta": { + "logs": [{"level": "error", "hidden": "true", "msg": message}] + }, + } + ] + print(json.dumps(result)) + sys.exit() + + +def main() -> None: + """The main CLI interface entry point.""" + parser = argparse.ArgumentParser( + description="Security analysis of Ethereum smart contracts" + ) + create_parser(parser) + + # Get config values + + args = parser.parse_args() + parse_args(parser=parser, args=args) + + +def create_parser(parser: argparse.ArgumentParser) -> None: + """ + Creates the parser by setting all the possible arguments + :param parser: The parser + """ + parser.add_argument("solidity_file", nargs="*") + + commands = parser.add_argument_group("commands") + commands.add_argument("-g", "--graph", help="generate a control flow graph") + commands.add_argument( + "-V", + "--version", + action="store_true", + help="print the Mythril version number and exit", + ) + commands.add_argument( + "-x", + "--fire-lasers", + action="store_true", + help="detect vulnerabilities, use with -c, -a or solidity file(s)", + ) + commands.add_argument( + "--truffle", + action="store_true", + help="analyze a truffle project (run from project dir)", + ) + commands.add_argument( + "-d", "--disassemble", action="store_true", help="print disassembly" + ) + commands.add_argument( + "-j", + "--statespace-json", + help="dumps the statespace json", + metavar="OUTPUT_FILE", + ) + + inputs = parser.add_argument_group("input arguments") + inputs.add_argument( + "-c", + "--code", + help='hex-encoded bytecode string ("6060604052...")', + metavar="BYTECODE", + ) + inputs.add_argument( + "-f", + "--codefile", + help="file containing hex-encoded bytecode string", + metavar="BYTECODEFILE", + type=argparse.FileType("r"), + ) + inputs.add_argument( + "-a", + "--address", + help="pull contract from the blockchain", + metavar="CONTRACT_ADDRESS", + ) + inputs.add_argument( + "-l", + "--dynld", + action="store_true", + help="auto-load dependencies from the blockchain", + ) + inputs.add_argument( + "--no-onchain-storage-access", + action="store_true", + help="turns off getting the data from onchain contracts", + ) + inputs.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.", + ) + + outputs = parser.add_argument_group("output formats") + outputs.add_argument( + "-o", + "--outform", + choices=["text", "markdown", "json", "jsonv2"], + default="text", + help="report output format", + metavar="", + ) + outputs.add_argument( + "--verbose-report", + action="store_true", + help="Include debugging information in report", + ) + + database = parser.add_argument_group("local contracts database") + database.add_argument( + "-s", "--search", help="search the contract database", metavar="EXPRESSION" + ) + database.add_argument( + "--leveldb-dir", + help="specify leveldb directory for search or direct access operations", + metavar="LEVELDB_PATH", + ) + + utilities = parser.add_argument_group("utilities") + utilities.add_argument( + "--hash", help="calculate function signature hash", metavar="SIGNATURE" + ) + utilities.add_argument( + "--storage", + help="read state variables from storage index, use with -a", + metavar="INDEX,NUM_SLOTS,[array] / mapping,INDEX,[KEY1, KEY2...]", + ) + utilities.add_argument( + "--solv", + help="specify solidity compiler version. If not present, will try to install it (Experimental)", + metavar="SOLV", + ) + utilities.add_argument( + "--contract-hash-to-address", + help="returns corresponding address for a contract address hash", + metavar="SHA3_TO_LOOK_FOR", + ) + + options = parser.add_argument_group("options") + options.add_argument( + "-m", + "--modules", + help="Comma-separated list of security analysis modules", + metavar="MODULES", + ) + options.add_argument( + "--max-depth", + type=int, + default=50, + help="Maximum recursion depth for symbolic execution", + ) + options.add_argument( + "--strategy", + choices=["dfs", "bfs", "naive-random", "weighted-random"], + default="bfs", + help="Symbolic execution strategy", + ) + options.add_argument( + "-b", + "--loop-bound", + type=int, + default=4, + help="Bound loops at n iterations", + metavar="N", + ) + options.add_argument( + "-t", + "--transaction-count", + type=int, + default=2, + help="Maximum number of transactions issued by laser", + ) + options.add_argument( + "--execution-timeout", + type=int, + default=86400, + help="The amount of seconds to spend on symbolic execution", + ) + options.add_argument( + "--create-timeout", + type=int, + 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( + "--phrack", action="store_true", help="Phrack-style call graph" + ) + options.add_argument( + "--enable-physics", action="store_true", help="enable graph physics simulation" + ) + options.add_argument( + "-v", type=int, help="log level (0-5)", metavar="LOG_LEVEL", default=2 + ) + options.add_argument( + "-q", + "--query-signature", + action="store_true", + help="Lookup function signatures through www.4byte.directory", + ) + options.add_argument( + "--enable-iprof", action="store_true", help="enable the instruction profiler" + ) + options.add_argument( + "--disable-dependency-pruning", + action="store_true", + help="Deactivate dependency-based pruning", + ) + + rpc = parser.add_argument_group("RPC options") + + rpc.add_argument( + "--rpc", + help="custom RPC settings", + metavar="HOST:PORT / ganache / infura-[network_name]", + default="infura-mainnet", + ) + rpc.add_argument( + "--rpctls", type=bool, default=False, help="RPC connection over TLS" + ) + parser.add_argument("--epic", action="store_true", help=argparse.SUPPRESS) + + +def validate_args(parser: argparse.ArgumentParser, args: argparse.Namespace): + if not ( + args.search + or args.hash + or args.disassemble + or args.graph + or args.fire_lasers + or args.storage + or args.truffle + or args.statespace_json + or args.contract_hash_to_address + ): + parser.print_help() + sys.exit() + + if args.v: + if 0 <= args.v < 6: + log_levels = [ + logging.NOTSET, + logging.CRITICAL, + logging.ERROR, + logging.WARNING, + logging.INFO, + logging.DEBUG, + ] + coloredlogs.install( + fmt="%(name)s [%(levelname)s]: %(message)s", level=log_levels[args.v] + ) + logging.getLogger("mythril").setLevel(log_levels[args.v]) + else: + exit_with_error( + args.outform, "Invalid -v value, you can find valid values in usage" + ) + + if args.query_signature: + if sigs.ethereum_input_decoder is None: + exit_with_error( + args.outform, + "The --query-signature function requires the python package ethereum-input-decoder", + ) + + if args.enable_iprof: + if args.v < 4: + exit_with_error( + args.outform, + "--enable-iprof must be used with -v LOG_LEVEL where LOG_LEVEL >= 4", + ) + elif not (args.graph or args.fire_lasers or args.statespace_json): + exit_with_error( + args.outform, + "--enable-iprof must be used with one of -g, --graph, -x, --fire-lasers, -j and --statespace-json", + ) + + +def quick_commands(args: argparse.Namespace): + if args.hash: + print(MythrilDisassembler.hash_for_function_signature(args.hash)) + sys.exit() + + +def set_config(args: argparse.Namespace): + config = MythrilConfig() + if args.dynld or not args.no_onchain_storage_access and not (args.rpc or args.i): + config.set_api_from_config_path() + + if args.address: + # Establish RPC connection if necessary + config.set_api_rpc(rpc=args.rpc, rpctls=args.rpctls) + elif args.search or args.contract_hash_to_address: + # Open LevelDB if necessary + config.set_api_leveldb( + config.leveldb_dir if not args.leveldb_dir else args.leveldb_dir + ) + return config + + +def leveldb_search(config: MythrilConfig, args: argparse.Namespace): + if args.search or args.contract_hash_to_address: + leveldb_searcher = MythrilLevelDB(config.eth_db) + if args.search: + # Database search ops + leveldb_searcher.search_db(args.search) + + else: + # search corresponding address + try: + leveldb_searcher.contract_hash_to_address(args.contract_hash_to_address) + except AddressNotFoundError: + print("Address not found.") + + sys.exit() + + +def get_code(disassembler: MythrilDisassembler, args: argparse.Namespace): + address = None + if args.code: + # Load from bytecode + code = args.code[2:] if args.code.startswith("0x") else args.code + address, _ = disassembler.load_from_bytecode(code, args.bin_runtime) + elif args.codefile: + bytecode = "".join([l.strip() for l in args.codefile if len(l.strip()) > 0]) + bytecode = bytecode[2:] if bytecode.startswith("0x") else bytecode + address, _ = disassembler.load_from_bytecode(bytecode, args.bin_runtime) + elif args.address: + # Get bytecode from a contract address + address, _ = disassembler.load_from_address(args.address) + elif args.solidity_file: + # Compile Solidity source file(s) + if args.graph and len(args.solidity_file) > 1: + exit_with_error( + args.outform, + "Cannot generate call graphs from multiple input files. Please do it one at a time.", + ) + address, _ = disassembler.load_from_solidity( + args.solidity_file + ) # list of files + else: + exit_with_error( + args.outform, + "No input bytecode. Please provide EVM code via -c BYTECODE, -a ADDRESS, or -i SOLIDITY_FILES", + ) + return address + + +def execute_command( + disassembler: MythrilDisassembler, + address: str, + parser: argparse.ArgumentParser, + args: argparse.Namespace, +): + + if args.storage: + if not args.address: + exit_with_error( + args.outform, + "To read storage, provide the address of a deployed contract with the -a option.", + ) + + storage = disassembler.get_state_variable_from_storage( + address=address, params=[a.strip() for a in args.storage.strip().split(",")] + ) + print(storage) + return + + analyzer = MythrilAnalyzer( + strategy=args.strategy, + disassembler=disassembler, + address=address, + max_depth=args.max_depth, + execution_timeout=args.execution_timeout, + loop_bound=args.loop_bound, + create_timeout=args.create_timeout, + enable_iprof=args.enable_iprof, + disable_dependency_pruning=args.disable_dependency_pruning, + onchain_storage_access=not args.no_onchain_storage_access, + ) + + if args.disassemble: + # or mythril.disassemble(mythril.contracts[0]) + + if disassembler.contracts[0].code: + print("Runtime Disassembly: \n" + disassembler.contracts[0].get_easm()) + if disassembler.contracts[0].creation_code: + print("Disassembly: \n" + disassembler.contracts[0].get_creation_easm()) + + elif args.graph or args.fire_lasers: + if not disassembler.contracts: + exit_with_error( + args.outform, "input files do not contain any valid contracts" + ) + + if args.graph: + html = analyzer.graph_html( + contract=analyzer.contracts[0], + enable_physics=args.enable_physics, + phrackify=args.phrack, + transaction_count=args.transaction_count, + ) + + try: + with open(args.graph, "w") as f: + f.write(html) + except Exception as e: + exit_with_error(args.outform, "Error saving graph: " + str(e)) + + else: + try: + report = analyzer.fire_lasers( + modules=[m.strip() for m in args.modules.strip().split(",")] + if args.modules + else [], + verbose_report=args.verbose_report, + transaction_count=args.transaction_count, + ) + outputs = { + "json": report.as_json(), + "jsonv2": report.as_swc_standard_format(), + "text": report.as_text(), + "markdown": report.as_markdown(), + } + print(outputs[args.outform]) + except ModuleNotFoundError as e: + exit_with_error( + args.outform, "Error loading analyis modules: " + format(e) + ) + + elif args.statespace_json: + + if not analyzer.contracts: + exit_with_error( + args.outform, "input files do not contain any valid contracts" + ) + + statespace = analyzer.dump_statespace(contract=analyzer.contracts[0]) + + try: + with open(args.statespace_json, "w") as f: + json.dump(statespace, f) + except Exception as e: + exit_with_error(args.outform, "Error saving json: " + str(e)) + + else: + parser.print_help() + + +def parse_args(parser: argparse.ArgumentParser, args: argparse.Namespace) -> None: + """ + Parses the arguments + :param parser: The parser + :param args: The args + """ + + if args.epic: + path = os.path.dirname(os.path.realpath(__file__)) + sys.argv.remove("--epic") + os.system(" ".join(sys.argv) + " | python3 " + path + "/epic.py") + sys.exit() + + if args.version: + if args.outform == "json": + print(json.dumps({"version_str": VERSION})) + else: + print("Mythril version {}".format(VERSION)) + sys.exit() + + # Parse cmdline args + validate_args(parser, args) + try: + quick_commands(args) + config = set_config(args) + leveldb_search(config, args) + disassembler = MythrilDisassembler( + eth=config.eth, + solc_version=args.solv, + solc_args=args.solc_args, + enable_online_lookup=args.query_signature, + ) + if args.truffle: + try: + disassembler.analyze_truffle_project(args) + except FileNotFoundError: + print( + "Build directory not found. Make sure that you start the analysis from the project root, and that 'truffle compile' has executed successfully." + ) + sys.exit() + + address = get_code(disassembler, args) + execute_command( + disassembler=disassembler, address=address, parser=parser, args=args + ) + except CriticalError as ce: + exit_with_error(args.outform, str(ce)) + except Exception: + exit_with_error(args.outform, traceback.format_exc()) + + +if __name__ == "__main__": + main() diff --git a/tests/report_test.py b/tests/report_test.py deleted file mode 100644 index 73554880..00000000 --- a/tests/report_test.py +++ /dev/null @@ -1,200 +0,0 @@ -from mythril.analysis.report import Report -from mythril.analysis.security import fire_lasers, reset_callback_modules -from mythril.analysis.symbolic import SymExecWrapper -from mythril.ethereum import util -from mythril.solidity.soliditycontract import EVMContract -from multiprocessing import Pool, cpu_count -import pytest -import json -from tests import * -import difflib - - -def _fix_path(text): - return text.replace(str(TESTDATA), "") - - -def _fix_debug_data(json_str): - read_json = json.loads(json_str) - for issue in read_json["issues"]: - issue["tx_sequence"] = "" - - return json.dumps(read_json, sort_keys=True, indent=4) - - -def _add_jsonv2_stubs(json_str): - read_json = json.loads(json_str) - for issue in read_json[0]["issues"]: - issue["extra"]["discoveryTime"] = "" - issue["extra"]["testCase"] = "" - return json.dumps(read_json, sort_keys=True, indent=4) - - -def _generate_report(input_file): - contract = EVMContract(input_file.read_text(), enable_online_lookup=False) - sym = SymExecWrapper( - contract, - address=0xAFFEAFFEAFFEAFFEAFFEAFFEAFFEAFFEAFFEAFFE, - strategy="dfs", - execution_timeout=30, - transaction_count=1, - ) - issues = fire_lasers(sym) - - report = Report(contracts=[contract]) - for issue in issues: - issue.filename = "test-filename.sol" - report.append_issue(issue) - return report, input_file - - -@pytest.fixture(scope="module") -def reports(): - """Fixture that analyses all reports.""" - reset_callback_modules() - pool = Pool(cpu_count()) - input_files = sorted( - [f for f in TESTDATA_INPUTS.iterdir() if f.name != "environments.sol.o"] - ) - results = pool.map(_generate_report, input_files) - - return results - - -def _assert_empty(changed_files, postfix): - """Asserts there are no changed files and otherwise builds error - message.""" - message = "" - for input_file in changed_files: - output_expected = ( - (TESTDATA_OUTPUTS_EXPECTED / (input_file.name + postfix)) - .read_text() - .splitlines(1) - ) - output_current = ( - (TESTDATA_OUTPUTS_CURRENT / (input_file.name + postfix)) - .read_text() - .splitlines(1) - ) - - difference = "".join(difflib.unified_diff(output_expected, output_current)) - message += "Found differing file for input: {} \n Difference: \n {} \n".format( - str(input_file), str(difference) - ) - - assert message == "", message - - -def _assert_empty_json(changed_files, postfix=".json"): - """Asserts there are no changed files and otherwise builds error - message.""" - expected = [] - actual = [] - - def ordered(obj): - """ - - :param obj: - :return: - """ - if isinstance(obj, dict): - return sorted((k, ordered(v)) for k, v in obj.items()) - elif isinstance(obj, list): - return sorted(ordered(x) for x in obj) - else: - return obj - - for input_file in changed_files: - output_expected = json.loads( - (TESTDATA_OUTPUTS_EXPECTED / (input_file.name + postfix)).read_text() - ) - output_current = json.loads( - (TESTDATA_OUTPUTS_CURRENT / (input_file.name + postfix)).read_text() - ) - - if not ordered(output_expected) == ordered(output_current): - expected.append(output_expected) - actual.append(output_current) - print("Found difference in {}".format(str(input_file))) - - assert expected == actual - - -def _get_changed_files(postfix, report_builder, reports): - """Returns a generator for all unexpected changes in generated reports. - - :param postfix: The applicable postfix - :param report_builder: serialization function - :param reports: The reports to serialize - :return: Changed files - """ - for report, input_file in reports: - output_expected = TESTDATA_OUTPUTS_EXPECTED / (input_file.name + postfix) - output_current = TESTDATA_OUTPUTS_CURRENT / (input_file.name + postfix) - output_current.write_text(report_builder(report)) - if not (output_expected.read_text() == output_current.read_text()): - yield input_file - - -def _get_changed_files_json(report_builder, reports, postfix=".json"): - def ordered(obj): - """ - - :param obj: - :return: - """ - if isinstance(obj, dict): - return sorted((k, ordered(v)) for k, v in obj.items()) - elif isinstance(obj, list): - return sorted(ordered(x) for x in obj) - else: - return obj - - for report, input_file in reports: - output_expected = TESTDATA_OUTPUTS_EXPECTED / (input_file.name + postfix) - output_current = TESTDATA_OUTPUTS_CURRENT / (input_file.name + postfix) - output_current.write_text(report_builder(report)) - - if not ordered(json.loads(output_expected.read_text())) == ordered( - json.loads(output_current.read_text()) - ): - yield input_file - - -def test_json_report(reports): - _assert_empty_json( - _get_changed_files_json( - lambda report: _fix_path(_fix_debug_data(report.as_json())).strip(), reports - ) - ) - - -def test_markdown_report(reports): - _assert_empty( - _get_changed_files( - ".markdown", lambda report: _fix_path(report.as_markdown()), reports - ), - ".markdown", - ) - - -def test_text_report(reports): - _assert_empty( - _get_changed_files( - ".text", lambda report: _fix_path(report.as_text()), reports - ), - ".text", - ) - - -def test_jsonv2_report(reports): - _assert_empty_json( - _get_changed_files_json( - lambda report: _fix_path( - _add_jsonv2_stubs(report.as_swc_standard_format()) - ).strip(), - reports, - ".jsonv2", - ), - ".jsonv2", - ) diff --git a/tests/testdata/outputs_expected/calls.sol.o.json b/tests/testdata/outputs_expected/calls.sol.o.json deleted file mode 100644 index 0219f575..00000000 --- a/tests/testdata/outputs_expected/calls.sol.o.json +++ /dev/null @@ -1,123 +0,0 @@ -{ - "error": null, - "issues": [ - { - "address": 661, - "contract": "Unknown", - "description": "A call to a user-supplied address is executed.\nThe callee address of an external message call can be set by the caller. Note that the callee can contain arbitrary code and may re-enter any function in this contract. Review the business logic carefully to prevent averse effects on the contract state.", - "function": "thisisfine()", - "max_gas_used": 1254, - "min_gas_used": 643, - "severity": "Medium", - "sourceMap": null, - "swc-id": "107", - "title": "External Call To User-Supplied Address", - "tx_sequence": "" - }, - { - "address": 661, - "contract": "Unknown", - "description": "The return value of a message call is not checked.\nExternal calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states.", - "function": "thisisfine()", - "max_gas_used": 35972, - "min_gas_used": 1361, - "severity": "Low", - "sourceMap": null, - "swc-id": "104", - "title": "Unchecked Call Return Value", - "tx_sequence": "" - }, - { - "address": 779, - "contract": "Unknown", - "description": "A call to a user-supplied address is executed.\nThe callee address of an external message call can be set by the caller. Note that the callee can contain arbitrary code and may re-enter any function in this contract. Review the business logic carefully to prevent averse effects on the contract state.", - "function": "callstoredaddress()", - "max_gas_used": 1298, - "min_gas_used": 687, - "severity": "Medium", - "sourceMap": null, - "swc-id": "107", - "title": "External Call To User-Supplied Address", - "tx_sequence": "" - }, - { - "address": 779, - "contract": "Unknown", - "description": "The return value of a message call is not checked.\nExternal calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states.", - "function": "callstoredaddress()", - "max_gas_used": 36016, - "min_gas_used": 1405, - "severity": "Low", - "sourceMap": null, - "swc-id": "104", - "title": "Unchecked Call Return Value", - "tx_sequence": "" - }, - { - "address": 858, - "contract": "Unknown", - "description": "A call to a user-supplied address is executed.\nThe callee address of an external message call can be set by the caller. Note that the callee can contain arbitrary code and may re-enter any function in this contract. Review the business logic carefully to prevent averse effects on the contract state.", - "function": "reentrancy()", - "max_gas_used": 1320, - "min_gas_used": 709, - "severity": "Medium", - "sourceMap": null, - "swc-id": "107", - "title": "External Call To User-Supplied Address", - "tx_sequence": "" - }, - { - "address": 858, - "contract": "Unknown", - "description": "The return value of a message call is not checked.\nExternal calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states.", - "function": "reentrancy()", - "max_gas_used": 61052, - "min_gas_used": 6441, - "severity": "Low", - "sourceMap": null, - "swc-id": "104", - "title": "Unchecked Call Return Value", - "tx_sequence": "" - }, - { - "address": 869, - "contract": "Unknown", - "description": "The contract account state is changed after an external call. \nConsider that the called contract could re-enter the function before this state change takes place. This can lead to business logic vulnerabilities.", - "function": "reentrancy()", - "max_gas_used": null, - "min_gas_used": null, - "severity": "Medium", - "sourceMap": null, - "swc-id": "107", - "title": "State change after external call", - "tx_sequence": "" - }, - { - "address": 912, - "contract": "Unknown", - "description": "A call to a user-supplied address is executed.\nThe callee address of an external message call can be set by the caller. Note that the callee can contain arbitrary code and may re-enter any function in this contract. Review the business logic carefully to prevent averse effects on the contract state.", - "function": "calluseraddress(address)", - "max_gas_used": 616, - "min_gas_used": 335, - "severity": "Medium", - "sourceMap": null, - "swc-id": "107", - "title": "External Call To User-Supplied Address", - "tx_sequence": "" - }, - { - "address": 912, - "contract": "Unknown", - "description": "The return value of a message call is not checked.\nExternal calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states.", - "function": "calluseraddress(address)", - "max_gas_used": 35336, - "min_gas_used": 1055, - "severity": "Low", - "sourceMap": null, - "swc-id": "104", - "title": "Unchecked Call Return Value", - "tx_sequence": "" - } - ], - "success": true -} \ No newline at end of file diff --git a/tests/testdata/outputs_expected/calls.sol.o.jsonv2 b/tests/testdata/outputs_expected/calls.sol.o.jsonv2 deleted file mode 100644 index 9bab6f6a..00000000 --- a/tests/testdata/outputs_expected/calls.sol.o.jsonv2 +++ /dev/null @@ -1,174 +0,0 @@ -[ - { - "issues": [ - { - "description": { - "head": "A call to a user-supplied address is executed.", - "tail": "The callee address of an external message call can be set by the caller. Note that the callee can contain arbitrary code and may re-enter any function in this contract. Review the business logic carefully to prevent averse effects on the contract state." - }, - "extra": { - "discoveryTime": "", - "testCase": "" - }, - "locations": [ - { - "sourceMap": "661:1:0" - } - ], - "severity": "Medium", - "swcID": "SWC-107", - "swcTitle": "Reentrancy" - }, - { - "description": { - "head": "A call to a user-supplied address is executed.", - "tail": "The callee address of an external message call can be set by the caller. Note that the callee can contain arbitrary code and may re-enter any function in this contract. Review the business logic carefully to prevent averse effects on the contract state." - }, - "extra": { - "discoveryTime": "", - "testCase": "" - }, - "locations": [ - { - "sourceMap": "779:1:0" - } - ], - "severity": "Medium", - "swcID": "SWC-107", - "swcTitle": "Reentrancy" - }, - { - "description": { - "head": "A call to a user-supplied address is executed.", - "tail": "The callee address of an external message call can be set by the caller. Note that the callee can contain arbitrary code and may re-enter any function in this contract. Review the business logic carefully to prevent averse effects on the contract state." - }, - "extra": { - "discoveryTime": "", - "testCase": "" - }, - "locations": [ - { - "sourceMap": "858:1:0" - } - ], - "severity": "Medium", - "swcID": "SWC-107", - "swcTitle": "Reentrancy" - }, - { - "description": { - "head": "A call to a user-supplied address is executed.", - "tail": "The callee address of an external message call can be set by the caller. Note that the callee can contain arbitrary code and may re-enter any function in this contract. Review the business logic carefully to prevent averse effects on the contract state." - }, - "extra": { - "discoveryTime": "", - "testCase": "" - }, - "locations": [ - { - "sourceMap": "912:1:0" - } - ], - "severity": "Medium", - "swcID": "SWC-107", - "swcTitle": "Reentrancy" - }, - { - "description": { - "head": "The contract account state is changed after an external call. ", - "tail": "Consider that the called contract could re-enter the function before this state change takes place. This can lead to business logic vulnerabilities." - }, - "extra": { - "discoveryTime": "", - "testCase": "" - }, - "locations": [ - { - "sourceMap": "869:1:0" - } - ], - "severity": "Medium", - "swcID": "SWC-107", - "swcTitle": "Reentrancy" - }, - { - "description": { - "head": "The return value of a message call is not checked.", - "tail": "External calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states." - }, - "extra": { - "discoveryTime": "", - "testCase": "" - }, - "locations": [ - { - "sourceMap": "661:1:0" - } - ], - "severity": "Low", - "swcID": "SWC-104", - "swcTitle": "Unchecked Call Return Value" - }, - { - "description": { - "head": "The return value of a message call is not checked.", - "tail": "External calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states." - }, - "extra": { - "discoveryTime": "", - "testCase": "" - }, - "locations": [ - { - "sourceMap": "779:1:0" - } - ], - "severity": "Low", - "swcID": "SWC-104", - "swcTitle": "Unchecked Call Return Value" - }, - { - "description": { - "head": "The return value of a message call is not checked.", - "tail": "External calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states." - }, - "extra": { - "discoveryTime": "", - "testCase": "" - }, - "locations": [ - { - "sourceMap": "858:1:0" - } - ], - "severity": "Low", - "swcID": "SWC-104", - "swcTitle": "Unchecked Call Return Value" - }, - { - "description": { - "head": "The return value of a message call is not checked.", - "tail": "External calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states." - }, - "extra": { - "discoveryTime": "", - "testCase": "" - }, - "locations": [ - { - "sourceMap": "912:1:0" - } - ], - "severity": "Low", - "swcID": "SWC-104", - "swcTitle": "Unchecked Call Return Value" - } - ], - "meta": {}, - "sourceFormat": "evm-byzantium-bytecode", - "sourceList": [ - "0x7cbb77986c6b1bf6e945cd3fba06d3ea3d28cfc49cdfdc9571ec30703ac5862f" - ], - "sourceType": "raw-bytecode" - } -] \ No newline at end of file diff --git a/tests/testdata/outputs_expected/calls.sol.o.markdown b/tests/testdata/outputs_expected/calls.sol.o.markdown deleted file mode 100644 index 490ad89e..00000000 --- a/tests/testdata/outputs_expected/calls.sol.o.markdown +++ /dev/null @@ -1,117 +0,0 @@ -# Analysis results for test-filename.sol - -## External Call To User-Supplied Address -- SWC ID: 107 -- Severity: Medium -- Contract: Unknown -- Function name: `thisisfine()` -- PC address: 661 -- Estimated Gas Usage: 643 - 1254 - -### Description - -A call to a user-supplied address is executed. -The callee address of an external message call can be set by the caller. Note that the callee can contain arbitrary code and may re-enter any function in this contract. Review the business logic carefully to prevent averse effects on the contract state. - -## Unchecked Call Return Value -- SWC ID: 104 -- Severity: Low -- Contract: Unknown -- Function name: `thisisfine()` -- PC address: 661 -- Estimated Gas Usage: 1361 - 35972 - -### Description - -The return value of a message call is not checked. -External calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states. - -## External Call To User-Supplied Address -- SWC ID: 107 -- Severity: Medium -- Contract: Unknown -- Function name: `callstoredaddress()` -- PC address: 779 -- Estimated Gas Usage: 687 - 1298 - -### Description - -A call to a user-supplied address is executed. -The callee address of an external message call can be set by the caller. Note that the callee can contain arbitrary code and may re-enter any function in this contract. Review the business logic carefully to prevent averse effects on the contract state. - -## Unchecked Call Return Value -- SWC ID: 104 -- Severity: Low -- Contract: Unknown -- Function name: `callstoredaddress()` -- PC address: 779 -- Estimated Gas Usage: 1405 - 36016 - -### Description - -The return value of a message call is not checked. -External calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states. - -## External Call To User-Supplied Address -- SWC ID: 107 -- Severity: Medium -- Contract: Unknown -- Function name: `reentrancy()` -- PC address: 858 -- Estimated Gas Usage: 709 - 1320 - -### Description - -A call to a user-supplied address is executed. -The callee address of an external message call can be set by the caller. Note that the callee can contain arbitrary code and may re-enter any function in this contract. Review the business logic carefully to prevent averse effects on the contract state. - -## Unchecked Call Return Value -- SWC ID: 104 -- Severity: Low -- Contract: Unknown -- Function name: `reentrancy()` -- PC address: 858 -- Estimated Gas Usage: 6441 - 61052 - -### Description - -The return value of a message call is not checked. -External calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states. - -## State change after external call -- SWC ID: 107 -- Severity: Medium -- Contract: Unknown -- Function name: `reentrancy()` -- PC address: 869 - -### Description - -The contract account state is changed after an external call. -Consider that the called contract could re-enter the function before this state change takes place. This can lead to business logic vulnerabilities. - -## External Call To User-Supplied Address -- SWC ID: 107 -- Severity: Medium -- Contract: Unknown -- Function name: `calluseraddress(address)` -- PC address: 912 -- Estimated Gas Usage: 335 - 616 - -### Description - -A call to a user-supplied address is executed. -The callee address of an external message call can be set by the caller. Note that the callee can contain arbitrary code and may re-enter any function in this contract. Review the business logic carefully to prevent averse effects on the contract state. - -## Unchecked Call Return Value -- SWC ID: 104 -- Severity: Low -- Contract: Unknown -- Function name: `calluseraddress(address)` -- PC address: 912 -- Estimated Gas Usage: 1055 - 35336 - -### Description - -The return value of a message call is not checked. -External calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states. diff --git a/tests/testdata/outputs_expected/calls.sol.o.text b/tests/testdata/outputs_expected/calls.sol.o.text deleted file mode 100644 index d38e8f03..00000000 --- a/tests/testdata/outputs_expected/calls.sol.o.text +++ /dev/null @@ -1,98 +0,0 @@ -==== External Call To User-Supplied Address ==== -SWC ID: 107 -Severity: Medium -Contract: Unknown -Function name: thisisfine() -PC address: 661 -Estimated Gas Usage: 643 - 1254 -A call to a user-supplied address is executed. -The callee address of an external message call can be set by the caller. Note that the callee can contain arbitrary code and may re-enter any function in this contract. Review the business logic carefully to prevent averse effects on the contract state. --------------------- - -==== Unchecked Call Return Value ==== -SWC ID: 104 -Severity: Low -Contract: Unknown -Function name: thisisfine() -PC address: 661 -Estimated Gas Usage: 1361 - 35972 -The return value of a message call is not checked. -External calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states. --------------------- - -==== External Call To User-Supplied Address ==== -SWC ID: 107 -Severity: Medium -Contract: Unknown -Function name: callstoredaddress() -PC address: 779 -Estimated Gas Usage: 687 - 1298 -A call to a user-supplied address is executed. -The callee address of an external message call can be set by the caller. Note that the callee can contain arbitrary code and may re-enter any function in this contract. Review the business logic carefully to prevent averse effects on the contract state. --------------------- - -==== Unchecked Call Return Value ==== -SWC ID: 104 -Severity: Low -Contract: Unknown -Function name: callstoredaddress() -PC address: 779 -Estimated Gas Usage: 1405 - 36016 -The return value of a message call is not checked. -External calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states. --------------------- - -==== External Call To User-Supplied Address ==== -SWC ID: 107 -Severity: Medium -Contract: Unknown -Function name: reentrancy() -PC address: 858 -Estimated Gas Usage: 709 - 1320 -A call to a user-supplied address is executed. -The callee address of an external message call can be set by the caller. Note that the callee can contain arbitrary code and may re-enter any function in this contract. Review the business logic carefully to prevent averse effects on the contract state. --------------------- - -==== Unchecked Call Return Value ==== -SWC ID: 104 -Severity: Low -Contract: Unknown -Function name: reentrancy() -PC address: 858 -Estimated Gas Usage: 6441 - 61052 -The return value of a message call is not checked. -External calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states. --------------------- - -==== State change after external call ==== -SWC ID: 107 -Severity: Medium -Contract: Unknown -Function name: reentrancy() -PC address: 869 -The contract account state is changed after an external call. -Consider that the called contract could re-enter the function before this state change takes place. This can lead to business logic vulnerabilities. --------------------- - -==== External Call To User-Supplied Address ==== -SWC ID: 107 -Severity: Medium -Contract: Unknown -Function name: calluseraddress(address) -PC address: 912 -Estimated Gas Usage: 335 - 616 -A call to a user-supplied address is executed. -The callee address of an external message call can be set by the caller. Note that the callee can contain arbitrary code and may re-enter any function in this contract. Review the business logic carefully to prevent averse effects on the contract state. --------------------- - -==== Unchecked Call Return Value ==== -SWC ID: 104 -Severity: Low -Contract: Unknown -Function name: calluseraddress(address) -PC address: 912 -Estimated Gas Usage: 1055 - 35336 -The return value of a message call is not checked. -External calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states. --------------------- - diff --git a/tests/testdata/outputs_expected/ether_send.sol.o.json b/tests/testdata/outputs_expected/ether_send.sol.o.json deleted file mode 100644 index 1d2e4a19..00000000 --- a/tests/testdata/outputs_expected/ether_send.sol.o.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "error": null, - "issues": [ - { - "address": 722, - "contract": "Unknown", - "description": "Anyone can withdraw ETH from the contract account.\nArbitrary 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.", - "function": "withdrawfunds()", - "max_gas_used": 1749, - "min_gas_used": 1138, - "severity": "High", - "sourceMap": null, - "swc-id": "105", - "title": "Unprotected Ether Withdrawal", - "tx_sequence": "" - }, - { - "address": 883, - "contract": "Unknown", - "description": "The binary addition can overflow.\nThe operands of the addition operation are not sufficiently constrained. The addition could therefore result in an integer overflow. Prevent the overflow by checking inputs or ensure sure that the overflow is caught by an assertion.", - "function": "invest()", - "max_gas_used": 26883, - "min_gas_used": 6598, - "severity": "High", - "sourceMap": null, - "swc-id": "101", - "title": "Integer Overflow", - "tx_sequence": "" - } - ], - "success": true -} \ No newline at end of file diff --git a/tests/testdata/outputs_expected/ether_send.sol.o.jsonv2 b/tests/testdata/outputs_expected/ether_send.sol.o.jsonv2 deleted file mode 100644 index e848bd2f..00000000 --- a/tests/testdata/outputs_expected/ether_send.sol.o.jsonv2 +++ /dev/null @@ -1,48 +0,0 @@ -[ - { - "issues": [ - { - "description": { - "head": "Anyone can withdraw ETH from the contract account.", - "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." - }, - "extra": { - "discoveryTime": "", - "testCase": "" - }, - "locations": [ - { - "sourceMap": "722:1:0" - } - ], - "severity": "High", - "swcID": "SWC-105", - "swcTitle": "Unprotected Ether Withdrawal" - }, - { - "description": { - "head": "The binary addition can overflow.", - "tail": "The operands of the addition operation are not sufficiently constrained. The addition could therefore result in an integer overflow. Prevent the overflow by checking inputs or ensure sure that the overflow is caught by an assertion." - }, - "extra": { - "discoveryTime": "", - "testCase": "" - }, - "locations": [ - { - "sourceMap": "883:1:0" - } - ], - "severity": "High", - "swcID": "SWC-101", - "swcTitle": "Integer Overflow and Underflow" - } - ], - "meta": {}, - "sourceFormat": "evm-byzantium-bytecode", - "sourceList": [ - "0x3746c7c2ae7b0d4c3f8b1905df9a7ea169b9f93bec68a10a00b4c9d27a18c6fb" - ], - "sourceType": "raw-bytecode" - } -] \ No newline at end of file diff --git a/tests/testdata/outputs_expected/ether_send.sol.o.markdown b/tests/testdata/outputs_expected/ether_send.sol.o.markdown deleted file mode 100644 index 2e1c2a9e..00000000 --- a/tests/testdata/outputs_expected/ether_send.sol.o.markdown +++ /dev/null @@ -1,27 +0,0 @@ -# Analysis results for test-filename.sol - -## Unprotected Ether Withdrawal -- SWC ID: 105 -- Severity: High -- Contract: Unknown -- Function name: `withdrawfunds()` -- PC address: 722 -- Estimated Gas Usage: 1138 - 1749 - -### Description - -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. - -## Integer Overflow -- SWC ID: 101 -- Severity: High -- Contract: Unknown -- Function name: `invest()` -- PC address: 883 -- Estimated Gas Usage: 6598 - 26883 - -### Description - -The binary addition can overflow. -The operands of the addition operation are not sufficiently constrained. The addition could therefore result in an integer overflow. Prevent the overflow by checking inputs or ensure sure that the overflow is caught by an assertion. diff --git a/tests/testdata/outputs_expected/ether_send.sol.o.text b/tests/testdata/outputs_expected/ether_send.sol.o.text deleted file mode 100644 index 493978be..00000000 --- a/tests/testdata/outputs_expected/ether_send.sol.o.text +++ /dev/null @@ -1,22 +0,0 @@ -==== Unprotected Ether Withdrawal ==== -SWC ID: 105 -Severity: High -Contract: Unknown -Function name: withdrawfunds() -PC address: 722 -Estimated Gas Usage: 1138 - 1749 -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. --------------------- - -==== Integer Overflow ==== -SWC ID: 101 -Severity: High -Contract: Unknown -Function name: invest() -PC address: 883 -Estimated Gas Usage: 6598 - 26883 -The binary addition can overflow. -The operands of the addition operation are not sufficiently constrained. The addition could therefore result in an integer overflow. Prevent the overflow by checking inputs or ensure sure that the overflow is caught by an assertion. --------------------- - diff --git a/tests/testdata/outputs_expected/exceptions.sol.o.json b/tests/testdata/outputs_expected/exceptions.sol.o.json deleted file mode 100644 index 19030e55..00000000 --- a/tests/testdata/outputs_expected/exceptions.sol.o.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "error": null, - "issues": [ - { - "address": 446, - "contract": "Unknown", - "description": "A reachable exception has been detected.\nIt is possible to trigger an exception (opcode 0xfe). Exceptions can be caused by type errors, division by zero, out-of-bounds array access, or assert violations. Note that explicit `assert()` should only be used to check invariants. Use `require()` for regular input checking.", - "function": "assert3(uint256)", - "max_gas_used": 301, - "min_gas_used": 206, - "severity": "Low", - "sourceMap": null, - "swc-id": "110", - "title": "Exception State", - "tx_sequence": "" - }, - { - "address": 484, - "contract": "Unknown", - "description": "A reachable exception has been detected.\nIt is possible to trigger an exception (opcode 0xfe). Exceptions can be caused by type errors, division by zero, out-of-bounds array access, or assert violations. Note that explicit `assert()` should only be used to check invariants. Use `require()` for regular input checking.", - "function": "arrayaccess(uint256)", - "max_gas_used": 351, - "min_gas_used": 256, - "severity": "Low", - "sourceMap": null, - "swc-id": "110", - "title": "Exception State", - "tx_sequence": "" - }, - { - "address": 506, - "contract": "Unknown", - "description": "A reachable exception has been detected.\nIt is possible to trigger an exception (opcode 0xfe). Exceptions can be caused by type errors, division by zero, out-of-bounds array access, or assert violations. Note that explicit `assert()` should only be used to check invariants. Use `require()` for regular input checking.", - "function": "divisionby0(uint256)", - "max_gas_used": 367, - "min_gas_used": 272, - "severity": "Low", - "sourceMap": null, - "swc-id": "110", - "title": "Exception State", - "tx_sequence": "" - }, - { - "address": 531, - "contract": "Unknown", - "description": "A reachable exception has been detected.\nIt is possible to trigger an exception (opcode 0xfe). Exceptions can be caused by type errors, division by zero, out-of-bounds array access, or assert violations. Note that explicit `assert()` should only be used to check invariants. Use `require()` for regular input checking.", - "function": "assert1()", - "max_gas_used": 363, - "min_gas_used": 268, - "severity": "Low", - "sourceMap": null, - "swc-id": "110", - "title": "Exception State", - "tx_sequence": "" - } - ], - "success": true -} \ No newline at end of file diff --git a/tests/testdata/outputs_expected/exceptions.sol.o.jsonv2 b/tests/testdata/outputs_expected/exceptions.sol.o.jsonv2 deleted file mode 100644 index 43b6ca48..00000000 --- a/tests/testdata/outputs_expected/exceptions.sol.o.jsonv2 +++ /dev/null @@ -1,84 +0,0 @@ -[ - { - "issues": [ - { - "description": { - "head": "A reachable exception has been detected.", - "tail": "It is possible to trigger an exception (opcode 0xfe). Exceptions can be caused by type errors, division by zero, out-of-bounds array access, or assert violations. Note that explicit `assert()` should only be used to check invariants. Use `require()` for regular input checking." - }, - "extra": { - "discoveryTime": "", - "testCase": "" - }, - "locations": [ - { - "sourceMap": "446:1:0" - } - ], - "severity": "Low", - "swcID": "SWC-110", - "swcTitle": "Assert Violation" - }, - { - "description": { - "head": "A reachable exception has been detected.", - "tail": "It is possible to trigger an exception (opcode 0xfe). Exceptions can be caused by type errors, division by zero, out-of-bounds array access, or assert violations. Note that explicit `assert()` should only be used to check invariants. Use `require()` for regular input checking." - }, - "extra": { - "discoveryTime": "", - "testCase": "" - }, - "locations": [ - { - "sourceMap": "484:1:0" - } - ], - "severity": "Low", - "swcID": "SWC-110", - "swcTitle": "Assert Violation" - }, - { - "description": { - "head": "A reachable exception has been detected.", - "tail": "It is possible to trigger an exception (opcode 0xfe). Exceptions can be caused by type errors, division by zero, out-of-bounds array access, or assert violations. Note that explicit `assert()` should only be used to check invariants. Use `require()` for regular input checking." - }, - "extra": { - "discoveryTime": "", - "testCase": "" - }, - "locations": [ - { - "sourceMap": "506:1:0" - } - ], - "severity": "Low", - "swcID": "SWC-110", - "swcTitle": "Assert Violation" - }, - { - "description": { - "head": "A reachable exception has been detected.", - "tail": "It is possible to trigger an exception (opcode 0xfe). Exceptions can be caused by type errors, division by zero, out-of-bounds array access, or assert violations. Note that explicit `assert()` should only be used to check invariants. Use `require()` for regular input checking." - }, - "extra": { - "discoveryTime": "", - "testCase": "" - }, - "locations": [ - { - "sourceMap": "531:1:0" - } - ], - "severity": "Low", - "swcID": "SWC-110", - "swcTitle": "Assert Violation" - } - ], - "meta": {}, - "sourceFormat": "evm-byzantium-bytecode", - "sourceList": [ - "0x4a773a86bc6fb269f88bf09bb3094de29b6073cf13b1760e9d01d957f50a9dfd" - ], - "sourceType": "raw-bytecode" - } -] \ No newline at end of file diff --git a/tests/testdata/outputs_expected/exceptions.sol.o.markdown b/tests/testdata/outputs_expected/exceptions.sol.o.markdown deleted file mode 100644 index c5da9834..00000000 --- a/tests/testdata/outputs_expected/exceptions.sol.o.markdown +++ /dev/null @@ -1,53 +0,0 @@ -# Analysis results for test-filename.sol - -## Exception State -- SWC ID: 110 -- Severity: Low -- Contract: Unknown -- Function name: `assert3(uint256)` -- PC address: 446 -- Estimated Gas Usage: 206 - 301 - -### Description - -A reachable exception has been detected. -It is possible to trigger an exception (opcode 0xfe). Exceptions can be caused by type errors, division by zero, out-of-bounds array access, or assert violations. Note that explicit `assert()` should only be used to check invariants. Use `require()` for regular input checking. - -## Exception State -- SWC ID: 110 -- Severity: Low -- Contract: Unknown -- Function name: `arrayaccess(uint256)` -- PC address: 484 -- Estimated Gas Usage: 256 - 351 - -### Description - -A reachable exception has been detected. -It is possible to trigger an exception (opcode 0xfe). Exceptions can be caused by type errors, division by zero, out-of-bounds array access, or assert violations. Note that explicit `assert()` should only be used to check invariants. Use `require()` for regular input checking. - -## Exception State -- SWC ID: 110 -- Severity: Low -- Contract: Unknown -- Function name: `divisionby0(uint256)` -- PC address: 506 -- Estimated Gas Usage: 272 - 367 - -### Description - -A reachable exception has been detected. -It is possible to trigger an exception (opcode 0xfe). Exceptions can be caused by type errors, division by zero, out-of-bounds array access, or assert violations. Note that explicit `assert()` should only be used to check invariants. Use `require()` for regular input checking. - -## Exception State -- SWC ID: 110 -- Severity: Low -- Contract: Unknown -- Function name: `assert1()` -- PC address: 531 -- Estimated Gas Usage: 268 - 363 - -### Description - -A reachable exception has been detected. -It is possible to trigger an exception (opcode 0xfe). Exceptions can be caused by type errors, division by zero, out-of-bounds array access, or assert violations. Note that explicit `assert()` should only be used to check invariants. Use `require()` for regular input checking. diff --git a/tests/testdata/outputs_expected/exceptions.sol.o.text b/tests/testdata/outputs_expected/exceptions.sol.o.text deleted file mode 100644 index cfee4d39..00000000 --- a/tests/testdata/outputs_expected/exceptions.sol.o.text +++ /dev/null @@ -1,44 +0,0 @@ -==== Exception State ==== -SWC ID: 110 -Severity: Low -Contract: Unknown -Function name: assert3(uint256) -PC address: 446 -Estimated Gas Usage: 206 - 301 -A reachable exception has been detected. -It is possible to trigger an exception (opcode 0xfe). Exceptions can be caused by type errors, division by zero, out-of-bounds array access, or assert violations. Note that explicit `assert()` should only be used to check invariants. Use `require()` for regular input checking. --------------------- - -==== Exception State ==== -SWC ID: 110 -Severity: Low -Contract: Unknown -Function name: arrayaccess(uint256) -PC address: 484 -Estimated Gas Usage: 256 - 351 -A reachable exception has been detected. -It is possible to trigger an exception (opcode 0xfe). Exceptions can be caused by type errors, division by zero, out-of-bounds array access, or assert violations. Note that explicit `assert()` should only be used to check invariants. Use `require()` for regular input checking. --------------------- - -==== Exception State ==== -SWC ID: 110 -Severity: Low -Contract: Unknown -Function name: divisionby0(uint256) -PC address: 506 -Estimated Gas Usage: 272 - 367 -A reachable exception has been detected. -It is possible to trigger an exception (opcode 0xfe). Exceptions can be caused by type errors, division by zero, out-of-bounds array access, or assert violations. Note that explicit `assert()` should only be used to check invariants. Use `require()` for regular input checking. --------------------- - -==== Exception State ==== -SWC ID: 110 -Severity: Low -Contract: Unknown -Function name: assert1() -PC address: 531 -Estimated Gas Usage: 268 - 363 -A reachable exception has been detected. -It is possible to trigger an exception (opcode 0xfe). Exceptions can be caused by type errors, division by zero, out-of-bounds array access, or assert violations. Note that explicit `assert()` should only be used to check invariants. Use `require()` for regular input checking. --------------------- - diff --git a/tests/testdata/outputs_expected/kinds_of_calls.sol.o.json b/tests/testdata/outputs_expected/kinds_of_calls.sol.o.json deleted file mode 100644 index c2ee1fd0..00000000 --- a/tests/testdata/outputs_expected/kinds_of_calls.sol.o.json +++ /dev/null @@ -1,84 +0,0 @@ -{ - "error": null, - "issues": [ - { - "address": 618, - "contract": "Unknown", - "description": "The return value of a message call is not checked.\nExternal calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states.", - "function": "_function_0x141f32ff", - "max_gas_used": 35865, - "min_gas_used": 1113, - "severity": "Low", - "sourceMap": null, - "swc-id": "104", - "title": "Unchecked Call Return Value", - "tx_sequence": "" - }, - { - "address": 618, - "contract": "Unknown", - "description": "Use of callcode is deprecated.\nThe callcode method executes code of another contract in the context of the caller account. Due to a bug in the implementation it does not persist sender and value over the call. It was therefore deprecated and may be removed in the future. Use the delegatecall method instead.", - "function": "_function_0x141f32ff", - "max_gas_used": 1141, - "min_gas_used": 389, - "severity": "Medium", - "sourceMap": null, - "swc-id": "111", - "title": "Use of callcode", - "tx_sequence": "" - }, - { - "address": 849, - "contract": "Unknown", - "description": "The contract delegates execution to another contract with a user-supplied address.\nThe 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. ", - "function": "_function_0x9b58bc26", - "max_gas_used": 35928, - "min_gas_used": 1176, - "severity": "Medium", - "sourceMap": null, - "swc-id": "112", - "title": "Delegatecall Proxy To User-Supplied Address", - "tx_sequence": "" - }, - { - "address": 849, - "contract": "Unknown", - "description": "The return value of a message call is not checked.\nExternal calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states.", - "function": "_function_0x9b58bc26", - "max_gas_used": 35928, - "min_gas_used": 1176, - "severity": "Low", - "sourceMap": null, - "swc-id": "104", - "title": "Unchecked Call Return Value", - "tx_sequence": "" - }, - { - "address": 1038, - "contract": "Unknown", - "description": "A call to a user-supplied address is executed.\nThe callee address of an external message call can be set by the caller. Note that the callee can contain arbitrary code and may re-enter any function in this contract. Review the business logic carefully to prevent averse effects on the contract state.", - "function": "_function_0xeea4c864", - "max_gas_used": 1229, - "min_gas_used": 477, - "severity": "Medium", - "sourceMap": null, - "swc-id": "107", - "title": "External Call To User-Supplied Address", - "tx_sequence": "" - }, - { - "address": 1038, - "contract": "Unknown", - "description": "The return value of a message call is not checked.\nExternal calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states.", - "function": "_function_0xeea4c864", - "max_gas_used": 35953, - "min_gas_used": 1201, - "severity": "Low", - "sourceMap": null, - "swc-id": "104", - "title": "Unchecked Call Return Value", - "tx_sequence": "" - } - ], - "success": true -} \ No newline at end of file diff --git a/tests/testdata/outputs_expected/kinds_of_calls.sol.o.jsonv2 b/tests/testdata/outputs_expected/kinds_of_calls.sol.o.jsonv2 deleted file mode 100644 index d4f5cf82..00000000 --- a/tests/testdata/outputs_expected/kinds_of_calls.sol.o.jsonv2 +++ /dev/null @@ -1,120 +0,0 @@ -[ - { - "issues": [ - { - "description": { - "head": "The contract delegates execution to another contract with a user-supplied address.", - "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. " - }, - "extra": { - "discoveryTime": "", - "testCase": "" - }, - "locations": [ - { - "sourceMap": "849:1:0" - } - ], - "severity": "Medium", - "swcID": "SWC-112", - "swcTitle": "Delegatecall to Untrusted Callee" - }, - { - "description": { - "head": "Use of callcode is deprecated.", - "tail": "The callcode method executes code of another contract in the context of the caller account. Due to a bug in the implementation it does not persist sender and value over the call. It was therefore deprecated and may be removed in the future. Use the delegatecall method instead." - }, - "extra": { - "discoveryTime": "", - "testCase": "" - }, - "locations": [ - { - "sourceMap": "618:1:0" - } - ], - "severity": "Medium", - "swcID": "SWC-111", - "swcTitle": "Use of Deprecated Solidity Functions" - }, - { - "description": { - "head": "A call to a user-supplied address is executed.", - "tail": "The callee address of an external message call can be set by the caller. Note that the callee can contain arbitrary code and may re-enter any function in this contract. Review the business logic carefully to prevent averse effects on the contract state." - }, - "extra": { - "discoveryTime": "", - "testCase": "" - }, - "locations": [ - { - "sourceMap": "1038:1:0" - } - ], - "severity": "Medium", - "swcID": "SWC-107", - "swcTitle": "Reentrancy" - }, - { - "description": { - "head": "The return value of a message call is not checked.", - "tail": "External calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states." - }, - "extra": { - "discoveryTime": "", - "testCase": "" - }, - "locations": [ - { - "sourceMap": "618:1:0" - } - ], - "severity": "Low", - "swcID": "SWC-104", - "swcTitle": "Unchecked Call Return Value" - }, - { - "description": { - "head": "The return value of a message call is not checked.", - "tail": "External calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states." - }, - "extra": { - "discoveryTime": "", - "testCase": "" - }, - "locations": [ - { - "sourceMap": "849:1:0" - } - ], - "severity": "Low", - "swcID": "SWC-104", - "swcTitle": "Unchecked Call Return Value" - }, - { - "description": { - "head": "The return value of a message call is not checked.", - "tail": "External calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states." - }, - "extra": { - "discoveryTime": "", - "testCase": "" - }, - "locations": [ - { - "sourceMap": "1038:1:0" - } - ], - "severity": "Low", - "swcID": "SWC-104", - "swcTitle": "Unchecked Call Return Value" - } - ], - "meta": {}, - "sourceFormat": "evm-byzantium-bytecode", - "sourceList": [ - "0x6daec61d05d8f1210661e7e7d1ed6d72bd6ade639398fac1e867aff50abfc1c1" - ], - "sourceType": "raw-bytecode" - } -] \ No newline at end of file diff --git a/tests/testdata/outputs_expected/kinds_of_calls.sol.o.markdown b/tests/testdata/outputs_expected/kinds_of_calls.sol.o.markdown deleted file mode 100644 index e6f7f11e..00000000 --- a/tests/testdata/outputs_expected/kinds_of_calls.sol.o.markdown +++ /dev/null @@ -1,79 +0,0 @@ -# Analysis results for test-filename.sol - -## Unchecked Call Return Value -- SWC ID: 104 -- Severity: Low -- Contract: Unknown -- Function name: `_function_0x141f32ff` -- PC address: 618 -- Estimated Gas Usage: 1113 - 35865 - -### Description - -The return value of a message call is not checked. -External calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states. - -## Use of callcode -- SWC ID: 111 -- Severity: Medium -- Contract: Unknown -- Function name: `_function_0x141f32ff` -- PC address: 618 -- Estimated Gas Usage: 389 - 1141 - -### Description - -Use of callcode is deprecated. -The callcode method executes code of another contract in the context of the caller account. Due to a bug in the implementation it does not persist sender and value over the call. It was therefore deprecated and may be removed in the future. Use the delegatecall method instead. - -## Delegatecall Proxy To User-Supplied Address -- SWC ID: 112 -- Severity: Medium -- Contract: Unknown -- Function name: `_function_0x9b58bc26` -- PC address: 849 -- Estimated Gas Usage: 1176 - 35928 - -### Description - -The contract delegates execution to another contract with a user-supplied address. -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. - -## Unchecked Call Return Value -- SWC ID: 104 -- Severity: Low -- Contract: Unknown -- Function name: `_function_0x9b58bc26` -- PC address: 849 -- Estimated Gas Usage: 1176 - 35928 - -### Description - -The return value of a message call is not checked. -External calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states. - -## External Call To User-Supplied Address -- SWC ID: 107 -- Severity: Medium -- Contract: Unknown -- Function name: `_function_0xeea4c864` -- PC address: 1038 -- Estimated Gas Usage: 477 - 1229 - -### Description - -A call to a user-supplied address is executed. -The callee address of an external message call can be set by the caller. Note that the callee can contain arbitrary code and may re-enter any function in this contract. Review the business logic carefully to prevent averse effects on the contract state. - -## Unchecked Call Return Value -- SWC ID: 104 -- Severity: Low -- Contract: Unknown -- Function name: `_function_0xeea4c864` -- PC address: 1038 -- Estimated Gas Usage: 1201 - 35953 - -### Description - -The return value of a message call is not checked. -External calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states. diff --git a/tests/testdata/outputs_expected/kinds_of_calls.sol.o.text b/tests/testdata/outputs_expected/kinds_of_calls.sol.o.text deleted file mode 100644 index 1bb3abad..00000000 --- a/tests/testdata/outputs_expected/kinds_of_calls.sol.o.text +++ /dev/null @@ -1,66 +0,0 @@ -==== Unchecked Call Return Value ==== -SWC ID: 104 -Severity: Low -Contract: Unknown -Function name: _function_0x141f32ff -PC address: 618 -Estimated Gas Usage: 1113 - 35865 -The return value of a message call is not checked. -External calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states. --------------------- - -==== Use of callcode ==== -SWC ID: 111 -Severity: Medium -Contract: Unknown -Function name: _function_0x141f32ff -PC address: 618 -Estimated Gas Usage: 389 - 1141 -Use of callcode is deprecated. -The callcode method executes code of another contract in the context of the caller account. Due to a bug in the implementation it does not persist sender and value over the call. It was therefore deprecated and may be removed in the future. Use the delegatecall method instead. --------------------- - -==== Delegatecall Proxy To User-Supplied Address ==== -SWC ID: 112 -Severity: Medium -Contract: Unknown -Function name: _function_0x9b58bc26 -PC address: 849 -Estimated Gas Usage: 1176 - 35928 -The contract delegates execution to another contract with a user-supplied address. -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. --------------------- - -==== Unchecked Call Return Value ==== -SWC ID: 104 -Severity: Low -Contract: Unknown -Function name: _function_0x9b58bc26 -PC address: 849 -Estimated Gas Usage: 1176 - 35928 -The return value of a message call is not checked. -External calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states. --------------------- - -==== External Call To User-Supplied Address ==== -SWC ID: 107 -Severity: Medium -Contract: Unknown -Function name: _function_0xeea4c864 -PC address: 1038 -Estimated Gas Usage: 477 - 1229 -A call to a user-supplied address is executed. -The callee address of an external message call can be set by the caller. Note that the callee can contain arbitrary code and may re-enter any function in this contract. Review the business logic carefully to prevent averse effects on the contract state. --------------------- - -==== Unchecked Call Return Value ==== -SWC ID: 104 -Severity: Low -Contract: Unknown -Function name: _function_0xeea4c864 -PC address: 1038 -Estimated Gas Usage: 1201 - 35953 -The return value of a message call is not checked. -External calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states. --------------------- - diff --git a/tests/testdata/outputs_expected/metacoin.sol.o.json b/tests/testdata/outputs_expected/metacoin.sol.o.json deleted file mode 100644 index 712f50c1..00000000 --- a/tests/testdata/outputs_expected/metacoin.sol.o.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "error": null, - "issues": [], - "success": true -} \ No newline at end of file diff --git a/tests/testdata/outputs_expected/metacoin.sol.o.jsonv2 b/tests/testdata/outputs_expected/metacoin.sol.o.jsonv2 deleted file mode 100644 index 40de69b4..00000000 --- a/tests/testdata/outputs_expected/metacoin.sol.o.jsonv2 +++ /dev/null @@ -1,11 +0,0 @@ -[ - { - "issues": [], - "meta": {}, - "sourceFormat": "evm-byzantium-bytecode", - "sourceList": [ - "0x0e6f727bb3301e02d3be831bf34357522fd2f1d40e90dff8e2214553b06b5f6c" - ], - "sourceType": "raw-bytecode" - } -] \ No newline at end of file diff --git a/tests/testdata/outputs_expected/metacoin.sol.o.markdown b/tests/testdata/outputs_expected/metacoin.sol.o.markdown deleted file mode 100644 index 321484fd..00000000 --- a/tests/testdata/outputs_expected/metacoin.sol.o.markdown +++ /dev/null @@ -1,3 +0,0 @@ -# Analysis results for None - -The analysis was completed successfully. No issues were detected. diff --git a/tests/testdata/outputs_expected/metacoin.sol.o.text b/tests/testdata/outputs_expected/metacoin.sol.o.text deleted file mode 100644 index 729320d8..00000000 --- a/tests/testdata/outputs_expected/metacoin.sol.o.text +++ /dev/null @@ -1 +0,0 @@ -The analysis was completed successfully. No issues were detected. diff --git a/tests/testdata/outputs_expected/multi_contracts.sol.o.json b/tests/testdata/outputs_expected/multi_contracts.sol.o.json deleted file mode 100644 index cf2fd3af..00000000 --- a/tests/testdata/outputs_expected/multi_contracts.sol.o.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "error": null, - "issues": [ - { - "address": 142, - "contract": "Unknown", - "description": "Anyone can withdraw ETH from the contract account.\nArbitrary 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.", - "function": "transfer()", - "max_gas_used": 467, - "min_gas_used": 186, - "severity": "High", - "sourceMap": null, - "swc-id": "105", - "title": "Unprotected Ether Withdrawal", - "tx_sequence": "" - } - ], - "success": true -} \ No newline at end of file diff --git a/tests/testdata/outputs_expected/multi_contracts.sol.o.jsonv2 b/tests/testdata/outputs_expected/multi_contracts.sol.o.jsonv2 deleted file mode 100644 index ec36d8ca..00000000 --- a/tests/testdata/outputs_expected/multi_contracts.sol.o.jsonv2 +++ /dev/null @@ -1,30 +0,0 @@ -[ - { - "issues": [ - { - "description": { - "head": "Anyone can withdraw ETH from the contract account.", - "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." - }, - "extra": { - "discoveryTime": "", - "testCase": "" - }, - "locations": [ - { - "sourceMap": "142:1:0" - } - ], - "severity": "High", - "swcID": "SWC-105", - "swcTitle": "Unprotected Ether Withdrawal" - } - ], - "meta": {}, - "sourceFormat": "evm-byzantium-bytecode", - "sourceList": [ - "0xbc9c3d9db56d20cf4ca3b6fd88ff9215cf728a092cca1ed8edb83272b933ff5b" - ], - "sourceType": "raw-bytecode" - } -] \ No newline at end of file diff --git a/tests/testdata/outputs_expected/multi_contracts.sol.o.markdown b/tests/testdata/outputs_expected/multi_contracts.sol.o.markdown deleted file mode 100644 index a7eac008..00000000 --- a/tests/testdata/outputs_expected/multi_contracts.sol.o.markdown +++ /dev/null @@ -1,14 +0,0 @@ -# Analysis results for test-filename.sol - -## Unprotected Ether Withdrawal -- SWC ID: 105 -- Severity: High -- Contract: Unknown -- Function name: `transfer()` -- PC address: 142 -- Estimated Gas Usage: 186 - 467 - -### Description - -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. diff --git a/tests/testdata/outputs_expected/multi_contracts.sol.o.text b/tests/testdata/outputs_expected/multi_contracts.sol.o.text deleted file mode 100644 index a8388020..00000000 --- a/tests/testdata/outputs_expected/multi_contracts.sol.o.text +++ /dev/null @@ -1,11 +0,0 @@ -==== Unprotected Ether Withdrawal ==== -SWC ID: 105 -Severity: High -Contract: Unknown -Function name: transfer() -PC address: 142 -Estimated Gas Usage: 186 - 467 -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. --------------------- - diff --git a/tests/testdata/outputs_expected/nonascii.sol.o.json b/tests/testdata/outputs_expected/nonascii.sol.o.json deleted file mode 100644 index 712f50c1..00000000 --- a/tests/testdata/outputs_expected/nonascii.sol.o.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "error": null, - "issues": [], - "success": true -} \ No newline at end of file diff --git a/tests/testdata/outputs_expected/nonascii.sol.o.jsonv2 b/tests/testdata/outputs_expected/nonascii.sol.o.jsonv2 deleted file mode 100644 index 0667ad8c..00000000 --- a/tests/testdata/outputs_expected/nonascii.sol.o.jsonv2 +++ /dev/null @@ -1,11 +0,0 @@ -[ - { - "issues": [], - "meta": {}, - "sourceFormat": "evm-byzantium-bytecode", - "sourceList": [ - "0x11a78eb09819f505ba4f10747e6d1f7a44480e602c67573b7abac2f733a85d93" - ], - "sourceType": "raw-bytecode" - } -] \ No newline at end of file diff --git a/tests/testdata/outputs_expected/nonascii.sol.o.markdown b/tests/testdata/outputs_expected/nonascii.sol.o.markdown deleted file mode 100644 index 321484fd..00000000 --- a/tests/testdata/outputs_expected/nonascii.sol.o.markdown +++ /dev/null @@ -1,3 +0,0 @@ -# Analysis results for None - -The analysis was completed successfully. No issues were detected. diff --git a/tests/testdata/outputs_expected/nonascii.sol.o.text b/tests/testdata/outputs_expected/nonascii.sol.o.text deleted file mode 100644 index 729320d8..00000000 --- a/tests/testdata/outputs_expected/nonascii.sol.o.text +++ /dev/null @@ -1 +0,0 @@ -The analysis was completed successfully. No issues were detected. diff --git a/tests/testdata/outputs_expected/origin.sol.o.json b/tests/testdata/outputs_expected/origin.sol.o.json deleted file mode 100644 index 6d79baf7..00000000 --- a/tests/testdata/outputs_expected/origin.sol.o.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "error": null, - "issues": [ - { - "address": 317, - "contract": "Unknown", - "description": "Use of tx.origin is deprecated.\nThe smart contract retrieves the transaction origin (tx.origin) using msg.origin. Use of msg.origin is deprecated and the instruction may be removed in the future. Use msg.sender instead.\nSee also: https://solidity.readthedocs.io/en/develop/security-considerations.html#tx-origin", - "function": "transferOwnership(address)", - "max_gas_used": 1051, - "min_gas_used": 626, - "severity": "Medium", - "sourceMap": null, - "swc-id": "111", - "title": "Use of tx.origin", - "tx_sequence": "" - } - ], - "success": true -} \ No newline at end of file diff --git a/tests/testdata/outputs_expected/origin.sol.o.jsonv2 b/tests/testdata/outputs_expected/origin.sol.o.jsonv2 deleted file mode 100644 index ec679550..00000000 --- a/tests/testdata/outputs_expected/origin.sol.o.jsonv2 +++ /dev/null @@ -1,30 +0,0 @@ -[ - { - "issues": [ - { - "description": { - "head": "Use of tx.origin is deprecated.", - "tail": "The smart contract retrieves the transaction origin (tx.origin) using msg.origin. Use of msg.origin is deprecated and the instruction may be removed in the future. Use msg.sender instead.\nSee also: https://solidity.readthedocs.io/en/develop/security-considerations.html#tx-origin" - }, - "extra": { - "discoveryTime": "", - "testCase": "" - }, - "locations": [ - { - "sourceMap": "317:1:0" - } - ], - "severity": "Medium", - "swcID": "SWC-111", - "swcTitle": "Use of Deprecated Solidity Functions" - } - ], - "meta": {}, - "sourceFormat": "evm-byzantium-bytecode", - "sourceList": [ - "0x25b20ef097dfc0aa56a932c4e09f06ee02a69c005767df86877f48c6c2412f03" - ], - "sourceType": "raw-bytecode" - } -] \ No newline at end of file diff --git a/tests/testdata/outputs_expected/origin.sol.o.markdown b/tests/testdata/outputs_expected/origin.sol.o.markdown deleted file mode 100644 index 1f5f83ac..00000000 --- a/tests/testdata/outputs_expected/origin.sol.o.markdown +++ /dev/null @@ -1,15 +0,0 @@ -# Analysis results for test-filename.sol - -## Use of tx.origin -- SWC ID: 111 -- Severity: Medium -- Contract: Unknown -- Function name: `transferOwnership(address)` -- PC address: 317 -- Estimated Gas Usage: 626 - 1051 - -### Description - -Use of tx.origin is deprecated. -The smart contract retrieves the transaction origin (tx.origin) using msg.origin. Use of msg.origin is deprecated and the instruction may be removed in the future. Use msg.sender instead. -See also: https://solidity.readthedocs.io/en/develop/security-considerations.html#tx-origin diff --git a/tests/testdata/outputs_expected/origin.sol.o.text b/tests/testdata/outputs_expected/origin.sol.o.text deleted file mode 100644 index b7ebc992..00000000 --- a/tests/testdata/outputs_expected/origin.sol.o.text +++ /dev/null @@ -1,12 +0,0 @@ -==== Use of tx.origin ==== -SWC ID: 111 -Severity: Medium -Contract: Unknown -Function name: transferOwnership(address) -PC address: 317 -Estimated Gas Usage: 626 - 1051 -Use of tx.origin is deprecated. -The smart contract retrieves the transaction origin (tx.origin) using msg.origin. Use of msg.origin is deprecated and the instruction may be removed in the future. Use msg.sender instead. -See also: https://solidity.readthedocs.io/en/develop/security-considerations.html#tx-origin --------------------- - diff --git a/tests/testdata/outputs_expected/overflow.sol.o.json b/tests/testdata/outputs_expected/overflow.sol.o.json deleted file mode 100644 index 16a2253b..00000000 --- a/tests/testdata/outputs_expected/overflow.sol.o.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "error": null, - "issues": [ - { - "address": 567, - "contract": "Unknown", - "description": "The binary subtraction can underflow.\nThe operands of the subtraction operation are not sufficiently constrained. The subtraction could therefore result in an integer underflow. Prevent the underflow by checking inputs or ensure sure that the underflow is caught by an assertion.", - "function": "sendeth(address,uint256)", - "max_gas_used": 78155, - "min_gas_used": 17019, - "severity": "High", - "sourceMap": null, - "swc-id": "101", - "title": "Integer Underflow", - "tx_sequence": "" - }, - { - "address": 649, - "contract": "Unknown", - "description": "The binary subtraction can underflow.\nThe operands of the subtraction operation are not sufficiently constrained. The subtraction could therefore result in an integer underflow. Prevent the underflow by checking inputs or ensure sure that the underflow is caught by an assertion.", - "function": "sendeth(address,uint256)", - "max_gas_used": 78155, - "min_gas_used": 17019, - "severity": "High", - "sourceMap": null, - "swc-id": "101", - "title": "Integer Underflow", - "tx_sequence": "" - }, - { - "address": 725, - "contract": "Unknown", - "description": "The binary addition can overflow.\nThe operands of the addition operation are not sufficiently constrained. The addition could therefore result in an integer overflow. Prevent the overflow by checking inputs or ensure sure that the overflow is caught by an assertion.", - "function": "sendeth(address,uint256)", - "max_gas_used": 78155, - "min_gas_used": 17019, - "severity": "High", - "sourceMap": null, - "swc-id": "101", - "title": "Integer Overflow", - "tx_sequence": "" - } - ], - "success": true -} diff --git a/tests/testdata/outputs_expected/overflow.sol.o.jsonv2 b/tests/testdata/outputs_expected/overflow.sol.o.jsonv2 deleted file mode 100644 index 53028f4a..00000000 --- a/tests/testdata/outputs_expected/overflow.sol.o.jsonv2 +++ /dev/null @@ -1,66 +0,0 @@ -[ - { - "issues": [ - { - "description": { - "head": "The binary subtraction can underflow.", - "tail": "The operands of the subtraction operation are not sufficiently constrained. The subtraction could therefore result in an integer underflow. Prevent the underflow by checking inputs or ensure sure that the underflow is caught by an assertion." - }, - "extra": { - "discoveryTime": "", - "testCase": "" - }, - "locations": [ - { - "sourceMap": "567:1:0" - } - ], - "severity": "High", - "swcID": "SWC-101", - "swcTitle": "Integer Overflow and Underflow" - }, - { - "description": { - "head": "The binary subtraction can underflow.", - "tail": "The operands of the subtraction operation are not sufficiently constrained. The subtraction could therefore result in an integer underflow. Prevent the underflow by checking inputs or ensure sure that the underflow is caught by an assertion." - }, - "extra": { - "discoveryTime": "", - "testCase": "" - }, - "locations": [ - { - "sourceMap": "649:1:0" - } - ], - "severity": "High", - "swcID": "SWC-101", - "swcTitle": "Integer Overflow and Underflow" - }, - { - "description": { - "head": "The binary addition can overflow.", - "tail": "The operands of the addition operation are not sufficiently constrained. The addition could therefore result in an integer overflow. Prevent the overflow by checking inputs or ensure sure that the overflow is caught by an assertion." - }, - "extra": { - "discoveryTime": "", - "testCase": "" - }, - "locations": [ - { - "sourceMap": "725:1:0" - } - ], - "severity": "High", - "swcID": "SWC-101", - "swcTitle": "Integer Overflow and Underflow" - } - ], - "meta": {}, - "sourceFormat": "evm-byzantium-bytecode", - "sourceList": [ - "0xf230bec502569e8b7e7737616d0ad0f200c436624e3c223e5398c0615cd2d6b9" - ], - "sourceType": "raw-bytecode" - } -] diff --git a/tests/testdata/outputs_expected/overflow.sol.o.markdown b/tests/testdata/outputs_expected/overflow.sol.o.markdown deleted file mode 100644 index 82642a1e..00000000 --- a/tests/testdata/outputs_expected/overflow.sol.o.markdown +++ /dev/null @@ -1,40 +0,0 @@ -# Analysis results for test-filename.sol - -## Integer Underflow -- SWC ID: 101 -- Severity: High -- Contract: Unknown -- Function name: `sendeth(address,uint256)` -- PC address: 567 -- Estimated Gas Usage: 17019 - 78155 - -### Description - -The binary subtraction can underflow. -The operands of the subtraction operation are not sufficiently constrained. The subtraction could therefore result in an integer underflow. Prevent the underflow by checking inputs or ensure sure that the underflow is caught by an assertion. - -## Integer Underflow -- SWC ID: 101 -- Severity: High -- Contract: Unknown -- Function name: `sendeth(address,uint256)` -- PC address: 649 -- Estimated Gas Usage: 17019 - 78155 - -### Description - -The binary subtraction can underflow. -The operands of the subtraction operation are not sufficiently constrained. The subtraction could therefore result in an integer underflow. Prevent the underflow by checking inputs or ensure sure that the underflow is caught by an assertion. - -## Integer Overflow -- SWC ID: 101 -- Severity: High -- Contract: Unknown -- Function name: `sendeth(address,uint256)` -- PC address: 725 -- Estimated Gas Usage: 17019 - 78155 - -### Description - -The binary addition can overflow. -The operands of the addition operation are not sufficiently constrained. The addition could therefore result in an integer overflow. Prevent the overflow by checking inputs or ensure sure that the overflow is caught by an assertion. diff --git a/tests/testdata/outputs_expected/overflow.sol.o.text b/tests/testdata/outputs_expected/overflow.sol.o.text deleted file mode 100644 index e70dda5b..00000000 --- a/tests/testdata/outputs_expected/overflow.sol.o.text +++ /dev/null @@ -1,33 +0,0 @@ -==== Integer Underflow ==== -SWC ID: 101 -Severity: High -Contract: Unknown -Function name: sendeth(address,uint256) -PC address: 567 -Estimated Gas Usage: 17019 - 78155 -The binary subtraction can underflow. -The operands of the subtraction operation are not sufficiently constrained. The subtraction could therefore result in an integer underflow. Prevent the underflow by checking inputs or ensure sure that the underflow is caught by an assertion. --------------------- - -==== Integer Underflow ==== -SWC ID: 101 -Severity: High -Contract: Unknown -Function name: sendeth(address,uint256) -PC address: 649 -Estimated Gas Usage: 17019 - 78155 -The binary subtraction can underflow. -The operands of the subtraction operation are not sufficiently constrained. The subtraction could therefore result in an integer underflow. Prevent the underflow by checking inputs or ensure sure that the underflow is caught by an assertion. --------------------- - -==== Integer Overflow ==== -SWC ID: 101 -Severity: High -Contract: Unknown -Function name: sendeth(address,uint256) -PC address: 725 -Estimated Gas Usage: 17019 - 78155 -The binary addition can overflow. -The operands of the addition operation are not sufficiently constrained. The addition could therefore result in an integer overflow. Prevent the overflow by checking inputs or ensure sure that the overflow is caught by an assertion. --------------------- - diff --git a/tests/testdata/outputs_expected/returnvalue.sol.o.json b/tests/testdata/outputs_expected/returnvalue.sol.o.json deleted file mode 100644 index bd7c8a97..00000000 --- a/tests/testdata/outputs_expected/returnvalue.sol.o.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "error": null, - "issues": [ - { - "address": 196, - "contract": "Unknown", - "description": "A call to a user-supplied address is executed.\nThe callee address of an external message call can be set by the caller. Note that the callee can contain arbitrary code and may re-enter any function in this contract. Review the business logic carefully to prevent averse effects on the contract state.", - "function": "callchecked()", - "max_gas_used": 1210, - "min_gas_used": 599, - "severity": "Medium", - "sourceMap": null, - "swc-id": "107", - "title": "External Call To User-Supplied Address", - "tx_sequence": "" - }, - { - "address": 285, - "contract": "Unknown", - "description": "A call to a user-supplied address is executed.\nThe callee address of an external message call can be set by the caller. Note that the callee can contain arbitrary code and may re-enter any function in this contract. Review the business logic carefully to prevent averse effects on the contract state.", - "function": "callnotchecked()", - "max_gas_used": 1232, - "min_gas_used": 621, - "severity": "Medium", - "sourceMap": null, - "swc-id": "107", - "title": "External Call To User-Supplied Address", - "tx_sequence": "" - }, - { - "address": 285, - "contract": "Unknown", - "description": "The return value of a message call is not checked.\nExternal calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states.", - "function": "callnotchecked()", - "max_gas_used": 35950, - "min_gas_used": 1339, - "severity": "Low", - "sourceMap": null, - "swc-id": "104", - "title": "Unchecked Call Return Value", - "tx_sequence": "" - } - ], - "success": true -} \ No newline at end of file diff --git a/tests/testdata/outputs_expected/returnvalue.sol.o.jsonv2 b/tests/testdata/outputs_expected/returnvalue.sol.o.jsonv2 deleted file mode 100644 index 8e5bf428..00000000 --- a/tests/testdata/outputs_expected/returnvalue.sol.o.jsonv2 +++ /dev/null @@ -1,66 +0,0 @@ -[ - { - "issues": [ - { - "description": { - "head": "A call to a user-supplied address is executed.", - "tail": "The callee address of an external message call can be set by the caller. Note that the callee can contain arbitrary code and may re-enter any function in this contract. Review the business logic carefully to prevent averse effects on the contract state." - }, - "extra": { - "discoveryTime": "", - "testCase": "" - }, - "locations": [ - { - "sourceMap": "196:1:0" - } - ], - "severity": "Medium", - "swcID": "SWC-107", - "swcTitle": "Reentrancy" - }, - { - "description": { - "head": "A call to a user-supplied address is executed.", - "tail": "The callee address of an external message call can be set by the caller. Note that the callee can contain arbitrary code and may re-enter any function in this contract. Review the business logic carefully to prevent averse effects on the contract state." - }, - "extra": { - "discoveryTime": "", - "testCase": "" - }, - "locations": [ - { - "sourceMap": "285:1:0" - } - ], - "severity": "Medium", - "swcID": "SWC-107", - "swcTitle": "Reentrancy" - }, - { - "description": { - "head": "The return value of a message call is not checked.", - "tail": "External calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states." - }, - "extra": { - "discoveryTime": "", - "testCase": "" - }, - "locations": [ - { - "sourceMap": "285:1:0" - } - ], - "severity": "Low", - "swcID": "SWC-104", - "swcTitle": "Unchecked Call Return Value" - } - ], - "meta": {}, - "sourceFormat": "evm-byzantium-bytecode", - "sourceList": [ - "0xb191cf6cc0d8cc37a91c9d88019cc011b932169fb5776df616e2bb9cd93b4039" - ], - "sourceType": "raw-bytecode" - } -] \ No newline at end of file diff --git a/tests/testdata/outputs_expected/returnvalue.sol.o.markdown b/tests/testdata/outputs_expected/returnvalue.sol.o.markdown deleted file mode 100644 index 5309f405..00000000 --- a/tests/testdata/outputs_expected/returnvalue.sol.o.markdown +++ /dev/null @@ -1,40 +0,0 @@ -# Analysis results for test-filename.sol - -## External Call To User-Supplied Address -- SWC ID: 107 -- Severity: Medium -- Contract: Unknown -- Function name: `callchecked()` -- PC address: 196 -- Estimated Gas Usage: 599 - 1210 - -### Description - -A call to a user-supplied address is executed. -The callee address of an external message call can be set by the caller. Note that the callee can contain arbitrary code and may re-enter any function in this contract. Review the business logic carefully to prevent averse effects on the contract state. - -## External Call To User-Supplied Address -- SWC ID: 107 -- Severity: Medium -- Contract: Unknown -- Function name: `callnotchecked()` -- PC address: 285 -- Estimated Gas Usage: 621 - 1232 - -### Description - -A call to a user-supplied address is executed. -The callee address of an external message call can be set by the caller. Note that the callee can contain arbitrary code and may re-enter any function in this contract. Review the business logic carefully to prevent averse effects on the contract state. - -## Unchecked Call Return Value -- SWC ID: 104 -- Severity: Low -- Contract: Unknown -- Function name: `callnotchecked()` -- PC address: 285 -- Estimated Gas Usage: 1339 - 35950 - -### Description - -The return value of a message call is not checked. -External calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states. diff --git a/tests/testdata/outputs_expected/returnvalue.sol.o.text b/tests/testdata/outputs_expected/returnvalue.sol.o.text deleted file mode 100644 index baff23ea..00000000 --- a/tests/testdata/outputs_expected/returnvalue.sol.o.text +++ /dev/null @@ -1,33 +0,0 @@ -==== External Call To User-Supplied Address ==== -SWC ID: 107 -Severity: Medium -Contract: Unknown -Function name: callchecked() -PC address: 196 -Estimated Gas Usage: 599 - 1210 -A call to a user-supplied address is executed. -The callee address of an external message call can be set by the caller. Note that the callee can contain arbitrary code and may re-enter any function in this contract. Review the business logic carefully to prevent averse effects on the contract state. --------------------- - -==== External Call To User-Supplied Address ==== -SWC ID: 107 -Severity: Medium -Contract: Unknown -Function name: callnotchecked() -PC address: 285 -Estimated Gas Usage: 621 - 1232 -A call to a user-supplied address is executed. -The callee address of an external message call can be set by the caller. Note that the callee can contain arbitrary code and may re-enter any function in this contract. Review the business logic carefully to prevent averse effects on the contract state. --------------------- - -==== Unchecked Call Return Value ==== -SWC ID: 104 -Severity: Low -Contract: Unknown -Function name: callnotchecked() -PC address: 285 -Estimated Gas Usage: 1339 - 35950 -The return value of a message call is not checked. -External calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states. --------------------- - diff --git a/tests/testdata/outputs_expected/suicide.sol.o.json b/tests/testdata/outputs_expected/suicide.sol.o.json deleted file mode 100644 index 1c98a444..00000000 --- a/tests/testdata/outputs_expected/suicide.sol.o.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "error": null, - "issues": [ - { - "address": 146, - "contract": "Unknown", - "description": "The contract can be killed by anyone.\nAnyone can kill this contract and withdraw its balance to an arbitrary address.", - "function": "kill(address)", - "max_gas_used": 263, - "min_gas_used": 168, - "severity": "High", - "sourceMap": null, - "swc-id": "106", - "title": "Unprotected Selfdestruct", - "tx_sequence": "" - } - ], - "success": true -} \ No newline at end of file diff --git a/tests/testdata/outputs_expected/suicide.sol.o.jsonv2 b/tests/testdata/outputs_expected/suicide.sol.o.jsonv2 deleted file mode 100644 index 30daf88a..00000000 --- a/tests/testdata/outputs_expected/suicide.sol.o.jsonv2 +++ /dev/null @@ -1,30 +0,0 @@ -[ - { - "issues": [ - { - "description": { - "head": "The contract can be killed by anyone.", - "tail": "Anyone can kill this contract and withdraw its balance to an arbitrary address." - }, - "extra": { - "discoveryTime": "", - "testCase": "" - }, - "locations": [ - { - "sourceMap": "146:1:0" - } - ], - "severity": "High", - "swcID": "SWC-106", - "swcTitle": "Unprotected SELFDESTRUCT Instruction" - } - ], - "meta": {}, - "sourceFormat": "evm-byzantium-bytecode", - "sourceList": [ - "0x2fb801366b61a05b30550481a1c8f7d5f20de0b93d9f2f2ce2b28c4e322033c9" - ], - "sourceType": "raw-bytecode" - } -] \ No newline at end of file diff --git a/tests/testdata/outputs_expected/suicide.sol.o.markdown b/tests/testdata/outputs_expected/suicide.sol.o.markdown deleted file mode 100644 index f31b9f3f..00000000 --- a/tests/testdata/outputs_expected/suicide.sol.o.markdown +++ /dev/null @@ -1,14 +0,0 @@ -# Analysis results for test-filename.sol - -## Unprotected Selfdestruct -- SWC ID: 106 -- Severity: High -- Contract: Unknown -- Function name: `kill(address)` -- PC address: 146 -- Estimated Gas Usage: 168 - 263 - -### Description - -The contract can be killed by anyone. -Anyone can kill this contract and withdraw its balance to an arbitrary address. diff --git a/tests/testdata/outputs_expected/suicide.sol.o.text b/tests/testdata/outputs_expected/suicide.sol.o.text deleted file mode 100644 index 45dd0295..00000000 --- a/tests/testdata/outputs_expected/suicide.sol.o.text +++ /dev/null @@ -1,11 +0,0 @@ -==== Unprotected Selfdestruct ==== -SWC ID: 106 -Severity: High -Contract: Unknown -Function name: kill(address) -PC address: 146 -Estimated Gas Usage: 168 - 263 -The contract can be killed by anyone. -Anyone can kill this contract and withdraw its balance to an arbitrary address. --------------------- - diff --git a/tests/testdata/outputs_expected/underflow.sol.o.json b/tests/testdata/outputs_expected/underflow.sol.o.json deleted file mode 100644 index 416d1176..00000000 --- a/tests/testdata/outputs_expected/underflow.sol.o.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "error": null, - "issues": [ - { - "address": 567, - "contract": "Unknown", - "description": "The binary subtraction can underflow.\nThe operands of the subtraction operation are not sufficiently constrained. The subtraction could therefore result in an integer underflow. Prevent the underflow by checking inputs or ensure sure that the underflow is caught by an assertion.", - "function": "sendeth(address,uint256)", - "max_gas_used": 52861, - "min_gas_used": 11915, - "severity": "High", - "sourceMap": null, - "swc-id": "101", - "title": "Integer Underflow", - "tx_sequence": "" - }, - { - "address": 649, - "contract": "Unknown", - "description": "The binary subtraction can underflow.\nThe operands of the subtraction operation are not sufficiently constrained. The subtraction could therefore result in an integer underflow. Prevent the underflow by checking inputs or ensure sure that the underflow is caught by an assertion.", - "function": "sendeth(address,uint256)", - "max_gas_used": 52861, - "min_gas_used": 11915, - "severity": "High", - "sourceMap": null, - "swc-id": "101", - "title": "Integer Underflow", - "tx_sequence": "" - }, - { - "address": 725, - "contract": "Unknown", - "description": "The binary addition can overflow.\nThe operands of the addition operation are not sufficiently constrained. The addition could therefore result in an integer overflow. Prevent the overflow by checking inputs or ensure sure that the overflow is caught by an assertion.", - "function": "sendeth(address,uint256)", - "max_gas_used": 52861, - "min_gas_used": 11915, - "severity": "High", - "sourceMap": null, - "swc-id": "101", - "title": "Integer Overflow", - "tx_sequence": "" - } - ], - "success": true -} diff --git a/tests/testdata/outputs_expected/underflow.sol.o.jsonv2 b/tests/testdata/outputs_expected/underflow.sol.o.jsonv2 deleted file mode 100644 index c99aae49..00000000 --- a/tests/testdata/outputs_expected/underflow.sol.o.jsonv2 +++ /dev/null @@ -1,66 +0,0 @@ -[ - { - "issues": [ - { - "description": { - "head": "The binary subtraction can underflow.", - "tail": "The operands of the subtraction operation are not sufficiently constrained. The subtraction could therefore result in an integer underflow. Prevent the underflow by checking inputs or ensure sure that the underflow is caught by an assertion." - }, - "extra": { - "discoveryTime": "", - "testCase": "" - }, - "locations": [ - { - "sourceMap": "567:1:0" - } - ], - "severity": "High", - "swcID": "SWC-101", - "swcTitle": "Integer Overflow and Underflow" - }, - { - "description": { - "head": "The binary subtraction can underflow.", - "tail": "The operands of the subtraction operation are not sufficiently constrained. The subtraction could therefore result in an integer underflow. Prevent the underflow by checking inputs or ensure sure that the underflow is caught by an assertion." - }, - "extra": { - "discoveryTime": "", - "testCase": "" - }, - "locations": [ - { - "sourceMap": "649:1:0" - } - ], - "severity": "High", - "swcID": "SWC-101", - "swcTitle": "Integer Overflow and Underflow" - }, - { - "description": { - "head": "The binary addition can overflow.", - "tail": "The operands of the addition operation are not sufficiently constrained. The addition could therefore result in an integer overflow. Prevent the overflow by checking inputs or ensure sure that the overflow is caught by an assertion." - }, - "extra": { - "discoveryTime": "", - "testCase": "" - }, - "locations": [ - { - "sourceMap": "725:1:0" - } - ], - "severity": "High", - "swcID": "SWC-101", - "swcTitle": "Integer Overflow and Underflow" - } - ], - "meta": {}, - "sourceFormat": "evm-byzantium-bytecode", - "sourceList": [ - "0xabef56740bf7795a9f8732e4781ebd27f2977f8a4997e3ff11cee79a4ba6c0ce" - ], - "sourceType": "raw-bytecode" - } -] diff --git a/tests/testdata/outputs_expected/underflow.sol.o.markdown b/tests/testdata/outputs_expected/underflow.sol.o.markdown deleted file mode 100644 index acc444d4..00000000 --- a/tests/testdata/outputs_expected/underflow.sol.o.markdown +++ /dev/null @@ -1,40 +0,0 @@ -# Analysis results for test-filename.sol - -## Integer Underflow -- SWC ID: 101 -- Severity: High -- Contract: Unknown -- Function name: `sendeth(address,uint256)` -- PC address: 567 -- Estimated Gas Usage: 11915 - 52861 - -### Description - -The binary subtraction can underflow. -The operands of the subtraction operation are not sufficiently constrained. The subtraction could therefore result in an integer underflow. Prevent the underflow by checking inputs or ensure sure that the underflow is caught by an assertion. - -## Integer Underflow -- SWC ID: 101 -- Severity: High -- Contract: Unknown -- Function name: `sendeth(address,uint256)` -- PC address: 649 -- Estimated Gas Usage: 11915 - 52861 - -### Description - -The binary subtraction can underflow. -The operands of the subtraction operation are not sufficiently constrained. The subtraction could therefore result in an integer underflow. Prevent the underflow by checking inputs or ensure sure that the underflow is caught by an assertion. - -## Integer Overflow -- SWC ID: 101 -- Severity: High -- Contract: Unknown -- Function name: `sendeth(address,uint256)` -- PC address: 725 -- Estimated Gas Usage: 11915 - 52861 - -### Description - -The binary addition can overflow. -The operands of the addition operation are not sufficiently constrained. The addition could therefore result in an integer overflow. Prevent the overflow by checking inputs or ensure sure that the overflow is caught by an assertion. diff --git a/tests/testdata/outputs_expected/underflow.sol.o.text b/tests/testdata/outputs_expected/underflow.sol.o.text deleted file mode 100644 index 498ff588..00000000 --- a/tests/testdata/outputs_expected/underflow.sol.o.text +++ /dev/null @@ -1,33 +0,0 @@ -==== Integer Underflow ==== -SWC ID: 101 -Severity: High -Contract: Unknown -Function name: sendeth(address,uint256) -PC address: 567 -Estimated Gas Usage: 11915 - 52861 -The binary subtraction can underflow. -The operands of the subtraction operation are not sufficiently constrained. The subtraction could therefore result in an integer underflow. Prevent the underflow by checking inputs or ensure sure that the underflow is caught by an assertion. --------------------- - -==== Integer Underflow ==== -SWC ID: 101 -Severity: High -Contract: Unknown -Function name: sendeth(address,uint256) -PC address: 649 -Estimated Gas Usage: 11915 - 52861 -The binary subtraction can underflow. -The operands of the subtraction operation are not sufficiently constrained. The subtraction could therefore result in an integer underflow. Prevent the underflow by checking inputs or ensure sure that the underflow is caught by an assertion. --------------------- - -==== Integer Overflow ==== -SWC ID: 101 -Severity: High -Contract: Unknown -Function name: sendeth(address,uint256) -PC address: 725 -Estimated Gas Usage: 11915 - 52861 -The binary addition can overflow. -The operands of the addition operation are not sufficiently constrained. The addition could therefore result in an integer overflow. Prevent the overflow by checking inputs or ensure sure that the overflow is caught by an assertion. --------------------- - From 19b4659be52add4000ce98b90bcf058bc8f8cdc0 Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 28 Jun 2019 14:39:58 -0700 Subject: [PATCH 017/164] Convert --solc-args parameter to --solc-json --- mythril/ethereum/util.py | 45 +++++++++++------------ mythril/interfaces/cli.py | 9 +++-- mythril/interfaces/old_cli.py | 7 ++-- mythril/mythril/mythril_disassembler.py | 12 ++++--- mythril/solidity/soliditycontract.py | 18 ++++++---- mythril/support/signatures.py | 47 +++++-------------------- 6 files changed, 59 insertions(+), 79 deletions(-) diff --git a/mythril/ethereum/util.py b/mythril/ethereum/util.py index 3e608b3a..da1c6a9b 100644 --- a/mythril/ethereum/util.py +++ b/mythril/ethereum/util.py @@ -24,41 +24,37 @@ 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, "--standard-json", "--allow-paths", "."] - cmd = [solc_binary, "--combined-json", "bin,bin-runtime,srcmap,srcmap-runtime,ast"] - cmd = [solc_binary, "--standard-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) - + 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": { - "outputSelection": { - "*": { - "": ["ast"], - "*": ["metadata", "evm.bytecode", "evm.deployedBytecode"], - } - } - }, + "settings": settings, } ) @@ -67,6 +63,7 @@ def get_solc_json(file, solc_binary="solc", solc_args=None): stdout, stderr = p.communicate(bytes(input_json, "utf8")) ret = p.returncode + # TODO: check json.loads(out)['errors'] for fatal errors. if ret != 0: raise CompilerError( "Solc experienced a fatal error (code %d).\n\n%s" diff --git a/mythril/interfaces/cli.py b/mythril/interfaces/cli.py index 5afa4bf2..4977eb24 100644 --- a/mythril/interfaces/cli.py +++ b/mythril/interfaces/cli.py @@ -165,7 +165,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)", @@ -723,12 +726,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 4c9d4222..19d1326a 100644 --- a/mythril/interfaces/old_cli.py +++ b/mythril/interfaces/old_cli.py @@ -230,7 +230,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" ) @@ -523,7 +526,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_json=args.solc_json, enable_online_lookup=args.query_signature, ) if args.truffle: diff --git a/mythril/mythril/mythril_disassembler.py b/mythril/mythril/mythril_disassembler.py index 5e1e72c3..0f5fbed1 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/solidity/soliditycontract.py b/mythril/solidity/soliditycontract.py index 513916a7..6adbc9ea 100644 --- a/mythril/solidity/soliditycontract.py +++ b/mythril/solidity/soliditycontract.py @@ -44,14 +44,16 @@ 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 contractName in data["contracts"][input_file].keys(): @@ -63,7 +65,7 @@ def get_contracts_from_file(input_file, solc_args=None, solc_binary="solc"): yield SolidityContract( input_file=input_file, name=contractName, - solc_args=solc_args, + solc_settings_json=solc_settings_json, solc_binary=solc_binary, ) except KeyError: @@ -73,8 +75,12 @@ 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 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]: From 120d0b2e2d94e7e83377896494ae78cd4b0e7343 Mon Sep 17 00:00:00 2001 From: Nikhil Date: Thu, 11 Jul 2019 20:29:57 +0530 Subject: [PATCH 018/164] Fix the json v2 accounts field --- mythril/analysis/solver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mythril/analysis/solver.py b/mythril/analysis/solver.py index ae1faf2c..104dbb4c 100644 --- a/mythril/analysis/solver.py +++ b/mythril/analysis/solver.py @@ -135,7 +135,7 @@ def _get_concrete_state(initial_accounts: Dict, min_price_dict: Dict[str, int]): data["storage"] = str(account.storage) data["balance"] = min_price_dict.get(address, 0) accounts[hex(address)] = data - return accounts + return {"accounts": accounts} def _get_concrete_transaction(model: z3.Model, transaction: BaseTransaction): From b1405f41d6c558dfac81f1b7945ff05f0ab3e995 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Thu, 11 Jul 2019 19:22:13 +0200 Subject: [PATCH 019/164] Start pruning from second iteration --- .../laser/ethereum/plugins/implementations/dependency_pruner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py b/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py index 60e19c44..bb78fdd8 100644 --- a/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py +++ b/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py @@ -301,7 +301,7 @@ class DependencyPruner(LaserPlugin): """ # Don't skip any blocks in the contract creation transaction - if self.iteration < 1: + if self.iteration < 2: return # Don't skip newly discovered blocks From 8def32cb380b76d589c1e78e0510a3937315b19d Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Thu, 11 Jul 2019 21:21:13 +0200 Subject: [PATCH 020/164] Update __version__.py --- mythril/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mythril/__version__.py b/mythril/__version__.py index e9c67101..069a8c91 100644 --- a/mythril/__version__.py +++ b/mythril/__version__.py @@ -4,4 +4,4 @@ This file is suitable for sourcing inside POSIX shell, e.g. bash as well as for importing into Python. """ -__version__ = "v0.21.8" +__version__ = "v0.21.9" From 5216c01a612adaf5c2fcb8311ea67eec74178fee Mon Sep 17 00:00:00 2001 From: e-ngo Date: Thu, 11 Jul 2019 22:30:55 -0700 Subject: [PATCH 021/164] Added messages to help solidity version mismatches --- mythril/interfaces/cli.py | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/mythril/interfaces/cli.py b/mythril/interfaces/cli.py index fc665b8a..b9af3349 100644 --- a/mythril/interfaces/cli.py +++ b/mythril/interfaces/cli.py @@ -12,6 +12,7 @@ import os import sys import coloredlogs +import re import traceback import mythril.support.signatures as sigs @@ -622,6 +623,30 @@ def contract_hash_to_address(args: Namespace): sys.exit() +def is_solv_mismatch_error(error_message: str): + """ + checks an error message to determine if related to solidity version mismatch + :param error_message: + :return: Boolean + """ + + return True if "Error: Source file requires different compiler version" in error_message else False + + +def extract_compatible_solv(error_message: str): + """ + extracts the desired solidity version from the CompilerError message + :param error_message: + :return: String, (ie. 0.5.10) + """ + + # this corresponds to the line "pragma solidity ..." + solv_definition = error_message.split("\n")[-3] + # get the version number + solv_match = re.search(r'[0-9]+\.[0-9]+\.[0-9]+', solv_definition) + return "" if solv_match is None else solv_match[0] + + def parse_args_and_execute(parser: ArgumentParser, args: Namespace) -> None: """ Parses the arguments @@ -681,7 +706,14 @@ def parse_args_and_execute(parser: ArgumentParser, args: Namespace) -> None: disassembler=disassembler, address=address, parser=parser, args=args ) except CriticalError as ce: - exit_with_error(args.__dict__.get("outform", "text"), str(ce)) + # Extract error message + error_msg = str(ce) + # Check if error is related to solv mismatch + if is_solv_mismatch_error(error_msg): + # Inform users of potential fix + error_msg = error_msg + "\nSolidityVersionMismatch: Try adding the option \"--solv " + extract_compatible_solv(error_msg) + "\"\n" + + exit_with_error(args.__dict__.get("outform", "text"), error_msg) except Exception: exit_with_error(args.__dict__.get("outform", "text"), traceback.format_exc()) From e88b9e184219f4db6d2302b4ca4137c7bc41eb61 Mon Sep 17 00:00:00 2001 From: e-ngo Date: Fri, 12 Jul 2019 20:54:39 -0700 Subject: [PATCH 022/164] Moved solidity version mismatch error checking from cli to mythril_disassembler --- mythril/interfaces/cli.py | 34 +------------------------ mythril/mythril/mythril_disassembler.py | 25 +++++++++++++++++- 2 files changed, 25 insertions(+), 34 deletions(-) diff --git a/mythril/interfaces/cli.py b/mythril/interfaces/cli.py index b9af3349..fc665b8a 100644 --- a/mythril/interfaces/cli.py +++ b/mythril/interfaces/cli.py @@ -12,7 +12,6 @@ import os import sys import coloredlogs -import re import traceback import mythril.support.signatures as sigs @@ -623,30 +622,6 @@ def contract_hash_to_address(args: Namespace): sys.exit() -def is_solv_mismatch_error(error_message: str): - """ - checks an error message to determine if related to solidity version mismatch - :param error_message: - :return: Boolean - """ - - return True if "Error: Source file requires different compiler version" in error_message else False - - -def extract_compatible_solv(error_message: str): - """ - extracts the desired solidity version from the CompilerError message - :param error_message: - :return: String, (ie. 0.5.10) - """ - - # this corresponds to the line "pragma solidity ..." - solv_definition = error_message.split("\n")[-3] - # get the version number - solv_match = re.search(r'[0-9]+\.[0-9]+\.[0-9]+', solv_definition) - return "" if solv_match is None else solv_match[0] - - def parse_args_and_execute(parser: ArgumentParser, args: Namespace) -> None: """ Parses the arguments @@ -706,14 +681,7 @@ def parse_args_and_execute(parser: ArgumentParser, args: Namespace) -> None: disassembler=disassembler, address=address, parser=parser, args=args ) except CriticalError as ce: - # Extract error message - error_msg = str(ce) - # Check if error is related to solv mismatch - if is_solv_mismatch_error(error_msg): - # Inform users of potential fix - error_msg = error_msg + "\nSolidityVersionMismatch: Try adding the option \"--solv " + extract_compatible_solv(error_msg) + "\"\n" - - exit_with_error(args.__dict__.get("outform", "text"), error_msg) + exit_with_error(args.__dict__.get("outform", "text"), str(ce)) except Exception: exit_with_error(args.__dict__.get("outform", "text"), traceback.format_exc()) diff --git a/mythril/mythril/mythril_disassembler.py b/mythril/mythril/mythril_disassembler.py index 5e1e72c3..7c1a90c0 100644 --- a/mythril/mythril/mythril_disassembler.py +++ b/mythril/mythril/mythril_disassembler.py @@ -186,7 +186,30 @@ class MythrilDisassembler: except FileNotFoundError: raise CriticalError("Input file not found: " + file) except CompilerError as e: - raise CriticalError(e) + # Extract error message as string + error_msg = str(e) + # Check if error is related to solidity version mismatch + if ( + "Error: Source file requires different compiler version" + in error_msg + ): + # Grab relevant line "pragma solidity ...", excluding any comments + solv_pragma_line = error_msg.split("\n")[-3].split("//")[0] + # Grab solidity version from relevant line + solv_match = re.findall(r"[0-9]+\.[0-9]+\.[0-9]+", solv_pragma_line) + # Check if single match exists, else offer generic suggestion + error_suggestion = ( + "" if len(solv_match) != 1 else solv_match[0] + ) + # Give helpful tip + error_msg = ( + error_msg + + '\nSolidityVersionMismatch: Try adding the option "--solv ' + + error_suggestion + + '"\n' + ) + + raise CriticalError(error_msg) except NoContractFoundError: log.error( "The file " + file + " does not contain a compilable contract." From 7d9194a06cd0882e68d7f561e1936a90949de1a7 Mon Sep 17 00:00:00 2001 From: Nikhil Date: Sat, 13 Jul 2019 19:10:44 +0530 Subject: [PATCH 023/164] Pad while comparing unequal functions --- mythril/laser/smt/bitvec.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/mythril/laser/smt/bitvec.py b/mythril/laser/smt/bitvec.py index 1b8080f8..ff84cfb5 100644 --- a/mythril/laser/smt/bitvec.py +++ b/mythril/laser/smt/bitvec.py @@ -10,6 +10,16 @@ from mythril.laser.smt.expression import Expression Annotations = Set[Any] # fmt: off +import operator + + +def _padded_operation(a: z3.BitVec, b: z3.BitVec, operator): + if a.size() == b.size(): + return operator(a, b) + if a.size() < b.size(): + a, b = b, a + b = z3.Concat(z3.BitVecVal(0, a.size() - b.size()), b) + return operator(a, b) class BitVec(Expression[z3.BitVecRef]): @@ -203,10 +213,9 @@ class BitVec(Expression[z3.BitVecRef]): union = self.annotations.union(other.annotations) # Some of the BitVecs can be 512 bit due to sha3() - if self.raw.size() != other.raw.size(): - return Bool(z3.BoolVal(False), annotations=union) + eq_check = _padded_operation(self.raw, other.raw, operator.eq) # MYPY: fix complaints due to z3 overriding __eq__ - return Bool(cast(z3.BoolRef, self.raw == other.raw), annotations=union) + return Bool(cast(z3.BoolRef, eq_check), annotations=union) # MYPY: fix complains about overriding __ne__ def __ne__(self, other: Union[int, "BitVec"]) -> Bool: # type: ignore @@ -224,10 +233,9 @@ class BitVec(Expression[z3.BitVecRef]): union = self.annotations.union(other.annotations) # Some of the BitVecs can be 512 bit due to sha3() - if self.raw.size() != other.raw.size(): - return Bool(z3.BoolVal(True), annotations=union) + neq_check = _padded_operation(self.raw, other.raw, operator.ne) # MYPY: fix complaints due to z3 overriding __eq__ - return Bool(cast(z3.BoolRef, self.raw != other.raw), annotations=union) + return Bool(cast(z3.BoolRef, neq_check), annotations=union) def _handle_shift(self, other: Union[int, "BitVec"], operator: Callable) -> "BitVec": """ From 6322730701128353efdae7d667f9a2fc9584291c Mon Sep 17 00:00:00 2001 From: Nikhil Date: Sat, 13 Jul 2019 21:26:58 +0530 Subject: [PATCH 024/164] Fix the usage of operators --- mythril/laser/smt/bitvec.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/mythril/laser/smt/bitvec.py b/mythril/laser/smt/bitvec.py index ff84cfb5..df537582 100644 --- a/mythril/laser/smt/bitvec.py +++ b/mythril/laser/smt/bitvec.py @@ -1,7 +1,7 @@ """This module provides classes for an SMT abstraction of bit vectors.""" from typing import Union, overload, List, Set, cast, Any, Optional, Callable -from operator import lshift, rshift +from operator import lshift, rshift, ne, eq import z3 from mythril.laser.smt.bool import Bool, And, Or @@ -10,7 +10,6 @@ from mythril.laser.smt.expression import Expression Annotations = Set[Any] # fmt: off -import operator def _padded_operation(a: z3.BitVec, b: z3.BitVec, operator): @@ -213,7 +212,7 @@ class BitVec(Expression[z3.BitVecRef]): union = self.annotations.union(other.annotations) # Some of the BitVecs can be 512 bit due to sha3() - eq_check = _padded_operation(self.raw, other.raw, operator.eq) + eq_check = _padded_operation(self.raw, other.raw, eq) # MYPY: fix complaints due to z3 overriding __eq__ return Bool(cast(z3.BoolRef, eq_check), annotations=union) @@ -233,7 +232,7 @@ class BitVec(Expression[z3.BitVecRef]): union = self.annotations.union(other.annotations) # Some of the BitVecs can be 512 bit due to sha3() - neq_check = _padded_operation(self.raw, other.raw, operator.ne) + neq_check = _padded_operation(self.raw, other.raw, ne) # MYPY: fix complaints due to z3 overriding __eq__ return Bool(cast(z3.BoolRef, neq_check), annotations=union) From 062e9a3bbabecaf1541a195235d19fc0081c56e7 Mon Sep 17 00:00:00 2001 From: Nikhil Date: Sat, 13 Jul 2019 21:27:20 +0530 Subject: [PATCH 025/164] Fix type hints --- mythril/laser/smt/__init__.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/mythril/laser/smt/__init__.py b/mythril/laser/smt/__init__.py index 407f6565..b3d6de59 100644 --- a/mythril/laser/smt/__init__.py +++ b/mythril/laser/smt/__init__.py @@ -21,13 +21,13 @@ from mythril.laser.smt.bool import Bool, is_true, is_false, Or, Not, And from mythril.laser.smt.array import K, Array, BaseArray from mythril.laser.smt.solver import Solver, Optimize, SolverStatistics from mythril.laser.smt.model import Model - +import mythril.laser.smt.bool as smtbool from typing import Union, Any, Optional, Set, TypeVar, Generic import z3 Annotations = Optional[Set[Any]] -T = TypeVar("T", bound=Union[bool.Bool, z3.BoolRef]) +T = TypeVar("T", bound=Union[smtbool.Bool, z3.BoolRef]) U = TypeVar("U", bound=Union[BitVec, z3.BitVecRef]) @@ -105,14 +105,16 @@ class SymbolFactory(Generic[T, U]): raise NotImplementedError() -class _SmtSymbolFactory(SymbolFactory[bool.Bool, BitVec]): +class _SmtSymbolFactory(SymbolFactory[smtbool.Bool, BitVec]): """ An implementation of a SymbolFactory that creates symbols using the classes in: mythril.laser.smt """ @staticmethod - def Bool(value: "__builtins__.bool", annotations: Annotations = None) -> bool.Bool: + def Bool( + value: "__builtins__.bool", annotations: Annotations = None + ) -> smtbool.Bool: """ Creates a Bool with concrete value :param value: The boolean value From cd6246a4dc7d3be4fcd14ebf0edd04d3c52fed8c Mon Sep 17 00:00:00 2001 From: Nikhil Date: Sat, 13 Jul 2019 21:35:14 +0530 Subject: [PATCH 026/164] Fix imports --- mythril/laser/smt/__init__.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/mythril/laser/smt/__init__.py b/mythril/laser/smt/__init__.py index b3d6de59..f441948e 100644 --- a/mythril/laser/smt/__init__.py +++ b/mythril/laser/smt/__init__.py @@ -21,13 +21,13 @@ from mythril.laser.smt.bool import Bool, is_true, is_false, Or, Not, And from mythril.laser.smt.array import K, Array, BaseArray from mythril.laser.smt.solver import Solver, Optimize, SolverStatistics from mythril.laser.smt.model import Model -import mythril.laser.smt.bool as smtbool +from mythril.laser.smt.bool import Bool as SMTBool from typing import Union, Any, Optional, Set, TypeVar, Generic import z3 Annotations = Optional[Set[Any]] -T = TypeVar("T", bound=Union[smtbool.Bool, z3.BoolRef]) +T = TypeVar("T", bound=Union[SMTBool, z3.BoolRef]) U = TypeVar("U", bound=Union[BitVec, z3.BitVecRef]) @@ -105,16 +105,14 @@ class SymbolFactory(Generic[T, U]): raise NotImplementedError() -class _SmtSymbolFactory(SymbolFactory[smtbool.Bool, BitVec]): +class _SmtSymbolFactory(SymbolFactory[SMTBool, BitVec]): """ An implementation of a SymbolFactory that creates symbols using the classes in: mythril.laser.smt """ @staticmethod - def Bool( - value: "__builtins__.bool", annotations: Annotations = None - ) -> smtbool.Bool: + def Bool(value: "__builtins__.bool", annotations: Annotations = None) -> SMTBool: """ Creates a Bool with concrete value :param value: The boolean value @@ -122,7 +120,7 @@ class _SmtSymbolFactory(SymbolFactory[smtbool.Bool, BitVec]): :return: The freshly created Bool() """ raw = z3.BoolVal(value) - return Bool(raw, annotations) + return SMTBool(raw, annotations) @staticmethod def BitVecVal(value: int, size: int, annotations: Annotations = None) -> BitVec: From 9b20f4db8f48baa36a329ed7312b19278cdf3c99 Mon Sep 17 00:00:00 2001 From: e-ngo Date: Sat, 13 Jul 2019 14:34:56 -0700 Subject: [PATCH 027/164] Removed unnecessary comments --- mythril/mythril/mythril_disassembler.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/mythril/mythril/mythril_disassembler.py b/mythril/mythril/mythril_disassembler.py index 7c1a90c0..bfd7e23c 100644 --- a/mythril/mythril/mythril_disassembler.py +++ b/mythril/mythril/mythril_disassembler.py @@ -186,7 +186,6 @@ class MythrilDisassembler: except FileNotFoundError: raise CriticalError("Input file not found: " + file) except CompilerError as e: - # Extract error message as string error_msg = str(e) # Check if error is related to solidity version mismatch if ( @@ -197,11 +196,9 @@ class MythrilDisassembler: solv_pragma_line = error_msg.split("\n")[-3].split("//")[0] # Grab solidity version from relevant line solv_match = re.findall(r"[0-9]+\.[0-9]+\.[0-9]+", solv_pragma_line) - # Check if single match exists, else offer generic suggestion error_suggestion = ( "" if len(solv_match) != 1 else solv_match[0] ) - # Give helpful tip error_msg = ( error_msg + '\nSolidityVersionMismatch: Try adding the option "--solv ' From c9a35c3f60d323f58f467362492d16037a2ddbef Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Sun, 14 Jul 2019 03:41:48 +0200 Subject: [PATCH 028/164] Bump version number --- mythril/__version__.py | 2 +- mythril/laser/smt/bitvecfunc.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/mythril/__version__.py b/mythril/__version__.py index 069a8c91..5984877d 100644 --- a/mythril/__version__.py +++ b/mythril/__version__.py @@ -4,4 +4,4 @@ This file is suitable for sourcing inside POSIX shell, e.g. bash as well as for importing into Python. """ -__version__ = "v0.21.9" +__version__ = "v0.21.10" diff --git a/mythril/laser/smt/bitvecfunc.py b/mythril/laser/smt/bitvecfunc.py index 2b7b9e63..78b20e3e 100644 --- a/mythril/laser/smt/bitvecfunc.py +++ b/mythril/laser/smt/bitvecfunc.py @@ -72,10 +72,15 @@ def _comparison_helper( ): return Bool(z3.BoolVal(default_value), annotations=union) + ''' return And( Bool(cast(z3.BoolRef, operation(a.raw, b.raw)), annotations=union), a.input_ == b.input_ if inputs_equal else a.input_ != b.input_, ) + ''' + + return a.input_ == b.input_ + class BitVecFunc(BitVec): From 8824258c58196339910099edbfe52ed1be854903 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Sun, 14 Jul 2019 09:38:25 +0200 Subject: [PATCH 029/164] Black --- mythril/laser/smt/bitvecfunc.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mythril/laser/smt/bitvecfunc.py b/mythril/laser/smt/bitvecfunc.py index 78b20e3e..973f599f 100644 --- a/mythril/laser/smt/bitvecfunc.py +++ b/mythril/laser/smt/bitvecfunc.py @@ -72,17 +72,16 @@ def _comparison_helper( ): return Bool(z3.BoolVal(default_value), annotations=union) - ''' + """ return And( Bool(cast(z3.BoolRef, operation(a.raw, b.raw)), annotations=union), a.input_ == b.input_ if inputs_equal else a.input_ != b.input_, ) - ''' + """ return a.input_ == b.input_ - class BitVecFunc(BitVec): """A bit vector function symbol. Used in place of functions like sha3.""" From 8bff312bd0e46728ac479f06f49e7807f4dca048 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Sun, 14 Jul 2019 09:39:43 +0200 Subject: [PATCH 030/164] Undo last commit --- mythril/laser/smt/bitvecfunc.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/mythril/laser/smt/bitvecfunc.py b/mythril/laser/smt/bitvecfunc.py index 973f599f..6a96d2f1 100644 --- a/mythril/laser/smt/bitvecfunc.py +++ b/mythril/laser/smt/bitvecfunc.py @@ -72,12 +72,10 @@ def _comparison_helper( ): return Bool(z3.BoolVal(default_value), annotations=union) - """ return And( Bool(cast(z3.BoolRef, operation(a.raw, b.raw)), annotations=union), a.input_ == b.input_ if inputs_equal else a.input_ != b.input_, ) - """ return a.input_ == b.input_ From f5f0ed0e759654cb4d4f1ef6119356872570b6fd Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Mon, 15 Jul 2019 00:02:40 +0200 Subject: [PATCH 031/164] Add function names to report output --- mythril/analysis/report.py | 22 +++++++++++++++++++ .../templates/report_as_markdown.jinja2 | 2 +- .../analysis/templates/report_as_text.jinja2 | 2 +- mythril/disassembler/disassembly.py | 6 ++--- 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/mythril/analysis/report.py b/mythril/analysis/report.py index 7491b614..7411d6fa 100644 --- a/mythril/analysis/report.py +++ b/mythril/analysis/report.py @@ -11,6 +11,7 @@ from mythril.analysis.swc_data import SWC_TO_TITLE from mythril.support.source_support import Source from mythril.support.start_time import StartTime from mythril.support.support_utils import get_code_hash +from mythril.support.signatures import SignatureDB from time import time log = logging.getLogger(__name__) @@ -151,6 +152,26 @@ class Issue: else: self.source_mapping = self.address + def resolve_function_names(self): + """ Resolves function names for each step """ + + if ( + self.transaction_sequence is None + or "steps" not in self.transaction_sequence + ): + return + + signatures = SignatureDB() + + for step in self.transaction_sequence["steps"]: + _hash = step["input"][:10] + sig = signatures.get(_hash) + + if len(sig) > 0: + step["name"] = sig[0] + else: + step["name"] = "unknown" + class Report: """A report containing the content of multiple issues.""" @@ -187,6 +208,7 @@ class Report: """ m = hashlib.md5() m.update((issue.contract + str(issue.address) + issue.title).encode("utf-8")) + issue.resolve_function_names() self.issues[m.digest()] = issue def as_text(self): diff --git a/mythril/analysis/templates/report_as_markdown.jinja2 b/mythril/analysis/templates/report_as_markdown.jinja2 index 4fd73f84..4583eb26 100644 --- a/mythril/analysis/templates/report_as_markdown.jinja2 +++ b/mythril/analysis/templates/report_as_markdown.jinja2 @@ -32,7 +32,7 @@ In file: {{ issue.filename }}:{{ issue.lineno }} {% if step == issue.tx_sequence.steps[0] and step.input != "0x" and step.origin == "0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe" %} Caller: [CREATOR], data: [CONTRACT CREATION], value: {{ step.value }} {% else %} -Caller: {% if step.origin == "0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe" %}[CREATOR]{% elif step.origin == "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" %}[ATTACKER]{% else %}[SOMEGUY]{% endif %}, data: {{ step.input }}, value: {{ step.value }} +Caller: {% if step.origin == "0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe" %}[CREATOR]{% elif step.origin == "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" %}[ATTACKER]{% else %}[SOMEGUY]{% endif %}, function: {{ step.name }}, txdata: {{ step.input }}, value: {{ step.value }} {% endif %} {% endfor %} {% endif %} diff --git a/mythril/analysis/templates/report_as_text.jinja2 b/mythril/analysis/templates/report_as_text.jinja2 index dd70bb45..d7b84355 100644 --- a/mythril/analysis/templates/report_as_text.jinja2 +++ b/mythril/analysis/templates/report_as_text.jinja2 @@ -25,7 +25,7 @@ Transaction Sequence: {% if step == issue.tx_sequence.steps[0] and step.input != "0x" and step.origin == "0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe" %} Caller: [CREATOR], data: [CONTRACT CREATION], value: {{ step.value }} {% else %} -Caller: {% if step.origin == "0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe" %}[CREATOR]{% elif step.origin == "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" %}[ATTACKER]{% else %}[SOMEGUY]{% endif %}, data: {{ step.input }}, value: {{ step.value }} +Caller: {% if step.origin == "0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe" %}[CREATOR]{% elif step.origin == "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" %}[ATTACKER]{% else %}[SOMEGUY]{% endif %}, function: {{ step.name }}, txdata: {{ step.input }}, value: {{ step.value }} {% endif %} {% endfor %} {% endif %} diff --git a/mythril/disassembler/disassembly.py b/mythril/disassembler/disassembly.py index 595b2583..d51927a1 100644 --- a/mythril/disassembler/disassembly.py +++ b/mythril/disassembler/disassembly.py @@ -84,10 +84,8 @@ def get_function_info( # Append with missing 0s at the beginning function_hash = "0x" + instruction_list[index]["argument"][2:].rjust(8, "0") function_names = signature_database.get(function_hash) - if len(function_names) > 1: - # In this case there was an ambiguous result - function_name = "[{}] (ambiguous)".format(", ".join(function_names)) - elif len(function_names) == 1: + + if len(function_names) > 0: function_name = function_names[0] else: function_name = "_function_" + function_hash From f556ae66a202b37f3f39f119ff3ef1bb588bdfa8 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Mon, 15 Jul 2019 10:07:15 +0200 Subject: [PATCH 032/164] Catch exception caused by invalid signature --- mythril/analysis/report.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/mythril/analysis/report.py b/mythril/analysis/report.py index 7411d6fa..1a39c0ff 100644 --- a/mythril/analysis/report.py +++ b/mythril/analysis/report.py @@ -165,11 +165,15 @@ class Issue: for step in self.transaction_sequence["steps"]: _hash = step["input"][:10] - sig = signatures.get(_hash) - if len(sig) > 0: - step["name"] = sig[0] - else: + try: + sig = signatures.get(_hash) + + if len(sig) > 0: + step["name"] = sig[0] + else: + step["name"] = "unknown" + except ValueError: step["name"] = "unknown" From a09cb37c13aba97b2bf48193226d9ceba499f81c Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Mon, 15 Jul 2019 14:51:49 +0200 Subject: [PATCH 033/164] Format account balance as hex string --- mythril/analysis/solver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mythril/analysis/solver.py b/mythril/analysis/solver.py index 104dbb4c..41e2b0d5 100644 --- a/mythril/analysis/solver.py +++ b/mythril/analysis/solver.py @@ -133,7 +133,7 @@ def _get_concrete_state(initial_accounts: Dict, min_price_dict: Dict[str, int]): data["nonce"] = account.nonce data["code"] = account.code.bytecode data["storage"] = str(account.storage) - data["balance"] = min_price_dict.get(address, 0) + data["balance"] = hex(min_price_dict.get(address, 0)) accounts[hex(address)] = data return {"accounts": accounts} From b8c82af3b98e30ca9447d610a6a6b5859c1c90de Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Mon, 15 Jul 2019 16:50:53 +0200 Subject: [PATCH 034/164] Bump version to 0.21.11 --- mythril/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mythril/__version__.py b/mythril/__version__.py index 5984877d..8b926541 100644 --- a/mythril/__version__.py +++ b/mythril/__version__.py @@ -4,4 +4,4 @@ This file is suitable for sourcing inside POSIX shell, e.g. bash as well as for importing into Python. """ -__version__ = "v0.21.10" +__version__ = "v0.21.11" From 216b999884d195a96683a830ba59b3a0302e859c Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Mon, 15 Jul 2019 22:45:48 +0200 Subject: [PATCH 035/164] Un-stringify storage field --- mythril/analysis/solver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mythril/analysis/solver.py b/mythril/analysis/solver.py index 41e2b0d5..968215bc 100644 --- a/mythril/analysis/solver.py +++ b/mythril/analysis/solver.py @@ -132,7 +132,7 @@ def _get_concrete_state(initial_accounts: Dict, min_price_dict: Dict[str, int]): data = dict() # type: Dict[str, Union[int, str]] data["nonce"] = account.nonce data["code"] = account.code.bytecode - data["storage"] = str(account.storage) + data["storage"] = account.storage.printable_storage data["balance"] = hex(min_price_dict.get(address, 0)) accounts[hex(address)] = data return {"accounts": accounts} From 69b1d256bfa305b08bd8accc99f8f2f70686cc6d Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Tue, 16 Jul 2019 08:17:52 +0200 Subject: [PATCH 036/164] Bump version number --- mythril/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mythril/__version__.py b/mythril/__version__.py index 8b926541..3490630e 100644 --- a/mythril/__version__.py +++ b/mythril/__version__.py @@ -4,4 +4,4 @@ This file is suitable for sourcing inside POSIX shell, e.g. bash as well as for importing into Python. """ -__version__ = "v0.21.11" +__version__ = "v0.21.12" From 3fd35627067a4e0a960b57973862fc7b18f8e866 Mon Sep 17 00:00:00 2001 From: Nikhil Date: Tue, 16 Jul 2019 15:02:47 +0530 Subject: [PATCH 037/164] Set solver timeout and loop bound for analysis modules --- mythril/analysis/analysis_args.py | 28 ++++++++++++++++++++++ mythril/analysis/modules/deprecated_ops.py | 5 ++-- mythril/analysis/modules/dos.py | 3 ++- mythril/analysis/solver.py | 3 ++- mythril/analysis/symbolic.py | 2 +- mythril/interfaces/cli.py | 7 ++++++ mythril/interfaces/old_cli.py | 7 ++++++ mythril/mythril/mythril_analyzer.py | 5 ++++ 8 files changed, 55 insertions(+), 5 deletions(-) create mode 100644 mythril/analysis/analysis_args.py diff --git a/mythril/analysis/analysis_args.py b/mythril/analysis/analysis_args.py new file mode 100644 index 00000000..5df241ec --- /dev/null +++ b/mythril/analysis/analysis_args.py @@ -0,0 +1,28 @@ +from mythril.support.support_utils import Singleton + + +class AnalysisArgs(object, metaclass=Singleton): + """ + This module helps in preventing args being sent through multiple of classes to reach analysis modules + """ + + def __init__(self): + self._loop_bound = 4 + self._solver_timeout = 100000 + + def set_loop_bound(self, loop_bound: int): + self._loop_bound = loop_bound + + def set_solver_timeout(self, solver_timeout: int): + self._solver_timeout = solver_timeout + + @property + def loop_bound(self): + return self._loop_bound + + @property + def solver_timeout(self): + return self._solver_timeout + + +analysis_args = AnalysisArgs() diff --git a/mythril/analysis/modules/deprecated_ops.py b/mythril/analysis/modules/deprecated_ops.py index a0b69f98..7e495b5b 100644 --- a/mythril/analysis/modules/deprecated_ops.py +++ b/mythril/analysis/modules/deprecated_ops.py @@ -35,6 +35,7 @@ class DeprecatedOperationsModule(DetectionModule): 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) @@ -74,13 +75,13 @@ class DeprecatedOperationsModule(DetectionModule): ) swc_id = DEPRECATED_FUNCTIONS_USAGE else: - return + return [] try: transaction_sequence = get_transaction_sequence( state, state.mstate.constraints ) except UnsatError: - return + return [] issue = Issue( contract=state.environment.active_account.contract_name, function_name=state.environment.active_function_name, diff --git a/mythril/analysis/modules/dos.py b/mythril/analysis/modules/dos.py index 20426727..2ee08abe 100644 --- a/mythril/analysis/modules/dos.py +++ b/mythril/analysis/modules/dos.py @@ -7,6 +7,7 @@ from mythril.analysis.swc_data import DOS_WITH_BLOCK_GAS_LIMIT from mythril.analysis.report import Issue from mythril.analysis.modules.base import DetectionModule from mythril.analysis.solver import get_transaction_sequence, UnsatError +from mythril.analysis.analysis_args import analysis_args from mythril.laser.ethereum.state.global_state import GlobalState from mythril.laser.ethereum.state.annotation import StateAnnotation from mythril.laser.ethereum import util @@ -90,7 +91,7 @@ class DosModule(DetectionModule): else: annotation.jump_targets[target] = 1 - if annotation.jump_targets[target] > 2: + if annotation.jump_targets[target] > min(2, analysis_args.loop_bound - 1): annotation.loop_start = address elif annotation.loop_start is not None: diff --git a/mythril/analysis/solver.py b/mythril/analysis/solver.py index 968215bc..6a14fb68 100644 --- a/mythril/analysis/solver.py +++ b/mythril/analysis/solver.py @@ -4,6 +4,7 @@ from typing import Dict, Tuple, Union from z3 import sat, unknown, FuncInterp import z3 +from mythril.analysis.analysis_args import analysis_args from mythril.laser.ethereum.state.global_state import GlobalState from mythril.laser.ethereum.state.constraints import Constraints from mythril.laser.ethereum.transaction import BaseTransaction @@ -29,7 +30,7 @@ def get_model(constraints, minimize=(), maximize=(), enforce_execution_time=True :return: """ s = Optimize() - timeout = 100000 + timeout = analysis_args.solver_timeout if enforce_execution_time: timeout = min(timeout, time_handler.time_remaining() - 500) if timeout <= 0: diff --git a/mythril/analysis/symbolic.py b/mythril/analysis/symbolic.py index e6c2bf24..2c4d5a4d 100644 --- a/mythril/analysis/symbolic.py +++ b/mythril/analysis/symbolic.py @@ -47,7 +47,7 @@ class SymExecWrapper: dynloader=None, max_depth=22, execution_timeout=None, - loop_bound=2, + loop_bound=4, create_timeout=None, transaction_count=2, modules=(), diff --git a/mythril/interfaces/cli.py b/mythril/interfaces/cli.py index fc665b8a..c4643c2d 100644 --- a/mythril/interfaces/cli.py +++ b/mythril/interfaces/cli.py @@ -346,6 +346,12 @@ def create_analyzer_parser(analyzer_parser: ArgumentParser): default=86400, help="The amount of seconds to spend on symbolic execution", ) + options.add_argument( + "--solver-timeout", + type=int, + default=100000, + help="The maximum amount of time(in milli seconds) the solver spends for queries from analysis modules", + ) options.add_argument( "--create-timeout", type=int, @@ -552,6 +558,7 @@ def execute_command( enable_iprof=args.enable_iprof, disable_dependency_pruning=args.disable_dependency_pruning, onchain_storage_access=not args.no_onchain_storage_access, + solver_timeout=args.solver_timeout, ) if not disassembler.contracts: diff --git a/mythril/interfaces/old_cli.py b/mythril/interfaces/old_cli.py index 0157c2b7..6ca1b2c4 100644 --- a/mythril/interfaces/old_cli.py +++ b/mythril/interfaces/old_cli.py @@ -213,6 +213,12 @@ def create_parser(parser: argparse.ArgumentParser) -> None: default=2, help="Maximum number of transactions issued by laser", ) + options.add_argument( + "--solver-timeout", + type=int, + default=100000, + help="The maximum amount of time(in milli seconds) the solver spends for queries from analysis modules", + ) options.add_argument( "--execution-timeout", type=int, @@ -419,6 +425,7 @@ def execute_command( enable_iprof=args.enable_iprof, disable_dependency_pruning=args.disable_dependency_pruning, onchain_storage_access=not args.no_onchain_storage_access, + solver_timeout=args.solver_timeout, ) if args.disassemble: diff --git a/mythril/mythril/mythril_analyzer.py b/mythril/mythril/mythril_analyzer.py index 3bef8203..497c4c33 100644 --- a/mythril/mythril/mythril_analyzer.py +++ b/mythril/mythril/mythril_analyzer.py @@ -10,6 +10,7 @@ from mythril.support.source_support import Source from mythril.support.loader import DynLoader from mythril.analysis.symbolic import SymExecWrapper from mythril.analysis.callgraph import generate_graph +from mythril.analysis.analysis_args import analysis_args from mythril.analysis.traceexplore import get_serializable_statespace from mythril.analysis.security import fire_lasers, retrieve_callback_issues from mythril.analysis.report import Report, Issue @@ -39,6 +40,7 @@ class MythrilAnalyzer: create_timeout: Optional[int] = None, enable_iprof: bool = False, disable_dependency_pruning: bool = False, + solver_timeout: Optional[int] = None, ): """ @@ -60,6 +62,9 @@ class MythrilAnalyzer: self.enable_iprof = enable_iprof self.disable_dependency_pruning = disable_dependency_pruning + analysis_args.set_loop_bound(loop_bound) + analysis_args.set_solver_timeout(solver_timeout) + def dump_statespace(self, contract: EVMContract = None) -> str: """ Returns serializable statespace of the contract From e5b6ed81b5d70e574f3a8ffcd699b9825d77b0a7 Mon Sep 17 00:00:00 2001 From: Nikhil Date: Tue, 16 Jul 2019 15:16:58 +0530 Subject: [PATCH 038/164] Set only for non None values --- mythril/analysis/analysis_args.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mythril/analysis/analysis_args.py b/mythril/analysis/analysis_args.py index 5df241ec..bb7e9330 100644 --- a/mythril/analysis/analysis_args.py +++ b/mythril/analysis/analysis_args.py @@ -11,10 +11,12 @@ class AnalysisArgs(object, metaclass=Singleton): self._solver_timeout = 100000 def set_loop_bound(self, loop_bound: int): - self._loop_bound = loop_bound + if loop_bound is not None: + self._loop_bound = loop_bound def set_solver_timeout(self, solver_timeout: int): - self._solver_timeout = solver_timeout + if solver_timeout is not None: + self._solver_timeout = solver_timeout @property def loop_bound(self): From 005ee6150b87838fb75764a6ddb478b5844ef2c7 Mon Sep 17 00:00:00 2001 From: palkeo Date: Tue, 16 Jul 2019 15:41:49 +0200 Subject: [PATCH 039/164] Load contracts from the blockchain by default. Renames --no-on-chain-storage-access to --no-onchain-access. This control both storage and contract code loading. --- mythril/interfaces/cli.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mythril/interfaces/cli.py b/mythril/interfaces/cli.py index fc665b8a..ac7bf450 100644 --- a/mythril/interfaces/cli.py +++ b/mythril/interfaces/cli.py @@ -350,7 +350,7 @@ def create_analyzer_parser(analyzer_parser: ArgumentParser): "--create-timeout", type=int, default=10, - help="The amount of seconds to spend on " "the initial contract creation", + help="The amount of seconds to spend on the initial contract creation", ) options.add_argument( "-l", @@ -360,8 +360,9 @@ def create_analyzer_parser(analyzer_parser: ArgumentParser): ) options.add_argument( "--no-onchain-storage-access", + "--no-onchain-access", action="store_true", - help="turns off getting the data from onchain contracts", + help="turns off getting the data from onchain contracts (both loading storage and contract code)", ) options.add_argument( @@ -552,6 +553,7 @@ def execute_command( enable_iprof=args.enable_iprof, disable_dependency_pruning=args.disable_dependency_pruning, onchain_storage_access=not args.no_onchain_storage_access, + requires_dynld=not args.no_onchain_storage_access, ) if not disassembler.contracts: From 5d61aae5b0b9eab5f823bb0689cd561237f814e4 Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Tue, 16 Jul 2019 19:29:38 +0530 Subject: [PATCH 040/164] Remove solc version check in tests --- all_tests.sh | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/all_tests.sh b/all_tests.sh index 61cd7d67..7fd0ae81 100755 --- a/all_tests.sh +++ b/all_tests.sh @@ -7,22 +7,6 @@ assert sys.version_info[0:2] >= (3,5), \ """Please make sure you are using Python 3.5 or later. You ran with {}""".format(sys.version)' || exit $? -echo "Checking solc version..." -out=$(solc --version) || { - echo 2>&1 "Please make sure you have solc installed, version 0.4.21 or greater" - - } -case $out in - *Version:\ 0.4.2[1-9]* ) - echo $out - ;; - * ) - echo $out - echo "Please make sure your solc version is at least 0.4.21" - exit 1 - ;; -esac - echo "Checking that truffle is installed..." if ! which truffle ; then echo "Please make sure you have etherum truffle installed (npm install -g truffle)" From 534e8ba71d1a081e32faff6ab912b927cba0cf8c Mon Sep 17 00:00:00 2001 From: Nikhil Date: Wed, 17 Jul 2019 10:47:42 +0530 Subject: [PATCH 041/164] Change defaults --- mythril/analysis/analysis_args.py | 4 ++-- mythril/interfaces/cli.py | 2 +- mythril/interfaces/old_cli.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mythril/analysis/analysis_args.py b/mythril/analysis/analysis_args.py index bb7e9330..fcd5fc36 100644 --- a/mythril/analysis/analysis_args.py +++ b/mythril/analysis/analysis_args.py @@ -7,8 +7,8 @@ class AnalysisArgs(object, metaclass=Singleton): """ def __init__(self): - self._loop_bound = 4 - self._solver_timeout = 100000 + self._loop_bound = 3 + self._solver_timeout = 10000 def set_loop_bound(self, loop_bound: int): if loop_bound is not None: diff --git a/mythril/interfaces/cli.py b/mythril/interfaces/cli.py index c4643c2d..eab502a1 100644 --- a/mythril/interfaces/cli.py +++ b/mythril/interfaces/cli.py @@ -349,7 +349,7 @@ def create_analyzer_parser(analyzer_parser: ArgumentParser): options.add_argument( "--solver-timeout", type=int, - default=100000, + default=10000, help="The maximum amount of time(in milli seconds) the solver spends for queries from analysis modules", ) options.add_argument( diff --git a/mythril/interfaces/old_cli.py b/mythril/interfaces/old_cli.py index 6ca1b2c4..0deedc85 100644 --- a/mythril/interfaces/old_cli.py +++ b/mythril/interfaces/old_cli.py @@ -216,7 +216,7 @@ def create_parser(parser: argparse.ArgumentParser) -> None: options.add_argument( "--solver-timeout", type=int, - default=100000, + default=10000, help="The maximum amount of time(in milli seconds) the solver spends for queries from analysis modules", ) options.add_argument( From dea8d373d1dc8088939257d8f883aad2e106be69 Mon Sep 17 00:00:00 2001 From: Nikhil Date: Wed, 17 Jul 2019 10:49:03 +0530 Subject: [PATCH 042/164] Change loopbound default in symbolic.py --- mythril/analysis/symbolic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mythril/analysis/symbolic.py b/mythril/analysis/symbolic.py index 2c4d5a4d..8528a51f 100644 --- a/mythril/analysis/symbolic.py +++ b/mythril/analysis/symbolic.py @@ -47,7 +47,7 @@ class SymExecWrapper: dynloader=None, max_depth=22, execution_timeout=None, - loop_bound=4, + loop_bound=3, create_timeout=None, transaction_count=2, modules=(), From 79307c4a09b539ccadb49b3fb58922c2de947418 Mon Sep 17 00:00:00 2001 From: palkeo Date: Wed, 17 Jul 2019 09:20:53 +0200 Subject: [PATCH 043/164] Fix bug #1168 by checking eth is not None in the loader. Refactor the loader. (#1169) * Fix bug #1168 by checking eth is not None in the loader. Also refactor the loader quite a bit: - remove the cache dictionary, and use functools.lru_cache instead - also use lru_cache for caching contract code - add type annotations, now mypy would be complaining if we didn't "self.eth" for None. - use log.debug() with %s, it's good pratice and allow advanced logging to group by log messages / parameters if needed. * Fix type annotation. * Exception -> ValueError in loader.py * Remove unused import. --- mythril/support/loader.py | 52 +++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 30 deletions(-) diff --git a/mythril/support/loader.py b/mythril/support/loader.py index f46feff5..2da25d7b 100644 --- a/mythril/support/loader.py +++ b/mythril/support/loader.py @@ -3,6 +3,11 @@ and dependencies.""" from mythril.disassembler.disassembly import Disassembly import logging import re +import functools +from mythril.ethereum.interface.rpc.client import EthJsonRpc +from typing import Optional + +LRU_CACHE_SIZE = 4096 log = logging.getLogger(__name__) @@ -10,7 +15,9 @@ log = logging.getLogger(__name__) class DynLoader: """The dynamic loader class.""" - def __init__(self, eth, contract_loading=True, storage_loading=True): + def __init__( + self, eth: Optional[EthJsonRpc], contract_loading=True, storage_loading=True + ): """ :param eth: @@ -18,11 +25,11 @@ class DynLoader: :param storage_loading: """ self.eth = eth - self.storage_cache = {} self.contract_loading = contract_loading self.storage_loading = storage_loading - def read_storage(self, contract_address: str, index: int): + @functools.lru_cache(LRU_CACHE_SIZE) + def read_storage(self, contract_address: str, index: int) -> str: """ :param contract_address: @@ -30,43 +37,28 @@ class DynLoader: :return: """ if not self.storage_loading: - raise Exception( + raise ValueError( "Cannot load from the storage when the storage_loading flag is false" ) + if not self.eth: + raise ValueError("Cannot load from the storage when eth is None") - try: - contract_ref = self.storage_cache[contract_address] - data = contract_ref[index] - - except KeyError: - - self.storage_cache[contract_address] = {} - - data = self.eth.eth_getStorageAt( - contract_address, position=index, block="latest" - ) - - self.storage_cache[contract_address][index] = data - - except IndexError: - - data = self.eth.eth_getStorageAt( - contract_address, position=index, block="latest" - ) - - self.storage_cache[contract_address][index] = data - - return data + return self.eth.eth_getStorageAt( + contract_address, position=index, block="latest" + ) - def dynld(self, dependency_address): + @functools.lru_cache(LRU_CACHE_SIZE) + def dynld(self, dependency_address: str) -> Optional[Disassembly]: """ :param dependency_address: :return: """ if not self.contract_loading: raise ValueError("Cannot load contract when contract_loading flag is false") + if not self.eth: + raise ValueError("Cannot load from the storage when eth is None") - log.debug("Dynld at contract " + dependency_address) + log.debug("Dynld at contract %s", dependency_address) # Ensure that dependency_address is the correct length, with 0s prepended as needed. dependency_address = ( @@ -81,7 +73,7 @@ class DynLoader: else: return None - log.debug("Dependency address: " + dependency_address) + log.debug("Dependency address: %s", dependency_address) code = self.eth.eth_getCode(dependency_address) From eaebc2361f02bbbb8e5fedec6967e623f5ee2949 Mon Sep 17 00:00:00 2001 From: JoranHonig Date: Wed, 17 Jul 2019 13:59:56 +0200 Subject: [PATCH 044/164] Nested bitvec func (#1163) * add test to reproduce nested function bug * add test for other level of bitvecfunc * add appropriate handling for nested bitvecfuncs * add return character * ignore hybrid functions for condition permeation * add tests to check if correct inputs are found * copy bitvecfunc logic to generic handlers * clean up & move logic to separate module Removes cyclic dependency and redundant code * style * fix imports * do uniq & iff application * implement initial If for bvf * style * add return * only deal with eq comparison * fix type issues * Fix a crash * Fix the issue in statechange module * Fix for mypy * Use correct operator --- mythril/analysis/modules/ether_thief.py | 3 +- .../modules/state_change_external_calls.py | 4 +- mythril/laser/ethereum/state/account.py | 22 +- mythril/laser/smt/__init__.py | 7 +- mythril/laser/smt/array.py | 3 +- mythril/laser/smt/bitvec.py | 276 +---------------- mythril/laser/smt/bitvec_helper.py | 291 ++++++++++++++++++ mythril/laser/smt/bitvecfunc.py | 70 ++++- tests/laser/smt/bitvecfunc_test.py | 165 +++++++++- 9 files changed, 541 insertions(+), 300 deletions(-) create mode 100644 mythril/laser/smt/bitvec_helper.py diff --git a/mythril/analysis/modules/ether_thief.py b/mythril/analysis/modules/ether_thief.py index 76952a54..a6836f7b 100644 --- a/mythril/analysis/modules/ether_thief.py +++ b/mythril/analysis/modules/ether_thief.py @@ -15,8 +15,7 @@ from mythril.analysis.swc_data import UNPROTECTED_ETHER_WITHDRAWAL from mythril.exceptions import UnsatError from mythril.laser.ethereum.transaction import ContractCreationTransaction from mythril.laser.ethereum.state.global_state import GlobalState -from mythril.laser.smt import UGT, Sum, symbol_factory, BVAddNoOverflow -from mythril.laser.smt.bitvec import If +from mythril.laser.smt import UGT, Sum, symbol_factory, BVAddNoOverflow, If log = logging.getLogger(__name__) diff --git a/mythril/analysis/modules/state_change_external_calls.py b/mythril/analysis/modules/state_change_external_calls.py index 9b331dd0..c410b4c0 100644 --- a/mythril/analysis/modules/state_change_external_calls.py +++ b/mythril/analysis/modules/state_change_external_calls.py @@ -171,7 +171,9 @@ class StateChange(DetectionModule): for annotation in annotations: if not annotation.state_change_states: continue - vulnerabilities.append(annotation.get_issue(global_state)) + issue = annotation.get_issue(global_state) + if issue: + vulnerabilities.append(issue) return vulnerabilities @staticmethod diff --git a/mythril/laser/ethereum/state/account.py b/mythril/laser/ethereum/state/account.py index f5196287..1a32ad37 100644 --- a/mythril/laser/ethereum/state/account.py +++ b/mythril/laser/ethereum/state/account.py @@ -20,6 +20,26 @@ from mythril.disassembler.disassembly import Disassembly from mythril.laser.smt import symbol_factory +class StorageRegion: + def __getitem__(self, item): + raise NotImplementedError + + def __setitem__(self, key, value): + raise NotImplementedError + + +class ArrayStorageRegion(StorageRegion): + """ An ArrayStorageRegion is a storage region that leverages smt array theory to resolve expressions""" + + pass + + +class IteStorageRegion(StorageRegion): + """ An IteStorageRegion is a storage region that uses Ite statements to implement a storage""" + + pass + + class Storage: """Storage class represents the storage of an Account.""" @@ -114,7 +134,7 @@ class Storage: key = self._sanitize(key.input_) storage[key] = value - def __deepcopy__(self, memodict={}): + def __deepcopy__(self, memodict=dict()): concrete = isinstance(self._standard_storage, K) storage = Storage( concrete=concrete, address=self.address, dynamic_loader=self.dynld diff --git a/mythril/laser/smt/__init__.py b/mythril/laser/smt/__init__.py index f441948e..6ab752ce 100644 --- a/mythril/laser/smt/__init__.py +++ b/mythril/laser/smt/__init__.py @@ -1,8 +1,10 @@ -from mythril.laser.smt.bitvec import ( - BitVec, +from mythril.laser.smt.bitvec import BitVec + +from mythril.laser.smt.bitvec_helper import ( If, UGT, ULT, + ULE, Concat, Extract, URem, @@ -15,6 +17,7 @@ from mythril.laser.smt.bitvec import ( BVSubNoUnderflow, LShR, ) + from mythril.laser.smt.bitvecfunc import BitVecFunc from mythril.laser.smt.expression import Expression, simplify from mythril.laser.smt.bool import Bool, is_true, is_false, Or, Not, And diff --git a/mythril/laser/smt/array.py b/mythril/laser/smt/array.py index 00107df1..9289b290 100644 --- a/mythril/laser/smt/array.py +++ b/mythril/laser/smt/array.py @@ -8,7 +8,8 @@ default values over a certain range. from typing import cast import z3 -from mythril.laser.smt.bitvec import BitVec, If +from mythril.laser.smt.bitvec import BitVec +from mythril.laser.smt.bitvec_helper import If from mythril.laser.smt.bool import Bool diff --git a/mythril/laser/smt/bitvec.py b/mythril/laser/smt/bitvec.py index df537582..b308e863 100644 --- a/mythril/laser/smt/bitvec.py +++ b/mythril/laser/smt/bitvec.py @@ -1,10 +1,11 @@ """This module provides classes for an SMT abstraction of bit vectors.""" -from typing import Union, overload, List, Set, cast, Any, Optional, Callable from operator import lshift, rshift, ne, eq +from typing import Union, Set, cast, Any, Optional, Callable + import z3 -from mythril.laser.smt.bool import Bool, And, Or +from mythril.laser.smt.bool import Bool from mythril.laser.smt.expression import Expression Annotations = Set[Any] @@ -276,276 +277,5 @@ class BitVec(Expression[z3.BitVecRef]): return self.raw.__hash__() -def _comparison_helper( - a: BitVec, b: BitVec, operation: Callable, default_value: bool, inputs_equal: bool -) -> Bool: - annotations = a.annotations.union(b.annotations) - if isinstance(a, BitVecFunc): - if not a.symbolic and not b.symbolic: - return Bool(operation(a.raw, b.raw), annotations=annotations) - - if ( - not isinstance(b, BitVecFunc) - or not a.func_name - or not a.input_ - or not a.func_name == b.func_name - ): - return Bool(z3.BoolVal(default_value), annotations=annotations) - - return And( - Bool(operation(a.raw, b.raw), annotations=annotations), - a.input_ == b.input_ if inputs_equal else a.input_ != b.input_, - ) - - return Bool(operation(a.raw, b.raw), annotations) - - -def _arithmetic_helper(a: BitVec, b: BitVec, operation: Callable) -> BitVec: - raw = operation(a.raw, b.raw) - union = a.annotations.union(b.annotations) - - if isinstance(a, BitVecFunc) and isinstance(b, BitVecFunc): - return BitVecFunc(raw=raw, func_name=None, input_=None, annotations=union) - elif isinstance(a, BitVecFunc): - return BitVecFunc( - raw=raw, func_name=a.func_name, input_=a.input_, annotations=union - ) - elif isinstance(b, BitVecFunc): - return BitVecFunc( - raw=raw, func_name=b.func_name, input_=b.input_, annotations=union - ) - - return BitVec(raw, annotations=union) - - -def LShR(a: BitVec, b: BitVec): - return _arithmetic_helper(a, b, z3.LShR) - - -def If(a: Union[Bool, bool], b: Union[BitVec, int], c: Union[BitVec, int]) -> BitVec: - """Create an if-then-else expression. - - :param a: - :param b: - :param c: - :return: - """ - # TODO: Handle BitVecFunc - - if not isinstance(a, Bool): - a = Bool(z3.BoolVal(a)) - if not isinstance(b, BitVec): - b = BitVec(z3.BitVecVal(b, 256)) - if not isinstance(c, BitVec): - c = BitVec(z3.BitVecVal(c, 256)) - union = a.annotations.union(b.annotations).union(c.annotations) - return BitVec(z3.If(a.raw, b.raw, c.raw), union) - - -def UGT(a: BitVec, b: BitVec) -> Bool: - """Create an unsigned greater than expression. - - :param a: - :param b: - :return: - """ - return _comparison_helper(a, b, z3.UGT, default_value=False, inputs_equal=False) - - -def UGE(a: BitVec, b: BitVec) -> Bool: - """Create an unsigned greater or equals expression. - - :param a: - :param b: - :return: - """ - return Or(UGT(a, b), a == b) - - -def ULT(a: BitVec, b: BitVec) -> Bool: - """Create an unsigned less than expression. - - :param a: - :param b: - :return: - """ - return _comparison_helper(a, b, z3.ULT, default_value=False, inputs_equal=False) - - -def ULE(a: BitVec, b: BitVec) -> Bool: - """Create an unsigned less than expression. - - :param a: - :param b: - :return: - """ - return Or(ULT(a, b), a == b) - - -@overload -def Concat(*args: List[BitVec]) -> BitVec: ... - - -@overload -def Concat(*args: BitVec) -> BitVec: ... - - -def Concat(*args: Union[BitVec, List[BitVec]]) -> BitVec: - """Create a concatenation expression. - - :param args: - :return: - """ - # The following statement is used if a list is provided as an argument to concat - if len(args) == 1 and isinstance(args[0], list): - bvs = args[0] # type: List[BitVec] - else: - bvs = cast(List[BitVec], args) - - nraw = z3.Concat([a.raw for a in bvs]) - annotations = set() # type: Annotations - bitvecfunc = False - for bv in bvs: - annotations = annotations.union(bv.annotations) - if isinstance(bv, BitVecFunc): - bitvecfunc = True - - if bitvecfunc: - # Added this not so good and misleading NOTATION to help with this - str_hash = ",".join(["hashed({})".format(hash(bv)) for bv in bvs]) - input_string = "MisleadingNotationConcat({})".format(str_hash) - - return BitVecFunc( - raw=nraw, func_name="Hybrid", input_=BitVec(z3.BitVec(input_string, 256), annotations=annotations) - ) - - return BitVec(nraw, annotations) - - -def Extract(high: int, low: int, bv: BitVec) -> BitVec: - """Create an extract expression. - - :param high: - :param low: - :param bv: - :return: - """ - raw = z3.Extract(high, low, bv.raw) - if isinstance(bv, BitVecFunc): - input_string = "MisleadingNotationExtract({}, {}, hashed({}))".format(high, low, hash(bv)) - # Is there a better value to set func_name and input to in this case? - return BitVecFunc( - raw=raw, func_name="Hybrid", input_=BitVec(z3.BitVec(input_string, 256), annotations=bv.annotations) - ) - - return BitVec(raw, annotations=bv.annotations) - - -def URem(a: BitVec, b: BitVec) -> BitVec: - """Create an unsigned remainder expression. - - :param a: - :param b: - :return: - """ - return _arithmetic_helper(a, b, z3.URem) - - -def SRem(a: BitVec, b: BitVec) -> BitVec: - """Create a signed remainder expression. - - :param a: - :param b: - :return: - """ - return _arithmetic_helper(a, b, z3.SRem) - - -def UDiv(a: BitVec, b: BitVec) -> BitVec: - """Create an unsigned division expression. - - :param a: - :param b: - :return: - """ - return _arithmetic_helper(a, b, z3.UDiv) - - -def Sum(*args: BitVec) -> BitVec: - """Create sum expression. - - :return: - """ - raw = z3.Sum([a.raw for a in args]) - annotations = set() # type: Annotations - bitvecfuncs = [] - - for bv in args: - annotations = annotations.union(bv.annotations) - if isinstance(bv, BitVecFunc): - bitvecfuncs.append(bv) - - if len(bitvecfuncs) >= 2: - return BitVecFunc(raw=raw, func_name="Hybrid", input_=None, annotations=annotations) - elif len(bitvecfuncs) == 1: - return BitVecFunc( - raw=raw, - func_name=bitvecfuncs[0].func_name, - input_=bitvecfuncs[0].input_, - annotations=annotations, - ) - - return BitVec(raw, annotations) - - -def BVAddNoOverflow(a: Union[BitVec, int], b: Union[BitVec, int], signed: bool) -> Bool: - """Creates predicate that verifies that the addition doesn't overflow. - - :param a: - :param b: - :param signed: - :return: - """ - if not isinstance(a, BitVec): - a = BitVec(z3.BitVecVal(a, 256)) - if not isinstance(b, BitVec): - b = BitVec(z3.BitVecVal(b, 256)) - return Bool(z3.BVAddNoOverflow(a.raw, b.raw, signed)) - - -def BVMulNoOverflow(a: Union[BitVec, int], b: Union[BitVec, int], signed: bool) -> Bool: - """Creates predicate that verifies that the multiplication doesn't - overflow. - - :param a: - :param b: - :param signed: - :return: - """ - if not isinstance(a, BitVec): - a = BitVec(z3.BitVecVal(a, 256)) - if not isinstance(b, BitVec): - b = BitVec(z3.BitVecVal(b, 256)) - return Bool(z3.BVMulNoOverflow(a.raw, b.raw, signed)) - - -def BVSubNoUnderflow( - a: Union[BitVec, int], b: Union[BitVec, int], signed: bool -) -> Bool: - """Creates predicate that verifies that the subtraction doesn't overflow. - - :param a: - :param b: - :param signed: - :return: - """ - if not isinstance(a, BitVec): - a = BitVec(z3.BitVecVal(a, 256)) - if not isinstance(b, BitVec): - b = BitVec(z3.BitVecVal(b, 256)) - - return Bool(z3.BVSubNoUnderflow(a.raw, b.raw, signed)) - - # TODO: Fix circular import issues from mythril.laser.smt.bitvecfunc import BitVecFunc diff --git a/mythril/laser/smt/bitvec_helper.py b/mythril/laser/smt/bitvec_helper.py new file mode 100644 index 00000000..8e68e0c9 --- /dev/null +++ b/mythril/laser/smt/bitvec_helper.py @@ -0,0 +1,291 @@ +from typing import Union, overload, List, Set, cast, Any, Optional, Callable +from operator import lshift, rshift, ne, eq +import z3 + +from mythril.laser.smt.bool import Bool, And, Or +from mythril.laser.smt.bitvec import BitVec +from mythril.laser.smt.bitvecfunc import BitVecFunc +from mythril.laser.smt.bitvecfunc import _arithmetic_helper as _func_arithmetic_helper +from mythril.laser.smt.bitvecfunc import _comparison_helper as _func_comparison_helper + +Annotations = Set[Any] + + +def _comparison_helper( + a: BitVec, b: BitVec, operation: Callable, default_value: bool, inputs_equal: bool +) -> Bool: + annotations = a.annotations.union(b.annotations) + if isinstance(a, BitVecFunc): + return _func_comparison_helper(a, b, operation, default_value, inputs_equal) + return Bool(operation(a.raw, b.raw), annotations) + + +def _arithmetic_helper(a: BitVec, b: BitVec, operation: Callable) -> BitVec: + raw = operation(a.raw, b.raw) + union = a.annotations.union(b.annotations) + + if isinstance(a, BitVecFunc): + return _func_arithmetic_helper(a, b, operation) + elif isinstance(b, BitVecFunc): + return _func_arithmetic_helper(b, a, operation) + + return BitVec(raw, annotations=union) + + +def LShR(a: BitVec, b: BitVec): + return _arithmetic_helper(a, b, z3.LShR) + + +def If(a: Union[Bool, bool], b: Union[BitVec, int], c: Union[BitVec, int]) -> BitVec: + """Create an if-then-else expression. + + :param a: + :param b: + :param c: + :return: + """ + # TODO: Handle BitVecFunc + + if not isinstance(a, Bool): + a = Bool(z3.BoolVal(a)) + if not isinstance(b, BitVec): + b = BitVec(z3.BitVecVal(b, 256)) + if not isinstance(c, BitVec): + c = BitVec(z3.BitVecVal(c, 256)) + union = a.annotations.union(b.annotations).union(c.annotations) + + bvf = [] # type: List[BitVecFunc] + if isinstance(a, BitVecFunc): + bvf += [a] + if isinstance(b, BitVecFunc): + bvf += [b] + if isinstance(c, BitVecFunc): + bvf += [c] + if bvf: + raw = z3.If(a.raw, b.raw, c.raw) + nested_functions = [nf for func in bvf for nf in func.nested_functions] + bvf + return BitVecFunc(raw, func_name="Hybrid", nested_functions=nested_functions) + + return BitVec(z3.If(a.raw, b.raw, c.raw), union) + + +def UGT(a: BitVec, b: BitVec) -> Bool: + """Create an unsigned greater than expression. + + :param a: + :param b: + :return: + """ + return _comparison_helper(a, b, z3.UGT, default_value=False, inputs_equal=False) + + +def UGE(a: BitVec, b: BitVec) -> Bool: + """Create an unsigned greater or equals expression. + + :param a: + :param b: + :return: + """ + return Or(UGT(a, b), a == b) + + +def ULT(a: BitVec, b: BitVec) -> Bool: + """Create an unsigned less than expression. + + :param a: + :param b: + :return: + """ + return _comparison_helper(a, b, z3.ULT, default_value=False, inputs_equal=False) + + +def ULE(a: BitVec, b: BitVec) -> Bool: + """Create an unsigned less than expression. + + :param a: + :param b: + :return: + """ + return Or(ULT(a, b), a == b) + + +@overload +def Concat(*args: List[BitVec]) -> BitVec: + ... + + +@overload +def Concat(*args: BitVec) -> BitVec: + ... + + +def Concat(*args: Union[BitVec, List[BitVec]]) -> BitVec: + """Create a concatenation expression. + + :param args: + :return: + """ + # The following statement is used if a list is provided as an argument to concat + if len(args) == 1 and isinstance(args[0], list): + bvs = args[0] # type: List[BitVec] + else: + bvs = cast(List[BitVec], args) + + nraw = z3.Concat([a.raw for a in bvs]) + annotations = set() # type: Annotations + + nested_functions = [] # type: List[BitVecFunc] + for bv in bvs: + annotations = annotations.union(bv.annotations) + if isinstance(bv, BitVecFunc): + nested_functions += bv.nested_functions + nested_functions += [bv] + + if nested_functions: + return BitVecFunc( + raw=nraw, + func_name="Hybrid", + input_=BitVec(z3.BitVec("", 256), annotations=annotations), + nested_functions=nested_functions, + ) + + return BitVec(nraw, annotations) + + +def Extract(high: int, low: int, bv: BitVec) -> BitVec: + """Create an extract expression. + + :param high: + :param low: + :param bv: + :return: + """ + raw = z3.Extract(high, low, bv.raw) + if isinstance(bv, BitVecFunc): + input_string = "" + # Is there a better value to set func_name and input to in this case? + return BitVecFunc( + raw=raw, + func_name="Hybrid", + input_=BitVec(z3.BitVec(input_string, 256), annotations=bv.annotations), + nested_functions=bv.nested_functions + [bv], + ) + + return BitVec(raw, annotations=bv.annotations) + + +def URem(a: BitVec, b: BitVec) -> BitVec: + """Create an unsigned remainder expression. + + :param a: + :param b: + :return: + """ + return _arithmetic_helper(a, b, z3.URem) + + +def SRem(a: BitVec, b: BitVec) -> BitVec: + """Create a signed remainder expression. + + :param a: + :param b: + :return: + """ + return _arithmetic_helper(a, b, z3.SRem) + + +def UDiv(a: BitVec, b: BitVec) -> BitVec: + """Create an unsigned division expression. + + :param a: + :param b: + :return: + """ + return _arithmetic_helper(a, b, z3.UDiv) + + +def Sum(*args: BitVec) -> BitVec: + """Create sum expression. + + :return: + """ + raw = z3.Sum([a.raw for a in args]) + annotations = set() # type: Annotations + bitvecfuncs = [] + + for bv in args: + annotations = annotations.union(bv.annotations) + if isinstance(bv, BitVecFunc): + bitvecfuncs.append(bv) + + nested_functions = [ + nf for func in bitvecfuncs for nf in func.nested_functions + ] + bitvecfuncs + + if len(bitvecfuncs) >= 2: + return BitVecFunc( + raw=raw, + func_name="Hybrid", + input_=None, + annotations=annotations, + nested_functions=nested_functions, + ) + elif len(bitvecfuncs) == 1: + return BitVecFunc( + raw=raw, + func_name=bitvecfuncs[0].func_name, + input_=bitvecfuncs[0].input_, + annotations=annotations, + nested_functions=nested_functions, + ) + + return BitVec(raw, annotations) + + +def BVAddNoOverflow(a: Union[BitVec, int], b: Union[BitVec, int], signed: bool) -> Bool: + """Creates predicate that verifies that the addition doesn't overflow. + + :param a: + :param b: + :param signed: + :return: + """ + if not isinstance(a, BitVec): + a = BitVec(z3.BitVecVal(a, 256)) + if not isinstance(b, BitVec): + b = BitVec(z3.BitVecVal(b, 256)) + return Bool(z3.BVAddNoOverflow(a.raw, b.raw, signed)) + + +def BVMulNoOverflow(a: Union[BitVec, int], b: Union[BitVec, int], signed: bool) -> Bool: + """Creates predicate that verifies that the multiplication doesn't + overflow. + + :param a: + :param b: + :param signed: + :return: + """ + if not isinstance(a, BitVec): + a = BitVec(z3.BitVecVal(a, 256)) + if not isinstance(b, BitVec): + b = BitVec(z3.BitVecVal(b, 256)) + return Bool(z3.BVMulNoOverflow(a.raw, b.raw, signed)) + + +def BVSubNoUnderflow( + a: Union[BitVec, int], b: Union[BitVec, int], signed: bool +) -> Bool: + """Creates predicate that verifies that the subtraction doesn't overflow. + + :param a: + :param b: + :param signed: + :return: + """ + if not isinstance(a, BitVec): + a = BitVec(z3.BitVecVal(a, 256)) + if not isinstance(b, BitVec): + b = BitVec(z3.BitVecVal(b, 256)) + + return Bool(z3.BVSubNoUnderflow(a.raw, b.raw, signed)) diff --git a/mythril/laser/smt/bitvecfunc.py b/mythril/laser/smt/bitvecfunc.py index 6a96d2f1..c645b146 100644 --- a/mythril/laser/smt/bitvecfunc.py +++ b/mythril/laser/smt/bitvecfunc.py @@ -1,11 +1,10 @@ -from typing import Optional, Union, cast, Callable - +import operator +from itertools import product +from typing import Optional, Union, cast, Callable, List import z3 -from mythril.laser.smt.bitvec import BitVec, Bool, And, Annotations -from mythril.laser.smt.bool import Or - -import operator +from mythril.laser.smt.bitvec import BitVec, Annotations +from mythril.laser.smt.bool import Or, Bool, And def _arithmetic_helper( @@ -26,18 +25,19 @@ def _arithmetic_helper( union = a.annotations.union(b.annotations) if isinstance(b, BitVecFunc): - # TODO: Find better value to set input and name to in this case? - input_string = "MisleadingNotationop(invhash({}) {} invhash({})".format( - hash(a), operation, hash(b) - ) return BitVecFunc( raw=raw, func_name="Hybrid", - input_=BitVec(z3.BitVec(input_string, 256), annotations=union), + input_=BitVec(z3.BitVec("", 256), annotations=union), + nested_functions=a.nested_functions + b.nested_functions + [a, b], ) return BitVecFunc( - raw=raw, func_name=a.func_name, input_=a.input_, annotations=union + raw=raw, + func_name=a.func_name, + input_=a.input_, + annotations=union, + nested_functions=a.nested_functions + [a], ) @@ -62,23 +62,58 @@ def _comparison_helper( union = a.annotations.union(b.annotations) if not a.symbolic and not b.symbolic: + if operation == z3.UGT: + operation = operator.gt + if operation == z3.ULT: + operation = operator.lt return Bool(z3.BoolVal(operation(a.value, b.value)), annotations=union) - if ( not isinstance(b, BitVecFunc) or not a.func_name or not a.input_ or not a.func_name == b.func_name + or str(operation) not in ("", "") ): return Bool(z3.BoolVal(default_value), annotations=union) + condition = True + for a_nest, b_nest in product(a.nested_functions, b.nested_functions): + if a_nest.func_name != b_nest.func_name: + continue + if a_nest.func_name == "Hybrid": + continue + # a.input (eq/neq) b.input ==> a == b + if inputs_equal: + condition = z3.And( + condition, + z3.Or( + z3.Not((a_nest.input_ == b_nest.input_).raw), + (a_nest.raw == b_nest.raw), + ), + z3.Or( + z3.Not((a_nest.raw == b_nest.raw)), + (a_nest.input_ == b_nest.input_).raw, + ), + ) + else: + condition = z3.And( + condition, + z3.Or( + z3.Not((a_nest.input_ != b_nest.input_).raw), + (a_nest.raw == b_nest.raw), + ), + z3.Or( + z3.Not((a_nest.raw == b_nest.raw)), + (a_nest.input_ != b_nest.input_).raw, + ), + ) + return And( Bool(cast(z3.BoolRef, operation(a.raw, b.raw)), annotations=union), + Bool(condition) if b.nested_functions else Bool(True), a.input_ == b.input_ if inputs_equal else a.input_ != b.input_, ) - return a.input_ == b.input_ - class BitVecFunc(BitVec): """A bit vector function symbol. Used in place of functions like sha3.""" @@ -89,6 +124,7 @@ class BitVecFunc(BitVec): func_name: Optional[str], input_: "BitVec" = None, annotations: Optional[Annotations] = None, + nested_functions: Optional[List["BitVecFunc"]] = None, ): """ @@ -100,6 +136,10 @@ class BitVecFunc(BitVec): self.func_name = func_name self.input_ = input_ + self.nested_functions = nested_functions or [] + self.nested_functions = list(dict.fromkeys(self.nested_functions)) + if isinstance(input_, BitVecFunc): + self.nested_functions.extend(input_.nested_functions) super().__init__(raw, annotations) def __add__(self, other: Union[int, "BitVec"]) -> "BitVecFunc": diff --git a/tests/laser/smt/bitvecfunc_test.py b/tests/laser/smt/bitvecfunc_test.py index ea19dad1..37217c73 100644 --- a/tests/laser/smt/bitvecfunc_test.py +++ b/tests/laser/smt/bitvecfunc_test.py @@ -1,4 +1,4 @@ -from mythril.laser.smt import Solver, symbol_factory, bitvec +from mythril.laser.smt import Solver, symbol_factory, UGT, UGE, ULT, ULE import z3 import pytest @@ -42,10 +42,10 @@ def test_bitvecfunc_arithmetic(operation, expected): (operator.le, z3.sat), (operator.gt, z3.unsat), (operator.ge, z3.sat), - (bitvec.UGT, z3.unsat), - (bitvec.UGE, z3.sat), - (bitvec.ULT, z3.unsat), - (bitvec.ULE, z3.sat), + (UGT, z3.unsat), + (UGE, z3.sat), + (ULT, z3.unsat), + (ULE, z3.sat), ], ) def test_bitvecfunc_bitvecfunc_comparison(operation, expected): @@ -80,3 +80,158 @@ def test_bitvecfunc_bitvecfuncval_comparison(): # Assert assert s.check() == z3.sat assert s.model().eval(input2.raw) == 1337 + + +def test_bitvecfunc_nested_comparison(): + # arrange + s = Solver() + + input1 = symbol_factory.BitVecSym("input1", 256) + input2 = symbol_factory.BitVecSym("input2", 256) + + bvf1 = symbol_factory.BitVecFuncSym("bvf1", "sha3", 256, input_=input1) + bvf2 = symbol_factory.BitVecFuncSym("bvf2", "sha3", 256, input_=bvf1) + + bvf3 = symbol_factory.BitVecFuncSym("bvf3", "sha3", 256, input_=input2) + bvf4 = symbol_factory.BitVecFuncSym("bvf4", "sha3", 256, input_=bvf3) + + # Act + s.add(input1 == input2) + s.add(bvf2 == bvf4) + + # Assert + assert s.check() == z3.sat + + +def test_bitvecfunc_unequal_nested_comparison(): + # arrange + s = Solver() + + input1 = symbol_factory.BitVecSym("input1", 256) + input2 = symbol_factory.BitVecSym("input2", 256) + + bvf1 = symbol_factory.BitVecFuncSym("bvf1", "sha3", 256, input_=input1) + bvf2 = symbol_factory.BitVecFuncSym("bvf2", "sha3", 256, input_=bvf1) + + bvf3 = symbol_factory.BitVecFuncSym("bvf3", "sha3", 256, input_=input2) + bvf4 = symbol_factory.BitVecFuncSym("bvf4", "sha3", 256, input_=bvf3) + + # Act + s.add(input1 != input2) + s.add(bvf2 == bvf4) + + # Assert + assert s.check() == z3.unsat + + +def test_bitvecfunc_ext_nested_comparison(): + # arrange + s = Solver() + + input1 = symbol_factory.BitVecSym("input1", 256) + input2 = symbol_factory.BitVecSym("input2", 256) + input3 = symbol_factory.BitVecSym("input3", 256) + input4 = symbol_factory.BitVecSym("input4", 256) + + bvf1 = symbol_factory.BitVecFuncSym("bvf1", "sha3", 256, input_=input1) + bvf2 = symbol_factory.BitVecFuncSym("bvf2", "sha3", 256, input_=bvf1 + input3) + + bvf3 = symbol_factory.BitVecFuncSym("bvf3", "sha3", 256, input_=input2) + bvf4 = symbol_factory.BitVecFuncSym("bvf4", "sha3", 256, input_=bvf3 + input4) + + # Act + s.add(input1 == input2) + s.add(input3 == input4) + s.add(bvf2 == bvf4) + + # Assert + assert s.check() == z3.sat + + +def test_bitvecfunc_ext_unequal_nested_comparison(): + # Arrange + s = Solver() + + input1 = symbol_factory.BitVecSym("input1", 256) + input2 = symbol_factory.BitVecSym("input2", 256) + input3 = symbol_factory.BitVecSym("input3", 256) + input4 = symbol_factory.BitVecSym("input4", 256) + + bvf1 = symbol_factory.BitVecFuncSym("bvf1", "sha3", 256, input_=input1) + bvf2 = symbol_factory.BitVecFuncSym("bvf2", "sha3", 256, input_=bvf1 + input3) + + bvf3 = symbol_factory.BitVecFuncSym("bvf3", "sha3", 256, input_=input2) + bvf4 = symbol_factory.BitVecFuncSym("bvf4", "sha3", 256, input_=bvf3 + input4) + + # Act + s.add(input1 == input2) + s.add(input3 != input4) + s.add(bvf2 == bvf4) + + # Assert + assert s.check() == z3.unsat + + +def test_bitvecfunc_ext_unequal_nested_comparison_f(): + # Arrange + s = Solver() + + input1 = symbol_factory.BitVecSym("input1", 256) + input2 = symbol_factory.BitVecSym("input2", 256) + input3 = symbol_factory.BitVecSym("input3", 256) + input4 = symbol_factory.BitVecSym("input4", 256) + + bvf1 = symbol_factory.BitVecFuncSym("bvf1", "sha3", 256, input_=input1) + bvf2 = symbol_factory.BitVecFuncSym("bvf2", "sha3", 256, input_=bvf1 + input3) + + bvf3 = symbol_factory.BitVecFuncSym("bvf3", "sha3", 256, input_=input2) + bvf4 = symbol_factory.BitVecFuncSym("bvf4", "sha3", 256, input_=bvf3 + input4) + + # Act + s.add(input1 != input2) + s.add(input3 == input4) + s.add(bvf2 == bvf4) + + # Assert + assert s.check() == z3.unsat + + +def test_bitvecfunc_find_input(): + # Arrange + s = Solver() + + input1 = symbol_factory.BitVecSym("input1", 256) + input2 = symbol_factory.BitVecSym("input2", 256) + + bvf1 = symbol_factory.BitVecFuncSym("bvf1", "sha3", 256, input_=input1) + bvf2 = symbol_factory.BitVecFuncSym("bvf3", "sha3", 256, input_=input2) + + # Act + s.add(input1 == symbol_factory.BitVecVal(1, 256)) + s.add(bvf1 == bvf2) + + # Assert + assert s.check() == z3.sat + assert s.model()[input2.raw] == 1 + + +def test_bitvecfunc_nested_find_input(): + # Arrange + s = Solver() + + input1 = symbol_factory.BitVecSym("input1", 256) + input2 = symbol_factory.BitVecSym("input2", 256) + + bvf1 = symbol_factory.BitVecFuncSym("bvf1", "sha3", 256, input_=input1) + bvf2 = symbol_factory.BitVecFuncSym("bvf2", "sha3", 256, input_=bvf1) + + bvf3 = symbol_factory.BitVecFuncSym("bvf3", "sha3", 256, input_=input2) + bvf4 = symbol_factory.BitVecFuncSym("bvf4", "sha3", 256, input_=bvf3) + + # Act + s.add(input1 == symbol_factory.BitVecVal(123, 256)) + s.add(bvf2 == bvf4) + + # Assert + assert s.check() == z3.sat + assert s.model()[input2.raw] == 123 From 8ee33f48ddb860ff873231cab8640e675d7f3ae8 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Thu, 18 Jul 2019 21:03:47 +0200 Subject: [PATCH 045/164] Update README.md --- README.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/README.md b/README.md index 586d6fd8..6074ac61 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,46 @@ See the [Wiki](https://github.com/ConsenSys/mythril/wiki/Installation-and-Setup) ## Usage +Run: + +``` +$ myth analyze +``` + +Or: + +``` +$ myth analyze -a +``` + +Specify the maximum number of transaction to explore with `-t `. You can also set a timeout with `--execution timeout `. Example ([source code](https://gist.github.com/b-mueller/b7c852f5ccaee91da04a789bd1c5ee4b)): + +``` +$ myth analyze killbilly.sol -t3 --execution-timeout 60 +==== Unprotected Selfdestruct ==== +SWC ID: 106 +Severity: High +Contract: KillBilly +Function name: commencekilling() +PC address: 534 +Estimated Gas Usage: 596 - 1021 +The contract can be killed by anyone. +Anyone can kill this contract and withdraw its balance to an arbitrary address. +-------------------- +In file: killbilly.sol:22 + +selfdestruct(msg.sender) + +-------------------- +Transaction Sequence: + +Caller: [CREATOR], data: [CONTRACT CREATION], value: 0x0 +Caller: [ATTACKER], function: killerize(address), txdata: 0x9fa299cc0101010101010101010101011020200840000080808004014001010101010101, value: 0x0 +Caller: [ATTACKER], function: activatekillability(address), txdata: 0x5aa60cd80101010101010101010101011020200840000080808004014001010101010101, value: 0x0 +Caller: [ATTACKER], function: commencekilling(), txdata: 0x7c11da20, value: 0x0 +``` + + Instructions for using Mythril are found on the [Wiki](https://github.com/ConsenSys/mythril/wiki). For support or general discussions please join the Mythril community on [Discord](https://discord.gg/E3YrVtG). From dd64fd5f5bab1dfdd9f3776c9b2e7bd71e5169ca Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Thu, 18 Jul 2019 21:09:33 +0200 Subject: [PATCH 046/164] Update README.md --- README.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 6074ac61..669b86da 100644 --- a/README.md +++ b/README.md @@ -48,17 +48,16 @@ Or: $ myth analyze -a ``` -Specify the maximum number of transaction to explore with `-t `. You can also set a timeout with `--execution timeout `. Example ([source code](https://gist.github.com/b-mueller/b7c852f5ccaee91da04a789bd1c5ee4b)): +Specify the maximum number of transaction to explore with `-t `. You can also set a timeout with `--execution timeout `. Example ([source code](https://gist.github.com/b-mueller/2b251297ce88aa7628680f50f177a81a#file-killbilly-sol)): ``` -$ myth analyze killbilly.sol -t3 --execution-timeout 60 ==== Unprotected Selfdestruct ==== SWC ID: 106 Severity: High Contract: KillBilly Function name: commencekilling() -PC address: 534 -Estimated Gas Usage: 596 - 1021 +PC address: 354 +Estimated Gas Usage: 574 - 999 The contract can be killed by anyone. Anyone can kill this contract and withdraw its balance to an arbitrary address. -------------------- @@ -70,8 +69,8 @@ selfdestruct(msg.sender) Transaction Sequence: Caller: [CREATOR], data: [CONTRACT CREATION], value: 0x0 -Caller: [ATTACKER], function: killerize(address), txdata: 0x9fa299cc0101010101010101010101011020200840000080808004014001010101010101, value: 0x0 -Caller: [ATTACKER], function: activatekillability(address), txdata: 0x5aa60cd80101010101010101010101011020200840000080808004014001010101010101, value: 0x0 +Caller: [ATTACKER], function: killerize(address), txdata: 0x9fa299ccbebebebebebebebebebebebedeadbeefdeadbeefdeadbeefdeadbeefdeadbeef, value: 0x0 +Caller: [ATTACKER], function: activatekillability(), txdata: 0x84057065, value: 0x0 Caller: [ATTACKER], function: commencekilling(), txdata: 0x7c11da20, value: 0x0 ``` From 35d88019bd760e3afbc6e2c8cd7a9efd8fb2bed8 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Thu, 18 Jul 2019 21:09:58 +0200 Subject: [PATCH 047/164] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 669b86da..4eee32a3 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ Or: $ myth analyze -a ``` -Specify the maximum number of transaction to explore with `-t `. You can also set a timeout with `--execution timeout `. Example ([source code](https://gist.github.com/b-mueller/2b251297ce88aa7628680f50f177a81a#file-killbilly-sol)): +Specify the maximum number of transaction to explore with `-t `. You can also set a timeout with `--execution-timeout `. Example ([source code](https://gist.github.com/b-mueller/2b251297ce88aa7628680f50f177a81a#file-killbilly-sol)): ``` ==== Unprotected Selfdestruct ==== From 074d5a74be2c022d3c2bf419e729621d2696eb1b Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 22 Jul 2019 09:33:52 -0700 Subject: [PATCH 048/164] WIP uselsess commit --- mythril/analysis/modules/ether_thief.py | 12 +++++++++--- .../analysis/templates/report_as_text.jinja2 | 2 ++ mythril/laser/ethereum/call.py | 18 ++++++++++++++++-- mythril/laser/ethereum/instructions.py | 17 ++++++++++++++--- mythril/laser/ethereum/state/account.py | 2 ++ mythril/laser/ethereum/state/world_state.py | 5 ++++- .../ethereum/transaction/transaction_models.py | 5 +++-- 7 files changed, 50 insertions(+), 11 deletions(-) diff --git a/mythril/analysis/modules/ether_thief.py b/mythril/analysis/modules/ether_thief.py index 76952a54..b5dc8c02 100644 --- a/mythril/analysis/modules/ether_thief.py +++ b/mythril/analysis/modules/ether_thief.py @@ -114,15 +114,21 @@ class EtherThief(DetectionModule): Require that the current transaction is sent by the attacker and that the Ether is sent to the attacker's address. """ - + attacker_address_bitvec = symbol_factory.BitVecVal(ATTACKER_ADDRESS, 256) + eth_recieved_by_attacker = ( + value + + state.world_state.balances[attacker_address_bitvec] + - state.world_state.starting_balances[attacker_address_bitvec] + ) + eth_recieved_by_attacker.simplify() + print(eth_recieved_by_attacker) constraints += [ - UGT(value, eth_sent_by_attacker), + UGT(eth_recieved_by_attacker, eth_sent_by_attacker), target == ATTACKER_ADDRESS, state.current_transaction.caller == ATTACKER_ADDRESS, ] try: - transaction_sequence = solver.get_transaction_sequence(state, constraints) issue = Issue( diff --git a/mythril/analysis/templates/report_as_text.jinja2 b/mythril/analysis/templates/report_as_text.jinja2 index dd70bb45..776b661d 100644 --- a/mythril/analysis/templates/report_as_text.jinja2 +++ b/mythril/analysis/templates/report_as_text.jinja2 @@ -21,6 +21,8 @@ In file: {{ issue.filename }}:{{ issue.lineno }} {% if issue.tx_sequence %} Transaction Sequence: +{{ issue.tx_sequence.initialState }} + {% for step in issue.tx_sequence.steps %} {% if step == issue.tx_sequence.steps[0] and step.input != "0x" and step.origin == "0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe" %} Caller: [CREATOR], data: [CONTRACT CREATION], value: {{ step.value }} diff --git a/mythril/laser/ethereum/call.py b/mythril/laser/ethereum/call.py index 918c1d3f..7cb084b0 100644 --- a/mythril/laser/ethereum/call.py +++ b/mythril/laser/ethereum/call.py @@ -49,7 +49,11 @@ def get_call_parameters( callee_account = None call_data = get_call_data(global_state, memory_input_offset, memory_input_size) - if int(callee_address, 16) >= 5 or int(callee_address, 16) == 0: + if ( + isinstance(callee_address, BitVec) + or int(callee_address, 16) >= 5 + or int(callee_address, 16) == 0 + ): callee_account = get_callee_account( global_state, callee_address, dynamic_loader ) @@ -88,6 +92,8 @@ def get_callee_address( log.debug("CALL to: " + str(simplify(symbolic_to_address))) if match is None or dynamic_loader is None: + # TODO: Fix types + return symbolic_to_address raise ValueError() index = int(match.group(1)) @@ -111,7 +117,9 @@ def get_callee_address( def get_callee_account( - global_state: GlobalState, callee_address: str, dynamic_loader: DynLoader + global_state: GlobalState, + callee_address: Union[str, BitVec], + dynamic_loader: DynLoader, ): """Gets the callees account from the global_state. @@ -123,6 +131,12 @@ def get_callee_account( environment = global_state.environment accounts = global_state.accounts + if isinstance(callee_address, BitVec): + if callee_address.symbolic: + return Account(callee_address, balances=global_state.world_state.balances) + else: + callee_address = callee_address.value + try: return global_state.accounts[int(callee_address, 16)] except KeyError: diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py index b3e3eaa6..79c00945 100644 --- a/mythril/laser/ethereum/instructions.py +++ b/mythril/laser/ethereum/instructions.py @@ -25,7 +25,7 @@ from mythril.laser.smt import ( Bool, Not, LShR, -) + BVSubNoUnderflow, UGE) from mythril.laser.smt import symbol_factory import mythril.laser.ethereum.util as helper @@ -840,7 +840,11 @@ class Instruction: """ state = global_state.mstate address = state.stack.pop() - state.stack.append(global_state.new_bitvec("balance_at_" + str(address), 256)) + + balance = global_state.world_state.balances[ + global_state.environment.active_account.address + ] + state.stack.append(balance) return [global_state] @StateTransition() @@ -1688,7 +1692,14 @@ class Instruction: ) if callee_account is not None and callee_account.code.bytecode == "": - log.debug("The call is related to ether transfer between accounts") + log.warning( + "The call is related to ether transfer between accounts" + ) # TODO: was debug + + global_state.mstate.constraints.append(UGE(global_state.world_state.balances[environment.active_account.address], value)) + #global_state.world_state.balances[environment.active_account.address] -= value + global_state.world_state.balances[callee_account.address] += value + global_state.mstate.stack.append( global_state.new_bitvec("retval_" + str(instr["address"]), 256) ) diff --git a/mythril/laser/ethereum/state/account.py b/mythril/laser/ethereum/state/account.py index f5196287..9e83f63a 100644 --- a/mythril/laser/ethereum/state/account.py +++ b/mythril/laser/ethereum/state/account.py @@ -169,6 +169,8 @@ class Account: self._balances = balances self.balance = lambda: self._balances[self.address] + # TODO: Add initial balance + def __str__(self) -> str: return str(self.as_dict) diff --git a/mythril/laser/ethereum/state/world_state.py b/mythril/laser/ethereum/state/world_state.py index 236da526..d5851533 100644 --- a/mythril/laser/ethereum/state/world_state.py +++ b/mythril/laser/ethereum/state/world_state.py @@ -27,6 +27,7 @@ class WorldState: """ self._accounts = {} # type: Dict[int, Account] self.balances = Array("balance", 256, 256) + self.starting_balances = copy(self.balances) self.node = None # type: Optional['Node'] self.transaction_sequence = transaction_sequence or [] @@ -60,6 +61,7 @@ class WorldState: annotations=new_annotations, ) new_world_state.balances = copy(self.balances) + new_world_state.starting_balances = copy(self.starting_balances) for account in self._accounts.values(): new_world_state.put_account(copy(account)) new_world_state.node = self.node @@ -115,7 +117,7 @@ class WorldState: concrete_storage=concrete_storage, ) if balance: - new_account.set_balance(symbol_factory.BitVecVal(balance, 256)) + new_account.add_balance(symbol_factory.BitVecVal(balance, 256)) self.put_account(new_account) return new_account @@ -180,5 +182,6 @@ class WorldState: :param account: """ + assert not account.address.symbolic # ??? self._accounts[account.address.value] = account account._balances = self.balances diff --git a/mythril/laser/ethereum/transaction/transaction_models.py b/mythril/laser/ethereum/transaction/transaction_models.py index 93cc7e3b..2f5c34cf 100644 --- a/mythril/laser/ethereum/transaction/transaction_models.py +++ b/mythril/laser/ethereum/transaction/transaction_models.py @@ -13,7 +13,7 @@ from mythril.laser.ethereum.state.environment import Environment from mythril.laser.ethereum.state.global_state import GlobalState from mythril.laser.ethereum.state.world_state import WorldState from mythril.disassembler.disassembly import Disassembly -from mythril.laser.smt import symbol_factory +from mythril.laser.smt import symbol_factory, BVSubNoUnderflow, UGE _next_transaction_id = 0 @@ -117,7 +117,8 @@ class BaseTransaction: receiver = environment.active_account.address value = environment.callvalue - global_state.world_state.balances[sender] -= value + global_state.mstate.constraints.append(UGE(global_state.world_state.balances[sender], value)) + #global_state.world_state.balances[sender] -= value global_state.world_state.balances[receiver] += value return global_state From b3d522b49ca28af1284339a6accbcb93ef4dc88b Mon Sep 17 00:00:00 2001 From: e-ngo <52668908+e-ngo@users.noreply.github.com> Date: Tue, 23 Jul 2019 22:06:24 -0700 Subject: [PATCH 049/164] Enable coverage based search strategy through CLI (#1171) * Enable coverage based search strategy through CLI * Ran black on files changed * Refactored code * Removed redundant code --- mythril/analysis/symbolic.py | 7 ++++++- mythril/interfaces/cli.py | 6 ++++++ mythril/laser/ethereum/svm.py | 9 +++++++++ mythril/mythril/mythril_analyzer.py | 5 +++++ 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/mythril/analysis/symbolic.py b/mythril/analysis/symbolic.py index 8528a51f..4164758f 100644 --- a/mythril/analysis/symbolic.py +++ b/mythril/analysis/symbolic.py @@ -55,6 +55,7 @@ class SymExecWrapper: enable_iprof=False, disable_dependency_pruning=False, run_analysis_modules=True, + enable_coverage_strategy=False, ): """ @@ -102,6 +103,8 @@ class SymExecWrapper: hex(ATTACKER_ADDRESS): attacker_account, } + instruction_laser_plugin = PluginFactory.build_instruction_coverage_plugin() + self.laser = svm.LaserEVM( dynamic_loader=dynloader, max_depth=max_depth, @@ -111,6 +114,8 @@ class SymExecWrapper: transaction_count=transaction_count, requires_statespace=requires_statespace, enable_iprof=enable_iprof, + enable_coverage_strategy=enable_coverage_strategy, + instruction_laser_plugin=instruction_laser_plugin, ) if loop_bound is not None: @@ -118,7 +123,7 @@ class SymExecWrapper: plugin_loader = LaserPluginLoader(self.laser) plugin_loader.load(PluginFactory.build_mutation_pruner_plugin()) - plugin_loader.load(PluginFactory.build_instruction_coverage_plugin()) + plugin_loader.load(instruction_laser_plugin) if not disable_dependency_pruning: plugin_loader.load(PluginFactory.build_dependency_pruner_plugin()) diff --git a/mythril/interfaces/cli.py b/mythril/interfaces/cli.py index f18aa254..839d1fd5 100644 --- a/mythril/interfaces/cli.py +++ b/mythril/interfaces/cli.py @@ -391,6 +391,11 @@ def create_analyzer_parser(analyzer_parser: ArgumentParser): action="store_true", help="Deactivate dependency-based pruning", ) + options.add_argument( + "--enable-coverage-strategy", + action="store_true", + help="enable coverage based search strategy", + ) def validate_args(args: Namespace): @@ -561,6 +566,7 @@ def execute_command( onchain_storage_access=not args.no_onchain_storage_access, solver_timeout=args.solver_timeout, requires_dynld=not args.no_onchain_storage_access, + enable_coverage_strategy=args.enable_coverage_strategy, ) if not disassembler.contracts: diff --git a/mythril/laser/ethereum/svm.py b/mythril/laser/ethereum/svm.py index 9755fc33..c5d5c68e 100644 --- a/mythril/laser/ethereum/svm.py +++ b/mythril/laser/ethereum/svm.py @@ -55,6 +55,8 @@ class LaserEVM: transaction_count=2, requires_statespace=True, enable_iprof=False, + enable_coverage_strategy=False, + instruction_laser_plugin=None, ) -> None: """ Initializes the laser evm object @@ -102,6 +104,13 @@ class LaserEVM: self.iprof = InstructionProfiler() if enable_iprof else None + if enable_coverage_strategy: + from mythril.laser.ethereum.plugins.implementations.coverage.coverage_strategy import ( + CoverageStrategy, + ) + + self.strategy = CoverageStrategy(self.strategy, instruction_laser_plugin) + log.info("LASER EVM initialized with dynamic loader: " + str(dynamic_loader)) def extend_strategy(self, extension: ABCMeta, *args) -> None: diff --git a/mythril/mythril/mythril_analyzer.py b/mythril/mythril/mythril_analyzer.py index 497c4c33..deb86281 100644 --- a/mythril/mythril/mythril_analyzer.py +++ b/mythril/mythril/mythril_analyzer.py @@ -41,6 +41,7 @@ class MythrilAnalyzer: enable_iprof: bool = False, disable_dependency_pruning: bool = False, solver_timeout: Optional[int] = None, + enable_coverage_strategy: bool = False, ): """ @@ -61,6 +62,7 @@ class MythrilAnalyzer: self.create_timeout = create_timeout self.enable_iprof = enable_iprof self.disable_dependency_pruning = disable_dependency_pruning + self.enable_coverage_strategy = enable_coverage_strategy analysis_args.set_loop_bound(loop_bound) analysis_args.set_solver_timeout(solver_timeout) @@ -86,6 +88,7 @@ class MythrilAnalyzer: enable_iprof=self.enable_iprof, disable_dependency_pruning=self.disable_dependency_pruning, run_analysis_modules=False, + enable_coverage_strategy=self.enable_coverage_strategy, ) return get_serializable_statespace(sym) @@ -121,6 +124,7 @@ class MythrilAnalyzer: enable_iprof=self.enable_iprof, disable_dependency_pruning=self.disable_dependency_pruning, run_analysis_modules=False, + enable_coverage_strategy=self.enable_coverage_strategy, ) return generate_graph(sym, physics=enable_physics, phrackify=phrackify) @@ -158,6 +162,7 @@ class MythrilAnalyzer: compulsory_statespace=False, enable_iprof=self.enable_iprof, disable_dependency_pruning=self.disable_dependency_pruning, + enable_coverage_strategy=self.enable_coverage_strategy, ) issues = fire_lasers(sym, modules) From 584bdf13d68b4ebf674168ce150ae5b8127b9d31 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 24 Jul 2019 15:51:38 -0700 Subject: [PATCH 050/164] First draft of ether balance modeling --- mythril/analysis/modules/ether_thief.py | 64 ++++++------- mythril/analysis/solver.py | 37 +++++--- .../analysis/templates/report_as_text.jinja2 | 2 - mythril/laser/ethereum/call.py | 12 +-- mythril/laser/ethereum/instructions.py | 91 +++++++++++++++++-- mythril/laser/ethereum/state/account.py | 2 - mythril/laser/ethereum/state/world_state.py | 2 +- .../transaction/transaction_models.py | 20 ++-- tests/laser/evm_testsuite/evm_test.py | 4 +- 9 files changed, 156 insertions(+), 78 deletions(-) diff --git a/mythril/analysis/modules/ether_thief.py b/mythril/analysis/modules/ether_thief.py index 95fbe2fb..cf668b25 100644 --- a/mythril/analysis/modules/ether_thief.py +++ b/mythril/analysis/modules/ether_thief.py @@ -15,7 +15,15 @@ from mythril.analysis.swc_data import UNPROTECTED_ETHER_WITHDRAWAL from mythril.exceptions import UnsatError from mythril.laser.ethereum.transaction import ContractCreationTransaction from mythril.laser.ethereum.state.global_state import GlobalState -from mythril.laser.smt import UGT, Sum, symbol_factory, BVAddNoOverflow, If +from mythril.laser.smt import ( + UGT, + Sum, + symbol_factory, + BVAddNoOverflow, + If, + simplify, + UGE, +) log = logging.getLogger(__name__) @@ -74,55 +82,35 @@ class EtherThief(DetectionModule): :param state: :return: """ + state = copy(state) instruction = state.get_current_instruction() - if instruction["opcode"] != "CALL": - return [] - - address = instruction["address"] - value = state.mstate.stack[-3] target = state.mstate.stack[-2] - eth_sent_by_attacker = symbol_factory.BitVecVal(0, 256) - constraints = copy(state.mstate.constraints) - for tx in state.world_state.transaction_sequence: - """ - Constraint: The call value must be greater than the sum of Ether sent by the attacker over all - transactions. This prevents false positives caused by legitimate refund functions. - Also constrain the addition from overflowing (otherwise the solver produces solutions with - ridiculously high call values). - """ - constraints += [BVAddNoOverflow(eth_sent_by_attacker, tx.call_value, False)] - eth_sent_by_attacker = Sum( - eth_sent_by_attacker, - tx.call_value * If(tx.caller == ATTACKER_ADDRESS, 1, 0), - ) - - """ - Constraint: All transactions must originate from regular users (not the creator/owner). - This prevents false positives where the owner willingly transfers ownership to another address. - """ - - if not isinstance(tx, ContractCreationTransaction): - constraints += [tx.caller != CREATOR_ADDRESS] - """ Require that the current transaction is sent by the attacker and - that the Ether is sent to the attacker's address. + that the Ether sent to the attacker's address is greater than the + amount of Ether the attacker sent. """ attacker_address_bitvec = symbol_factory.BitVecVal(ATTACKER_ADDRESS, 256) - eth_recieved_by_attacker = ( - value - + state.world_state.balances[attacker_address_bitvec] - - state.world_state.starting_balances[attacker_address_bitvec] - ) - eth_recieved_by_attacker.simplify() - print(eth_recieved_by_attacker) + + constraints += [ + UGE( + state.world_state.balances[state.environment.active_account.address], + value, + ) + ] + state.world_state.balances[attacker_address_bitvec] += value + state.world_state.balances[state.environment.active_account.address] -= value + constraints += [ - UGT(eth_recieved_by_attacker, eth_sent_by_attacker), + UGT( + state.world_state.balances[attacker_address_bitvec], + state.world_state.starting_balances[attacker_address_bitvec], + ), target == ATTACKER_ADDRESS, state.current_transaction.caller == ATTACKER_ADDRESS, ] diff --git a/mythril/analysis/solver.py b/mythril/analysis/solver.py index 6a14fb68..547e5d11 100644 --- a/mythril/analysis/solver.py +++ b/mythril/analysis/solver.py @@ -95,7 +95,7 @@ def get_transaction_sequence( concrete_transactions = [] tx_constraints, minimize = _set_minimisation_constraints( - transaction_sequence, constraints.copy(), [], 5000 + transaction_sequence, constraints.copy(), [], 5000, global_state.world_state ) try: @@ -103,19 +103,23 @@ def get_transaction_sequence( except UnsatError: raise UnsatError - min_price_dict = {} # type: Dict[str, int] + # Include creation account in initial state + # Note: This contains the code, which should not exist until after the first tx + initial_world_state = transaction_sequence[0].world_state + initial_accounts = initial_world_state.accounts + for transaction in transaction_sequence: concrete_transaction = _get_concrete_transaction(model, transaction) concrete_transactions.append(concrete_transaction) - caller = concrete_transaction["origin"] - value = int(concrete_transaction["value"], 16) - min_price_dict[caller] = min_price_dict.get(caller, 0) + value - - if isinstance(transaction_sequence[0], ContractCreationTransaction): - initial_accounts = transaction_sequence[0].prev_world_state.accounts - else: - initial_accounts = transaction_sequence[0].world_state.accounts + min_price_dict = {} # type: Dict[str, int] + for address in initial_accounts.keys(): + min_price_dict[address] = model.eval( + initial_world_state.starting_balances[ + symbol_factory.BitVecVal(address, 256) + ].raw, + model_completion=True, + ).as_long() concrete_initial_state = _get_concrete_state(initial_accounts, min_price_dict) @@ -171,7 +175,7 @@ def _get_concrete_transaction(model: z3.Model, transaction: BaseTransaction): def _set_minimisation_constraints( - transaction_sequence, constraints, minimize, max_size + transaction_sequence, constraints, minimize, max_size, world_state ) -> Tuple[Constraints, tuple]: """ Set constraints that minimise key transaction values @@ -192,6 +196,15 @@ def _set_minimisation_constraints( # Minimize minimize.append(transaction.call_data.calldatasize) - minimize.append(transaction.call_value) + + for account in world_state.accounts.values(): + # Lazy way to prevent overflows and to ensure "reasonable" balances + # Each account starts with less than 100 ETH + constraints.append( + UGE( + symbol_factory.BitVecVal(100000000000000000000, 256), + world_state.starting_balances[account.address], + ) + ) return constraints, tuple(minimize) diff --git a/mythril/analysis/templates/report_as_text.jinja2 b/mythril/analysis/templates/report_as_text.jinja2 index 8fa2454a..d7b84355 100644 --- a/mythril/analysis/templates/report_as_text.jinja2 +++ b/mythril/analysis/templates/report_as_text.jinja2 @@ -21,8 +21,6 @@ In file: {{ issue.filename }}:{{ issue.lineno }} {% if issue.tx_sequence %} Transaction Sequence: -{{ issue.tx_sequence.initialState }} - {% for step in issue.tx_sequence.steps %} {% if step == issue.tx_sequence.steps[0] and step.input != "0x" and step.origin == "0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe" %} Caller: [CREATOR], data: [CONTRACT CREATION], value: {{ step.value }} diff --git a/mythril/laser/ethereum/call.py b/mythril/laser/ethereum/call.py index 7cb084b0..f29c34bf 100644 --- a/mythril/laser/ethereum/call.py +++ b/mythril/laser/ethereum/call.py @@ -88,13 +88,12 @@ def get_callee_address( except TypeError: log.debug("Symbolic call encountered") - match = re.search(r"storage_(\d+)", str(simplify(symbolic_to_address))) + match = re.search(r"Storage\[(\d+)\]", str(simplify(symbolic_to_address))) log.debug("CALL to: " + str(simplify(symbolic_to_address))) if match is None or dynamic_loader is None: # TODO: Fix types return symbolic_to_address - raise ValueError() index = int(match.group(1)) log.debug("Dynamic contract address at storage index {}".format(index)) @@ -106,8 +105,7 @@ def get_callee_address( ) # TODO: verify whether this happens or not except: - log.debug("Error accessing contract storage.") - raise ValueError + return symbolic_to_address # testrpc simply returns the address, geth response is more elaborate. if not re.match(r"^0x[0-9a-f]{40}$", callee_address): @@ -135,7 +133,7 @@ def get_callee_account( if callee_address.symbolic: return Account(callee_address, balances=global_state.world_state.balances) else: - callee_address = callee_address.value + callee_address = hex(callee_address.value)[2:] try: return global_state.accounts[int(callee_address, 16)] @@ -223,12 +221,12 @@ def get_call_data( def native_call( global_state: GlobalState, - callee_address: str, + callee_address: Union[str, BitVec], call_data: BaseCalldata, memory_out_offset: Union[int, Expression], memory_out_size: Union[int, Expression], ) -> Union[List[GlobalState], None]: - if not 0 < int(callee_address, 16) < 5: + if isinstance(callee_address, BitVec) or not 0 < int(callee_address, 16) < 5: return None log.debug("Native contract called: " + callee_address) diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py index 79c00945..eda8a037 100644 --- a/mythril/laser/ethereum/instructions.py +++ b/mythril/laser/ethereum/instructions.py @@ -25,7 +25,9 @@ from mythril.laser.smt import ( Bool, Not, LShR, - BVSubNoUnderflow, UGE) + BVSubNoUnderflow, + UGE, +) from mythril.laser.smt import symbol_factory import mythril.laser.ethereum.util as helper @@ -1619,7 +1621,7 @@ class Instruction: transfer_amount = global_state.environment.active_account.balance() # Often the target of the suicide instruction will be symbolic # If it isn't then we'll transfer the balance to the indicated contract - global_state.world_state[target].add_balance(transfer_amount) + global_state.world_state.balances[target] += transfer_amount global_state.environment.active_account = deepcopy( global_state.environment.active_account @@ -1692,13 +1694,20 @@ class Instruction: ) if callee_account is not None and callee_account.code.bytecode == "": - log.warning( - "The call is related to ether transfer between accounts" - ) # TODO: was debug + log.debug("The call is related to ether transfer between accounts") + sender = environment.active_account.address + receiver = callee_account.address + value = ( + value + if isinstance(value, BitVec) + else symbol_factory.BitVecVal(value, 256) + ) - global_state.mstate.constraints.append(UGE(global_state.world_state.balances[environment.active_account.address], value)) - #global_state.world_state.balances[environment.active_account.address] -= value - global_state.world_state.balances[callee_account.address] += value + global_state.mstate.constraints.append( + UGE(global_state.world_state.balances[sender], value) + ) + global_state.world_state.balances[receiver] += value + global_state.world_state.balances[sender] -= value global_state.mstate.stack.append( global_state.new_bitvec("retval_" + str(instr["address"]), 256) @@ -1815,6 +1824,28 @@ class Instruction: callee_address, callee_account, call_data, value, gas, _, _ = get_call_parameters( global_state, self.dynamic_loader, True ) + + if callee_account is not None and callee_account.code.bytecode == "": + log.debug("The call is related to ether transfer between accounts") + sender = global_state.environment.active_account.address + receiver = callee_account.address + value = ( + value + if isinstance(value, BitVec) + else symbol_factory.BitVecVal(value, 256) + ) + + global_state.mstate.constraints.append( + UGE(global_state.world_state.balances[sender], value) + ) + global_state.world_state.balances[receiver] += value + global_state.world_state.balances[sender] -= value + + global_state.mstate.stack.append( + global_state.new_bitvec("retval_" + str(instr["address"]), 256) + ) + return [global_state] + except ValueError as e: log.debug( "Could not determine required parameters for call, putting fresh symbol on the stack. \n{}".format( @@ -1918,6 +1949,28 @@ class Instruction: callee_address, callee_account, call_data, value, gas, _, _ = get_call_parameters( global_state, self.dynamic_loader ) + + if callee_account is not None and callee_account.code.bytecode == "": + log.debug("The call is related to ether transfer between accounts") + sender = environment.active_account.address + receiver = callee_account.address + value = ( + value + if isinstance(value, BitVec) + else symbol_factory.BitVecVal(value, 256) + ) + + global_state.mstate.constraints.append( + UGE(global_state.world_state.balances[sender], value) + ) + global_state.world_state.balances[receiver] += value + global_state.world_state.balances[sender] -= value + + global_state.mstate.stack.append( + global_state.new_bitvec("retval_" + str(instr["address"]), 256) + ) + return [global_state] + except ValueError as e: log.debug( "Could not determine required parameters for call, putting fresh symbol on the stack. \n{}".format( @@ -2019,6 +2072,28 @@ class Instruction: callee_address, callee_account, call_data, value, gas, memory_out_offset, memory_out_size = get_call_parameters( global_state, self.dynamic_loader ) + + if callee_account is not None and callee_account.code.bytecode == "": + log.debug("The call is related to ether transfer between accounts") + sender = global_state.environment.active_account.address + receiver = callee_account.address + value = ( + value + if isinstance(value, BitVec) + else symbol_factory.BitVecVal(value, 256) + ) + + global_state.mstate.constraints.append( + UGE(global_state.world_state.balances[sender], value) + ) + global_state.world_state.balances[receiver] += value + global_state.world_state.balances[sender] -= value + + global_state.mstate.stack.append( + global_state.new_bitvec("retval_" + str(instr["address"]), 256) + ) + return [global_state] + except ValueError as e: log.debug( "Could not determine required parameters for call, putting fresh symbol on the stack. \n{}".format( diff --git a/mythril/laser/ethereum/state/account.py b/mythril/laser/ethereum/state/account.py index 51d61d27..1a32ad37 100644 --- a/mythril/laser/ethereum/state/account.py +++ b/mythril/laser/ethereum/state/account.py @@ -189,8 +189,6 @@ class Account: self._balances = balances self.balance = lambda: self._balances[self.address] - # TODO: Add initial balance - def __str__(self) -> str: return str(self.as_dict) diff --git a/mythril/laser/ethereum/state/world_state.py b/mythril/laser/ethereum/state/world_state.py index d5851533..5227099e 100644 --- a/mythril/laser/ethereum/state/world_state.py +++ b/mythril/laser/ethereum/state/world_state.py @@ -182,6 +182,6 @@ class WorldState: :param account: """ - assert not account.address.symbolic # ??? + assert not account.address.symbolic # ??? self._accounts[account.address.value] = account account._balances = self.balances diff --git a/mythril/laser/ethereum/transaction/transaction_models.py b/mythril/laser/ethereum/transaction/transaction_models.py index 2f5c34cf..f29e9236 100644 --- a/mythril/laser/ethereum/transaction/transaction_models.py +++ b/mythril/laser/ethereum/transaction/transaction_models.py @@ -4,7 +4,7 @@ execution.""" import array from copy import deepcopy from z3 import ExprRef -from typing import Union, Optional, cast +from typing import Union, Optional from mythril.laser.ethereum.state.calldata import ConcreteCalldata from mythril.laser.ethereum.state.account import Account @@ -12,8 +12,10 @@ from mythril.laser.ethereum.state.calldata import BaseCalldata, SymbolicCalldata from mythril.laser.ethereum.state.environment import Environment from mythril.laser.ethereum.state.global_state import GlobalState from mythril.laser.ethereum.state.world_state import WorldState -from mythril.disassembler.disassembly import Disassembly -from mythril.laser.smt import symbol_factory, BVSubNoUnderflow, UGE +from mythril.laser.smt import symbol_factory, UGE, BitVec +import logging + +log = logging.getLogger(__name__) _next_transaction_id = 0 @@ -115,11 +117,17 @@ class BaseTransaction: sender = environment.sender receiver = environment.active_account.address - value = environment.callvalue + value = ( + environment.callvalue + if isinstance(environment.callvalue, BitVec) + else symbol_factory.BitVecVal(environment.callvalue, 256) + ) - global_state.mstate.constraints.append(UGE(global_state.world_state.balances[sender], value)) - #global_state.world_state.balances[sender] -= value + global_state.mstate.constraints.append( + UGE(global_state.world_state.balances[sender], value) + ) global_state.world_state.balances[receiver] += value + global_state.world_state.balances[sender] -= value return global_state diff --git a/tests/laser/evm_testsuite/evm_test.py b/tests/laser/evm_testsuite/evm_test.py index 244b5844..e5d1727d 100644 --- a/tests/laser/evm_testsuite/evm_test.py +++ b/tests/laser/evm_testsuite/evm_test.py @@ -165,8 +165,8 @@ def test_vmtest( # no more work to do if error happens or out of gas assert len(laser_evm.open_states) == 0 else: - assert len(laser_evm.open_states) == 1 - world_state = laser_evm.open_states[0] + assert len(final_states) == 1 + world_state = final_states[0].world_state for address, details in post_condition.items(): account = world_state[symbol_factory.BitVecVal(int(address, 16), 256)] From a288da7bb5c42a3e443eccb82a73ea48a03b6dda Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 24 Jul 2019 16:28:03 -0700 Subject: [PATCH 051/164] Clean up leftover code --- mythril/analysis/modules/ether_thief.py | 17 +---- mythril/laser/ethereum/call.py | 1 - mythril/laser/ethereum/instructions.py | 73 ++++++++------------- mythril/laser/ethereum/state/world_state.py | 1 - 4 files changed, 30 insertions(+), 62 deletions(-) diff --git a/mythril/analysis/modules/ether_thief.py b/mythril/analysis/modules/ether_thief.py index cf668b25..948e9c89 100644 --- a/mythril/analysis/modules/ether_thief.py +++ b/mythril/analysis/modules/ether_thief.py @@ -1,29 +1,16 @@ """This module contains the detection code for unauthorized ether withdrawal.""" import logging -import json from copy import copy from mythril.analysis import solver from mythril.analysis.modules.base import DetectionModule from mythril.analysis.report import Issue -from mythril.laser.ethereum.transaction.symbolic import ( - ATTACKER_ADDRESS, - CREATOR_ADDRESS, -) +from mythril.laser.ethereum.transaction.symbolic import ATTACKER_ADDRESS from mythril.analysis.swc_data import UNPROTECTED_ETHER_WITHDRAWAL from mythril.exceptions import UnsatError -from mythril.laser.ethereum.transaction import ContractCreationTransaction from mythril.laser.ethereum.state.global_state import GlobalState -from mythril.laser.smt import ( - UGT, - Sum, - symbol_factory, - BVAddNoOverflow, - If, - simplify, - UGE, -) +from mythril.laser.smt import UGT, symbol_factory, UGE log = logging.getLogger(__name__) diff --git a/mythril/laser/ethereum/call.py b/mythril/laser/ethereum/call.py index f29c34bf..e31f0474 100644 --- a/mythril/laser/ethereum/call.py +++ b/mythril/laser/ethereum/call.py @@ -92,7 +92,6 @@ def get_callee_address( log.debug("CALL to: " + str(simplify(symbolic_to_address))) if match is None or dynamic_loader is None: - # TODO: Fix types return symbolic_to_address index = int(match.group(1)) diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py index eda8a037..80dcd840 100644 --- a/mythril/laser/ethereum/instructions.py +++ b/mythril/laser/ethereum/instructions.py @@ -25,7 +25,6 @@ from mythril.laser.smt import ( Bool, Not, LShR, - BVSubNoUnderflow, UGE, ) from mythril.laser.smt import symbol_factory @@ -56,6 +55,30 @@ TT256 = 2 ** 256 TT256M1 = 2 ** 256 - 1 +def transfer_ether( + global_state: GlobalState, + sender: BitVec, + receiver: BitVec, + value: Union[int, BitVec], +): + """ + Perform an Ether transfer between two accounts + + :param global_state: The global state in which the Ether transfer occurs + :param sender: The sender of the Ether + :param receiver: The recipient of the Ether + :param value: The amount of Ether to send + :return: + """ + value = value if isinstance(value, BitVec) else symbol_factory.BitVecVal(value, 256) + + global_state.mstate.constraints.append( + UGE(global_state.world_state.balances[sender], value) + ) + global_state.world_state.balances[receiver] += value + global_state.world_state.balances[sender] -= value + + class StateTransition(object): """Decorator that handles global state copy and original return. @@ -1697,17 +1720,7 @@ class Instruction: log.debug("The call is related to ether transfer between accounts") sender = environment.active_account.address receiver = callee_account.address - value = ( - value - if isinstance(value, BitVec) - else symbol_factory.BitVecVal(value, 256) - ) - - global_state.mstate.constraints.append( - UGE(global_state.world_state.balances[sender], value) - ) - global_state.world_state.balances[receiver] += value - global_state.world_state.balances[sender] -= value + transfer_ether(sender, receiver, value) global_state.mstate.stack.append( global_state.new_bitvec("retval_" + str(instr["address"]), 256) @@ -1829,17 +1842,7 @@ class Instruction: log.debug("The call is related to ether transfer between accounts") sender = global_state.environment.active_account.address receiver = callee_account.address - value = ( - value - if isinstance(value, BitVec) - else symbol_factory.BitVecVal(value, 256) - ) - - global_state.mstate.constraints.append( - UGE(global_state.world_state.balances[sender], value) - ) - global_state.world_state.balances[receiver] += value - global_state.world_state.balances[sender] -= value + transfer_ether(sender, receiver, value) global_state.mstate.stack.append( global_state.new_bitvec("retval_" + str(instr["address"]), 256) @@ -1954,17 +1957,7 @@ class Instruction: log.debug("The call is related to ether transfer between accounts") sender = environment.active_account.address receiver = callee_account.address - value = ( - value - if isinstance(value, BitVec) - else symbol_factory.BitVecVal(value, 256) - ) - - global_state.mstate.constraints.append( - UGE(global_state.world_state.balances[sender], value) - ) - global_state.world_state.balances[receiver] += value - global_state.world_state.balances[sender] -= value + transfer_ether(sender, receiver, value) global_state.mstate.stack.append( global_state.new_bitvec("retval_" + str(instr["address"]), 256) @@ -2077,17 +2070,7 @@ class Instruction: log.debug("The call is related to ether transfer between accounts") sender = global_state.environment.active_account.address receiver = callee_account.address - value = ( - value - if isinstance(value, BitVec) - else symbol_factory.BitVecVal(value, 256) - ) - - global_state.mstate.constraints.append( - UGE(global_state.world_state.balances[sender], value) - ) - global_state.world_state.balances[receiver] += value - global_state.world_state.balances[sender] -= value + transfer_ether(sender, receiver, value) global_state.mstate.stack.append( global_state.new_bitvec("retval_" + str(instr["address"]), 256) diff --git a/mythril/laser/ethereum/state/world_state.py b/mythril/laser/ethereum/state/world_state.py index 5227099e..20628877 100644 --- a/mythril/laser/ethereum/state/world_state.py +++ b/mythril/laser/ethereum/state/world_state.py @@ -182,6 +182,5 @@ class WorldState: :param account: """ - assert not account.address.symbolic # ??? self._accounts[account.address.value] = account account._balances = self.balances From 267fbf06ebf05105777d3c02127de558cc154863 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 24 Jul 2019 22:44:04 -0700 Subject: [PATCH 052/164] Fix missing argument to function --- mythril/laser/ethereum/instructions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py index 80dcd840..e5347f2f 100644 --- a/mythril/laser/ethereum/instructions.py +++ b/mythril/laser/ethereum/instructions.py @@ -1720,7 +1720,7 @@ class Instruction: log.debug("The call is related to ether transfer between accounts") sender = environment.active_account.address receiver = callee_account.address - transfer_ether(sender, receiver, value) + transfer_ether(global_state, sender, receiver, value) global_state.mstate.stack.append( global_state.new_bitvec("retval_" + str(instr["address"]), 256) @@ -1842,7 +1842,7 @@ class Instruction: log.debug("The call is related to ether transfer between accounts") sender = global_state.environment.active_account.address receiver = callee_account.address - transfer_ether(sender, receiver, value) + transfer_ether(global_state, sender, receiver, value) global_state.mstate.stack.append( global_state.new_bitvec("retval_" + str(instr["address"]), 256) @@ -1957,7 +1957,7 @@ class Instruction: log.debug("The call is related to ether transfer between accounts") sender = environment.active_account.address receiver = callee_account.address - transfer_ether(sender, receiver, value) + transfer_ether(global_state, sender, receiver, value) global_state.mstate.stack.append( global_state.new_bitvec("retval_" + str(instr["address"]), 256) @@ -2070,7 +2070,7 @@ class Instruction: log.debug("The call is related to ether transfer between accounts") sender = global_state.environment.active_account.address receiver = callee_account.address - transfer_ether(sender, receiver, value) + transfer_ether(global_state, sender, receiver, value) global_state.mstate.stack.append( global_state.new_bitvec("retval_" + str(instr["address"]), 256) From 8071a3c617870ab653a275a43da7b0ae57f9d9d0 Mon Sep 17 00:00:00 2001 From: e-ngo <52668908+e-ngo@users.noreply.github.com> Date: Thu, 25 Jul 2019 00:16:39 -0700 Subject: [PATCH 053/164] Updated jinja templates to report initial state of the transaction sequences. (#1173) --- mythril/analysis/templates/report_as_markdown.jinja2 | 6 ++++++ mythril/analysis/templates/report_as_text.jinja2 | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/mythril/analysis/templates/report_as_markdown.jinja2 b/mythril/analysis/templates/report_as_markdown.jinja2 index 4583eb26..9e690c43 100644 --- a/mythril/analysis/templates/report_as_markdown.jinja2 +++ b/mythril/analysis/templates/report_as_markdown.jinja2 @@ -26,6 +26,12 @@ In file: {{ issue.filename }}:{{ issue.lineno }} {% endif %} {% if issue.tx_sequence %} +### Initial State: + +{% for key, value in issue.tx_sequence.initialState.accounts.items() %} +Account: {% if key == "0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe" %}[CREATOR]{% elif key == "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" %}[ATTACKER]{% else %}[SOMEGUY]{% endif %}, balance: {{value.balance}}, nonce:{{value.nonce}}, storage:{{value.storage}} +{% endfor %} + ### Transaction Sequence {% for step in issue.tx_sequence.steps %} diff --git a/mythril/analysis/templates/report_as_text.jinja2 b/mythril/analysis/templates/report_as_text.jinja2 index d7b84355..aa5c4876 100644 --- a/mythril/analysis/templates/report_as_text.jinja2 +++ b/mythril/analysis/templates/report_as_text.jinja2 @@ -19,6 +19,12 @@ In file: {{ issue.filename }}:{{ issue.lineno }} -------------------- {% endif %} {% if issue.tx_sequence %} +Initial State: + +{% for key, value in issue.tx_sequence.initialState.accounts.items() %} +Account: {% if key == "0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe" %}[CREATOR]{% elif key == "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" %}[ATTACKER]{% else %}[SOMEGUY]{% endif %}, balance: {{value.balance}}, nonce:{{value.nonce}}, storage:{{value.storage}} +{% endfor %} + Transaction Sequence: {% for step in issue.tx_sequence.steps %} From 9cb791958d669c32d0416f9289e7ce7751d62ec8 Mon Sep 17 00:00:00 2001 From: Aleksandr Sobolev Date: Thu, 25 Jul 2019 17:18:22 +0600 Subject: [PATCH 054/164] Inject missing env variables (#1174) --- .circleci/config.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index f18896f1..ae1a05d1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -103,6 +103,8 @@ jobs: name: Run Edelweiss command: | docker run \ + -e CIRCLE_BRANCH=$CIRCLE_BRANCH \ + -e CIRCLE_SHA1=$CIRCLE_SHA1 \ -e CIRCLE_BUILD_NUM=$CIRCLE_BUILD_NUM \ -e CIRCLE_BUILD_URL=$CIRCLE_BUILD_URL \ -e CIRCLE_WEBHOOK_URL=$CIRCLE_WEBHOOK_URL \ From 2aa0d39646f767c32d28772645911c91c9377ceb Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 25 Jul 2019 12:03:06 -0700 Subject: [PATCH 055/164] Only add constraint to symbolic Ether balances --- mythril/analysis/solver.py | 11 ++++++----- .../laser/ethereum/transaction/transaction_models.py | 1 - 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/mythril/analysis/solver.py b/mythril/analysis/solver.py index 547e5d11..82e3f2ee 100644 --- a/mythril/analysis/solver.py +++ b/mythril/analysis/solver.py @@ -200,11 +200,12 @@ def _set_minimisation_constraints( for account in world_state.accounts.values(): # Lazy way to prevent overflows and to ensure "reasonable" balances # Each account starts with less than 100 ETH - constraints.append( - UGE( - symbol_factory.BitVecVal(100000000000000000000, 256), - world_state.starting_balances[account.address], + if account.address.symbolic: + constraints.append( + UGE( + symbol_factory.BitVecVal(100000000000000000000, 256), + world_state.starting_balances[account.address], + ) ) - ) return constraints, tuple(minimize) diff --git a/mythril/laser/ethereum/transaction/transaction_models.py b/mythril/laser/ethereum/transaction/transaction_models.py index f29e9236..bccdf7aa 100644 --- a/mythril/laser/ethereum/transaction/transaction_models.py +++ b/mythril/laser/ethereum/transaction/transaction_models.py @@ -189,7 +189,6 @@ class ContractCreationTransaction(BaseTransaction): 0, concrete_storage=True, creator=caller.value ) callee_account.contract_name = contract_name - # TODO: set correct balance for new account super().__init__( world_state=world_state, callee_account=callee_account, From ed032165d44d8640582d97e8416afced80553774 Mon Sep 17 00:00:00 2001 From: e-ngo Date: Sun, 28 Jul 2019 23:43:40 -0700 Subject: [PATCH 056/164] Refactored interfaces to run custom modules --- mythril/analysis/security.py | 73 ++++++++++++++++++++--------- mythril/analysis/symbolic.py | 16 +++++-- mythril/interfaces/cli.py | 8 ++++ mythril/mythril/mythril_analyzer.py | 16 +++++-- 4 files changed, 83 insertions(+), 30 deletions(-) diff --git a/mythril/analysis/security.py b/mythril/analysis/security.py index 71f0a516..5c5e520a 100644 --- a/mythril/analysis/security.py +++ b/mythril/analysis/security.py @@ -6,22 +6,28 @@ from mythril.analysis import modules import pkgutil import importlib.util import logging +import os +import sys log = logging.getLogger(__name__) OPCODE_LIST = [c[0] for _, c in opcodes.items()] -def reset_callback_modules(): +def reset_callback_modules(module_names=(), custom_modules_directory=""): """Clean the issue records of every callback-based module.""" - modules = get_detection_modules("callback") + modules = get_detection_modules("callback", module_names, custom_modules_directory) for module in modules: module.detector.reset_module() -def get_detection_module_hooks(modules, hook_type="pre"): +def get_detection_module_hooks(modules, hook_type="pre", custom_modules_directory=""): hook_dict = defaultdict(list) - _modules = get_detection_modules(entrypoint="callback", include_modules=modules) + _modules = get_detection_modules( + entrypoint="callback", + include_modules=modules, + custom_modules_directory=custom_modules_directory, + ) for module in _modules: hooks = ( module.detector.pre_hooks @@ -45,14 +51,13 @@ def get_detection_module_hooks(modules, hook_type="pre"): return dict(hook_dict) -def get_detection_modules(entrypoint, include_modules=()): +def get_detection_modules(entrypoint, include_modules=(), custom_modules_directory=""): """ :param entrypoint: :param include_modules: :return: """ - module = importlib.import_module("mythril.analysis.modules.base") module.log.setLevel(log.level) @@ -60,27 +65,43 @@ def get_detection_modules(entrypoint, include_modules=()): _modules = [] - if not include_modules: - for loader, module_name, _ in pkgutil.walk_packages(modules.__path__): + for loader, module_name, _ in pkgutil.walk_packages(modules.__path__): + if include_modules and module_name not in include_modules: + continue + + if module_name != "base": + module = importlib.import_module("mythril.analysis.modules." + module_name) + module.log.setLevel(log.level) + if module.detector.entrypoint == entrypoint: + _modules.append(module) + if custom_modules_directory: + custom_modules = [os.path.abspath(custom_modules_directory)] + sys.path.append(custom_modules_directory) + + for loader, module_name, _ in pkgutil.walk_packages(custom_modules): + if include_modules and module_name not in include_modules: + continue + if module_name != "base": - module = importlib.import_module( - "mythril.analysis.modules." + module_name - ) + module = importlib.import_module(module_name, custom_modules[0]) module.log.setLevel(log.level) if module.detector.entrypoint == entrypoint: _modules.append(module) - else: - for module_name in include_modules: - module = importlib.import_module("mythril.analysis.modules." + module_name) - if module.__name__ != "base" and module.detector.entrypoint == entrypoint: - module.log.setLevel(log.level) - _modules.append(module) + """ + for loader, module_name, _ in pkgutil.walk_packages([custom_modules_path]): + + custom_modules_path = os.path.abspath("custom/") + sys.path.append(custom_modules_path); + module = importlib.import_module( + module_name, custom_modules_path + ) + """ log.info("Found %s detection modules", len(_modules)) return _modules -def fire_lasers(statespace, module_names=()): +def fire_lasers(statespace, module_names=(), custom_modules_directory=""): """ :param statespace: @@ -91,22 +112,28 @@ def fire_lasers(statespace, module_names=()): issues = [] for module in get_detection_modules( - entrypoint="post", include_modules=module_names + entrypoint="post", + include_modules=module_names, + custom_modules_directory=custom_modules_directory, ): log.info("Executing " + module.detector.name) issues += module.detector.execute(statespace) - issues += retrieve_callback_issues(module_names) + issues += retrieve_callback_issues(module_names, custom_modules_directory) return issues -def retrieve_callback_issues(module_names=()): +def retrieve_callback_issues(module_names=(), custom_modules_directory=""): issues = [] for module in get_detection_modules( - entrypoint="callback", include_modules=module_names + entrypoint="callback", + include_modules=module_names, + custom_modules_directory=custom_modules_directory, ): log.debug("Retrieving results for " + module.detector.name) issues += module.detector.issues - reset_callback_modules() + reset_callback_modules( + module_names=module_names, custom_modules_directory=custom_modules_directory + ) return issues diff --git a/mythril/analysis/symbolic.py b/mythril/analysis/symbolic.py index 4164758f..02b5c8ca 100644 --- a/mythril/analysis/symbolic.py +++ b/mythril/analysis/symbolic.py @@ -56,6 +56,7 @@ class SymExecWrapper: disable_dependency_pruning=False, run_analysis_modules=True, enable_coverage_strategy=False, + custom_modules_directory="", ): """ @@ -93,7 +94,8 @@ class SymExecWrapper: ) requires_statespace = ( - compulsory_statespace or len(get_detection_modules("post", modules)) > 0 + compulsory_statespace + or len(get_detection_modules("post", modules, custom_modules_directory)) > 0 ) if not contract.creation_code: self.accounts = {hex(ATTACKER_ADDRESS): attacker_account} @@ -135,11 +137,19 @@ class SymExecWrapper: if run_analysis_modules: self.laser.register_hooks( hook_type="pre", - hook_dict=get_detection_module_hooks(modules, hook_type="pre"), + hook_dict=get_detection_module_hooks( + modules, + hook_type="pre", + custom_modules_directory=custom_modules_directory, + ), ) self.laser.register_hooks( hook_type="post", - hook_dict=get_detection_module_hooks(modules, hook_type="post"), + hook_dict=get_detection_module_hooks( + modules, + hook_type="post", + custom_modules_directory=custom_modules_directory, + ), ) if isinstance(contract, SolidityContract): diff --git a/mythril/interfaces/cli.py b/mythril/interfaces/cli.py index 839d1fd5..486b5aaf 100644 --- a/mythril/interfaces/cli.py +++ b/mythril/interfaces/cli.py @@ -396,6 +396,11 @@ def create_analyzer_parser(analyzer_parser: ArgumentParser): action="store_true", help="enable coverage based search strategy", ) + options.add_argument( + "--custom-modules-directory", + help="designates a separate directory to search for security modules from", + metavar="PATH_TO_CUSTOM_MODULES_DIRECTORY", + ) def validate_args(args: Namespace): @@ -567,6 +572,9 @@ def execute_command( solver_timeout=args.solver_timeout, requires_dynld=not args.no_onchain_storage_access, enable_coverage_strategy=args.enable_coverage_strategy, + custom_modules_directory=args.custom_modules_directory + if args.custom_modules_directory + else "", ) if not disassembler.contracts: diff --git a/mythril/mythril/mythril_analyzer.py b/mythril/mythril/mythril_analyzer.py index deb86281..d5e87369 100644 --- a/mythril/mythril/mythril_analyzer.py +++ b/mythril/mythril/mythril_analyzer.py @@ -42,6 +42,7 @@ class MythrilAnalyzer: disable_dependency_pruning: bool = False, solver_timeout: Optional[int] = None, enable_coverage_strategy: bool = False, + custom_modules_directory: str = "", ): """ @@ -63,6 +64,7 @@ class MythrilAnalyzer: self.enable_iprof = enable_iprof self.disable_dependency_pruning = disable_dependency_pruning self.enable_coverage_strategy = enable_coverage_strategy + self.custom_modules_directory = custom_modules_directory analysis_args.set_loop_bound(loop_bound) analysis_args.set_solver_timeout(solver_timeout) @@ -89,6 +91,7 @@ class MythrilAnalyzer: disable_dependency_pruning=self.disable_dependency_pruning, run_analysis_modules=False, enable_coverage_strategy=self.enable_coverage_strategy, + custom_modules_directory=self.custom_modules_directory, ) return get_serializable_statespace(sym) @@ -125,6 +128,7 @@ class MythrilAnalyzer: disable_dependency_pruning=self.disable_dependency_pruning, run_analysis_modules=False, enable_coverage_strategy=self.enable_coverage_strategy, + custom_modules_directory=self.custom_modules_directory, ) return generate_graph(sym, physics=enable_physics, phrackify=phrackify) @@ -163,18 +167,22 @@ class MythrilAnalyzer: enable_iprof=self.enable_iprof, disable_dependency_pruning=self.disable_dependency_pruning, enable_coverage_strategy=self.enable_coverage_strategy, + custom_modules_directory=self.custom_modules_directory, ) - - issues = fire_lasers(sym, modules) + issues = fire_lasers(sym, modules, self.custom_modules_directory) except KeyboardInterrupt: log.critical("Keyboard Interrupt") - issues = retrieve_callback_issues(modules) + issues = retrieve_callback_issues( + modules, self.custom_modules_directory + ) except Exception: log.critical( "Exception occurred, aborting analysis. Please report this issue to the Mythril GitHub page.\n" + traceback.format_exc() ) - issues = retrieve_callback_issues(modules) + issues = retrieve_callback_issues( + modules, self.custom_modules_directory + ) exceptions.append(traceback.format_exc()) for issue in issues: issue.add_code_info(contract) From dbe1f5210aab74340016c441477448754943800d Mon Sep 17 00:00:00 2001 From: palkeo Date: Mon, 29 Jul 2019 18:24:22 +0200 Subject: [PATCH 057/164] Various small fixes. --- mythril/analysis/modules/ether_thief.py | 16 ++++++++++------ mythril/laser/ethereum/call.py | 16 ++++++++-------- mythril/laser/ethereum/instructions.py | 2 +- mythril/laser/ethereum/state/account.py | 3 ++- mythril/laser/ethereum/svm.py | 17 ++++++++++------- 5 files changed, 31 insertions(+), 23 deletions(-) diff --git a/mythril/analysis/modules/ether_thief.py b/mythril/analysis/modules/ether_thief.py index a6836f7b..d7098179 100644 --- a/mythril/analysis/modules/ether_thief.py +++ b/mythril/analysis/modules/ether_thief.py @@ -21,7 +21,7 @@ log = logging.getLogger(__name__) DESCRIPTION = """ -Search for cases where Ether can be withdrawn to a user-specified address. +Search for cases where Ether can be withdrawn to a user-specified address. An issue is reported if: @@ -79,8 +79,6 @@ class EtherThief(DetectionModule): if instruction["opcode"] != "CALL": return [] - address = instruction["address"] - value = state.mstate.stack[-3] target = state.mstate.stack[-2] @@ -92,10 +90,10 @@ class EtherThief(DetectionModule): """ Constraint: The call value must be greater than the sum of Ether sent by the attacker over all transactions. This prevents false positives caused by legitimate refund functions. - Also constrain the addition from overflowing (otherwise the solver produces solutions with + Also constrain the addition from overflowing (otherwise the solver produces solutions with ridiculously high call values). """ - constraints += [BVAddNoOverflow(eth_sent_by_attacker, tx.call_value, False)] + constraints += [BVAddNoOverflow(eth_sent_by_attacker, tx.call_value, signed=False)] eth_sent_by_attacker = Sum( eth_sent_by_attacker, tx.call_value * If(tx.caller == ATTACKER_ADDRESS, 1, 0), @@ -120,6 +118,8 @@ class EtherThief(DetectionModule): state.current_transaction.caller == ATTACKER_ADDRESS, ] + log.debug("Constraints: %s", constraints) + try: transaction_sequence = solver.get_transaction_sequence(state, constraints) @@ -140,9 +140,13 @@ class EtherThief(DetectionModule): gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used), ) except UnsatError: - log.debug("[ETHER_THIEF] no model found") + log.debug("No model found") return [] + log.debug("Found ether thief issue: %s", issue.as_dict) + + log.debug("Model: %s", solver.pretty_print_model(solver.get_model(constraints))) + return [issue] diff --git a/mythril/laser/ethereum/call.py b/mythril/laser/ethereum/call.py index 918c1d3f..b003e2fb 100644 --- a/mythril/laser/ethereum/call.py +++ b/mythril/laser/ethereum/call.py @@ -4,7 +4,7 @@ parameters for the new global state.""" import logging import re -from typing import Union, List, cast, Callable +from typing import Union, List, cast, Callable, Optional import mythril.laser.ethereum.util as util from mythril.laser.ethereum import natives @@ -84,6 +84,7 @@ def get_callee_address( except TypeError: log.debug("Symbolic call encountered") + # TODO: This is broken. Now it should be Storage[(\d+)]. match = re.search(r"storage_(\d+)", str(simplify(symbolic_to_address))) log.debug("CALL to: " + str(simplify(symbolic_to_address))) @@ -99,7 +100,7 @@ def get_callee_address( "0x{:040X}".format(environment.active_account.address.value), index ) # TODO: verify whether this happens or not - except: + except Exception: log.debug("Error accessing contract storage.") raise ValueError @@ -120,28 +121,27 @@ def get_callee_account( :param dynamic_loader: dynamic loader to use :return: Account belonging to callee """ - environment = global_state.environment accounts = global_state.accounts try: return global_state.accounts[int(callee_address, 16)] except KeyError: # We have a valid call address, but contract is not in the modules list - log.debug("Module with address " + callee_address + " not loaded.") + log.debug("Module with address %s not loaded.", callee_address) if dynamic_loader is None: - raise ValueError() + raise ValueError("dynamic_loader is None") log.debug("Attempting to load dependency") try: code = dynamic_loader.dynld(callee_address) except ValueError as error: - log.debug("Unable to execute dynamic loader because: {}".format(str(error))) + log.debug("Unable to execute dynamic loader because: %s", error) raise error if code is None: log.debug("No code returned, not a contract account?") - raise ValueError() + raise ValueError("No code returned") log.debug("Dependency loaded: " + callee_address) callee_account = Account( @@ -213,7 +213,7 @@ def native_call( call_data: BaseCalldata, memory_out_offset: Union[int, Expression], memory_out_size: Union[int, Expression], -) -> Union[List[GlobalState], None]: +) -> Optional[List[GlobalState]]: if not 0 < int(callee_address, 16) < 5: return None diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py index b3e3eaa6..16cb4d38 100644 --- a/mythril/laser/ethereum/instructions.py +++ b/mythril/laser/ethereum/instructions.py @@ -172,7 +172,7 @@ class Instruction: :return: """ # Generalize some ops - log.debug("Evaluating {}".format(self.op_code)) + log.debug("Evaluating %s at %i", self.op_code, global_state.mstate.pc) op = self.op_code.lower() if self.op_code.startswith("PUSH"): op = "push" diff --git a/mythril/laser/ethereum/state/account.py b/mythril/laser/ethereum/state/account.py index 1a32ad37..6f50e8ec 100644 --- a/mythril/laser/ethereum/state/account.py +++ b/mythril/laser/ethereum/state/account.py @@ -93,7 +93,8 @@ class Storage: ) self.printable_storage[item] = storage[item] return storage[item] - except ValueError: + except ValueError as e: + log.debug("Couldn't read storage at %s: %s", item, e) pass return simplify(storage[item]) diff --git a/mythril/laser/ethereum/svm.py b/mythril/laser/ethereum/svm.py index c5d5c68e..c9fc393c 100644 --- a/mythril/laser/ethereum/svm.py +++ b/mythril/laser/ethereum/svm.py @@ -3,7 +3,7 @@ import logging from collections import defaultdict from copy import copy from datetime import datetime, timedelta -from typing import Callable, Dict, DefaultDict, List, Tuple, Union +from typing import Callable, Dict, DefaultDict, List, Tuple, Optional from mythril.laser.ethereum.cfg import NodeFlags, Node, Edge, JumpType from mythril.laser.ethereum.evm_exceptions import StackUnderflowException @@ -75,6 +75,7 @@ class LaserEVM: self.total_states = 0 self.dynamic_loader = dynamic_loader + # TODO: What about using a deque here? self.work_list = [] # type: List[GlobalState] self.strategy = strategy(self.work_list, max_depth) self.max_depth = max_depth @@ -209,7 +210,7 @@ class LaserEVM: for hook in self._stop_sym_trans_hooks: hook() - def exec(self, create=False, track_gas=False) -> Union[List[GlobalState], None]: + def exec(self, create=False, track_gas=False) -> Optional[List[GlobalState]]: """ :param create: @@ -223,6 +224,7 @@ class LaserEVM: and create and self.time + timedelta(seconds=self.create_timeout) <= datetime.now() ): + log.debug("Hit create timeout, returning.") return final_states + [global_state] if track_gas else None if ( @@ -231,6 +233,7 @@ class LaserEVM: <= datetime.now() and not create ): + log.debug("Hit execution timeout, returning.") return final_states + [global_state] if track_gas else None try: @@ -243,8 +246,7 @@ class LaserEVM: state for state in new_states if state.mstate.constraints.is_possible ] - self.manage_cfg(op_code, new_states) - + self.manage_cfg(op_code, new_states) # TODO: What about op_code is None? if new_states: self.work_list += new_states elif track_gas: @@ -265,11 +267,11 @@ class LaserEVM: def execute_state( self, global_state: GlobalState - ) -> Tuple[List[GlobalState], Union[str, None]]: - """ + ) -> Tuple[List[GlobalState], Optional[str]]: + """Execute a single instruction in global_state. :param global_state: - :return: + :return: A list of successor states. """ # Execute hooks for hook in self._execute_state_hooks: @@ -405,6 +407,7 @@ class LaserEVM: for state in new_states: self._new_node_state(state) elif opcode == "JUMPI": + assert len(new_states) <= 2 for state in new_states: self._new_node_state( state, JumpType.CONDITIONAL, state.mstate.constraints[-1] From 518e33b45b1de974e94eb0779c9c91d8d1f2f6ee Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 29 Jul 2019 09:25:04 -0700 Subject: [PATCH 058/164] Fix incorrect values in some suicide evm tests --- .../evm_testsuite/VMTests/vmSystemOperations/suicide0.json | 4 ++-- .../VMTests/vmSystemOperations/suicideNotExistingAccount.json | 4 ++-- .../VMTests/vmSystemOperations/suicideSendEtherToMe.json | 4 ++-- tests/laser/evm_testsuite/evm_test.py | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/laser/evm_testsuite/VMTests/vmSystemOperations/suicide0.json b/tests/laser/evm_testsuite/VMTests/vmSystemOperations/suicide0.json index ae80e8f6..456a7c16 100644 --- a/tests/laser/evm_testsuite/VMTests/vmSystemOperations/suicide0.json +++ b/tests/laser/evm_testsuite/VMTests/vmSystemOperations/suicide0.json @@ -47,7 +47,7 @@ } }, "0xcd1722f3947def4cf144679da39c4c32bdc35681" : { - "balance" : "0x17", + "balance" : "0x0186a0", "code" : "0x6000355415600957005b60203560003555", "nonce" : "0x00", "storage" : { @@ -55,4 +55,4 @@ } } } -} \ No newline at end of file +} diff --git a/tests/laser/evm_testsuite/VMTests/vmSystemOperations/suicideNotExistingAccount.json b/tests/laser/evm_testsuite/VMTests/vmSystemOperations/suicideNotExistingAccount.json index ecf5d3bc..b72d6bcc 100644 --- a/tests/laser/evm_testsuite/VMTests/vmSystemOperations/suicideNotExistingAccount.json +++ b/tests/laser/evm_testsuite/VMTests/vmSystemOperations/suicideNotExistingAccount.json @@ -54,7 +54,7 @@ } }, "0xcd1722f3947def4cf144679da39c4c32bdc35681" : { - "balance" : "0x17", + "balance" : "0x0186a0", "code" : "0x6000355415600957005b60203560003555", "nonce" : "0x00", "storage" : { @@ -62,4 +62,4 @@ } } } -} \ No newline at end of file +} diff --git a/tests/laser/evm_testsuite/VMTests/vmSystemOperations/suicideSendEtherToMe.json b/tests/laser/evm_testsuite/VMTests/vmSystemOperations/suicideSendEtherToMe.json index 7a8f19b8..f27557ed 100644 --- a/tests/laser/evm_testsuite/VMTests/vmSystemOperations/suicideSendEtherToMe.json +++ b/tests/laser/evm_testsuite/VMTests/vmSystemOperations/suicideSendEtherToMe.json @@ -47,7 +47,7 @@ } }, "0xcd1722f3947def4cf144679da39c4c32bdc35681" : { - "balance" : "0x17", + "balance" : "0x0186a0", "code" : "0x6000355415600957005b60203560003555", "nonce" : "0x00", "storage" : { @@ -55,4 +55,4 @@ } } } -} \ No newline at end of file +} diff --git a/tests/laser/evm_testsuite/evm_test.py b/tests/laser/evm_testsuite/evm_test.py index e5d1727d..244b5844 100644 --- a/tests/laser/evm_testsuite/evm_test.py +++ b/tests/laser/evm_testsuite/evm_test.py @@ -165,8 +165,8 @@ def test_vmtest( # no more work to do if error happens or out of gas assert len(laser_evm.open_states) == 0 else: - assert len(final_states) == 1 - world_state = final_states[0].world_state + assert len(laser_evm.open_states) == 1 + world_state = laser_evm.open_states[0] for address, details in post_condition.items(): account = world_state[symbol_factory.BitVecVal(int(address, 16), 256)] From 83f0cc966de4a5e61f3aad82820cf066acb047cc Mon Sep 17 00:00:00 2001 From: palkeo Date: Mon, 29 Jul 2019 18:35:37 +0200 Subject: [PATCH 059/164] Reformat with black. --- mythril/analysis/modules/ether_thief.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mythril/analysis/modules/ether_thief.py b/mythril/analysis/modules/ether_thief.py index d7098179..67df35cd 100644 --- a/mythril/analysis/modules/ether_thief.py +++ b/mythril/analysis/modules/ether_thief.py @@ -93,7 +93,9 @@ class EtherThief(DetectionModule): Also constrain the addition from overflowing (otherwise the solver produces solutions with ridiculously high call values). """ - constraints += [BVAddNoOverflow(eth_sent_by_attacker, tx.call_value, signed=False)] + constraints += [ + BVAddNoOverflow(eth_sent_by_attacker, tx.call_value, signed=False) + ] eth_sent_by_attacker = Sum( eth_sent_by_attacker, tx.call_value * If(tx.caller == ATTACKER_ADDRESS, 1, 0), From a2a85081f60c883c756f6b7b4e283e41900fb3de Mon Sep 17 00:00:00 2001 From: palkeo Date: Mon, 29 Jul 2019 19:39:01 +0200 Subject: [PATCH 060/164] Add a logger in account.py --- mythril/laser/ethereum/state/account.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mythril/laser/ethereum/state/account.py b/mythril/laser/ethereum/state/account.py index 6f50e8ec..26f04a01 100644 --- a/mythril/laser/ethereum/state/account.py +++ b/mythril/laser/ethereum/state/account.py @@ -2,6 +2,7 @@ This includes classes representing accounts and their storage. """ +import logging from copy import copy, deepcopy from typing import Any, Dict, Union, Tuple, cast @@ -19,6 +20,8 @@ from mythril.laser.smt import ( from mythril.disassembler.disassembly import Disassembly from mythril.laser.smt import symbol_factory +log = logging.getLogger(__name__) + class StorageRegion: def __getitem__(self, item): From 49fbb5f82ff151484785158bd238c871cc44232e Mon Sep 17 00:00:00 2001 From: palkeo Date: Mon, 29 Jul 2019 20:43:25 +0200 Subject: [PATCH 061/164] Fix #1176: retrieve map items. --- mythril/laser/ethereum/state/account.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mythril/laser/ethereum/state/account.py b/mythril/laser/ethereum/state/account.py index 26f04a01..b5dcb3ae 100644 --- a/mythril/laser/ethereum/state/account.py +++ b/mythril/laser/ethereum/state/account.py @@ -76,8 +76,9 @@ class Storage: if is_keccak_storage: item = self._sanitize(cast(BitVecFunc, item).input_) value = storage[item] + if ( - value.value == 0 + (value.value == 0 or value.value is None) # 0 for Array, None for K and self.address and item.symbolic is False and self.address.value != 0 From 739ac0337b60740c06a148b4233333a6b0cff4f1 Mon Sep 17 00:00:00 2001 From: palkeo Date: Mon, 29 Jul 2019 20:49:05 +0200 Subject: [PATCH 062/164] Fix #1176: load map items --- mythril/laser/ethereum/state/account.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mythril/laser/ethereum/state/account.py b/mythril/laser/ethereum/state/account.py index 1a32ad37..07e82fad 100644 --- a/mythril/laser/ethereum/state/account.py +++ b/mythril/laser/ethereum/state/account.py @@ -74,7 +74,7 @@ class Storage: item = self._sanitize(cast(BitVecFunc, item).input_) value = storage[item] if ( - value.value == 0 + (value.value == 0 or value.value is None) # 0 for Array, None for K and self.address and item.symbolic is False and self.address.value != 0 From d22b791ba733474814805ff9447862ec548a9171 Mon Sep 17 00:00:00 2001 From: palkeo Date: Mon, 29 Jul 2019 21:40:55 +0200 Subject: [PATCH 063/164] Remove logging messages, and a "pass" --- mythril/analysis/modules/ether_thief.py | 6 ------ mythril/laser/ethereum/state/account.py | 1 - 2 files changed, 7 deletions(-) diff --git a/mythril/analysis/modules/ether_thief.py b/mythril/analysis/modules/ether_thief.py index 67df35cd..c1c5c442 100644 --- a/mythril/analysis/modules/ether_thief.py +++ b/mythril/analysis/modules/ether_thief.py @@ -120,8 +120,6 @@ class EtherThief(DetectionModule): state.current_transaction.caller == ATTACKER_ADDRESS, ] - log.debug("Constraints: %s", constraints) - try: transaction_sequence = solver.get_transaction_sequence(state, constraints) @@ -145,10 +143,6 @@ class EtherThief(DetectionModule): log.debug("No model found") return [] - log.debug("Found ether thief issue: %s", issue.as_dict) - - log.debug("Model: %s", solver.pretty_print_model(solver.get_model(constraints))) - return [issue] diff --git a/mythril/laser/ethereum/state/account.py b/mythril/laser/ethereum/state/account.py index b5dcb3ae..929efea7 100644 --- a/mythril/laser/ethereum/state/account.py +++ b/mythril/laser/ethereum/state/account.py @@ -99,7 +99,6 @@ class Storage: return storage[item] except ValueError as e: log.debug("Couldn't read storage at %s: %s", item, e) - pass return simplify(storage[item]) From 0844497e412c59df4cb0ed1aecdfdf7c3210dce9 Mon Sep 17 00:00:00 2001 From: palkeo Date: Mon, 29 Jul 2019 22:06:39 +0200 Subject: [PATCH 064/164] =?UTF-8?q?Revert=20commit=20that=20should=20not?= =?UTF-8?q?=20have=20been=20here=20(other=C2=A0PR)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mythril/laser/ethereum/state/account.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mythril/laser/ethereum/state/account.py b/mythril/laser/ethereum/state/account.py index 929efea7..07fb4323 100644 --- a/mythril/laser/ethereum/state/account.py +++ b/mythril/laser/ethereum/state/account.py @@ -76,9 +76,8 @@ class Storage: if is_keccak_storage: item = self._sanitize(cast(BitVecFunc, item).input_) value = storage[item] - if ( - (value.value == 0 or value.value is None) # 0 for Array, None for K + value.value == 0 and self.address and item.symbolic is False and self.address.value != 0 From a8dc3aa7bc1c6e5809cdf020b5ce062e06ab60d2 Mon Sep 17 00:00:00 2001 From: e-ngo Date: Mon, 29 Jul 2019 20:24:04 -0700 Subject: [PATCH 065/164] Refactored code --- mythril/analysis/security.py | 46 +++++++++++++++++------------------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/mythril/analysis/security.py b/mythril/analysis/security.py index 5c5e520a..4d95497a 100644 --- a/mythril/analysis/security.py +++ b/mythril/analysis/security.py @@ -65,38 +65,36 @@ def get_detection_modules(entrypoint, include_modules=(), custom_modules_directo _modules = [] - for loader, module_name, _ in pkgutil.walk_packages(modules.__path__): + custom_modules_directory = os.path.abspath(custom_modules_directory) + + if custom_modules_directory and custom_modules_directory not in sys.path: + sys.path.append(custom_modules_directory) + + custom_packages = ( + list(pkgutil.walk_packages([custom_modules_directory])) + if custom_modules_directory + else [] + ) + packages = list(pkgutil.walk_packages(modules.__path__)) + custom_packages + + for loader, module_name, _ in packages: if include_modules and module_name not in include_modules: continue if module_name != "base": - module = importlib.import_module("mythril.analysis.modules." + module_name) + try: + module = importlib.import_module( + "mythril.analysis.modules." + module_name + ) + except ModuleNotFoundError: + try: + module = importlib.import_module(module_name) + except ModuleNotFoundError: + raise ModuleNotFoundError module.log.setLevel(log.level) if module.detector.entrypoint == entrypoint: _modules.append(module) - if custom_modules_directory: - custom_modules = [os.path.abspath(custom_modules_directory)] - sys.path.append(custom_modules_directory) - for loader, module_name, _ in pkgutil.walk_packages(custom_modules): - if include_modules and module_name not in include_modules: - continue - - if module_name != "base": - module = importlib.import_module(module_name, custom_modules[0]) - module.log.setLevel(log.level) - if module.detector.entrypoint == entrypoint: - _modules.append(module) - - """ - for loader, module_name, _ in pkgutil.walk_packages([custom_modules_path]): - - custom_modules_path = os.path.abspath("custom/") - sys.path.append(custom_modules_path); - module = importlib.import_module( - module_name, custom_modules_path - ) - """ log.info("Found %s detection modules", len(_modules)) return _modules From 80c21a37a2e6f1e0688119102a2fdf220f56c133 Mon Sep 17 00:00:00 2001 From: e-ngo Date: Mon, 29 Jul 2019 22:13:21 -0700 Subject: [PATCH 066/164] Fixed some logic --- mythril/analysis/security.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/mythril/analysis/security.py b/mythril/analysis/security.py index 4d95497a..39c007e2 100644 --- a/mythril/analysis/security.py +++ b/mythril/analysis/security.py @@ -65,14 +65,14 @@ def get_detection_modules(entrypoint, include_modules=(), custom_modules_directo _modules = [] - custom_modules_directory = os.path.abspath(custom_modules_directory) + custom_modules_path = os.path.abspath(custom_modules_directory) - if custom_modules_directory and custom_modules_directory not in sys.path: - sys.path.append(custom_modules_directory) + if custom_modules_directory and custom_modules_path not in sys.path: + sys.path.append(custom_modules_path) custom_packages = ( - list(pkgutil.walk_packages([custom_modules_directory])) - if custom_modules_directory + list(pkgutil.walk_packages([custom_modules_path])) + if custom_modules_path else [] ) packages = list(pkgutil.walk_packages(modules.__path__)) + custom_packages @@ -88,7 +88,7 @@ def get_detection_modules(entrypoint, include_modules=(), custom_modules_directo ) except ModuleNotFoundError: try: - module = importlib.import_module(module_name) + module = importlib.import_module(module_name, custom_modules_path) except ModuleNotFoundError: raise ModuleNotFoundError module.log.setLevel(log.level) From b044fcd9b197dc792dd5d3b091a0a00265488a8b Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Tue, 30 Jul 2019 13:51:58 +0200 Subject: [PATCH 067/164] Fix wrongly named constructor arg --- mythril/interfaces/old_cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mythril/interfaces/old_cli.py b/mythril/interfaces/old_cli.py index 4a203320..ce8c4392 100644 --- a/mythril/interfaces/old_cli.py +++ b/mythril/interfaces/old_cli.py @@ -527,7 +527,7 @@ def parse_args(parser: argparse.ArgumentParser, args: argparse.Namespace) -> Non disassembler = MythrilDisassembler( eth=config.eth, solc_version=args.solv, - solc_json=args.solc_json, + solc__settings_json=args.solc_json, enable_online_lookup=args.query_signature, ) if args.truffle: From daadda18860b52fc7f13581eb8a4c0ecea425264 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Tue, 30 Jul 2019 14:36:43 +0200 Subject: [PATCH 068/164] Update old_cli.py --- mythril/interfaces/old_cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mythril/interfaces/old_cli.py b/mythril/interfaces/old_cli.py index ce8c4392..dbf1e798 100644 --- a/mythril/interfaces/old_cli.py +++ b/mythril/interfaces/old_cli.py @@ -527,7 +527,7 @@ def parse_args(parser: argparse.ArgumentParser, args: argparse.Namespace) -> Non disassembler = MythrilDisassembler( eth=config.eth, solc_version=args.solv, - solc__settings_json=args.solc_json, + solc_settings_json=args.solc_json, enable_online_lookup=args.query_signature, ) if args.truffle: From 938dee8e6eb64bf740200d1ebf6f6248742edf03 Mon Sep 17 00:00:00 2001 From: e-ngo Date: Tue, 30 Jul 2019 18:20:40 -0700 Subject: [PATCH 069/164] Revert "Fixed some logic" This reverts commit 80c21a37a2e6f1e0688119102a2fdf220f56c133. --- mythril/analysis/security.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/mythril/analysis/security.py b/mythril/analysis/security.py index 39c007e2..4d95497a 100644 --- a/mythril/analysis/security.py +++ b/mythril/analysis/security.py @@ -65,14 +65,14 @@ def get_detection_modules(entrypoint, include_modules=(), custom_modules_directo _modules = [] - custom_modules_path = os.path.abspath(custom_modules_directory) + custom_modules_directory = os.path.abspath(custom_modules_directory) - if custom_modules_directory and custom_modules_path not in sys.path: - sys.path.append(custom_modules_path) + if custom_modules_directory and custom_modules_directory not in sys.path: + sys.path.append(custom_modules_directory) custom_packages = ( - list(pkgutil.walk_packages([custom_modules_path])) - if custom_modules_path + list(pkgutil.walk_packages([custom_modules_directory])) + if custom_modules_directory else [] ) packages = list(pkgutil.walk_packages(modules.__path__)) + custom_packages @@ -88,7 +88,7 @@ def get_detection_modules(entrypoint, include_modules=(), custom_modules_directo ) except ModuleNotFoundError: try: - module = importlib.import_module(module_name, custom_modules_path) + module = importlib.import_module(module_name) except ModuleNotFoundError: raise ModuleNotFoundError module.log.setLevel(log.level) From c6e8cc8ffa354ce1a7a6b30afeaf9b66efbe771a Mon Sep 17 00:00:00 2001 From: e-ngo Date: Tue, 30 Jul 2019 18:24:36 -0700 Subject: [PATCH 070/164] Revert "Refactored code" This reverts commit a8dc3aa7bc1c6e5809cdf020b5ce062e06ab60d2. --- mythril/analysis/security.py | 46 +++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/mythril/analysis/security.py b/mythril/analysis/security.py index 4d95497a..5c5e520a 100644 --- a/mythril/analysis/security.py +++ b/mythril/analysis/security.py @@ -65,36 +65,38 @@ def get_detection_modules(entrypoint, include_modules=(), custom_modules_directo _modules = [] - custom_modules_directory = os.path.abspath(custom_modules_directory) - - if custom_modules_directory and custom_modules_directory not in sys.path: - sys.path.append(custom_modules_directory) - - custom_packages = ( - list(pkgutil.walk_packages([custom_modules_directory])) - if custom_modules_directory - else [] - ) - packages = list(pkgutil.walk_packages(modules.__path__)) + custom_packages - - for loader, module_name, _ in packages: + for loader, module_name, _ in pkgutil.walk_packages(modules.__path__): if include_modules and module_name not in include_modules: continue if module_name != "base": - try: - module = importlib.import_module( - "mythril.analysis.modules." + module_name - ) - except ModuleNotFoundError: - try: - module = importlib.import_module(module_name) - except ModuleNotFoundError: - raise ModuleNotFoundError + module = importlib.import_module("mythril.analysis.modules." + module_name) module.log.setLevel(log.level) if module.detector.entrypoint == entrypoint: _modules.append(module) + if custom_modules_directory: + custom_modules = [os.path.abspath(custom_modules_directory)] + sys.path.append(custom_modules_directory) + for loader, module_name, _ in pkgutil.walk_packages(custom_modules): + if include_modules and module_name not in include_modules: + continue + + if module_name != "base": + module = importlib.import_module(module_name, custom_modules[0]) + module.log.setLevel(log.level) + if module.detector.entrypoint == entrypoint: + _modules.append(module) + + """ + for loader, module_name, _ in pkgutil.walk_packages([custom_modules_path]): + + custom_modules_path = os.path.abspath("custom/") + sys.path.append(custom_modules_path); + module = importlib.import_module( + module_name, custom_modules_path + ) + """ log.info("Found %s detection modules", len(_modules)) return _modules From db5db7cc8f7b928281ff698879dfa0b11faa674e Mon Sep 17 00:00:00 2001 From: e-ngo Date: Tue, 30 Jul 2019 18:42:53 -0700 Subject: [PATCH 071/164] Removed unnecessary comments and cleaned up code --- mythril/analysis/security.py | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/mythril/analysis/security.py b/mythril/analysis/security.py index 5c5e520a..ed741ecb 100644 --- a/mythril/analysis/security.py +++ b/mythril/analysis/security.py @@ -75,28 +75,20 @@ def get_detection_modules(entrypoint, include_modules=(), custom_modules_directo if module.detector.entrypoint == entrypoint: _modules.append(module) if custom_modules_directory: - custom_modules = [os.path.abspath(custom_modules_directory)] - sys.path.append(custom_modules_directory) + custom_modules_path = os.path.abspath(custom_modules_directory) + if custom_modules_path not in sys.path: + sys.path.append(custom_modules_path) - for loader, module_name, _ in pkgutil.walk_packages(custom_modules): + for loader, module_name, _ in pkgutil.walk_packages([custom_modules_path]): if include_modules and module_name not in include_modules: continue if module_name != "base": - module = importlib.import_module(module_name, custom_modules[0]) + module = importlib.import_module(module_name, custom_modules_path) module.log.setLevel(log.level) if module.detector.entrypoint == entrypoint: _modules.append(module) - """ - for loader, module_name, _ in pkgutil.walk_packages([custom_modules_path]): - - custom_modules_path = os.path.abspath("custom/") - sys.path.append(custom_modules_path); - module = importlib.import_module( - module_name, custom_modules_path - ) - """ log.info("Found %s detection modules", len(_modules)) return _modules From db449a32d86266f5ee1055ac31ee7b2fb9ea8735 Mon Sep 17 00:00:00 2001 From: e-ngo <52668908+e-ngo@users.noreply.github.com> Date: Thu, 1 Aug 2019 05:48:10 -0700 Subject: [PATCH 072/164] Added ProgramCounterException to display a clearer message when pc is out of bounds. (#1180) --- mythril/laser/ethereum/evm_exceptions.py | 6 ++++++ mythril/laser/ethereum/state/global_state.py | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/mythril/laser/ethereum/evm_exceptions.py b/mythril/laser/ethereum/evm_exceptions.py index 63460e2b..fd99aed5 100644 --- a/mythril/laser/ethereum/evm_exceptions.py +++ b/mythril/laser/ethereum/evm_exceptions.py @@ -35,3 +35,9 @@ class OutOfGasException(VmException): """A VM exception denoting the current execution has run out of gas.""" pass + + +class ProgramCounterException(VmException): + """A VM exception denoting an invalid PC value (No stop instruction is reached).""" + + pass diff --git a/mythril/laser/ethereum/state/global_state.py b/mythril/laser/ethereum/state/global_state.py index 86364a69..0c43980b 100644 --- a/mythril/laser/ethereum/state/global_state.py +++ b/mythril/laser/ethereum/state/global_state.py @@ -9,6 +9,7 @@ from mythril.laser.ethereum.cfg import Node from mythril.laser.ethereum.state.environment import Environment from mythril.laser.ethereum.state.machine_state import MachineState from mythril.laser.ethereum.state.annotation import StateAnnotation +from mythril.laser.ethereum.evm_exceptions import ProgramCounterException if TYPE_CHECKING: from mythril.laser.ethereum.state.world_state import WorldState @@ -88,6 +89,10 @@ class GlobalState: """ instructions = self.environment.code.instruction_list + if self.mstate.pc >= len(instructions): + raise ProgramCounterException( + "PC: {} can not be reached.".format(self.mstate.pc) + ) return instructions[self.mstate.pc] @property From 838de40858421130b3313c71a0f8238571becdcf Mon Sep 17 00:00:00 2001 From: NeolithEra <3226592650@qq.com> Date: Thu, 1 Aug 2019 21:16:27 +0800 Subject: [PATCH 073/164] Fix dependency conflict for issue (#1181) * Fix dependency conflict for issue * Fix dependency conflict for issue --- requirements.txt | 1 - setup.py | 1 - 2 files changed, 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 1b49350c..11382df3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,6 @@ eth-keys>=0.2.0b3,<0.3.0 eth-rlp>=0.1.0 eth-tester==0.1.0b32 eth-typing>=2.0.0 -eth-utils>=1.0.1 jinja2>=2.9 mock persistent>=4.2.0 diff --git a/setup.py b/setup.py index d76f546e..ce115193 100755 --- a/setup.py +++ b/setup.py @@ -32,7 +32,6 @@ REQUIRED = [ "py-solc", "plyvel", "eth_abi==1.3.0", - "eth-utils>=1.0.1", "eth-account>=0.1.0a2,<=0.3.0", "eth-hash>=0.1.0", "eth-keyfile>=0.5.1", From 8ada755aa7890cdddab66c1401b7ca3349dfa51b Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 1 Aug 2019 10:51:01 -0700 Subject: [PATCH 074/164] Minor grammar improvements --- mythril/interfaces/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mythril/interfaces/cli.py b/mythril/interfaces/cli.py index 486b5aaf..8962fa16 100644 --- a/mythril/interfaces/cli.py +++ b/mythril/interfaces/cli.py @@ -398,8 +398,8 @@ def create_analyzer_parser(analyzer_parser: ArgumentParser): ) options.add_argument( "--custom-modules-directory", - help="designates a separate directory to search for security modules from", - metavar="PATH_TO_CUSTOM_MODULES_DIRECTORY", + help="designates a separate directory to search for custom analysis modules", + metavar="CUSTOM_MODULES_DIRECTORY", ) From a0c5fa9bee2d44116cec66b8e5c11a962fb3fe9f Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Mon, 5 Aug 2019 14:18:15 +0200 Subject: [PATCH 075/164] Bump version number --- mythril/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mythril/__version__.py b/mythril/__version__.py index 3490630e..a9b138d9 100644 --- a/mythril/__version__.py +++ b/mythril/__version__.py @@ -4,4 +4,4 @@ This file is suitable for sourcing inside POSIX shell, e.g. bash as well as for importing into Python. """ -__version__ = "v0.21.12" +__version__ = "v0.21.13" From a22e2ba81fd46b6355a5589776d89c7ef9d0e7fd Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 5 Aug 2019 17:55:18 -0700 Subject: [PATCH 076/164] Fix error preventing contract name from being set --- mythril/solidity/soliditycontract.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mythril/solidity/soliditycontract.py b/mythril/solidity/soliditycontract.py index 6adbc9ea..534189d3 100644 --- a/mythril/solidity/soliditycontract.py +++ b/mythril/solidity/soliditycontract.py @@ -113,8 +113,11 @@ class SolidityContract(EVMContract): # If no contract name is specified, get the last bytecode entry for the input file else: - for filename, contract in sorted(data["contracts"][input_file].items()): + 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(";") From c0b74d8187ca0501ac807e175f0100108a425fd9 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 5 Aug 2019 18:23:08 -0700 Subject: [PATCH 077/164] Fix compilation error handling --- mythril/ethereum/util.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/mythril/ethereum/util.py b/mythril/ethereum/util.py index da1c6a9b..69905681 100644 --- a/mythril/ethereum/util.py +++ b/mythril/ethereum/util.py @@ -61,14 +61,7 @@ def get_solc_json(file, solc_binary="solc", solc_settings_json=None): try: p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) stdout, stderr = p.communicate(bytes(input_json, "utf8")) - ret = p.returncode - # TODO: check json.loads(out)['errors'] for fatal errors. - 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." @@ -76,10 +69,15 @@ def get_solc_json(file, solc_binary="solc", solc_settings_json=None): out = stdout.decode("UTF-8") - if not len(out): - raise CompilerError("Compilation failed.") + result = json.loads(out) + + for error in result["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): From 6f337f8bc7f0eaf14444b530b288e1f3c2e3b779 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 5 Aug 2019 19:05:16 -0700 Subject: [PATCH 078/164] Document MythX analysis --- docs/source/index.rst | 1 + docs/source/mythx-analysis.rst | 63 +++++++++++++++++++++++++++++++ docs/source/security-analysis.rst | 2 +- 3 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 docs/source/mythx-analysis.rst 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 From 73d11a6405284e5db3666959d7b4a5aa56315c4b Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 6 Aug 2019 09:08:02 -0700 Subject: [PATCH 079/164] Update wiki links to Read the Docs links --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4eee32a3..df3ad81c 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Install from Pypi: $ pip3 install mythril ``` -See the [Wiki](https://github.com/ConsenSys/mythril/wiki/Installation-and-Setup) for more detailed instructions. +See the [docs](https://mythril-classic.readthedocs.io/en/master/installation.html) for more detailed instructions. ## Usage @@ -75,7 +75,7 @@ Caller: [ATTACKER], function: commencekilling(), txdata: 0x7c11da20, value: 0x0 ``` -Instructions for using Mythril are found on the [Wiki](https://github.com/ConsenSys/mythril/wiki). +Instructions for using Mythril are found on the [docs](https://mythril-classic.readthedocs.io/en/master/). For support or general discussions please join the Mythril community on [Discord](https://discord.gg/E3YrVtG). From 29796dd8dae23bbead276160905a816eefe75720 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 6 Aug 2019 16:20:11 -0700 Subject: [PATCH 080/164] Fix bug compiling contracts without solc warnings --- mythril/ethereum/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mythril/ethereum/util.py b/mythril/ethereum/util.py index 69905681..7c9020f5 100644 --- a/mythril/ethereum/util.py +++ b/mythril/ethereum/util.py @@ -71,7 +71,7 @@ def get_solc_json(file, solc_binary="solc", solc_settings_json=None): result = json.loads(out) - for error in result["errors"]: + for error in result.get("errors", []): if error["severity"] == "error": raise CompilerError( "Solc experienced a fatal error.\n\n%s" % error["formattedMessage"] From b08a9d9d84d8159dd3a3665498bb4ec098a12db4 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 7 Aug 2019 12:00:18 -0700 Subject: [PATCH 081/164] Constrain all transaction callers to have <= 1000 ETH --- mythril/analysis/solver.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/mythril/analysis/solver.py b/mythril/analysis/solver.py index 82e3f2ee..8755c6c5 100644 --- a/mythril/analysis/solver.py +++ b/mythril/analysis/solver.py @@ -8,7 +8,7 @@ from mythril.analysis.analysis_args import analysis_args from mythril.laser.ethereum.state.global_state import GlobalState from mythril.laser.ethereum.state.constraints import Constraints from mythril.laser.ethereum.transaction import BaseTransaction -from mythril.laser.smt import UGE, Optimize, symbol_factory +from mythril.laser.smt import UGE, Optimize, symbol_factory, simplify from mythril.laser.ethereum.time_handler import time_handler from mythril.exceptions import UnsatError from mythril.laser.ethereum.transaction.transaction_models import ( @@ -197,15 +197,21 @@ def _set_minimisation_constraints( # Minimize minimize.append(transaction.call_data.calldatasize) + constraints.append( + UGE( + symbol_factory.BitVecVal(1000000000000000000000, 256), + world_state.starting_balances[transaction.caller], + ) + ) + for account in world_state.accounts.values(): # Lazy way to prevent overflows and to ensure "reasonable" balances # Each account starts with less than 100 ETH - if account.address.symbolic: - constraints.append( - UGE( - symbol_factory.BitVecVal(100000000000000000000, 256), - world_state.starting_balances[account.address], - ) + constraints.append( + UGE( + symbol_factory.BitVecVal(100000000000000000000, 256), + world_state.starting_balances[account.address], ) + ) return constraints, tuple(minimize) From 4bfaadea98bed96a7b4b9385614c99eb74f66261 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 7 Aug 2019 12:03:17 -0700 Subject: [PATCH 082/164] Remove unneeded import --- mythril/analysis/solver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mythril/analysis/solver.py b/mythril/analysis/solver.py index 8755c6c5..3358d537 100644 --- a/mythril/analysis/solver.py +++ b/mythril/analysis/solver.py @@ -8,7 +8,7 @@ from mythril.analysis.analysis_args import analysis_args from mythril.laser.ethereum.state.global_state import GlobalState from mythril.laser.ethereum.state.constraints import Constraints from mythril.laser.ethereum.transaction import BaseTransaction -from mythril.laser.smt import UGE, Optimize, symbol_factory, simplify +from mythril.laser.smt import UGE, Optimize, symbol_factory from mythril.laser.ethereum.time_handler import time_handler from mythril.exceptions import UnsatError from mythril.laser.ethereum.transaction.transaction_models import ( From 9dc1007a152eac16b3216298b4bd3f9a6ec68974 Mon Sep 17 00:00:00 2001 From: e-ngo Date: Wed, 7 Aug 2019 13:26:24 -0700 Subject: [PATCH 083/164] Implemented extcodehash --- mythril/laser/ethereum/instructions.py | 26 ++++++++++++---- tests/instructions/extcodehash_test.py | 43 ++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 6 deletions(-) create mode 100644 tests/instructions/extcodehash_test.py diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py index 16cb4d38..531aaf03 100644 --- a/mythril/laser/ethereum/instructions.py +++ b/mythril/laser/ethereum/instructions.py @@ -46,6 +46,8 @@ from mythril.laser.ethereum.transaction import ( ContractCreationTransaction, ) +from mythril.support.support_utils import get_code_hash + from mythril.support.loader import DynLoader log = logging.getLogger(__name__) @@ -1155,18 +1157,30 @@ class Instruction: global_state=global_state, ) - @StateTransition + @StateTransition() def extcodehash_(self, global_state: GlobalState) -> List[GlobalState]: """ :param global_state: :return: List of global states possible, list of size 1 in this case """ - # TODO: To be implemented - address = global_state.mstate.stack.pop() - global_state.mstate.stack.append( - global_state.new_bitvec("extcodehash_{}".format(str(address)), 256) - ) + world_state = global_state.world_state + stack = global_state.mstate.stack + address = stack.pop() + if address.symbolic: + log.debug("unsupported symbolic address for EXTCODEHASH") + stack.append(global_state.new_bitvec("extcodehash_" + str(address), 256)) + return [global_state] + address = address.value + + mask = int((symbol_factory.BitVecVal(TT256M1, 256) >> 96).value) + address = address & mask + if address not in world_state.accounts: + code_hash = symbol_factory.BitVecVal(0, 256) + else: + code = world_state.accounts_exist_or_load(hex(address), self.dynamic_loader) + code_hash = get_code_hash(code) + stack.append(code_hash) return [global_state] @StateTransition() diff --git a/tests/instructions/extcodehash_test.py b/tests/instructions/extcodehash_test.py new file mode 100644 index 00000000..e4586285 --- /dev/null +++ b/tests/instructions/extcodehash_test.py @@ -0,0 +1,43 @@ +from mythril.disassembler.disassembly import Disassembly +from mythril.laser.ethereum.state.environment import Environment +from mythril.laser.ethereum.state.account import Account +from mythril.laser.ethereum.state.machine_state import MachineState +from mythril.laser.ethereum.state.global_state import GlobalState +from mythril.laser.ethereum.state.world_state import WorldState +from mythril.laser.ethereum.instructions import Instruction +from mythril.laser.ethereum.transaction.transaction_models import MessageCallTransaction + +from mythril.support.support_utils import get_code_hash + +from mythril.laser.smt import symbol_factory + +def test_extcodehash_concrete(): + # Arrange + world_state = WorldState() + account = world_state.create_account(balance=10, address=101) + account.code = Disassembly("60606040") + world_state.create_account(balance = 10, address = 1000) + environment = Environment(account, None, None, None, None, None) + og_state = GlobalState( + world_state, environment, None, MachineState(gas_limit=8000000) + ) + og_state.transaction_stack.append( + (MessageCallTransaction(world_state=WorldState(), gas_limit=8000000), None) + ) + + instruction = Instruction("extcodehash", dynamic_loader=None) + + # If account does not exist, return 0 + og_state.mstate.stack = [symbol_factory.BitVecVal(1, 256)] + new_state = instruction.evaluate(og_state)[0] + assert new_state.mstate.stack[-1] == 0 + + # If account code does not exist, return hash of empty set. + og_state.mstate.stack = [symbol_factory.BitVecVal(1000, 256)] + new_state = instruction.evaluate(og_state)[0] + assert new_state.mstate.stack[-1] == '0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470' + + # If account code exists, return hash of the code. + og_state.mstate.stack = [symbol_factory.BitVecVal(101, 256)] + new_state = instruction.evaluate(og_state)[0] + assert new_state.mstate.stack[-1] == get_code_hash("60606040") \ No newline at end of file From 1a0ea91267af3323c71b44f1d1c44b20a9339e65 Mon Sep 17 00:00:00 2001 From: e-ngo Date: Wed, 7 Aug 2019 15:20:00 -0700 Subject: [PATCH 084/164] Refactored instructions for extcodehash and its test --- mythril/laser/ethereum/instructions.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py index 531aaf03..17173363 100644 --- a/mythril/laser/ethereum/instructions.py +++ b/mythril/laser/ethereum/instructions.py @@ -1166,15 +1166,14 @@ class Instruction: """ world_state = global_state.world_state stack = global_state.mstate.stack - address = stack.pop() + address = Extract(159, 0, stack.pop()) + if address.symbolic: log.debug("unsupported symbolic address for EXTCODEHASH") stack.append(global_state.new_bitvec("extcodehash_" + str(address), 256)) return [global_state] address = address.value - - mask = int((symbol_factory.BitVecVal(TT256M1, 256) >> 96).value) - address = address & mask + if address not in world_state.accounts: code_hash = symbol_factory.BitVecVal(0, 256) else: From 268d5d3bbbd136aaa01f782d69f021f813fd09df Mon Sep 17 00:00:00 2001 From: e-ngo Date: Wed, 7 Aug 2019 15:27:36 -0700 Subject: [PATCH 085/164] Ran black --- tests/instructions/extcodehash_test.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/instructions/extcodehash_test.py b/tests/instructions/extcodehash_test.py index e4586285..c677d5ef 100644 --- a/tests/instructions/extcodehash_test.py +++ b/tests/instructions/extcodehash_test.py @@ -11,12 +11,13 @@ from mythril.support.support_utils import get_code_hash from mythril.laser.smt import symbol_factory + def test_extcodehash_concrete(): # Arrange world_state = WorldState() account = world_state.create_account(balance=10, address=101) account.code = Disassembly("60606040") - world_state.create_account(balance = 10, address = 1000) + world_state.create_account(balance=10, address=1000) environment = Environment(account, None, None, None, None, None) og_state = GlobalState( world_state, environment, None, MachineState(gas_limit=8000000) @@ -24,9 +25,9 @@ def test_extcodehash_concrete(): og_state.transaction_stack.append( (MessageCallTransaction(world_state=WorldState(), gas_limit=8000000), None) ) - + instruction = Instruction("extcodehash", dynamic_loader=None) - + # If account does not exist, return 0 og_state.mstate.stack = [symbol_factory.BitVecVal(1, 256)] new_state = instruction.evaluate(og_state)[0] @@ -35,9 +36,12 @@ def test_extcodehash_concrete(): # If account code does not exist, return hash of empty set. og_state.mstate.stack = [symbol_factory.BitVecVal(1000, 256)] new_state = instruction.evaluate(og_state)[0] - assert new_state.mstate.stack[-1] == '0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470' + assert ( + new_state.mstate.stack[-1] + == "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470" + ) # If account code exists, return hash of the code. og_state.mstate.stack = [symbol_factory.BitVecVal(101, 256)] new_state = instruction.evaluate(og_state)[0] - assert new_state.mstate.stack[-1] == get_code_hash("60606040") \ No newline at end of file + assert new_state.mstate.stack[-1] == get_code_hash("60606040") From 7cf89a0dfee15169602eb96a346a7bf3e07f05fa Mon Sep 17 00:00:00 2001 From: e-ngo Date: Wed, 7 Aug 2019 15:49:10 -0700 Subject: [PATCH 086/164] Fixed linting errors --- mythril/laser/ethereum/instructions.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py index 17173363..d53b033d 100644 --- a/mythril/laser/ethereum/instructions.py +++ b/mythril/laser/ethereum/instructions.py @@ -1172,13 +1172,12 @@ class Instruction: log.debug("unsupported symbolic address for EXTCODEHASH") stack.append(global_state.new_bitvec("extcodehash_" + str(address), 256)) return [global_state] - address = address.value - if address not in world_state.accounts: + if address.value not in world_state.accounts: code_hash = symbol_factory.BitVecVal(0, 256) else: - code = world_state.accounts_exist_or_load(hex(address), self.dynamic_loader) - code_hash = get_code_hash(code) + code = world_state.accounts_exist_or_load(hex(address.value), self.dynamic_loader) + code_hash = symbol_factory.BitVecVal(int(get_code_hash(code), 16), 256) stack.append(code_hash) return [global_state] From 59dffe97f7a8be06be25c41ac56780a817044d81 Mon Sep 17 00:00:00 2001 From: e-ngo Date: Wed, 7 Aug 2019 15:57:37 -0700 Subject: [PATCH 087/164] Ran black --- mythril/laser/ethereum/instructions.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py index d53b033d..37e90acd 100644 --- a/mythril/laser/ethereum/instructions.py +++ b/mythril/laser/ethereum/instructions.py @@ -1176,7 +1176,9 @@ class Instruction: if address.value not in world_state.accounts: code_hash = symbol_factory.BitVecVal(0, 256) else: - code = world_state.accounts_exist_or_load(hex(address.value), self.dynamic_loader) + code = world_state.accounts_exist_or_load( + hex(address.value), self.dynamic_loader + ) code_hash = symbol_factory.BitVecVal(int(get_code_hash(code), 16), 256) stack.append(code_hash) return [global_state] From 83937a9a5545e312e771afdc94bad8de3fc91ba5 Mon Sep 17 00:00:00 2001 From: e-ngo Date: Wed, 7 Aug 2019 16:26:47 -0700 Subject: [PATCH 088/164] Fixed errors in asserts in extcodehash tests --- tests/instructions/extcodehash_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/instructions/extcodehash_test.py b/tests/instructions/extcodehash_test.py index c677d5ef..ff1fff52 100644 --- a/tests/instructions/extcodehash_test.py +++ b/tests/instructions/extcodehash_test.py @@ -37,11 +37,11 @@ def test_extcodehash_concrete(): og_state.mstate.stack = [symbol_factory.BitVecVal(1000, 256)] new_state = instruction.evaluate(og_state)[0] assert ( - new_state.mstate.stack[-1] - == "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470" + hex(new_state.mstate.stack[-1].value) + == get_code_hash("") ) # If account code exists, return hash of the code. og_state.mstate.stack = [symbol_factory.BitVecVal(101, 256)] new_state = instruction.evaluate(og_state)[0] - assert new_state.mstate.stack[-1] == get_code_hash("60606040") + assert hex(new_state.mstate.stack[-1].value) == get_code_hash("60606040") From 1e225eb7701ae11ade6cc6a9c017a00355e42f67 Mon Sep 17 00:00:00 2001 From: e-ngo Date: Wed, 7 Aug 2019 16:33:30 -0700 Subject: [PATCH 089/164] Ran black... --- tests/instructions/extcodehash_test.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/instructions/extcodehash_test.py b/tests/instructions/extcodehash_test.py index ff1fff52..e1ea30c1 100644 --- a/tests/instructions/extcodehash_test.py +++ b/tests/instructions/extcodehash_test.py @@ -36,10 +36,7 @@ def test_extcodehash_concrete(): # If account code does not exist, return hash of empty set. og_state.mstate.stack = [symbol_factory.BitVecVal(1000, 256)] new_state = instruction.evaluate(og_state)[0] - assert ( - hex(new_state.mstate.stack[-1].value) - == get_code_hash("") - ) + assert hex(new_state.mstate.stack[-1].value) == get_code_hash("") # If account code exists, return hash of the code. og_state.mstate.stack = [symbol_factory.BitVecVal(101, 256)] From 5f3a84107eeed5f5ff4a0454a6e262ff6bb9019c Mon Sep 17 00:00:00 2001 From: e-ngo <52668908+e-ngo@users.noreply.github.com> Date: Wed, 7 Aug 2019 18:15:45 -0700 Subject: [PATCH 090/164] "true" to True (#1188) --- mythril/analysis/report.py | 2 +- mythril/interfaces/cli.py | 4 +--- mythril/interfaces/old_cli.py | 4 +--- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/mythril/analysis/report.py b/mythril/analysis/report.py index 1a39c0ff..d40c466b 100644 --- a/mythril/analysis/report.py +++ b/mythril/analysis/report.py @@ -238,7 +238,7 @@ class Report: return {} logs = [] # type: List[Dict] for exception in self.exceptions: - logs += [{"level": "error", "hidden": "true", "msg": exception}] + logs += [{"level": "error", "hidden": True, "msg": exception}] return {"logs": logs} def as_swc_standard_format(self): diff --git a/mythril/interfaces/cli.py b/mythril/interfaces/cli.py index 8962fa16..2cbf3f4c 100644 --- a/mythril/interfaces/cli.py +++ b/mythril/interfaces/cli.py @@ -63,9 +63,7 @@ def exit_with_error(format_, message): "sourceType": "", "sourceFormat": "", "sourceList": [], - "meta": { - "logs": [{"level": "error", "hidden": "true", "msg": message}] - }, + "meta": {"logs": [{"level": "error", "hidden": True, "msg": message}]}, } ] print(json.dumps(result)) diff --git a/mythril/interfaces/old_cli.py b/mythril/interfaces/old_cli.py index 0deedc85..211d1c8e 100644 --- a/mythril/interfaces/old_cli.py +++ b/mythril/interfaces/old_cli.py @@ -44,9 +44,7 @@ def exit_with_error(format_, message): "sourceType": "", "sourceFormat": "", "sourceList": [], - "meta": { - "logs": [{"level": "error", "hidden": "true", "msg": message}] - }, + "meta": {"logs": [{"level": "error", "hidden": True, "msg": message}]}, } ] print(json.dumps(result)) From 4b1bcbeb3e86df0f0fb22eb80b10bcf2c1c6d71f Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Thu, 8 Aug 2019 06:46:14 +0530 Subject: [PATCH 091/164] Version update --- mythril/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mythril/__version__.py b/mythril/__version__.py index a9b138d9..b7808215 100644 --- a/mythril/__version__.py +++ b/mythril/__version__.py @@ -4,4 +4,4 @@ This file is suitable for sourcing inside POSIX shell, e.g. bash as well as for importing into Python. """ -__version__ = "v0.21.13" +__version__ = "v0.21.14" From 4f539f6bf069f277f1e4d009fd86449fd2d810f6 Mon Sep 17 00:00:00 2001 From: palkeo Date: Thu, 8 Aug 2019 20:35:34 +0200 Subject: [PATCH 092/164] Fix a bug: correctly copy printable storage over (#1191) * Fix a bug: the key for the accounts is an integer. * Fix a bug: correctly copy printable storage. Also fix another serialization bug that appears now that the printable storage is not empty. * Revert "Fix a bug: the key for the accounts is an integer." Should not have been included in that PR. --- mythril/analysis/solver.py | 2 +- mythril/laser/ethereum/state/account.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mythril/analysis/solver.py b/mythril/analysis/solver.py index 3358d537..d949dd57 100644 --- a/mythril/analysis/solver.py +++ b/mythril/analysis/solver.py @@ -137,7 +137,7 @@ def _get_concrete_state(initial_accounts: Dict, min_price_dict: Dict[str, int]): data = dict() # type: Dict[str, Union[int, str]] data["nonce"] = account.nonce data["code"] = account.code.bytecode - data["storage"] = account.storage.printable_storage + data["storage"] = str(account.storage) data["balance"] = hex(min_price_dict.get(address, 0)) accounts[hex(address)] = data return {"accounts": accounts} diff --git a/mythril/laser/ethereum/state/account.py b/mythril/laser/ethereum/state/account.py index 4c47516a..0b9aec62 100644 --- a/mythril/laser/ethereum/state/account.py +++ b/mythril/laser/ethereum/state/account.py @@ -144,7 +144,7 @@ class Storage: ) storage._standard_storage = deepcopy(self._standard_storage) storage._map_storage = deepcopy(self._map_storage) - storage.print_storage = copy(self.printable_storage) + storage.printable_storage = copy(self.printable_storage) return storage def __str__(self) -> str: From 8397ca89ad0ec5ff3f2a6fd1e3cce996d6e86bbc Mon Sep 17 00:00:00 2001 From: palkeo Date: Thu, 8 Aug 2019 20:52:16 +0200 Subject: [PATCH 093/164] Fix a bug: the key for the accounts is an integer. (#1190) --- mythril/laser/ethereum/call.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mythril/laser/ethereum/call.py b/mythril/laser/ethereum/call.py index 753d97d4..2e471037 100644 --- a/mythril/laser/ethereum/call.py +++ b/mythril/laser/ethereum/call.py @@ -125,8 +125,6 @@ def get_callee_account( :param dynamic_loader: dynamic loader to use :return: Account belonging to callee """ - accounts = global_state.accounts - if isinstance(callee_address, BitVec): if callee_address.symbolic: return Account(callee_address, balances=global_state.world_state.balances) @@ -161,7 +159,7 @@ def get_callee_account( dynamic_loader=dynamic_loader, balances=global_state.world_state.balances, ) - accounts[callee_address] = callee_account + global_state.accounts[int(callee_address, 16)] = callee_account return callee_account From 04d295b329f74b133c2cbfe27a54268815cc28f9 Mon Sep 17 00:00:00 2001 From: palkeo Date: Thu, 8 Aug 2019 19:26:03 +0200 Subject: [PATCH 094/164] Track what storage has been initialized, and load non-initialized storage. --- mythril/laser/ethereum/state/account.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/mythril/laser/ethereum/state/account.py b/mythril/laser/ethereum/state/account.py index 0b9aec62..98ca205c 100644 --- a/mythril/laser/ethereum/state/account.py +++ b/mythril/laser/ethereum/state/account.py @@ -60,6 +60,7 @@ class Storage: self.printable_storage = {} # type: Dict[BitVec, BitVec] self.dynld = dynamic_loader + self.storage_keys_loaded = set() self.address = address @staticmethod @@ -75,12 +76,11 @@ class Storage: storage, is_keccak_storage = self._get_corresponding_storage(item) if is_keccak_storage: item = self._sanitize(cast(BitVecFunc, item).input_) - value = storage[item] if ( - (value.value == 0 or value.value is None) # 0 for Array, None for K - and self.address - and item.symbolic is False + self.address and self.address.value != 0 + and int(item.value) not in self.storage_keys_loaded + and item.symbolic is False and (self.dynld and self.dynld.storage_loading) ): try: @@ -94,8 +94,8 @@ class Storage: ), 256, ) + self.storage_keys_loaded.add(int(item.value)) self.printable_storage[item] = storage[item] - return storage[item] except ValueError as e: log.debug("Couldn't read storage at %s: %s", item, e) @@ -136,6 +136,7 @@ class Storage: if is_keccak_storage: key = self._sanitize(key.input_) storage[key] = value + self.storage_keys_loaded.add(int(key.value)) def __deepcopy__(self, memodict=dict()): concrete = isinstance(self._standard_storage, K) @@ -145,6 +146,7 @@ class Storage: storage._standard_storage = deepcopy(self._standard_storage) storage._map_storage = deepcopy(self._map_storage) storage.printable_storage = copy(self.printable_storage) + storage.storage_keys_loaded = copy(self.storage_keys_loaded) return storage def __str__(self) -> str: From a9fd342baf0b5317935778a8cb66cc92783afea9 Mon Sep 17 00:00:00 2001 From: palkeo Date: Thu, 8 Aug 2019 19:50:05 +0200 Subject: [PATCH 095/164] Check if item is symbolic first. --- mythril/laser/ethereum/state/account.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mythril/laser/ethereum/state/account.py b/mythril/laser/ethereum/state/account.py index 98ca205c..2c1d8887 100644 --- a/mythril/laser/ethereum/state/account.py +++ b/mythril/laser/ethereum/state/account.py @@ -79,8 +79,8 @@ class Storage: if ( self.address and self.address.value != 0 - and int(item.value) not in self.storage_keys_loaded and item.symbolic is False + and int(item.value) not in self.storage_keys_loaded and (self.dynld and self.dynld.storage_loading) ): try: From b46f2a4001b2193a75d4e5af1053df85753350ea Mon Sep 17 00:00:00 2001 From: palkeo Date: Thu, 8 Aug 2019 20:04:15 +0200 Subject: [PATCH 096/164] Check that key is not symbolic in __setitem__ --- mythril/laser/ethereum/state/account.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mythril/laser/ethereum/state/account.py b/mythril/laser/ethereum/state/account.py index 2c1d8887..fc3204cf 100644 --- a/mythril/laser/ethereum/state/account.py +++ b/mythril/laser/ethereum/state/account.py @@ -136,7 +136,8 @@ class Storage: if is_keccak_storage: key = self._sanitize(key.input_) storage[key] = value - self.storage_keys_loaded.add(int(key.value)) + if key.symbolic is False: + self.storage_keys_loaded.add(int(key.value)) def __deepcopy__(self, memodict=dict()): concrete = isinstance(self._standard_storage, K) From ef1cbc405229a3ac66477d1d119b5fa76feec670 Mon Sep 17 00:00:00 2001 From: palkeo Date: Fri, 9 Aug 2019 00:03:56 +0200 Subject: [PATCH 097/164] Add type annotation. --- mythril/laser/ethereum/state/account.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mythril/laser/ethereum/state/account.py b/mythril/laser/ethereum/state/account.py index fc3204cf..53649221 100644 --- a/mythril/laser/ethereum/state/account.py +++ b/mythril/laser/ethereum/state/account.py @@ -4,7 +4,7 @@ This includes classes representing accounts and their storage. """ import logging from copy import copy, deepcopy -from typing import Any, Dict, Union, Tuple, cast +from typing import Any, Dict, Union, Tuple, Set, cast from mythril.laser.smt import ( @@ -60,7 +60,7 @@ class Storage: self.printable_storage = {} # type: Dict[BitVec, BitVec] self.dynld = dynamic_loader - self.storage_keys_loaded = set() + self.storage_keys_loaded = set() # type: Set[int] self.address = address @staticmethod From bcf528e13968a5189e6974db93c88d00c6c5502c Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 8 Aug 2019 17:42:32 -0700 Subject: [PATCH 098/164] Fix pruning of calls --- mythril/analysis/solver.py | 8 ++++ .../implementations/dependency_pruner.py | 39 +++++++++++++------ .../implementations/mutation_pruner.py | 4 ++ 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/mythril/analysis/solver.py b/mythril/analysis/solver.py index d949dd57..9504c71a 100644 --- a/mythril/analysis/solver.py +++ b/mythril/analysis/solver.py @@ -204,6 +204,14 @@ def _set_minimisation_constraints( ) ) + # FIXME: This shouldn't be needed. + constraints.append( + UGE( + symbol_factory.BitVecVal(1000000000000000000000, 256), + transaction.call_value, + ) + ) + for account in world_state.accounts.values(): # Lazy way to prevent overflows and to ensure "reasonable" balances # Each account starts with less than 100 ETH diff --git a/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py b/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py index bb78fdd8..b2b1ee16 100644 --- a/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py +++ b/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py @@ -25,6 +25,7 @@ class DependencyAnnotation(StateAnnotation): def __init__(self): self.storage_loaded = [] # type: List self.storage_written = {} # type: Dict[int, List] + self.has_call = False # type: bool self.path = [0] # type: List self.blocks_seen = set() # type: Set[int] @@ -32,6 +33,7 @@ class DependencyAnnotation(StateAnnotation): result = DependencyAnnotation() result.storage_loaded = copy(self.storage_loaded) result.storage_written = copy(self.storage_written) + result.has_call = self.has_call result.path = copy(self.path) result.blocks_seen = copy(self.blocks_seen) return result @@ -134,6 +136,7 @@ class DependencyPruner(LaserPlugin): def _reset(self): self.iteration = 0 + self.calls_on_path = {} # type: Dict[int, bool] self.sloads_on_path = {} # type: Dict[int, List[object]] self.sstores_on_path = {} # type: Dict[int, List[object]] self.storage_accessed_global = set() # type: Set @@ -166,6 +169,17 @@ class DependencyPruner(LaserPlugin): else: self.sstores_on_path[address] = [target_location] + def update_calls(self, path: List[int]) -> None: + """Update the dependency map for the block offsets on the given path. + + :param path + :param target_location + """ + + for address in path: + if address in self.sstores_on_path: + self.calls_on_path[address] = True + def wanna_execute(self, address: int, annotation: DependencyAnnotation) -> bool: """Decide whether the basic block starting at 'address' should be executed. @@ -175,6 +189,9 @@ class DependencyPruner(LaserPlugin): storage_write_cache = annotation.get_storage_write_cache(self.iteration - 1) + if address in self.calls_on_path: + return True + # Skip "pure" paths that don't have any dependencies. if address not in self.sloads_on_path: @@ -270,6 +287,13 @@ class DependencyPruner(LaserPlugin): self.update_sloads(annotation.path, location) self.storage_accessed_global.add(location) + @symbolic_vm.pre_hook("CALL") + def stop_hook(state: GlobalState): + annotation = get_dependency_annotation(state) + + self.update_calls(annotation.path) + annotation.has_call = True + @symbolic_vm.pre_hook("STOP") def stop_hook(state: GlobalState): _transaction_end(state) @@ -293,11 +317,14 @@ class DependencyPruner(LaserPlugin): for index in annotation.storage_written: self.update_sstores(annotation.path, index) + if annotation.has_call: + self.update_calls(annotation.path) + def _check_basic_block(address: int, annotation: DependencyAnnotation): """This method is where the actual pruning happens. :param address: Start address (bytecode offset) of the block - :param annotation + :param annotation: """ # Don't skip any blocks in the contract creation transaction @@ -338,13 +365,3 @@ class DependencyPruner(LaserPlugin): annotation.storage_loaded = [] world_state_annotation.annotations_stack.append(annotation) - - log.debug( - "Iteration {}: Adding world state at address {}, end of function {}.\nDependency map: {}\nStorage written: {}".format( - self.iteration, - state.get_current_instruction()["address"], - state.node.function_name, - self.sloads_on_path, - annotation.storage_written[self.iteration], - ) - ) diff --git a/mythril/laser/ethereum/plugins/implementations/mutation_pruner.py b/mythril/laser/ethereum/plugins/implementations/mutation_pruner.py index 8a6fc9d6..8b19b292 100644 --- a/mythril/laser/ethereum/plugins/implementations/mutation_pruner.py +++ b/mythril/laser/ethereum/plugins/implementations/mutation_pruner.py @@ -45,6 +45,10 @@ class MutationPruner(LaserPlugin): def mutator_hook(global_state: GlobalState): global_state.annotate(MutationAnnotation()) + @symbolic_vm.pre_hook("CALL") + def mutator_hook(global_state: GlobalState): + global_state.annotate(MutationAnnotation()) + @symbolic_vm.laser_hook("add_world_state") def world_state_filter_hook(global_state: GlobalState): if And( From 66c604b72c98799e7abb35aa82c0c37b429f3cb7 Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 8 Aug 2019 18:11:15 -0700 Subject: [PATCH 099/164] Fix invalid function names --- .../ethereum/plugins/implementations/dependency_pruner.py | 2 +- .../laser/ethereum/plugins/implementations/mutation_pruner.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py b/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py index b2b1ee16..bfff2c4d 100644 --- a/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py +++ b/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py @@ -288,7 +288,7 @@ class DependencyPruner(LaserPlugin): self.storage_accessed_global.add(location) @symbolic_vm.pre_hook("CALL") - def stop_hook(state: GlobalState): + def call_hook(state: GlobalState): annotation = get_dependency_annotation(state) self.update_calls(annotation.path) diff --git a/mythril/laser/ethereum/plugins/implementations/mutation_pruner.py b/mythril/laser/ethereum/plugins/implementations/mutation_pruner.py index 8b19b292..86755610 100644 --- a/mythril/laser/ethereum/plugins/implementations/mutation_pruner.py +++ b/mythril/laser/ethereum/plugins/implementations/mutation_pruner.py @@ -42,11 +42,11 @@ class MutationPruner(LaserPlugin): """ @symbolic_vm.pre_hook("SSTORE") - def mutator_hook(global_state: GlobalState): + def sstore_mutator_hook(global_state: GlobalState): global_state.annotate(MutationAnnotation()) @symbolic_vm.pre_hook("CALL") - def mutator_hook(global_state: GlobalState): + def call_mutator_hook(global_state: GlobalState): global_state.annotate(MutationAnnotation()) @symbolic_vm.laser_hook("add_world_state") From 52ba355b657f34c11dd405057a76e46ac8fb9bab Mon Sep 17 00:00:00 2001 From: e-ngo Date: Thu, 8 Aug 2019 23:30:52 -0700 Subject: [PATCH 100/164] Add logic for CREATE opcode, still need to add unit test for CREATE. Refactored extcodehash unit tests into separate test units. --- mythril/laser/ethereum/gas.py | 5 +-- mythril/laser/ethereum/instructions.py | 42 ++++++++++++++++++++++---- tests/instructions/extcodehash_test.py | 34 ++++++++++++--------- 3 files changed, 56 insertions(+), 25 deletions(-) diff --git a/mythril/laser/ethereum/gas.py b/mythril/laser/ethereum/gas.py index 7e283d15..4d8de488 100644 --- a/mythril/laser/ethereum/gas.py +++ b/mythril/laser/ethereum/gas.py @@ -180,10 +180,7 @@ OPCODE_GAS = { "LOG3": (4 * 375, 4 * 375 + 8 * 32), "LOG4": (5 * 375, 5 * 375 + 8 * 32), "CREATE": (32000, 32000), - "CREATE2": ( - 32000, - 32000, - ), # TODO: The gas value is dynamic, to be done while implementing create2 + "CREATE2": (32000, 32000), # TODO: Make the gas value is dynamic "CALL": (700, 700 + 9000 + 25000), "NATIVE_COST": calculate_native_gas, "CALLCODE": (700, 700 + 9000 + 25000), diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py index 1c663ded..35e98e0c 100644 --- a/mythril/laser/ethereum/instructions.py +++ b/mythril/laser/ethereum/instructions.py @@ -1605,12 +1605,42 @@ class Instruction: :param global_state: :return: """ - # TODO: implement me - state = global_state.mstate - state.stack.pop(), state.stack.pop(), state.stack.pop() - # Not supported - state.stack.append(0) - return [global_state] + mstate = global_state.mstate + environment = global_state.environment + world_state = global_state.world_state + + call_value, mem_offset, mem_size = ( + mstate.stack.pop(), + mstate.stack.pop(), + mstate.stack.pop(), + ) + + try: + call_data = mstate.memory[ + util.get_concrete_int(mem_offset) : util.get_concrete_int( + mem_offset + mem_size + ) + ] + except TypeError: + log.debug("Create with symbolic length or offset. Not supported") + + caller = environment.sender + gas_price = environment.gasprice + origin = environment.origin + transaction = ContractCreationTransaction( + world_state=world_state, + caller=caller, + call_data=call_data, + gas_price=gas_price, + origin=origin, + call_value=call_value, + ) + + contract_address = transaction.callee_account.address + + mstate.stack.append(contract_address) + + raise TransactionStartSignal(transaction, self.op_code) @StateTransition() def create2_(self, global_state: GlobalState) -> List[GlobalState]: diff --git a/tests/instructions/extcodehash_test.py b/tests/instructions/extcodehash_test.py index e1ea30c1..14f2ad65 100644 --- a/tests/instructions/extcodehash_test.py +++ b/tests/instructions/extcodehash_test.py @@ -11,33 +11,37 @@ from mythril.support.support_utils import get_code_hash from mythril.laser.smt import symbol_factory +# Arrange +world_state = WorldState() +account = world_state.create_account(balance=10, address=101) +account.code = Disassembly("60606040") +world_state.create_account(balance=10, address=1000) +environment = Environment(account, None, None, None, None, None) +og_state = GlobalState(world_state, environment, None, MachineState(gas_limit=8000000)) +og_state.transaction_stack.append( + (MessageCallTransaction(world_state=WorldState(), gas_limit=8000000), None) +) -def test_extcodehash_concrete(): - # Arrange - world_state = WorldState() - account = world_state.create_account(balance=10, address=101) - account.code = Disassembly("60606040") - world_state.create_account(balance=10, address=1000) - environment = Environment(account, None, None, None, None, None) - og_state = GlobalState( - world_state, environment, None, MachineState(gas_limit=8000000) - ) - og_state.transaction_stack.append( - (MessageCallTransaction(world_state=WorldState(), gas_limit=8000000), None) - ) - - instruction = Instruction("extcodehash", dynamic_loader=None) +instruction = Instruction("extcodehash", dynamic_loader=None) + + +def test_extcodehash_no_account(): # If account does not exist, return 0 og_state.mstate.stack = [symbol_factory.BitVecVal(1, 256)] new_state = instruction.evaluate(og_state)[0] assert new_state.mstate.stack[-1] == 0 + +def test_extcodehash_no_code(): + # If account code does not exist, return hash of empty set. og_state.mstate.stack = [symbol_factory.BitVecVal(1000, 256)] new_state = instruction.evaluate(og_state)[0] assert hex(new_state.mstate.stack[-1].value) == get_code_hash("") + +def test_extcodehash_return_hash(): # If account code exists, return hash of the code. og_state.mstate.stack = [symbol_factory.BitVecVal(101, 256)] new_state = instruction.evaluate(og_state)[0] From 2eb94f3043b1348e27dc40ed57423515251f6be2 Mon Sep 17 00:00:00 2001 From: palkeo Date: Fri, 9 Aug 2019 17:38:52 +0200 Subject: [PATCH 101/164] Small cleanups (no semantic changes) (#1196) * Small cleanups. * Run black. --- mythril/analysis/modules/suicide.py | 6 ++---- mythril/laser/ethereum/instructions.py | 8 +------- mythril/laser/ethereum/svm.py | 5 +++++ mythril/laser/ethereum/transaction/transaction_models.py | 8 ++++++++ 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/mythril/analysis/modules/suicide.py b/mythril/analysis/modules/suicide.py index 509b3137..b5a9b988 100644 --- a/mythril/analysis/modules/suicide.py +++ b/mythril/analysis/modules/suicide.py @@ -60,9 +60,7 @@ class SuicideModule(DetectionModule): to = state.mstate.stack[-1] - log.debug( - "[SUICIDE] SUICIDE in function " + state.environment.active_function_name - ) + log.debug("SUICIDE in function %s", state.environment.active_function_name) description_head = "The contract can be killed by anyone." @@ -103,7 +101,7 @@ class SuicideModule(DetectionModule): ) return [issue] except UnsatError: - log.info("[UNCHECKED_SUICIDE] no model found") + log.debug("No model found") return [] diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py index 9020babc..5d2b9e93 100644 --- a/mythril/laser/ethereum/instructions.py +++ b/mythril/laser/ethereum/instructions.py @@ -198,6 +198,7 @@ class Instruction: """ # Generalize some ops log.debug("Evaluating %s at %i", self.op_code, global_state.mstate.pc) + op = self.op_code.lower() if self.op_code.startswith("PUSH"): op = "push" @@ -783,15 +784,10 @@ class Instruction: log.debug("Unsupported symbolic calldata offset in CALLDATACOPY") dstart = simplify(op1) - size_sym = False try: size = util.get_concrete_int(op2) # type: Union[int, BitVec] except TypeError: log.debug("Unsupported symbolic size in CALLDATACOPY") - size = simplify(op2) - size_sym = True - - if size_sym: size = 320 # The excess size will get overwritten size = cast(int, size) @@ -1397,7 +1393,6 @@ class Instruction: state = global_state.mstate index = state.stack.pop() - state.stack.append(global_state.environment.active_account.storage[index]) return [global_state] @@ -1410,7 +1405,6 @@ class Instruction: """ state = global_state.mstate index, value = state.stack.pop(), state.stack.pop() - global_state.environment.active_account.storage[index] = value return [global_state] diff --git a/mythril/laser/ethereum/svm.py b/mythril/laser/ethereum/svm.py index c9fc393c..88b362c8 100644 --- a/mythril/laser/ethereum/svm.py +++ b/mythril/laser/ethereum/svm.py @@ -257,6 +257,7 @@ class LaserEVM: def _add_world_state(self, global_state: GlobalState): """ Stores the world_state of the passed global state in the open states""" + for hook in self._add_world_state_hooks: try: hook(global_state) @@ -325,6 +326,8 @@ class LaserEVM: new_global_state.node = global_state.node new_global_state.mstate.constraints = global_state.mstate.constraints + log.debug("Starting new transaction %s", start_signal.transaction) + return [new_global_state], op_code except TransactionEndSignal as end_signal: @@ -332,6 +335,8 @@ class LaserEVM: -1 ] + log.debug("Ending transaction %s.", transaction) + if return_global_state is None: if ( not isinstance(transaction, ContractCreationTransaction) diff --git a/mythril/laser/ethereum/transaction/transaction_models.py b/mythril/laser/ethereum/transaction/transaction_models.py index bccdf7aa..c2b18b9c 100644 --- a/mythril/laser/ethereum/transaction/transaction_models.py +++ b/mythril/laser/ethereum/transaction/transaction_models.py @@ -134,6 +134,14 @@ class BaseTransaction: def initial_global_state(self) -> GlobalState: raise NotImplementedError + def __str__(self) -> str: + return "{} {} from {} to {:#42x}".format( + self.__class__.__name__, + self.id, + self.caller, + int(str(self.callee_account.address)) if self.callee_account else -1, + ) + class MessageCallTransaction(BaseTransaction): """Transaction object models an transaction.""" From 0281be777b278c99630b88dc85cb81cec36c63cd Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Fri, 9 Aug 2019 09:58:31 -0700 Subject: [PATCH 102/164] Fix onsite storage by using proper variables (#1194) --- mythril/laser/ethereum/state/account.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/mythril/laser/ethereum/state/account.py b/mythril/laser/ethereum/state/account.py index 53649221..c6afa0c3 100644 --- a/mythril/laser/ethereum/state/account.py +++ b/mythril/laser/ethereum/state/account.py @@ -75,7 +75,9 @@ class Storage: def __getitem__(self, item: BitVec) -> BitVec: storage, is_keccak_storage = self._get_corresponding_storage(item) if is_keccak_storage: - item = self._sanitize(cast(BitVecFunc, item).input_) + sanitized_item = self._sanitize(cast(BitVecFunc, item).input_) + else: + sanitized_item = item if ( self.address and self.address.value != 0 @@ -84,7 +86,7 @@ class Storage: and (self.dynld and self.dynld.storage_loading) ): try: - storage[item] = symbol_factory.BitVecVal( + storage[sanitized_item] = symbol_factory.BitVecVal( int( self.dynld.read_storage( contract_address="0x{:040X}".format(self.address.value), @@ -95,11 +97,11 @@ class Storage: 256, ) self.storage_keys_loaded.add(int(item.value)) - self.printable_storage[item] = storage[item] + self.printable_storage[item] = storage[sanitized_item] except ValueError as e: log.debug("Couldn't read storage at %s: %s", item, e) - return simplify(storage[item]) + return simplify(storage[sanitized_item]) @staticmethod def get_map_index(key: BitVec) -> BitVec: From fa29f9b67ff1a02950625929097507d840a4bd4d Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Fri, 9 Aug 2019 18:39:14 -0700 Subject: [PATCH 103/164] Fix some ether problems and fix minimise (#1198) --- mythril/analysis/modules/ether_thief.py | 15 ++++++++++++++- mythril/analysis/solver.py | 10 +--------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/mythril/analysis/modules/ether_thief.py b/mythril/analysis/modules/ether_thief.py index 87dd5be0..65e5c8e8 100644 --- a/mythril/analysis/modules/ether_thief.py +++ b/mythril/analysis/modules/ether_thief.py @@ -6,10 +6,15 @@ from copy import copy from mythril.analysis import solver from mythril.analysis.modules.base import DetectionModule from mythril.analysis.report import Issue -from mythril.laser.ethereum.transaction.symbolic import ATTACKER_ADDRESS +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 + from mythril.laser.smt import UGT, symbol_factory, UGE log = logging.getLogger(__name__) @@ -82,6 +87,14 @@ class EtherThief(DetectionModule): that the Ether sent to the attacker's address is greater than the amount of Ether the attacker sent. """ + for tx in state.world_state.transaction_sequence: + """ + Constraint: All transactions must originate from regular users (not the creator/owner). + This prevents false positives where the owner willingly transfers ownership to another address. + """ + if not isinstance(tx, ContractCreationTransaction): + constraints += [tx.caller != CREATOR_ADDRESS] + attacker_address_bitvec = symbol_factory.BitVecVal(ATTACKER_ADDRESS, 256) constraints += [ diff --git a/mythril/analysis/solver.py b/mythril/analysis/solver.py index 9504c71a..b6959a82 100644 --- a/mythril/analysis/solver.py +++ b/mythril/analysis/solver.py @@ -196,7 +196,7 @@ def _set_minimisation_constraints( # Minimize minimize.append(transaction.call_data.calldatasize) - + minimize.append(transaction.call_value) constraints.append( UGE( symbol_factory.BitVecVal(1000000000000000000000, 256), @@ -204,14 +204,6 @@ def _set_minimisation_constraints( ) ) - # FIXME: This shouldn't be needed. - constraints.append( - UGE( - symbol_factory.BitVecVal(1000000000000000000000, 256), - transaction.call_value, - ) - ) - for account in world_state.accounts.values(): # Lazy way to prevent overflows and to ensure "reasonable" balances # Each account starts with less than 100 ETH From b3d20d22a53278be4809023f374a456c06d0185d Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Fri, 9 Aug 2019 20:30:11 -0700 Subject: [PATCH 104/164] Version update --- mythril/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mythril/__version__.py b/mythril/__version__.py index b7808215..8cb19bb6 100644 --- a/mythril/__version__.py +++ b/mythril/__version__.py @@ -4,4 +4,4 @@ This file is suitable for sourcing inside POSIX shell, e.g. bash as well as for importing into Python. """ -__version__ = "v0.21.14" +__version__ = "v0.21.15" From 7a025fadeefb5538b6f2b6a87051dc04fc7f8a04 Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Sun, 11 Aug 2019 18:22:42 -0700 Subject: [PATCH 105/164] Update the execution timeout for calls.sol test --- tests/laser/transaction/create_transaction_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/laser/transaction/create_transaction_test.py b/tests/laser/transaction/create_transaction_test.py index 5c4e93ad..574dcc06 100644 --- a/tests/laser/transaction/create_transaction_test.py +++ b/tests/laser/transaction/create_transaction_test.py @@ -44,7 +44,7 @@ def test_sym_exec(): contract, address=(util.get_indexed_address(0)), strategy="dfs", - execution_timeout=10, + execution_timeout=25, ) issues = fire_lasers(sym) From 06fce92c10f2c712f66cdeb41349ce6d92048f90 Mon Sep 17 00:00:00 2001 From: e-ngo Date: Sun, 11 Aug 2019 23:19:56 -0700 Subject: [PATCH 106/164] Refactored create_ opcode --- mythril/laser/ethereum/instructions.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py index 35e98e0c..40c46747 100644 --- a/mythril/laser/ethereum/instructions.py +++ b/mythril/laser/ethereum/instructions.py @@ -31,7 +31,7 @@ from mythril.laser.smt import symbol_factory import mythril.laser.ethereum.util as helper from mythril.laser.ethereum import util -from mythril.laser.ethereum.call import get_call_parameters, native_call +from mythril.laser.ethereum.call import get_call_parameters, native_call, get_call_data from mythril.laser.ethereum.evm_exceptions import ( VmException, StackUnderflowException, @@ -1616,17 +1616,20 @@ class Instruction: ) try: - call_data = mstate.memory[ - util.get_concrete_int(mem_offset) : util.get_concrete_int( - mem_offset + mem_size - ) - ] + call_data = get_call_data( + global_state, + util.get_concrete_int(mem_offset), + util.get_concrete_int(mem_offset + mem_size), + ) except TypeError: log.debug("Create with symbolic length or offset. Not supported") + mstate.stack.append(0, 256) + return [global_state] caller = environment.sender gas_price = environment.gasprice origin = environment.origin + transaction = ContractCreationTransaction( world_state=world_state, caller=caller, @@ -1636,10 +1639,6 @@ class Instruction: call_value=call_value, ) - contract_address = transaction.callee_account.address - - mstate.stack.append(contract_address) - raise TransactionStartSignal(transaction, self.op_code) @StateTransition() From dcdceefd5d0f6976236f6a77056e10186b9e6e56 Mon Sep 17 00:00:00 2001 From: palkeo Date: Mon, 12 Aug 2019 23:17:50 +0200 Subject: [PATCH 107/164] Fix support for calls forwarding the entire calldata. (#1195) * Fix support for calls forwarding the entire calldata. * Reformat with black. * uses_entire_calldata is always a Bool * Revert "uses_entire_calldata is always a Bool" This reverts commit 629cb531722cb73661353ccc2a1eb64af0c857d7. * Revert "Revert "uses_entire_calldata is always a Bool"" Didn't help, it passed before. Strange. Will need to find a way to investigate locally. --- mythril/laser/ethereum/call.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/mythril/laser/ethereum/call.py b/mythril/laser/ethereum/call.py index 2e471037..bc1c360c 100644 --- a/mythril/laser/ethereum/call.py +++ b/mythril/laser/ethereum/call.py @@ -16,7 +16,7 @@ from mythril.laser.ethereum.state.calldata import ( ConcreteCalldata, ) from mythril.laser.ethereum.state.global_state import GlobalState -from mythril.laser.smt import BitVec +from mythril.laser.smt import BitVec, Bool, is_true from mythril.laser.smt import simplify, Expression, symbol_factory from mythril.support.loader import DynLoader @@ -197,10 +197,10 @@ def get_call_data( ) uses_entire_calldata = simplify( - memory_size - global_state.environment.calldata.calldatasize == 0 + memory_size == global_state.environment.calldata.calldatasize ) - if uses_entire_calldata is True: + if is_true(uses_entire_calldata): return global_state.environment.calldata try: @@ -211,7 +211,9 @@ def get_call_data( ] return ConcreteCalldata(transaction_id, calldata_from_mem) except TypeError: - log.debug("Unsupported symbolic calldata offset") + log.debug( + "Unsupported symbolic calldata offset %s size %s", memory_start, memory_size + ) return SymbolicCalldata(transaction_id) From 42fec0c77095202987da11fa117ad211c93d67b5 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Wed, 14 Aug 2019 09:49:31 +0200 Subject: [PATCH 108/164] Change actor addresses in line with MythX test case conventions --- mythril/laser/ethereum/transaction/symbolic.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mythril/laser/ethereum/transaction/symbolic.py b/mythril/laser/ethereum/transaction/symbolic.py index 1a895048..16e0d54f 100644 --- a/mythril/laser/ethereum/transaction/symbolic.py +++ b/mythril/laser/ethereum/transaction/symbolic.py @@ -21,9 +21,10 @@ CREATOR_ADDRESS = 0xAFFEAFFEAFFEAFFEAFFEAFFEAFFEAFFEAFFEAFFE ATTACKER_ADDRESS = 0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF ACTOR_ADDRESSES = [ - symbol_factory.BitVecVal(0xAFFEAFFEAFFEAFFEAFFEAFFEAFFEAFFEAFFEAFFE, 256), - symbol_factory.BitVecVal(0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF, 256), - symbol_factory.BitVecVal(0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEE, 256), + symbol_factory.BitVecVal(0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA, 256), + symbol_factory.BitVecVal(0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB, 256), + symbol_factory.BitVecVal(0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC, 256), + symbol_factory.BitVecVal(0xDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD, 256), ] From 106bf49fda9dce41a6c5283792b5d65544e8b424 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Wed, 14 Aug 2019 19:52:44 +0200 Subject: [PATCH 109/164] Reduce number of actors --- mythril/laser/ethereum/transaction/symbolic.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mythril/laser/ethereum/transaction/symbolic.py b/mythril/laser/ethereum/transaction/symbolic.py index 16e0d54f..99bb7252 100644 --- a/mythril/laser/ethereum/transaction/symbolic.py +++ b/mythril/laser/ethereum/transaction/symbolic.py @@ -24,7 +24,6 @@ ACTOR_ADDRESSES = [ symbol_factory.BitVecVal(0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA, 256), symbol_factory.BitVecVal(0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB, 256), symbol_factory.BitVecVal(0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC, 256), - symbol_factory.BitVecVal(0xDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD, 256), ] From 48216680872db5cfff8c30ba0840b82cf35be735 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Thu, 15 Aug 2019 12:14:16 +0200 Subject: [PATCH 110/164] Fix standard actors list --- mythril/laser/ethereum/transaction/symbolic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mythril/laser/ethereum/transaction/symbolic.py b/mythril/laser/ethereum/transaction/symbolic.py index 99bb7252..2212e805 100644 --- a/mythril/laser/ethereum/transaction/symbolic.py +++ b/mythril/laser/ethereum/transaction/symbolic.py @@ -21,9 +21,9 @@ CREATOR_ADDRESS = 0xAFFEAFFEAFFEAFFEAFFEAFFEAFFEAFFEAFFEAFFE ATTACKER_ADDRESS = 0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF ACTOR_ADDRESSES = [ + symbol_factory.BitVecVal(0xAFFEAFFEAFFEAFFEAFFEAFFEAFFEAFFEAFFEAFFE, 256), + symbol_factory.BitVecVal(0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF, 256), symbol_factory.BitVecVal(0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA, 256), - symbol_factory.BitVecVal(0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB, 256), - symbol_factory.BitVecVal(0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC, 256), ] From 01724da877971027ea82a080a8e202c20bea69c0 Mon Sep 17 00:00:00 2001 From: e-ngo Date: Tue, 20 Aug 2019 00:29:46 -0700 Subject: [PATCH 111/164] Added new calculation to codesize. --- mythril/laser/ethereum/instructions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py index 5d2b9e93..1895eefe 100644 --- a/mythril/laser/ethereum/instructions.py +++ b/mythril/laser/ethereum/instructions.py @@ -902,7 +902,8 @@ class Instruction: state = global_state.mstate environment = global_state.environment disassembly = environment.code - state.stack.append(len(disassembly.bytecode) // 2) + no_of_bytes = min(len(disassembly.bytecode) / 2, 320) + (len(disassembly.bytecode) / 2) + state.stack.append(no_of_bytes) return [global_state] @StateTransition(enable_gas=False) From 4295b828f5810749baf3f037c76dda62e53c2c76 Mon Sep 17 00:00:00 2001 From: e-ngo Date: Wed, 21 Aug 2019 00:21:53 -0700 Subject: [PATCH 112/164] Starting implementation of creationdata. --- mythril/laser/ethereum/state/creationdata.py | 51 ++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 mythril/laser/ethereum/state/creationdata.py diff --git a/mythril/laser/ethereum/state/creationdata.py b/mythril/laser/ethereum/state/creationdata.py new file mode 100644 index 00000000..dbcd2991 --- /dev/null +++ b/mythril/laser/ethereum/state/creationdata.py @@ -0,0 +1,51 @@ +"""This module declares classes to represent creation code and call data.""" +from typing import cast, Union, Tuple, List + + +from enum import Enum +from typing import Any, Union + +from z3 import Model +from z3.z3types import Z3Exception + +from mythril.laser.ethereum.util import get_concrete_int +from mythril.laser.smt import ( + Array, + BitVec, + Bool, + Concat, + Expression, + If, + K, + simplify, + symbol_factory, +) + +from mythril.laser.smt.bitvec_helper import Sum + +from mythril.laser.ethereum.state.calldata import BaseCalldata + + +class CreationData: + """CreationData class This represents creation bytecode and calldata constructor argument provided when sending a + transaction to a contract.""" + + def __init__(self, code, calldata: BaseCalldata) -> None: + """ + + :param tx_id: + """ + self.code = code + self.calldata = calldata + + @property + def size(self) -> BitVec: + """ + + :return: codesize in bytes for this CreationData object + """ + calldata_size = self.calldata.calldatasize + code_size = symbol_factory.BitVecVal(len(self.code) // 2, 256) + if calldata_size.symbolic: + return Sum(code_size, calldata_size) + return code_size + calldata_size From c8d91b6f417e1db94269f651defba8cad5c25a72 Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Wed, 21 Aug 2019 17:45:19 +0530 Subject: [PATCH 113/164] Add the rest of the precompiles (#990) * Support modular exponentiation for concrete data * Add elliptic curve operations * Fix type hints and refactor code * Support usage of the rest of the native contracts * Remove unused imports * Add tests for elliptic curve functions * Use a constant for native functions count * Update py_ecc version * Use PRECOMPILE_COUNT over hardcoded value * Use shortened list comprehensives * Clean up imports * Use precompile count in checking precompile calls * Refactor code --- mythril/analysis/call_helpers.py | 3 +- mythril/analysis/modules/external_calls.py | 3 +- mythril/analysis/symbolic.py | 6 +- mythril/laser/ethereum/call.py | 13 +- mythril/laser/ethereum/natives.py | 168 +++++++++++++----- mythril/laser/ethereum/util.py | 24 +++ requirements.txt | 2 +- setup.py | 2 +- tests/laser/Precompiles/test_ec_add.py | 27 +++ .../laser/Precompiles/test_elliptic_curves.py | 35 ++++ tests/laser/Precompiles/test_elliptic_mul.py | 27 +++ tests/laser/Precompiles/test_mod_exp.py | 61 +++++++ 12 files changed, 321 insertions(+), 50 deletions(-) create mode 100644 tests/laser/Precompiles/test_ec_add.py create mode 100644 tests/laser/Precompiles/test_elliptic_curves.py create mode 100644 tests/laser/Precompiles/test_elliptic_mul.py create mode 100644 tests/laser/Precompiles/test_mod_exp.py diff --git a/mythril/analysis/call_helpers.py b/mythril/analysis/call_helpers.py index 6cb796df..270ff5af 100644 --- a/mythril/analysis/call_helpers.py +++ b/mythril/analysis/call_helpers.py @@ -4,6 +4,7 @@ from typing import Union from mythril.analysis.ops import VarType, Call, get_variable from mythril.laser.ethereum.state.global_state import GlobalState +from mythril.laser.ethereum.natives import PRECOMPILE_COUNT def get_call_from_state(state: GlobalState) -> Union[Call, None]: @@ -28,7 +29,7 @@ def get_call_from_state(state: GlobalState) -> Union[Call, None]: get_variable(stack[-7]), ) - if to.type == VarType.CONCRETE and 0 < to.val < 5: + if to.type == VarType.CONCRETE and 0 < to.val <= PRECOMPILE_COUNT: return None if meminstart.type == VarType.CONCRETE and meminsz.type == VarType.CONCRETE: diff --git a/mythril/analysis/modules/external_calls.py b/mythril/analysis/modules/external_calls.py index e80c8802..81cd6bd2 100644 --- a/mythril/analysis/modules/external_calls.py +++ b/mythril/analysis/modules/external_calls.py @@ -10,6 +10,7 @@ from mythril.laser.ethereum.transaction.transaction_models import ( 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 @@ -33,7 +34,7 @@ def _is_precompile_call(global_state: GlobalState): constraints += [ Or( to < symbol_factory.BitVecVal(1, 256), - to > symbol_factory.BitVecVal(16, 256), + to > symbol_factory.BitVecVal(PRECOMPILE_COUNT, 256), ) ] diff --git a/mythril/analysis/symbolic.py b/mythril/analysis/symbolic.py index 02b5c8ca..ce98b0ce 100644 --- a/mythril/analysis/symbolic.py +++ b/mythril/analysis/symbolic.py @@ -14,6 +14,7 @@ from mythril.laser.ethereum.strategy.basic import ( BasicSearchStrategy, ) +from mythril.laser.ethereum.natives import PRECOMPILE_COUNT from mythril.laser.ethereum.transaction.symbolic import ( ATTACKER_ADDRESS, CREATOR_ADDRESS, @@ -212,7 +213,10 @@ class SymExecWrapper: get_variable(stack[-7]), ) - if to.type == VarType.CONCRETE and to.val < 5: + if ( + to.type == VarType.CONCRETE + and 0 < to.val <= PRECOMPILE_COUNT + ): # ignore prebuilts continue diff --git a/mythril/laser/ethereum/call.py b/mythril/laser/ethereum/call.py index bc1c360c..24a8e2f3 100644 --- a/mythril/laser/ethereum/call.py +++ b/mythril/laser/ethereum/call.py @@ -10,13 +10,14 @@ import mythril.laser.ethereum.util as util from mythril.laser.ethereum import natives from mythril.laser.ethereum.gas import OPCODE_GAS from mythril.laser.ethereum.state.account import Account +from mythril.laser.ethereum.natives import PRECOMPILE_COUNT from mythril.laser.ethereum.state.calldata import ( BaseCalldata, SymbolicCalldata, ConcreteCalldata, ) from mythril.laser.ethereum.state.global_state import GlobalState -from mythril.laser.smt import BitVec, Bool, is_true +from mythril.laser.smt import BitVec, is_true from mythril.laser.smt import simplify, Expression, symbol_factory from mythril.support.loader import DynLoader @@ -51,7 +52,7 @@ def get_call_parameters( call_data = get_call_data(global_state, memory_input_offset, memory_input_size) if ( isinstance(callee_address, BitVec) - or int(callee_address, 16) >= 5 + or int(callee_address, 16) > PRECOMPILE_COUNT or int(callee_address, 16) == 0 ): callee_account = get_callee_account( @@ -223,8 +224,12 @@ def native_call( call_data: BaseCalldata, memory_out_offset: Union[int, Expression], memory_out_size: Union[int, Expression], -) -> Union[List[GlobalState], None]: - if isinstance(callee_address, BitVec) or not 0 < int(callee_address, 16) < 5: +) -> Optional[List[GlobalState]]: + + if ( + isinstance(callee_address, BitVec) + or not 0 < int(callee_address, 16) <= PRECOMPILE_COUNT + ): return None log.debug("Native contract called: " + callee_address) diff --git a/mythril/laser/ethereum/natives.py b/mythril/laser/ethereum/natives.py index ef38d8c9..991a172c 100644 --- a/mythril/laser/ethereum/natives.py +++ b/mythril/laser/ethereum/natives.py @@ -6,12 +6,20 @@ from typing import List, Union from ethereum.utils import ecrecover_to_pub from py_ecc.secp256k1 import N as secp256k1n +import py_ecc.optimized_bn128 as bn128 from rlp.utils import ALL_BYTES from mythril.laser.ethereum.state.calldata import BaseCalldata, ConcreteCalldata -from mythril.laser.ethereum.util import bytearray_to_int -from ethereum.utils import sha3 -from mythril.laser.smt import Concat, simplify +from mythril.laser.ethereum.util import extract_copy, extract32 +from ethereum.utils import ( + sha3, + big_endian_to_int, + safe_ord, + zpad, + int_to_big_endian, + encode_int32, +) +from ethereum.specials import validate_point log = logging.getLogger(__name__) @@ -22,35 +30,6 @@ class NativeContractException(Exception): pass -def int_to_32bytes( - i: int -) -> bytes: # used because int can't fit as bytes function's input - """ - - :param i: - :return: - """ - o = [0] * 32 - for x in range(32): - o[31 - x] = i & 0xFF - i >>= 8 - return bytes(o) - - -def extract32(data: bytearray, i: int) -> int: - """ - - :param data: - :param i: - :return: - """ - if i >= len(data): - return 0 - o = data[i : min(i + 32, len(data))] - o.extend(bytearray(32 - len(o))) - return bytearray_to_int(o) - - def ecrecover(data: List[int]) -> List[int]: """ @@ -59,14 +38,14 @@ def ecrecover(data: List[int]) -> List[int]: """ # TODO: Add type hints try: - byte_data = bytearray(data) - v = extract32(byte_data, 32) - r = extract32(byte_data, 64) - s = extract32(byte_data, 96) + bytes_data = bytearray(data) + v = extract32(bytes_data, 32) + r = extract32(bytes_data, 64) + s = extract32(bytes_data, 96) except TypeError: raise NativeContractException - message = b"".join([ALL_BYTES[x] for x in byte_data[0:32]]) + message = b"".join([ALL_BYTES[x] for x in bytes_data[0:32]]) if r >= secp256k1n or s >= secp256k1n or v < 27 or v > 28: return [] try: @@ -85,10 +64,10 @@ def sha256(data: List[int]) -> List[int]: :return: """ try: - byte_data = bytes(data) + bytes_data = bytes(data) except TypeError: raise NativeContractException - return list(bytearray(hashlib.sha256(byte_data).digest())) + return list(bytearray(hashlib.sha256(bytes_data).digest())) def ripemd160(data: List[int]) -> List[int]: @@ -120,6 +99,114 @@ def identity(data: List[int]) -> List[int]: return data +def mod_exp(data: List[int]) -> List[int]: + """ + TODO: Some symbolic parts can be handled here + Modular Exponentiation + :param data: Data with + :return: modular exponentiation + """ + bytes_data = bytearray(data) + baselen = extract32(bytes_data, 0) + explen = extract32(bytes_data, 32) + modlen = extract32(bytes_data, 64) + if baselen == 0: + return [0] * modlen + if modlen == 0: + return [] + + first_exp_bytes = extract32(bytes_data, 96 + baselen) >> (8 * max(32 - explen, 0)) + bitlength = -1 + while first_exp_bytes: + bitlength += 1 + first_exp_bytes >>= 1 + + base = bytearray(baselen) + extract_copy(bytes_data, base, 0, 96, baselen) + exp = bytearray(explen) + extract_copy(bytes_data, exp, 0, 96 + baselen, explen) + mod = bytearray(modlen) + extract_copy(bytes_data, mod, 0, 96 + baselen + explen, modlen) + if big_endian_to_int(mod) == 0: + return [0] * modlen + o = pow(big_endian_to_int(base), big_endian_to_int(exp), big_endian_to_int(mod)) + return [safe_ord(x) for x in zpad(int_to_big_endian(o), modlen)] + + +def ec_add(data: List[int]) -> List[int]: + bytes_data = bytearray(data) + x1 = extract32(bytes_data, 0) + y1 = extract32(bytes_data, 32) + x2 = extract32(bytes_data, 64) + y2 = extract32(bytes_data, 96) + p1 = validate_point(x1, y1) + p2 = validate_point(x2, y2) + if p1 is False or p2 is False: + return [] + o = bn128.normalize(bn128.add(p1, p2)) + return [safe_ord(x) for x in (encode_int32(o[0].n) + encode_int32(o[1].n))] + + +def ec_mul(data: List[int]) -> List[int]: + bytes_data = bytearray(data) + x = extract32(bytes_data, 0) + y = extract32(bytes_data, 32) + m = extract32(bytes_data, 64) + p = validate_point(x, y) + if p is False: + return [] + o = bn128.normalize(bn128.multiply(p, m)) + return [safe_ord(c) for c in (encode_int32(o[0].n) + encode_int32(o[1].n))] + + +def ec_pair(data: List[int]) -> List[int]: + if len(data) % 192: + return [] + + zero = (bn128.FQ2.one(), bn128.FQ2.one(), bn128.FQ2.zero()) + exponent = bn128.FQ12.one() + bytes_data = bytearray(data) + for i in range(0, len(bytes_data), 192): + x1 = extract32(bytes_data, i) + y1 = extract32(bytes_data, i + 32) + x2_i = extract32(bytes_data, i + 64) + x2_r = extract32(bytes_data, i + 96) + y2_i = extract32(bytes_data, i + 128) + y2_r = extract32(bytes_data, i + 160) + p1 = validate_point(x1, y1) + if p1 is False: + return [] + for v in (x2_i, x2_r, y2_i, y2_r): + if v >= bn128.field_modulus: + return [] + fq2_x = bn128.FQ2([x2_r, x2_i]) + fq2_y = bn128.FQ2([y2_r, y2_i]) + if (fq2_x, fq2_y) != (bn128.FQ2.zero(), bn128.FQ2.zero()): + p2 = (fq2_x, fq2_y, bn128.FQ2.one()) + if not bn128.is_on_curve(p2, bn128.b2): + return [] + else: + p2 = zero + if bn128.multiply(p2, bn128.curve_order)[-1] != bn128.FQ2.zero(): + return [] + exponent *= bn128.pairing(p2, p1, final_exponentiate=False) + result = bn128.final_exponentiate(exponent) == bn128.FQ12.one() + return [0] * 31 + [1 if result else 0] + + +PRECOMPILE_FUNCTIONS = ( + ecrecover, + sha256, + ripemd160, + identity, + mod_exp, + ec_add, + ec_mul, + ec_pair, +) +PRECOMPILE_COUNT = len(PRECOMPILE_FUNCTIONS) + + def native_contracts(address: int, data: BaseCalldata) -> List[int]: """Takes integer address 1, 2, 3, 4. @@ -127,11 +214,10 @@ def native_contracts(address: int, data: BaseCalldata) -> List[int]: :param data: :return: """ - functions = (ecrecover, sha256, ripemd160, identity) if isinstance(data, ConcreteCalldata): concrete_data = data.concrete(None) else: raise NativeContractException() - return functions[address - 1](concrete_data) + return PRECOMPILE_FUNCTIONS[address - 1](concrete_data) diff --git a/mythril/laser/ethereum/util.py b/mythril/laser/ethereum/util.py index 9cb5d950..4191173d 100644 --- a/mythril/laser/ethereum/util.py +++ b/mythril/laser/ethereum/util.py @@ -150,3 +150,27 @@ def bytearray_to_int(arr): for a in arr: o = (o << 8) + a return o + + +def extract_copy( + data: bytearray, mem: bytearray, memstart: int, datastart: int, size: int +): + for i in range(size): + if datastart + i < len(data): + mem[memstart + i] = data[datastart + i] + else: + mem[memstart + i] = 0 + + +def extract32(data: bytearray, i: int) -> int: + """ + + :param data: + :param i: + :return: + """ + if i >= len(data): + return 0 + o = data[i : min(i + 32, len(data))] + o.extend(bytearray(32 - len(o))) + return bytearray_to_int(o) diff --git a/requirements.txt b/requirements.txt index 11382df3..6901873e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ coloredlogs>=10.0 configparser>=3.5.0 coverage -py_ecc==1.4.2 +py_ecc==1.6.0 eth_abi==1.3.0 eth-account>=0.1.0a2,<=0.3.0 ethereum>=2.3.2 diff --git a/setup.py b/setup.py index ce115193..bee87da0 100755 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ REQUIRES_PYTHON = ">=3.5.0" # What packages are required for this module to be executed? REQUIRED = [ "coloredlogs>=10.0", - "py_ecc==1.4.2", + "py_ecc==1.6.0", "ethereum>=2.3.2", "z3-solver>=4.8.5.0", "requests", diff --git a/tests/laser/Precompiles/test_ec_add.py b/tests/laser/Precompiles/test_ec_add.py new file mode 100644 index 00000000..4ede2cf0 --- /dev/null +++ b/tests/laser/Precompiles/test_ec_add.py @@ -0,0 +1,27 @@ +from mock import patch +from eth_utils import decode_hex +from mythril.laser.ethereum.natives import ec_add +from py_ecc.optimized_bn128 import FQ + +VECTOR_A = decode_hex( + "0000000000000000000000000000000000000000000000000000000000000001" + "0000000000000000000000000000000000000000000000000000000000000020" + "0000000000000000000000000000000000000000000000000000000000000020" + "03" + "fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2e" + "fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f" +) + + +def test_ec_add_sanity(): + assert ec_add(VECTOR_A) == [] + + +@patch("mythril.laser.ethereum.natives.validate_point", return_value=1) +@patch("mythril.laser.ethereum.natives.bn128.add", return_value=1) +@patch("mythril.laser.ethereum.natives.bn128.normalize") +def test_ec_add(f1, f2, f3): + FQ.fielf_modulus = 128 + a = FQ(val=1) + f1.return_value = (a, a) + assert ec_add(VECTOR_A) == ([0] * 31 + [1]) * 2 diff --git a/tests/laser/Precompiles/test_elliptic_curves.py b/tests/laser/Precompiles/test_elliptic_curves.py new file mode 100644 index 00000000..28908f58 --- /dev/null +++ b/tests/laser/Precompiles/test_elliptic_curves.py @@ -0,0 +1,35 @@ +from mock import patch +from mythril.laser.ethereum.natives import ec_pair +from py_ecc.optimized_bn128 import FQ + + +def test_ec_pair_192_check(): + vec_c = [0] * 100 + assert ec_pair(vec_c) == [] + + +@patch("mythril.laser.ethereum.natives.validate_point", return_value=1) +@patch("mythril.laser.ethereum.natives.bn128.is_on_curve", return_value=True) +@patch("mythril.laser.ethereum.natives.bn128.pairing", return_value=1) +@patch("mythril.laser.ethereum.natives.bn128.normalize") +def test_ec_pair(f1, f2, f3, f4): + FQ.fielf_modulus = 100 + a = FQ(val=1) + f1.return_value = (a, a) + vec_c = [0] * 192 + assert ec_pair(vec_c) == [0] * 31 + [1] + + +@patch("mythril.laser.ethereum.natives.validate_point", return_value=False) +def test_ec_pair_point_validation_failure(f1): + vec_c = [0] * 192 + assert ec_pair(vec_c) == [] + + +@patch("mythril.laser.ethereum.natives.validate_point", return_value=1) +def test_ec_pair_field_exceed_mod(f1): + FQ.fielf_modulus = 100 + a = FQ(val=1) + f1.return_value = (a, a) + vec_c = [10] * 192 + assert ec_pair(vec_c) == [] diff --git a/tests/laser/Precompiles/test_elliptic_mul.py b/tests/laser/Precompiles/test_elliptic_mul.py new file mode 100644 index 00000000..c3b3be9d --- /dev/null +++ b/tests/laser/Precompiles/test_elliptic_mul.py @@ -0,0 +1,27 @@ +from mock import patch +from eth_utils import decode_hex +from mythril.laser.ethereum.natives import ec_mul +from py_ecc.optimized_bn128 import FQ + +VECTOR_A = decode_hex( + "0000000000000000000000000000000000000000000000000000000000000001" + "0000000000000000000000000000000000000000000000000000000000000020" + "0000000000000000000000000000000000000000000000000000000000000020" + "03" + "fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2e" + "fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f" +) + + +@patch("mythril.laser.ethereum.natives.validate_point", return_value=1) +@patch("mythril.laser.ethereum.natives.bn128.multiply", return_value=1) +@patch("mythril.laser.ethereum.natives.bn128.normalize") +def test_ec_mul(f1, f2, f3): + FQ.fielf_modulus = 128 + a = FQ(val=1) + f1.return_value = (a, a) + assert ec_mul(VECTOR_A) == ([0] * 31 + [1]) * 2 + + +def test_ec_mul_validation_failure(): + assert ec_mul(VECTOR_A) == [] diff --git a/tests/laser/Precompiles/test_mod_exp.py b/tests/laser/Precompiles/test_mod_exp.py new file mode 100644 index 00000000..d050c929 --- /dev/null +++ b/tests/laser/Precompiles/test_mod_exp.py @@ -0,0 +1,61 @@ +import pytest +from eth_utils import decode_hex +from mythril.laser.ethereum.natives import mod_exp +from ethereum.utils import big_endian_to_int + + +EIP198_VECTOR_A = decode_hex( + "0000000000000000000000000000000000000000000000000000000000000001" + "0000000000000000000000000000000000000000000000000000000000000020" + "0000000000000000000000000000000000000000000000000000000000000020" + "03" + "fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2e" + "fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f" +) + +EIP198_VECTOR_B = decode_hex( + "0000000000000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000000000000000000020" + "0000000000000000000000000000000000000000000000000000000000000020" + "fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2e" + "fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f" +) + +EIP198_VECTOR_C = decode_hex( + "0000000000000000000000000000000000000000000000000000000000000001" + "0000000000000000000000000000000000000000000000000000000000000002" + "0000000000000000000000000000000000000000000000000000000000000020" + "03" + "ffff" + "8000000000000000000000000000000000000000000000000000000000000000" + "07" +) + +EIP198_VECTOR_D = decode_hex( + "0000000000000000000000000000000000000000000000000000000000000001" + "0000000000000000000000000000000000000000000000000000000000000002" + "0000000000000000000000000000000000000000000000000000000000000020" + "03" + "ffff" + "80" +) + + +@pytest.mark.parametrize( + "data,expected", + ( + (EIP198_VECTOR_A, 1), + (EIP198_VECTOR_B, 0), + ( + EIP198_VECTOR_C, + 26689440342447178617115869845918039756797228267049433585260346420242739014315, + ), + ( + EIP198_VECTOR_D, + 26689440342447178617115869845918039756797228267049433585260346420242739014315, + ), + ), +) +def test_modexp_result(data, expected): + actual = mod_exp(data) + assert big_endian_to_int(actual) == expected From 871907f6c314d73ea2a885e07abf8c1d823afe3e Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 21 Aug 2019 10:27:51 -0400 Subject: [PATCH 114/164] Add checks for creation transaction in codesize/codecopy/calldatasize/calldatacopy --- mythril/laser/ethereum/instructions.py | 76 +++++++++++++------ .../laser/ethereum/transaction/symbolic.py | 4 +- .../transaction/transaction_models.py | 4 +- 3 files changed, 59 insertions(+), 25 deletions(-) diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py index 1895eefe..d010be0a 100644 --- a/mythril/laser/ethereum/instructions.py +++ b/mythril/laser/ethereum/instructions.py @@ -758,34 +758,33 @@ class Instruction: """ state = global_state.mstate environment = global_state.environment - state.stack.append(environment.calldata.calldatasize) - return [global_state] - @StateTransition() - def calldatacopy_(self, global_state: GlobalState) -> List[GlobalState]: - """ + if isinstance(global_state.current_transaction, ContractCreationTransaction): + log.debug("Attempt to use CALLDATASIZE in creation transaction") + state.stack.append(0) + else: + state.stack.append(environment.calldata.calldatasize) - :param global_state: - :return: - """ - state = global_state.mstate + return [global_state] + + @staticmethod + def _calldata_copy_helper(global_state, state, mstart, dstart, size): environment = global_state.environment - op0, op1, op2 = state.stack.pop(), state.stack.pop(), state.stack.pop() try: - mstart = util.get_concrete_int(op0) + mstart = util.get_concrete_int(mstart) except TypeError: log.debug("Unsupported symbolic memory offset in CALLDATACOPY") return [global_state] try: - dstart = util.get_concrete_int(op1) # type: Union[int, BitVec] + dstart = util.get_concrete_int(dstart) # type: Union[int, BitVec] except TypeError: log.debug("Unsupported symbolic calldata offset in CALLDATACOPY") - dstart = simplify(op1) + dstart = simplify(dstart) try: - size = util.get_concrete_int(op2) # type: Union[int, BitVec] + size = util.get_concrete_int(size) # type: Union[int, BitVec] except TypeError: log.debug("Unsupported symbolic size in CALLDATACOPY") size = 320 # The excess size will get overwritten @@ -839,6 +838,22 @@ class Instruction: ) return [global_state] + @StateTransition() + def calldatacopy_(self, global_state: GlobalState) -> List[GlobalState]: + """ + + :param global_state: + :return: + """ + state = global_state.mstate + op0, op1, op2 = state.stack.pop(), state.stack.pop(), state.stack.pop() + + if isinstance(global_state.current_transaction, ContractCreationTransaction): + log.debug("Attempt to use CALLDATACOPY in creation transaction") + return [global_state] + + return self._calldata_copy_helper(global_state, state, op0, op1, op2) + # Environment @StateTransition() def address_(self, global_state: GlobalState) -> List[GlobalState]: @@ -902,7 +917,12 @@ class Instruction: state = global_state.mstate environment = global_state.environment disassembly = environment.code - no_of_bytes = min(len(disassembly.bytecode) / 2, 320) + (len(disassembly.bytecode) / 2) + if isinstance(global_state.current_transaction, ContractCreationTransaction): + # Hacky way to ensure constructor arguments work - Max size is 5000 + # FIXME: Perhaps add some constraint here to ensure that concretization works correctly + no_of_bytes = len(disassembly.bytecode) // 2 + 5000 + else: + no_of_bytes = len(disassembly.bytecode) // 2 state.stack.append(no_of_bytes) return [global_state] @@ -1027,14 +1047,24 @@ class Instruction: global_state.mstate.stack.pop(), global_state.mstate.stack.pop(), ) - return self._code_copy_helper( - code=global_state.environment.code.bytecode, - memory_offset=memory_offset, - code_offset=code_offset, - size=size, - op="CODECOPY", - global_state=global_state, - ) + + if ( + isinstance(global_state.current_transaction, ContractCreationTransaction) + and code_offset >= len(global_state.environment.code.bytecode) // 2 + ): + offset = code_offset - len(global_state.environment.code.bytecode) // 2 + return self._calldata_copy_helper( + global_state, global_state.mstate, memory_offset, offset, size + ) + else: + return self._code_copy_helper( + code=global_state.environment.code.bytecode, + memory_offset=memory_offset, + code_offset=code_offset, + size=size, + op="CODECOPY", + global_state=global_state, + ) @StateTransition() def extcodesize_(self, global_state: GlobalState) -> List[GlobalState]: diff --git a/mythril/laser/ethereum/transaction/symbolic.py b/mythril/laser/ethereum/transaction/symbolic.py index 2212e805..ae18d3a3 100644 --- a/mythril/laser/ethereum/transaction/symbolic.py +++ b/mythril/laser/ethereum/transaction/symbolic.py @@ -85,6 +85,8 @@ def execute_contract_creation( new_account = None for open_world_state in open_states: next_transaction_id = get_next_transaction_id() + # call_data "should" be '[]', but it is easier to model the calldata symbolically + # and add logic in codecopy/codesize/calldatacopy/calldatasize than to model code "correctly" transaction = ContractCreationTransaction( world_state=open_world_state, identifier=next_transaction_id, @@ -98,7 +100,7 @@ def execute_contract_creation( code=Disassembly(contract_initialization_code), caller=symbol_factory.BitVecVal(CREATOR_ADDRESS, 256), contract_name=contract_name, - call_data=[], + call_data=None, # Hrmm call_value=symbol_factory.BitVecSym( "call_value{}".format(next_transaction_id), 256 ), diff --git a/mythril/laser/ethereum/transaction/transaction_models.py b/mythril/laser/ethereum/transaction/transaction_models.py index c2b18b9c..97f2f694 100644 --- a/mythril/laser/ethereum/transaction/transaction_models.py +++ b/mythril/laser/ethereum/transaction/transaction_models.py @@ -197,6 +197,8 @@ class ContractCreationTransaction(BaseTransaction): 0, concrete_storage=True, creator=caller.value ) callee_account.contract_name = contract_name + # init_call_data "should" be false, but it is easier to model the calldata symbolically + # and add logic in codecopy/codesize/calldatacopy/calldatasize than to model code "correctly" super().__init__( world_state=world_state, callee_account=callee_account, @@ -208,7 +210,7 @@ class ContractCreationTransaction(BaseTransaction): origin=origin, code=code, call_value=call_value, - init_call_data=False, + init_call_data=True, ) def initial_global_state(self) -> GlobalState: From 7096a2806b036f6a97007ad0321f80a6ee08c958 Mon Sep 17 00:00:00 2001 From: e-ngo Date: Wed, 21 Aug 2019 08:27:26 -0700 Subject: [PATCH 115/164] Removed creationdata as is unnecessary for now. --- mythril/laser/ethereum/state/creationdata.py | 51 -------------------- 1 file changed, 51 deletions(-) delete mode 100644 mythril/laser/ethereum/state/creationdata.py diff --git a/mythril/laser/ethereum/state/creationdata.py b/mythril/laser/ethereum/state/creationdata.py deleted file mode 100644 index dbcd2991..00000000 --- a/mythril/laser/ethereum/state/creationdata.py +++ /dev/null @@ -1,51 +0,0 @@ -"""This module declares classes to represent creation code and call data.""" -from typing import cast, Union, Tuple, List - - -from enum import Enum -from typing import Any, Union - -from z3 import Model -from z3.z3types import Z3Exception - -from mythril.laser.ethereum.util import get_concrete_int -from mythril.laser.smt import ( - Array, - BitVec, - Bool, - Concat, - Expression, - If, - K, - simplify, - symbol_factory, -) - -from mythril.laser.smt.bitvec_helper import Sum - -from mythril.laser.ethereum.state.calldata import BaseCalldata - - -class CreationData: - """CreationData class This represents creation bytecode and calldata constructor argument provided when sending a - transaction to a contract.""" - - def __init__(self, code, calldata: BaseCalldata) -> None: - """ - - :param tx_id: - """ - self.code = code - self.calldata = calldata - - @property - def size(self) -> BitVec: - """ - - :return: codesize in bytes for this CreationData object - """ - calldata_size = self.calldata.calldatasize - code_size = symbol_factory.BitVecVal(len(self.code) // 2, 256) - if calldata_size.symbolic: - return Sum(code_size, calldata_size) - return code_size + calldata_size From d390b4a6a48aaed4f88703e52bd6a98e416d7ecc Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 21 Aug 2019 11:47:14 -0400 Subject: [PATCH 116/164] Remove confusing comment --- mythril/laser/ethereum/transaction/symbolic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mythril/laser/ethereum/transaction/symbolic.py b/mythril/laser/ethereum/transaction/symbolic.py index ae18d3a3..b8b6692c 100644 --- a/mythril/laser/ethereum/transaction/symbolic.py +++ b/mythril/laser/ethereum/transaction/symbolic.py @@ -100,7 +100,7 @@ def execute_contract_creation( code=Disassembly(contract_initialization_code), caller=symbol_factory.BitVecVal(CREATOR_ADDRESS, 256), contract_name=contract_name, - call_data=None, # Hrmm + call_data=None, call_value=symbol_factory.BitVecSym( "call_value{}".format(next_transaction_id), 256 ), From 5e1fb6067037781479a8b832aeef7fcb197f6b64 Mon Sep 17 00:00:00 2001 From: e-ngo Date: Thu, 22 Aug 2019 18:27:03 -0700 Subject: [PATCH 117/164] Added CREATE --- mythril/laser/ethereum/instructions.py | 39 ++++++++++++++++++------- tests/instructions/createopcode_test.py | 16 ++++++++++ 2 files changed, 44 insertions(+), 11 deletions(-) create mode 100644 tests/instructions/createopcode_test.py diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py index ca3b9631..1b287e58 100644 --- a/mythril/laser/ethereum/instructions.py +++ b/mythril/laser/ethereum/instructions.py @@ -29,6 +29,8 @@ from mythril.laser.smt import ( ) from mythril.laser.smt import symbol_factory +from mythril.disassembler.disassembly import Disassembly + import mythril.laser.ethereum.util as helper from mythril.laser.ethereum import util from mythril.laser.ethereum.call import get_call_parameters, native_call, get_call_data @@ -904,7 +906,8 @@ class Instruction: state = global_state.mstate environment = global_state.environment disassembly = environment.code - state.stack.append(len(disassembly.bytecode) // 2) + no_of_bytes = len(disassembly.bytecode) // 2 + 5000 + state.stack.append(no_of_bytes) return [global_state] @StateTransition(enable_gas=False) @@ -1194,16 +1197,12 @@ class Instruction: address = Extract(159, 0, stack.pop()) if address.symbolic: - log.debug("unsupported symbolic address for EXTCODEHASH") - stack.append(global_state.new_bitvec("extcodehash_" + str(address), 256)) - return [global_state] - - if address.value not in world_state.accounts: + code_hash = symbol_factory.BitVecVal(int(get_code_hash(""), 16), 256) + elif address.value not in world_state.accounts: code_hash = symbol_factory.BitVecVal(0, 256) else: - code = world_state.accounts_exist_or_load( - hex(address.value), self.dynamic_loader - ) + addr = "0" * (40 - len(hex(address.value))) + hex(address.value) + code = world_state.accounts_exist_or_load(addr, self.dynamic_loader) code_hash = symbol_factory.BitVecVal(int(get_code_hash(code), 16), 256) stack.append(code_hash) return [global_state] @@ -1615,26 +1614,44 @@ class Instruction: util.get_concrete_int(mem_offset), util.get_concrete_int(mem_offset + mem_size), ) + except TypeError: log.debug("Create with symbolic length or offset. Not supported") mstate.stack.append(0, 256) return [global_state] - caller = environment.sender + call_data = call_data.concrete(None) + code_str = bytes.hex(bytes(call_data)) + # for i in call_data.size: + # if isinstance(call_data[0], int): + # code_str += "00" if not call_data[0] else bytes.hex([call_data[i]]) + # + + code = Disassembly(code_str) + + caller = environment.active_account.address gas_price = environment.gasprice origin = environment.origin transaction = ContractCreationTransaction( world_state=world_state, caller=caller, - call_data=call_data, + code=code, gas_price=gas_price, + gas_limit=mstate.gas_limit, origin=origin, call_value=call_value, ) raise TransactionStartSignal(transaction, self.op_code) + @StateTransition() + def create_post(self, global_state: GlobalState) -> List[GlobalState]: + + global_state.mstate.stack.append(global_state.world_state.accounts[-1].address) + + return [global_state] + @StateTransition() def create2_(self, global_state: GlobalState) -> List[GlobalState]: """ diff --git a/tests/instructions/createopcode_test.py b/tests/instructions/createopcode_test.py new file mode 100644 index 00000000..64dc7cdf --- /dev/null +++ b/tests/instructions/createopcode_test.py @@ -0,0 +1,16 @@ +from mythril.disassembler.disassembly import Disassembly +from mythril.laser.ethereum.state.environment import Environment +from mythril.laser.ethereum.state.account import Account +from mythril.laser.ethereum.state.machine_state import MachineState +from mythril.laser.ethereum.state.global_state import GlobalState +from mythril.laser.ethereum.state.world_state import WorldState +from mythril.laser.ethereum.instructions import Instruction +from mythril.laser.ethereum.transaction.transaction_models import MessageCallTransaction + +from mythril.support.support_utils import get_code_hash + +from mythril.laser.smt import symbol_factory + +def test_create(): + # TODO: ... + pass \ No newline at end of file From f58aaa1830c4bedde24e08803ff7287c46908724 Mon Sep 17 00:00:00 2001 From: ricengo Date: Fri, 23 Aug 2019 00:17:20 -0700 Subject: [PATCH 118/164] Updated create implementation. TODO: finish log, create2, and staticcall --- mythril/laser/ethereum/instructions.py | 30 ++++++++----------- mythril/laser/ethereum/svm.py | 6 ++++ .../transaction/transaction_models.py | 2 +- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py index e1e1f292..79948bc8 100644 --- a/mythril/laser/ethereum/instructions.py +++ b/mythril/laser/ethereum/instructions.py @@ -1632,18 +1632,10 @@ class Instruction: environment = global_state.environment world_state = global_state.world_state - call_value, mem_offset, mem_size = ( - mstate.stack.pop(), - mstate.stack.pop(), - mstate.stack.pop(), - ) + call_value, mem_offset, mem_size = mstate.pop(3) try: - call_data = get_call_data( - global_state, - util.get_concrete_int(mem_offset), - util.get_concrete_int(mem_offset + mem_size), - ) + call_data = get_call_data(global_state, mem_offset, mem_offset + mem_size) except TypeError: log.debug("Create with symbolic length or offset. Not supported") @@ -1651,11 +1643,16 @@ class Instruction: return [global_state] call_data = call_data.concrete(None) - code_str = bytes.hex(bytes(call_data)) - # for i in call_data.size: - # if isinstance(call_data[0], int): - # code_str += "00" if not call_data[0] else bytes.hex([call_data[i]]) - # + + code_end = 0 + for i in range(len(call_data)): + if not isinstance(call_data[i], int): + code_end = i + break + + code_str = bytes.hex(bytes(call_data[0:code_end])) + + constructor_arguments = call_data[code_end:] code = Disassembly(code_str) @@ -1667,6 +1664,7 @@ class Instruction: world_state=world_state, caller=caller, code=code, + call_data=constructor_arguments, gas_price=gas_price, gas_limit=mstate.gas_limit, origin=origin, @@ -1678,8 +1676,6 @@ class Instruction: @StateTransition() def create_post(self, global_state: GlobalState) -> List[GlobalState]: - global_state.mstate.stack.append(global_state.world_state.accounts[-1].address) - return [global_state] @StateTransition() diff --git a/mythril/laser/ethereum/svm.py b/mythril/laser/ethereum/svm.py index 88b362c8..f6b69a2a 100644 --- a/mythril/laser/ethereum/svm.py +++ b/mythril/laser/ethereum/svm.py @@ -375,6 +375,12 @@ class LaserEVM: :param return_data: :return: """ + if isinstance(global_state.current_transaction, ContractCreationTransaction): + return_global_state.mstate.stack.append( + global_state.environment.active_account.address + ) + return_global_state.mstate.min_gas_used += global_state.mstate.min_gas_used + return_global_state.mstate.max_gas_used += global_state.mstate.max_gas_used return_global_state.mstate.constraints += global_state.mstate.constraints # Resume execution of the transaction initializing instruction diff --git a/mythril/laser/ethereum/transaction/transaction_models.py b/mythril/laser/ethereum/transaction/transaction_models.py index 97f2f694..a85160e2 100644 --- a/mythril/laser/ethereum/transaction/transaction_models.py +++ b/mythril/laser/ethereum/transaction/transaction_models.py @@ -93,7 +93,7 @@ class BaseTransaction: self.call_data = ( call_data if isinstance(call_data, BaseCalldata) - else ConcreteCalldata(self.id, []) + else ConcreteCalldata(self.id, call_data) ) self.call_value = ( From e8b50b57efc3c1429bdf8e8cdbbd0b387af7a4a7 Mon Sep 17 00:00:00 2001 From: Eric N Date: Sat, 24 Aug 2019 00:20:07 -0700 Subject: [PATCH 119/164] Fix bug and comment code. Adding tests. --- mythril/laser/ethereum/instructions.py | 9 +++++-- mythril/laser/ethereum/svm.py | 1 + tests/instructions/create_test.py | 35 +++++++++++++++++++++++++ tests/instructions/createopcode_test.py | 16 ----------- 4 files changed, 43 insertions(+), 18 deletions(-) create mode 100644 tests/instructions/create_test.py delete mode 100644 tests/instructions/createopcode_test.py diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py index 79948bc8..6288d8f8 100644 --- a/mythril/laser/ethereum/instructions.py +++ b/mythril/laser/ethereum/instructions.py @@ -1635,16 +1635,21 @@ class Instruction: call_value, mem_offset, mem_size = mstate.pop(3) try: - call_data = get_call_data(global_state, mem_offset, mem_offset + mem_size) + call_data = get_call_data( + global_state, + util.get_concrete_int(mem_offset), + util.get_concrete_int(mem_offset + mem_size), + ) except TypeError: + # can be reached? log.debug("Create with symbolic length or offset. Not supported") mstate.stack.append(0, 256) return [global_state] call_data = call_data.concrete(None) - code_end = 0 + code_end = len(call_data) for i in range(len(call_data)): if not isinstance(call_data[i], int): code_end = i diff --git a/mythril/laser/ethereum/svm.py b/mythril/laser/ethereum/svm.py index f6b69a2a..572830bb 100644 --- a/mythril/laser/ethereum/svm.py +++ b/mythril/laser/ethereum/svm.py @@ -376,6 +376,7 @@ class LaserEVM: :return: """ if isinstance(global_state.current_transaction, ContractCreationTransaction): + # is this the proper place to put CREATE handle? return_global_state.mstate.stack.append( global_state.environment.active_account.address ) diff --git a/tests/instructions/create_test.py b/tests/instructions/create_test.py new file mode 100644 index 00000000..5941949d --- /dev/null +++ b/tests/instructions/create_test.py @@ -0,0 +1,35 @@ +from mythril.disassembler.disassembly import Disassembly +from mythril.laser.ethereum.state.environment import Environment +from mythril.laser.ethereum.state.account import Account +from mythril.laser.ethereum.state.machine_state import MachineState +from mythril.laser.ethereum.state.global_state import GlobalState +from mythril.laser.ethereum.state.world_state import WorldState +from mythril.laser.ethereum.instructions import Instruction +from mythril.laser.ethereum.transaction.transaction_models import MessageCallTransaction + +from mythril.laser.ethereum.svm import LaserEVM + +from mythril.laser.smt import symbol_factory + + +def test_create(): + creating_contract_runtime_code = "608060405260043610601c5760003560e01c8063efc81a8c146021575b600080fd5b60276029565b005b60006040516035906056565b604051809103906000f0801580156050573d6000803e3d6000fd5b50905050565b605b806100638339019056fe6080604052348015600f57600080fd5b50603e80601d6000396000f3fe6080604052600080fdfea265627a7a72315820f4040cbd444dcd2f49f1daf46a116eb32396f1cc84a9e79e3836d4bfe84d6bca64736f6c634300050b0032a265627a7a72315820e2350a73a28ed02b4dac678f2b77a330dc512c3cce8ca53fa1c30869f443553d64736f6c634300050b0032" + created_contract_init_code = "6080604052348015600f57600080fd5b50603e80601d6000396000f3fe6080604052600080fdfea265627a7a72315820f4040cbd444dcd2f49f1daf46a116eb32396f1cc84a9e79e3836d4bfe84d6bca64736f6c634300050b0032" + created_contract_runtime_code = "6080604052600080fdfea265627a7a72315820f4040cbd444dcd2f49f1daf46a116eb32396f1cc84a9e79e3836d4bfe84d6bca64736f6c634300050b0032" + world_state = WorldState() + account = world_state.create_account(balance=1000, address=101) + account.code = Disassembly(creating_contract_runtime_code) + environment = Environment(account, None, None, None, None, None) + og_state = GlobalState( + world_state, environment, None, MachineState(gas_limit=8000000) + ) + og_state.transaction_stack.append( + (MessageCallTransaction(world_state=WorldState(), gas_limit=8000000), None) + ) + + laser_evm = LaserEVM() + + new_states, op_code = laser_evm.execute_state(og_state) + # checks + + # TODO: come up with sequence, then grab an address from stack and check that its code is created. diff --git a/tests/instructions/createopcode_test.py b/tests/instructions/createopcode_test.py deleted file mode 100644 index 64dc7cdf..00000000 --- a/tests/instructions/createopcode_test.py +++ /dev/null @@ -1,16 +0,0 @@ -from mythril.disassembler.disassembly import Disassembly -from mythril.laser.ethereum.state.environment import Environment -from mythril.laser.ethereum.state.account import Account -from mythril.laser.ethereum.state.machine_state import MachineState -from mythril.laser.ethereum.state.global_state import GlobalState -from mythril.laser.ethereum.state.world_state import WorldState -from mythril.laser.ethereum.instructions import Instruction -from mythril.laser.ethereum.transaction.transaction_models import MessageCallTransaction - -from mythril.support.support_utils import get_code_hash - -from mythril.laser.smt import symbol_factory - -def test_create(): - # TODO: ... - pass \ No newline at end of file From 2f2c56ac6d04abdc166f7fdfc20aa09676b92274 Mon Sep 17 00:00:00 2001 From: Eric N Date: Sat, 24 Aug 2019 12:56:24 -0700 Subject: [PATCH 120/164] Add create_post logic --- mythril/laser/ethereum/instructions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py index 6288d8f8..792c39fa 100644 --- a/mythril/laser/ethereum/instructions.py +++ b/mythril/laser/ethereum/instructions.py @@ -1680,7 +1680,8 @@ class Instruction: @StateTransition() def create_post(self, global_state: GlobalState) -> List[GlobalState]: - + addr, call_value, mem_offset, mem_size = global_state.mstate.pop(4) + global_state.mstate.stack.append(addr) return [global_state] @StateTransition() From 43b15324a1ba18bab395621d92b7024049a79d21 Mon Sep 17 00:00:00 2001 From: Eric N Date: Sun, 25 Aug 2019 18:20:25 -0700 Subject: [PATCH 121/164] Add logic for create and create2. --- mythril/laser/ethereum/instructions.py | 63 +++++++++++++------ .../transaction/transaction_models.py | 3 +- 2 files changed, 46 insertions(+), 20 deletions(-) diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py index 792c39fa..984d0592 100644 --- a/mythril/laser/ethereum/instructions.py +++ b/mythril/laser/ethereum/instructions.py @@ -1231,7 +1231,7 @@ class Instruction: elif address.value not in world_state.accounts: code_hash = symbol_factory.BitVecVal(0, 256) else: - addr = "0" * (40 - len(hex(address.value))) + hex(address.value) + addr = "0" * (40 - len(hex(address.value)[2:])) + hex(address.value)[2:] code = world_state.accounts_exist_or_load(addr, self.dynamic_loader) code_hash = symbol_factory.BitVecVal(int(get_code_hash(code), 16), 256) stack.append(code_hash) @@ -1621,19 +1621,15 @@ class Instruction: # Not supported return [global_state] - @StateTransition() - def create_(self, global_state: GlobalState) -> List[GlobalState]: - """ + @staticmethod + def _create_transaction_helper( + global_state, call_value, mem_offset, mem_size, op_code + ): - :param global_state: - :return: - """ mstate = global_state.mstate environment = global_state.environment world_state = global_state.world_state - call_value, mem_offset, mem_size = mstate.pop(3) - try: call_data = get_call_data( global_state, @@ -1665,6 +1661,21 @@ class Instruction: gas_price = environment.gasprice origin = environment.origin + contract_address = None + if op_code is "CREATE2": + salt = hex(mstate.stack.pop().value)[2:] + salt = "0" * (64 - len(salt)) + salt + + addr = hex(caller.value)[2:] + addr = "0" * (40 - len(addr)) + addr + + contract_address = int( + get_code_hash("0xff" + addr + salt + get_code_hash(code_str)[2:])[26:], + 16, + ) + + # TODO: calculate gas cost + transaction = ContractCreationTransaction( world_state=world_state, caller=caller, @@ -1674,9 +1685,23 @@ class Instruction: gas_limit=mstate.gas_limit, origin=origin, call_value=call_value, + contract_address=contract_address, ) + raise TransactionStartSignal(transaction, op_code) - raise TransactionStartSignal(transaction, self.op_code) + @StateTransition() + def create_(self, global_state: GlobalState) -> List[GlobalState]: + """ + + :param global_state: + :return: + """ + + call_value, mem_offset, mem_size = global_state.mstate.pop(3) + + return self._create_transaction_helper( + global_state, call_value, mem_offset, mem_size, self.op_code + ) @StateTransition() def create_post(self, global_state: GlobalState) -> List[GlobalState]: @@ -1691,16 +1716,16 @@ class Instruction: :param global_state: :return: """ - # TODO: implement me - state = global_state.mstate - endowment, memory_start, memory_length, salt = ( - state.stack.pop(), - state.stack.pop(), - state.stack.pop(), - state.stack.pop(), + call_value, mem_offset, mem_size = global_state.mstate.pop(3) + + return self._create_transaction_helper( + global_state, call_value, mem_offset, mem_size, self.op_code ) - # Not supported - state.stack.append(0) + + @StateTransition() + def create2_post(self, global_state: GlobalState) -> List[GlobalState]: + addr, call_value, mem_offset, mem_size = global_state.mstate.pop(4) + global_state.mstate.stack.append(addr) return [global_state] @StateTransition() diff --git a/mythril/laser/ethereum/transaction/transaction_models.py b/mythril/laser/ethereum/transaction/transaction_models.py index a85160e2..a92421c8 100644 --- a/mythril/laser/ethereum/transaction/transaction_models.py +++ b/mythril/laser/ethereum/transaction/transaction_models.py @@ -191,10 +191,11 @@ class ContractCreationTransaction(BaseTransaction): code=None, call_value=None, contract_name=None, + contract_address=None, ) -> None: self.prev_world_state = deepcopy(world_state) callee_account = world_state.create_account( - 0, concrete_storage=True, creator=caller.value + 0, concrete_storage=True, creator=caller.value, address=contract_address ) callee_account.contract_name = contract_name # init_call_data "should" be false, but it is easier to model the calldata symbolically From 00dc912f2c12d0bdb4cb66292bda715bcaec5fa2 Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Mon, 26 Aug 2019 12:16:48 +0530 Subject: [PATCH 122/164] Refactor the cli and add help for the contract selection (#1203) --- mythril/interfaces/cli.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/mythril/interfaces/cli.py b/mythril/interfaces/cli.py index 2cbf3f4c..5b78ba38 100644 --- a/mythril/interfaces/cli.py +++ b/mythril/interfaces/cli.py @@ -15,7 +15,7 @@ import coloredlogs import traceback import mythril.support.signatures as sigs -from argparse import ArgumentParser, Namespace +from argparse import ArgumentParser, Namespace, RawTextHelpFormatter from mythril.exceptions import AddressNotFoundError, CriticalError from mythril.mythril import ( MythrilAnalyzer, @@ -174,6 +174,7 @@ def main() -> None: help="Triggers the analysis of the smart contract", parents=[rpc_parser, utilities_parser, input_parser, output_parser], aliases=ANALYZE_LIST[1:], + formatter_class=RawTextHelpFormatter, ) create_analyzer_parser(analyzer_parser) @@ -182,6 +183,7 @@ def main() -> None: help="Disassembles the smart contract", aliases=DISASSEMBLE_LIST[1:], parents=[rpc_parser, utilities_parser, input_parser], + formatter_class=RawTextHelpFormatter, ) create_disassemble_parser(disassemble_parser) @@ -223,7 +225,13 @@ def create_disassemble_parser(parser: ArgumentParser): :param parser: :return: """ - parser.add_argument("solidity_file", nargs="*") + # Using nargs=* would the implementation below for getting code for both disassemble and analyze + parser.add_argument( + "solidity_files", + nargs="*", + help="Inputs file name and contract name. Currently supports a single contract\n" + "usage: file1.sol:OptionalContractName", + ) def create_read_storage_parser(read_storage_parser: ArgumentParser): @@ -290,7 +298,12 @@ def create_analyzer_parser(analyzer_parser: ArgumentParser): :param analyzer_parser: :return: """ - analyzer_parser.add_argument("solidity_file", nargs="*") + analyzer_parser.add_argument( + "solidity_files", + nargs="*", + help="Inputs file name and contract name. \n" + "usage: file1.sol:OptionalContractName file2.sol file3.sol:OptionalContractName", + ) commands = analyzer_parser.add_argument_group("commands") commands.add_argument("-g", "--graph", help="generate a control flow graph") commands.add_argument( @@ -425,6 +438,8 @@ def validate_args(args: Namespace): exit_with_error( args.outform, "Invalid -v value, you can find valid values in usage" ) + if args.command in DISASSEMBLE_LIST and len(args.solidity_files) > 1: + exit_with_error("text", "Only a single arg is supported for using disassemble") if args.command in ANALYZE_LIST: if args.query_signature and sigs.ethereum_input_decoder is None: @@ -509,15 +524,15 @@ def load_code(disassembler: MythrilDisassembler, args: Namespace): elif args.__dict__.get("address", False): # Get bytecode from a contract address address, _ = disassembler.load_from_address(args.address) - elif args.__dict__.get("solidity_file", False): + elif args.__dict__.get("solidity_files", False): # Compile Solidity source file(s) - if args.command in ANALYZE_LIST and args.graph and len(args.solidity_file) > 1: + if args.command in ANALYZE_LIST and args.graph and len(args.solidity_files) > 1: exit_with_error( args.outform, "Cannot generate call graphs from multiple input files. Please do it one at a time.", ) address, _ = disassembler.load_from_solidity( - args.solidity_file + args.solidity_files ) # list of files else: exit_with_error( From 97ab3f9e9a4e140e72616e1613a1725c3c9fba51 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 26 Aug 2019 08:35:43 -0400 Subject: [PATCH 123/164] Add a descriptive comment to codecopy_ --- mythril/laser/ethereum/instructions.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py index d010be0a..d79a7ca5 100644 --- a/mythril/laser/ethereum/instructions.py +++ b/mythril/laser/ethereum/instructions.py @@ -1051,7 +1051,10 @@ class Instruction: if ( isinstance(global_state.current_transaction, ContractCreationTransaction) and code_offset >= len(global_state.environment.code.bytecode) // 2 - ): + ):ce + # Treat creation code after the expected disassembly as calldata. + # This is a slightly hacky way to ensure that symbolic constructor + # arguments work correctly. offset = code_offset - len(global_state.environment.code.bytecode) // 2 return self._calldata_copy_helper( global_state, global_state.mstate, memory_offset, offset, size From 3979cc27f2c569ced63b9f83516c1fc2ed34765c Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 27 Aug 2019 08:29:44 -0400 Subject: [PATCH 124/164] Fix typo --- mythril/laser/ethereum/instructions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py index d79a7ca5..6cf0d231 100644 --- a/mythril/laser/ethereum/instructions.py +++ b/mythril/laser/ethereum/instructions.py @@ -1051,7 +1051,7 @@ class Instruction: if ( isinstance(global_state.current_transaction, ContractCreationTransaction) and code_offset >= len(global_state.environment.code.bytecode) // 2 - ):ce + ): # Treat creation code after the expected disassembly as calldata. # This is a slightly hacky way to ensure that symbolic constructor # arguments work correctly. From bb3aabdb097c02e35d48a96bee32ecdba1755e6d Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 28 Aug 2019 10:01:26 -0400 Subject: [PATCH 125/164] Test issue constraints at the end of each transaction --- mythril/analysis/modules/base.py | 2 + mythril/analysis/modules/deprecated_ops.py | 29 ++--- mythril/analysis/modules/ether_thief.py | 56 ++++----- mythril/analysis/modules/external_calls.py | 1 - .../modules/state_change_external_calls.py | 31 +++-- mythril/analysis/potential_issues.py | 108 ++++++++++++++++++ mythril/laser/ethereum/svm.py | 4 +- 7 files changed, 169 insertions(+), 62 deletions(-) create mode 100644 mythril/analysis/potential_issues.py diff --git a/mythril/analysis/modules/base.py b/mythril/analysis/modules/base.py index d45072fd..89c031f8 100644 --- a/mythril/analysis/modules/base.py +++ b/mythril/analysis/modules/base.py @@ -3,6 +3,8 @@ modules.""" import logging from typing import List, Set + +from mythril.analysis.potential_issues import PotentialIssue from mythril.analysis.report import Issue log = logging.getLogger(__name__) diff --git a/mythril/analysis/modules/deprecated_ops.py b/mythril/analysis/modules/deprecated_ops.py index 7e495b5b..eda73a57 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 @@ -36,12 +38,10 @@ class DeprecatedOperationsModule(DetectionModule): 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/ether_thief.py b/mythril/analysis/modules/ether_thief.py index 65e5c8e8..90028acf 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 @@ -62,13 +63,12 @@ class EtherThief(DetectionModule): """ 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/external_calls.py b/mythril/analysis/modules/external_calls.py index 81cd6bd2..d450e2d5 100644 --- a/mythril/analysis/modules/external_calls.py +++ b/mythril/analysis/modules/external_calls.py @@ -15,7 +15,6 @@ 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__) diff --git a/mythril/analysis/modules/state_change_external_calls.py b/mythril/analysis/modules/state_change_external_calls.py index c410b4c0..33912766 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, ) @@ -107,9 +115,9 @@ class StateChange(DetectionModule): 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/potential_issues.py b/mythril/analysis/potential_issues.py new file mode 100644 index 00000000..f866bbca --- /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/laser/ethereum/svm.py b/mythril/laser/ethereum/svm.py index 88b362c8..627cad45 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 @@ -342,6 +343,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 = [] @@ -375,7 +378,6 @@ class LaserEVM: :param return_data: :return: """ - return_global_state.mstate.constraints += global_state.mstate.constraints # Resume execution of the transaction initializing instruction op_code = return_global_state.environment.code.instruction_list[ From b14f10f8f2e3e736d364c5c63575bf67f41b36e0 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 28 Aug 2019 10:50:47 -0400 Subject: [PATCH 126/164] Update external calls to report issues at the end of a transaction --- mythril/analysis/modules/external_calls.py | 35 ++++++++++++---------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/mythril/analysis/modules/external_calls.py b/mythril/analysis/modules/external_calls.py index d450e2d5..8a249a74 100644 --- a/mythril/analysis/modules/external_calls.py +++ b/mythril/analysis/modules/external_calls.py @@ -2,13 +2,17 @@ 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 @@ -64,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: @@ -82,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 @@ -98,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." @@ -109,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, @@ -119,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: @@ -137,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, @@ -147,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: From 35690d2dc797a44c8e880e0faf03f66601aca139 Mon Sep 17 00:00:00 2001 From: Eric N Date: Wed, 28 Aug 2019 23:22:00 -0700 Subject: [PATCH 127/164] Refactored delegatecall to ignore post conditions. --- mythril/analysis/modules/delegatecall.py | 58 ++++++++---------------- 1 file changed, 20 insertions(+), 38 deletions(-) diff --git a/mythril/analysis/modules/delegatecall.py b/mythril/analysis/modules/delegatecall.py index b0e0435d..3fd2c3dc 100644 --- a/mythril/analysis/modules/delegatecall.py +++ b/mythril/analysis/modules/delegatecall.py @@ -84,7 +84,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: @@ -108,48 +108,30 @@ class DelegateCallModule(DetectionModule): """ 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 [] - if op_code == "DELEGATECALL": - gas = state.mstate.stack[-1] - to = state.mstate.stack[-2] + 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) - - state.annotate(DelegateCallAnnotation(state, constraints)) + 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 + ) + return [ + DelegateCallAnnotation(state, constraints).get_issue( + state, transaction_sequence=transaction_sequence + ) + ] + 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() From 7c2f3dffc6d6192cec32718c26903d03deef6ad0 Mon Sep 17 00:00:00 2001 From: Eric N Date: Wed, 28 Aug 2019 23:51:33 -0700 Subject: [PATCH 128/164] Fix mypy --- mythril/analysis/modules/delegatecall.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mythril/analysis/modules/delegatecall.py b/mythril/analysis/modules/delegatecall.py index 3fd2c3dc..7e15567a 100644 --- a/mythril/analysis/modules/delegatecall.py +++ b/mythril/analysis/modules/delegatecall.py @@ -106,7 +106,6 @@ class DelegateCallModule(DetectionModule): :param state: the current state :return: returns the issues for that corresponding state """ - issues = [] op_code = state.get_current_instruction()["opcode"] gas = state.mstate.stack[-1] From e64dcbd05deaff9ca90b93cffe087f2bfc931ebb Mon Sep 17 00:00:00 2001 From: Eric N Date: Thu, 29 Aug 2019 07:47:06 -0700 Subject: [PATCH 129/164] Removed DelegateCallAnnotation --- mythril/analysis/modules/delegatecall.py | 83 ++++++++---------------- 1 file changed, 27 insertions(+), 56 deletions(-) diff --git a/mythril/analysis/modules/delegatecall.py b/mythril/analysis/modules/delegatecall.py index 7e15567a..98453614 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.""" @@ -124,11 +70,36 @@ class DelegateCallModule(DetectionModule): transaction_sequence = solver.get_transaction_sequence( state, state.mstate.constraints + constraints ) + + 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. " + ) + return [ - DelegateCallAnnotation(state, constraints).get_issue( - state, transaction_sequence=transaction_sequence + 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 [] From 02835c807de33a12baac25155c7d31db6b8fecef Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 30 Aug 2019 10:35:17 -0400 Subject: [PATCH 130/164] Concretize and display calldata transaction data --- mythril/analysis/solver.py | 17 +++++++++-------- .../templates/report_as_markdown.jinja2 | 2 +- .../analysis/templates/report_as_text.jinja2 | 2 +- mythril/laser/ethereum/instructions.py | 11 ++++++++--- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/mythril/analysis/solver.py b/mythril/analysis/solver.py index b6959a82..00de6bd4 100644 --- a/mythril/analysis/solver.py +++ b/mythril/analysis/solver.py @@ -152,16 +152,17 @@ def _get_concrete_transaction(model: z3.Model, transaction: BaseTransaction): "%x" % model.eval(transaction.caller.raw, model_completion=True).as_long() ).zfill(40) + input_ = "" if isinstance(transaction, ContractCreationTransaction): address = "" - input_ = transaction.code.bytecode - else: - input_ = "".join( - [ - hex(b)[2:] if len(hex(b)) % 2 == 0 else "0" + hex(b)[2:] - for b in transaction.call_data.concrete(model) - ] - ) + input_ += transaction.code.bytecode + + input_ += "".join( + [ + hex(b)[2:] if len(hex(b)) % 2 == 0 else "0" + hex(b)[2:] + for b in transaction.call_data.concrete(model) + ] + ) # Create concrete transaction dict concrete_transaction = dict() # type: Dict[str, str] diff --git a/mythril/analysis/templates/report_as_markdown.jinja2 b/mythril/analysis/templates/report_as_markdown.jinja2 index 9e690c43..78b0b7be 100644 --- a/mythril/analysis/templates/report_as_markdown.jinja2 +++ b/mythril/analysis/templates/report_as_markdown.jinja2 @@ -36,7 +36,7 @@ Account: {% if key == "0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe" %}[CREATOR]{% {% for step in issue.tx_sequence.steps %} {% if step == issue.tx_sequence.steps[0] and step.input != "0x" and step.origin == "0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe" %} -Caller: [CREATOR], data: [CONTRACT CREATION], value: {{ step.value }} +Caller: [CREATOR], data: {{ step.input }}, value: {{ step.value }} {% else %} Caller: {% if step.origin == "0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe" %}[CREATOR]{% elif step.origin == "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" %}[ATTACKER]{% else %}[SOMEGUY]{% endif %}, function: {{ step.name }}, txdata: {{ step.input }}, value: {{ step.value }} {% endif %} diff --git a/mythril/analysis/templates/report_as_text.jinja2 b/mythril/analysis/templates/report_as_text.jinja2 index aa5c4876..2201dbcf 100644 --- a/mythril/analysis/templates/report_as_text.jinja2 +++ b/mythril/analysis/templates/report_as_text.jinja2 @@ -29,7 +29,7 @@ Transaction Sequence: {% for step in issue.tx_sequence.steps %} {% if step == issue.tx_sequence.steps[0] and step.input != "0x" and step.origin == "0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe" %} -Caller: [CREATOR], data: [CONTRACT CREATION], value: {{ step.value }} +Caller: [CREATOR], data: {{ step.input }}, value: {{ step.value }} {% else %} Caller: {% if step.origin == "0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe" %}[CREATOR]{% elif step.origin == "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" %}[ATTACKER]{% else %}[SOMEGUY]{% endif %}, function: {{ step.name }}, txdata: {{ step.input }}, value: {{ step.value }} {% endif %} diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py index 6cf0d231..593be817 100644 --- a/mythril/laser/ethereum/instructions.py +++ b/mythril/laser/ethereum/instructions.py @@ -918,9 +918,13 @@ class Instruction: environment = global_state.environment disassembly = environment.code if isinstance(global_state.current_transaction, ContractCreationTransaction): - # Hacky way to ensure constructor arguments work - Max size is 5000 - # FIXME: Perhaps add some constraint here to ensure that concretization works correctly - no_of_bytes = len(disassembly.bytecode) // 2 + 5000 + # Hacky way to ensure constructor arguments work - Pick some reasonably large size. + no_of_bytes = ( + len(disassembly.bytecode) // 2 + 0x200 + ) # space for 16 32-byte arguments + global_state.mstate.constraints.append( + global_state.environment.calldata.size == no_of_bytes + ) else: no_of_bytes = len(disassembly.bytecode) // 2 state.stack.append(no_of_bytes) @@ -1056,6 +1060,7 @@ class Instruction: # This is a slightly hacky way to ensure that symbolic constructor # arguments work correctly. offset = code_offset - len(global_state.environment.code.bytecode) // 2 + log.warning("Doing hacky thing offset: {} size: {}".format(offset, size)) return self._calldata_copy_helper( global_state, global_state.mstate, memory_offset, offset, size ) From 9f04fa0c8a7e8bc16962e8303ac3a463adf6632c Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 30 Aug 2019 11:02:35 -0400 Subject: [PATCH 131/164] Fixes for latest pythx version --- mythril/laser/ethereum/util.py | 4 ++-- mythril/mythx/__init__.py | 2 +- requirements.txt | 2 +- setup.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mythril/laser/ethereum/util.py b/mythril/laser/ethereum/util.py index 9cb5d950..f0b42794 100644 --- a/mythril/laser/ethereum/util.py +++ b/mythril/laser/ethereum/util.py @@ -45,8 +45,8 @@ def get_instruction_index( """ index = 0 for instr in instruction_list: - if instr["address"] == address: - return index + if instr["address"] > address: + return index - 1 index += 1 return None diff --git a/mythril/mythx/__init__.py b/mythril/mythx/__init__.py index 631e8ba8..c5602459 100644 --- a/mythril/mythx/__init__.py +++ b/mythril/mythx/__init__.py @@ -75,7 +75,7 @@ def analyze(contracts: List[SolidityContract], analysis_mode: str = "quick") -> issue = Issue( contract=contract.name, function_name=None, - address=int(issue.locations[0].source_map.split(":")[0]), + address=issue.locations[0].source_map.components[0].offset, swc_id=issue.swc_id[4:], # remove 'SWC-' prefix title=issue.swc_title, bytecode=contract.creation_code, diff --git a/requirements.txt b/requirements.txt index 7e2032d8..3d96c2b9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,7 +21,7 @@ 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 diff --git a/setup.py b/setup.py index 969823b1..2c0662b9 100755 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ REQUIRED = [ "py_ecc==1.4.2", "ethereum>=2.3.2", "z3-solver>=4.8.5.0", - "requests", + "requests>=2.22.0", "py-solc", "plyvel", "eth_abi==1.3.0", From 134627e37e1582e2fb2f7fec30870ef7af7df5b4 Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 30 Aug 2019 11:33:02 -0400 Subject: [PATCH 132/164] Fix up some missed bugs --- mythril/interfaces/cli.py | 10 ++++++++-- mythril/laser/ethereum/util.py | 4 ++-- mythril/mythx/__init__.py | 6 +++++- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/mythril/interfaces/cli.py b/mythril/interfaces/cli.py index 6743e008..1501120f 100644 --- a/mythril/interfaces/cli.py +++ b/mythril/interfaces/cli.py @@ -218,8 +218,9 @@ def main() -> None: pro_parser = subparsers.add_parser( PRO_LIST[0], help="Analyzes input with the MythX API (https://mythx.io)", - aliases=PRO_LIST[1], + aliases=PRO_LIST[1:], parents=[utilities_parser, creation_input_parser, output_parser], + formatter_class=RawTextHelpFormatter, ) create_pro_parser(pro_parser) @@ -276,12 +277,17 @@ def create_pro_parser(parser: ArgumentParser): :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", ) - parser.add_argument("solidity_file", nargs="*") def create_read_storage_parser(read_storage_parser: ArgumentParser): diff --git a/mythril/laser/ethereum/util.py b/mythril/laser/ethereum/util.py index 52b8cf94..ff544559 100644 --- a/mythril/laser/ethereum/util.py +++ b/mythril/laser/ethereum/util.py @@ -45,8 +45,8 @@ def get_instruction_index( """ index = 0 for instr in instruction_list: - if instr["address"] > address: - return index - 1 + if instr["address"] >= address: + return index index += 1 return None diff --git a/mythril/mythx/__init__.py b/mythril/mythx/__init__.py index c5602459..ae105470 100644 --- a/mythril/mythx/__init__.py +++ b/mythril/mythx/__init__.py @@ -8,6 +8,10 @@ from mythril.solidity.soliditycontract import SolidityContract from pythx import Client +import logging + +log = logging.getLogger(__name__) + def analyze(contracts: List[SolidityContract], analysis_mode: str = "quick") -> Report: """ @@ -68,7 +72,7 @@ def analyze(contracts: List[SolidityContract], analysis_mode: str = "quick") -> ) while not c.analysis_ready(resp.uuid): - print(c.status(resp.uuid).analysis) + log.info(c.status(resp.uuid).analysis) time.sleep(5) for issue in c.report(resp.uuid): From c55af2a5a0fe7b0bb13b067ee2aa457087086f62 Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Wed, 4 Sep 2019 17:00:28 +0530 Subject: [PATCH 133/164] Implement static call (#991) * Implement static call * Refactor static call tests * Reuse call's post for staticcall * Change variable names and add docstring comments * Rename variable name * change the comment * Fix the tests based on the merge * Fix the tests * Remove extra declaration of WriteProtection * Fix static call test * Add the missing condition in callcode --- mythril/laser/ethereum/evm_exceptions.py | 6 + mythril/laser/ethereum/instructions.py | 192 +++++++++++------- mythril/laser/ethereum/state/environment.py | 4 +- mythril/laser/ethereum/svm.py | 4 +- .../transaction/transaction_models.py | 6 +- tests/instructions/static_call_test.py | 124 +++++++++++ 6 files changed, 260 insertions(+), 76 deletions(-) create mode 100644 tests/instructions/static_call_test.py diff --git a/mythril/laser/ethereum/evm_exceptions.py b/mythril/laser/ethereum/evm_exceptions.py index fd99aed5..7e2a7b8f 100644 --- a/mythril/laser/ethereum/evm_exceptions.py +++ b/mythril/laser/ethereum/evm_exceptions.py @@ -37,6 +37,12 @@ class OutOfGasException(VmException): pass +class WriteProtection(VmException): + """A VM exception denoting that a write operation is executed on a write protected environment""" + + pass + + class ProgramCounterException(VmException): """A VM exception denoting an invalid PC value (No stop instruction is reached).""" diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py index 5d2b9e93..cb2645ff 100644 --- a/mythril/laser/ethereum/instructions.py +++ b/mythril/laser/ethereum/instructions.py @@ -38,6 +38,7 @@ from mythril.laser.ethereum.evm_exceptions import ( InvalidJumpDestination, InvalidInstruction, OutOfGasException, + WriteProtection, ) from mythril.laser.ethereum.gas import OPCODE_GAS from mythril.laser.ethereum.state.global_state import GlobalState @@ -88,14 +89,18 @@ class StateTransition(object): if `increment_pc=True`. """ - def __init__(self, increment_pc=True, enable_gas=True): + def __init__( + self, increment_pc=True, enable_gas=True, is_state_mutation_instruction=False + ): """ :param increment_pc: :param enable_gas: + :param is_state_mutation_instruction: The function mutates state """ self.increment_pc = increment_pc self.enable_gas = enable_gas + self.is_state_mutation_instruction = is_state_mutation_instruction @staticmethod def call_on_state_copy(func: Callable, func_obj: "Instruction", state: GlobalState): @@ -165,6 +170,13 @@ class StateTransition(object): :param global_state: :return: """ + if self.is_state_mutation_instruction and global_state.environment.static: + raise WriteProtection( + "The function {} cannot be executed in a static call".format( + func.__name__[:-1] + ) + ) + new_global_states = self.call_on_state_copy(func, func_obj, global_state) new_global_states = [ self.accumulate_gas(state) for state in new_global_states @@ -1396,7 +1408,7 @@ class Instruction: state.stack.append(global_state.environment.active_account.storage[index]) return [global_state] - @StateTransition() + @StateTransition(is_state_mutation_instruction=True) def sstore_(self, global_state: GlobalState) -> List[GlobalState]: """ @@ -1563,7 +1575,7 @@ class Instruction: global_state.mstate.stack.append(global_state.new_bitvec("gas", 256)) return [global_state] - @StateTransition() + @StateTransition(is_state_mutation_instruction=True) def log_(self, global_state: GlobalState) -> List[GlobalState]: """ @@ -1578,7 +1590,7 @@ class Instruction: # Not supported return [global_state] - @StateTransition() + @StateTransition(is_state_mutation_instruction=True) def create_(self, global_state: GlobalState) -> List[GlobalState]: """ @@ -1586,13 +1598,14 @@ class Instruction: :return: """ # TODO: implement me + state = global_state.mstate state.stack.pop(), state.stack.pop(), state.stack.pop() # Not supported state.stack.append(0) return [global_state] - @StateTransition() + @StateTransition(is_state_mutation_instruction=True) def create2_(self, global_state: GlobalState) -> List[GlobalState]: """ @@ -1628,7 +1641,7 @@ class Instruction: return_data = state.memory[offset : offset + length] global_state.current_transaction.end(global_state, return_data) - @StateTransition() + @StateTransition(is_state_mutation_instruction=True) def suicide_(self, global_state: GlobalState): """ @@ -1733,6 +1746,21 @@ class Instruction: ) return [global_state] + if environment.static: + if isinstance(value, int) and value > 0: + raise WriteProtection( + "Cannot call with non zero value in a static call" + ) + if isinstance(value, BitVec): + if value.symbolic: + global_state.mstate.constraints.append( + value == symbol_factory.BitVecVal(0, 256) + ) + elif value.value > 0: + raise WriteProtection( + "Cannot call with non zero value in a static call" + ) + native_result = native_call( global_state, callee_address, call_data, memory_out_offset, memory_out_size ) @@ -1747,8 +1775,9 @@ class Instruction: callee_account=callee_account, call_data=call_data, call_value=value, + static=environment.static, ) - raise TransactionStartSignal(transaction, self.op_code) + raise TransactionStartSignal(transaction, self.op_code, global_state) @StateTransition() def call_post(self, global_state: GlobalState) -> List[GlobalState]: @@ -1757,65 +1786,8 @@ class Instruction: :param global_state: :return: """ - instr = global_state.get_current_instruction() - - try: - callee_address, callee_account, call_data, value, gas, memory_out_offset, memory_out_size = get_call_parameters( - global_state, self.dynamic_loader, True - ) - except ValueError as e: - log.debug( - "Could not determine required parameters for call, putting fresh symbol on the stack. \n{}".format( - e - ) - ) - global_state.mstate.stack.append( - global_state.new_bitvec("retval_" + str(instr["address"]), 256) - ) - return [global_state] - - if global_state.last_return_data is None: - # Put return value on stack - return_value = global_state.new_bitvec( - "retval_" + str(instr["address"]), 256 - ) - global_state.mstate.stack.append(return_value) - global_state.mstate.constraints.append(return_value == 0) - return [global_state] - - try: - memory_out_offset = ( - util.get_concrete_int(memory_out_offset) - if isinstance(memory_out_offset, Expression) - else memory_out_offset - ) - memory_out_size = ( - util.get_concrete_int(memory_out_size) - if isinstance(memory_out_size, Expression) - else memory_out_size - ) - except TypeError: - global_state.mstate.stack.append( - global_state.new_bitvec("retval_" + str(instr["address"]), 256) - ) - return [global_state] - - # Copy memory - global_state.mstate.mem_extend( - memory_out_offset, min(memory_out_size, len(global_state.last_return_data)) - ) - for i in range(min(memory_out_size, len(global_state.last_return_data))): - global_state.mstate.memory[ - i + memory_out_offset - ] = global_state.last_return_data[i] - - # Put return value on stack - return_value = global_state.new_bitvec("retval_" + str(instr["address"]), 256) - global_state.mstate.stack.append(return_value) - global_state.mstate.constraints.append(return_value == 1) - - return [global_state] + return self.post_handler(global_state, function_name="call") @StateTransition() def callcode_(self, global_state: GlobalState) -> List[GlobalState]: @@ -1831,7 +1803,6 @@ class Instruction: callee_address, callee_account, call_data, value, gas, _, _ = get_call_parameters( global_state, self.dynamic_loader, True ) - if callee_account is not None and callee_account.code.bytecode == "": log.debug("The call is related to ether transfer between accounts") sender = global_state.environment.active_account.address @@ -1864,8 +1835,9 @@ class Instruction: callee_account=environment.active_account, call_data=call_data, call_value=value, + static=environment.static, ) - raise TransactionStartSignal(transaction, self.op_code) + raise TransactionStartSignal(transaction, self.op_code, global_state) @StateTransition() def callcode_post(self, global_state: GlobalState) -> List[GlobalState]: @@ -1949,7 +1921,7 @@ class Instruction: if callee_account is not None and callee_account.code.bytecode == "": log.debug("The call is related to ether transfer between accounts") - sender = environment.active_account.address + sender = global_state.environment.active_account.address receiver = callee_account.address transfer_ether(global_state, sender, receiver, value) @@ -1979,8 +1951,9 @@ class Instruction: callee_account=environment.active_account, call_data=call_data, call_value=environment.callvalue, + static=environment.static, ) - raise TransactionStartSignal(transaction, self.op_code) + raise TransactionStartSignal(transaction, self.op_code, global_state) @StateTransition() def delegatecall_post(self, global_state: GlobalState) -> List[GlobalState]: @@ -2012,6 +1985,7 @@ class Instruction: "retval_" + str(instr["address"]), 256 ) global_state.mstate.stack.append(return_value) + global_state.mstate.constraints.append(return_value == 0) return [global_state] try: @@ -2053,8 +2027,8 @@ class Instruction: :param global_state: :return: """ - # TODO: implement me instr = global_state.get_current_instruction() + environment = global_state.environment try: callee_address, callee_account, call_data, value, gas, memory_out_offset, memory_out_size = get_call_parameters( global_state, self.dynamic_loader @@ -2062,7 +2036,7 @@ class Instruction: if callee_account is not None and callee_account.code.bytecode == "": log.debug("The call is related to ether transfer between accounts") - sender = global_state.environment.active_account.address + sender = environment.active_account.address receiver = callee_account.address transfer_ether(global_state, sender, receiver, value) @@ -2085,10 +2059,82 @@ class Instruction: native_result = native_call( global_state, callee_address, call_data, memory_out_offset, memory_out_size ) + if native_result: return native_result - global_state.mstate.stack.append( - global_state.new_bitvec("retval_" + str(instr["address"]), 256) + transaction = MessageCallTransaction( + world_state=global_state.world_state, + gas_price=environment.gasprice, + gas_limit=gas, + origin=environment.origin, + code=callee_account.code, + caller=environment.address, + callee_account=environment.active_account, + call_data=call_data, + call_value=value, + static=True, ) + raise TransactionStartSignal(transaction, self.op_code, global_state) + + def staticcall_post(self, global_state: GlobalState) -> List[GlobalState]: + return self.post_handler(global_state, function_name="staticcall") + + def post_handler(self, global_state, function_name: str): + instr = global_state.get_current_instruction() + + try: + callee_address, callee_account, call_data, value, gas, memory_out_offset, memory_out_size = get_call_parameters( + global_state, self.dynamic_loader, True + ) + except ValueError as e: + log.debug( + "Could not determine required parameters for {}, putting fresh symbol on the stack. \n{}".format( + function_name, e + ) + ) + global_state.mstate.stack.append( + global_state.new_bitvec("retval_" + str(instr["address"]), 256) + ) + return [global_state] + + if global_state.last_return_data is None: + # Put return value on stack + return_value = global_state.new_bitvec( + "retval_" + str(instr["address"]), 256 + ) + global_state.mstate.stack.append(return_value) + return [global_state] + + try: + memory_out_offset = ( + util.get_concrete_int(memory_out_offset) + if isinstance(memory_out_offset, Expression) + else memory_out_offset + ) + memory_out_size = ( + util.get_concrete_int(memory_out_size) + if isinstance(memory_out_size, Expression) + else memory_out_size + ) + except TypeError: + global_state.mstate.stack.append( + global_state.new_bitvec("retval_" + str(instr["address"]), 256) + ) + return [global_state] + + # Copy memory + global_state.mstate.mem_extend( + memory_out_offset, min(memory_out_size, len(global_state.last_return_data)) + ) + for i in range(min(memory_out_size, len(global_state.last_return_data))): + global_state.mstate.memory[ + i + memory_out_offset + ] = global_state.last_return_data[i] + + # Put return value on stack + return_value = global_state.new_bitvec("retval_" + str(instr["address"]), 256) + global_state.mstate.stack.append(return_value) + global_state.mstate.constraints.append(return_value == 1) + return [global_state] diff --git a/mythril/laser/ethereum/state/environment.py b/mythril/laser/ethereum/state/environment.py index a3300f9a..4cfb51e0 100644 --- a/mythril/laser/ethereum/state/environment.py +++ b/mythril/laser/ethereum/state/environment.py @@ -22,6 +22,7 @@ class Environment: callvalue: ExprRef, origin: ExprRef, code=None, + static=False, ) -> None: """ @@ -32,7 +33,7 @@ class Environment: :param callvalue: :param origin: :param code: - :param calldata_type: + :param static: Makes the environment static. """ # Metadata @@ -50,6 +51,7 @@ class Environment: self.gasprice = gasprice self.origin = origin self.callvalue = callvalue + self.static = static def __str__(self) -> str: """ diff --git a/mythril/laser/ethereum/svm.py b/mythril/laser/ethereum/svm.py index 88b362c8..8ef03531 100644 --- a/mythril/laser/ethereum/svm.py +++ b/mythril/laser/ethereum/svm.py @@ -324,7 +324,9 @@ class LaserEVM: global_state.transaction_stack ) + [(start_signal.transaction, global_state)] new_global_state.node = global_state.node - new_global_state.mstate.constraints = global_state.mstate.constraints + new_global_state.mstate.constraints = ( + start_signal.global_state.mstate.constraints + ) log.debug("Starting new transaction %s", start_signal.transaction) diff --git a/mythril/laser/ethereum/transaction/transaction_models.py b/mythril/laser/ethereum/transaction/transaction_models.py index c2b18b9c..fb62864d 100644 --- a/mythril/laser/ethereum/transaction/transaction_models.py +++ b/mythril/laser/ethereum/transaction/transaction_models.py @@ -45,9 +45,11 @@ class TransactionStartSignal(Exception): self, transaction: Union["MessageCallTransaction", "ContractCreationTransaction"], op_code: str, + global_state: GlobalState, ) -> None: self.transaction = transaction self.op_code = op_code + self.global_state = global_state class BaseTransaction: @@ -66,6 +68,7 @@ class BaseTransaction: code=None, call_value=None, init_call_data=True, + static=False, ) -> None: assert isinstance(world_state, WorldState) self.world_state = world_state @@ -101,7 +104,7 @@ class BaseTransaction: if call_value is not None else symbol_factory.BitVecSym("callvalue{}".format(identifier), 256) ) - + self.static = static self.return_data = None # type: str def initial_global_state_from_environment(self, environment, active_function): @@ -159,6 +162,7 @@ class MessageCallTransaction(BaseTransaction): self.call_value, self.origin, code=self.code or self.callee_account.code, + static=self.static, ) return super().initial_global_state_from_environment( environment, active_function="fallback" diff --git a/tests/instructions/static_call_test.py b/tests/instructions/static_call_test.py new file mode 100644 index 00000000..b9bcc26c --- /dev/null +++ b/tests/instructions/static_call_test.py @@ -0,0 +1,124 @@ +import pytest +from mock import patch + +from mythril.disassembler.disassembly import Disassembly +from mythril.laser.smt import symbol_factory +from mythril.laser.ethereum.state.environment import Environment +from mythril.laser.ethereum.state.account import Account +from mythril.laser.ethereum.state.machine_state import MachineState +from mythril.laser.ethereum.state.global_state import GlobalState +from mythril.laser.ethereum.state.world_state import WorldState +from mythril.laser.ethereum.instructions import Instruction +from mythril.laser.ethereum.transaction.transaction_models import MessageCallTransaction +from mythril.laser.ethereum.call import SymbolicCalldata +from mythril.laser.ethereum.transaction import TransactionStartSignal + +from mythril.laser.ethereum.evm_exceptions import WriteProtection + + +def get_global_state(): + active_account = Account("0x0", code=Disassembly("60606040")) + environment = Environment( + active_account, None, SymbolicCalldata("2"), None, None, None + ) + world_state = WorldState() + world_state.put_account(active_account) + state = GlobalState(world_state, environment, None, MachineState(gas_limit=8000000)) + state.transaction_stack.append( + (MessageCallTransaction(world_state=world_state, gas_limit=8000000), None) + ) + return state + + +@patch( + "mythril.laser.ethereum.instructions.get_call_parameters", + return_value=( + "0", + Account(code=Disassembly(code="0x00"), address="0x19"), + 0, + 0, + 0, + 0, + 0, + ), +) +def test_staticcall(f1): + # Arrange + state = get_global_state() + state.mstate.stack = [10, 10, 10, 10, 10, 10, 10, 10, 0] + instruction = Instruction("staticcall", dynamic_loader=None) + + # Act and Assert + with pytest.raises(TransactionStartSignal) as ts: + instruction.evaluate(state) + assert ts.value.transaction.static + assert ts.value.transaction.initial_global_state().environment.static + + +test_data = ( + "suicide", + "create", + "create2", + "log0", + "log1", + "log2", + "log3", + "log4", + "sstore", +) + + +@pytest.mark.parametrize("input", test_data) +def test_staticness(input): + # Arrange + state = get_global_state() + state.environment.static = True + state.mstate.stack = [] + instruction = Instruction(input, dynamic_loader=None) + + # Act and Assert + with pytest.raises(WriteProtection): + instruction.evaluate(state) + + +test_data_call = ((0, True), (100, False)) + + +@pytest.mark.parametrize("input, success", test_data_call) +@patch("mythril.laser.ethereum.instructions.get_call_parameters") +def test_staticness_call_concrete(f1, input, success): + # Arrange + state = get_global_state() + state.environment.static = True + state.mstate.stack = [] + code = Disassembly(code="616263") + f1.return_value = ("0", Account(code=code, address="0x19"), 0, input, 0, 0, 0) + instruction = Instruction("call", dynamic_loader=None) + + # Act and Assert + if success: + with pytest.raises(TransactionStartSignal) as ts: + instruction.evaluate(state) + assert ts.value.transaction.static + else: + with pytest.raises(WriteProtection): + instruction.evaluate(state) + + +@patch("mythril.laser.ethereum.instructions.get_call_parameters") +def test_staticness_call_symbolic(f1): + # Arrange + state = get_global_state() + state.environment.static = True + state.mstate.stack = [] + call_value = symbol_factory.BitVecSym("x", 256) + code = Disassembly(code="616263") + f1.return_value = ("0", Account(code=code, address="0x19"), 0, call_value, 0, 0, 0) + instruction = Instruction("call", dynamic_loader=None) + + # Act and Assert + with pytest.raises(TransactionStartSignal) as ts: + instruction.evaluate(state) + + assert ts.value.transaction.static + assert ts.value.global_state.mstate.constraints[-1] == (call_value == 0) From 4ea9827d663eb1fde412ede78b2a26710b0a1aec Mon Sep 17 00:00:00 2001 From: palkeo Date: Wed, 4 Sep 2019 14:43:58 +0200 Subject: [PATCH 134/164] Propagate annotations to the parent call, when a call succeeds. (#1197) * Propagate annotations to the parent call, when a call succeeds. This makes the MutationPruner behave correctly. * Add a copy_annotations_from method. * Run black. * WIP * Use persist_to_world_state and the world state instead. --- .../plugins/implementations/mutation_pruner.py | 16 ++++++++++++++-- mythril/laser/ethereum/state/annotation.py | 2 ++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/mythril/laser/ethereum/plugins/implementations/mutation_pruner.py b/mythril/laser/ethereum/plugins/implementations/mutation_pruner.py index 86755610..fa6aef58 100644 --- a/mythril/laser/ethereum/plugins/implementations/mutation_pruner.py +++ b/mythril/laser/ethereum/plugins/implementations/mutation_pruner.py @@ -15,7 +15,10 @@ class MutationAnnotation(StateAnnotation): This is the annotation used by the MutationPruner plugin to record mutations """ - pass + @property + def persist_to_world_state(self): + # This should persist among calls, and be but as a world state annotation. + return True class MutationPruner(LaserPlugin): @@ -44,10 +47,16 @@ class MutationPruner(LaserPlugin): @symbolic_vm.pre_hook("SSTORE") def sstore_mutator_hook(global_state: GlobalState): global_state.annotate(MutationAnnotation()) + assert len( + list(global_state.world_state.get_annotations(MutationAnnotation)) + ) @symbolic_vm.pre_hook("CALL") def call_mutator_hook(global_state: GlobalState): global_state.annotate(MutationAnnotation()) + assert len( + list(global_state.world_state.get_annotations(MutationAnnotation)) + ) @symbolic_vm.laser_hook("add_world_state") def world_state_filter_hook(global_state: GlobalState): @@ -63,5 +72,8 @@ class MutationPruner(LaserPlugin): global_state.current_transaction, ContractCreationTransaction ): return - if len(list(global_state.get_annotations(MutationAnnotation))) == 0: + if ( + len(list(global_state.world_state.get_annotations(MutationAnnotation))) + == 0 + ): raise PluginSkipWorldState diff --git a/mythril/laser/ethereum/state/annotation.py b/mythril/laser/ethereum/state/annotation.py index 6a321776..0f25a311 100644 --- a/mythril/laser/ethereum/state/annotation.py +++ b/mythril/laser/ethereum/state/annotation.py @@ -12,6 +12,8 @@ class StateAnnotation: traverse the state space themselves. """ + # TODO: Remove this? It seems to be used only in the MutationPruner, and + # we could simply use world state annotations if we want them to be persisted. @property def persist_to_world_state(self) -> bool: """If this function returns true then laser will also annotate the From 74cabd3d3cf3c0d5e579cac15c4ce7b53c26f46e Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 4 Sep 2019 10:41:34 -0400 Subject: [PATCH 135/164] Fix issue reporting for trial analysis --- mythril/mythx/__init__.py | 6 ++++-- mythril/solidity/soliditycontract.py | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/mythril/mythx/__init__.py b/mythril/mythx/__init__.py index ae105470..3f2b9616 100644 --- a/mythril/mythx/__init__.py +++ b/mythril/mythx/__init__.py @@ -79,8 +79,10 @@ def analyze(contracts: List[SolidityContract], analysis_mode: str = "quick") -> issue = Issue( contract=contract.name, function_name=None, - address=issue.locations[0].source_map.components[0].offset, - swc_id=issue.swc_id[4:], # remove 'SWC-' prefix + 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(), diff --git a/mythril/solidity/soliditycontract.py b/mythril/solidity/soliditycontract.py index 534189d3..772141c8 100644 --- a/mythril/solidity/soliditycontract.py +++ b/mythril/solidity/soliditycontract.py @@ -56,15 +56,15 @@ def get_contracts_from_file(input_file, solc_settings_json=None, solc_binary="so ) try: - for contractName in data["contracts"][input_file].keys(): + for contract_name in data["contracts"][input_file].keys(): if len( - data["contracts"][input_file][contractName]["evm"]["deployedBytecode"][ + data["contracts"][input_file][contract_name]["evm"]["deployedBytecode"][ "object" ] ): yield SolidityContract( input_file=input_file, - name=contractName, + name=contract_name, solc_settings_json=solc_settings_json, solc_binary=solc_binary, ) From 3c3153e24240e7c0c42afb4555c9b3b2c8fd2d3b Mon Sep 17 00:00:00 2001 From: Eric N Date: Thu, 5 Sep 2019 22:19:48 -0700 Subject: [PATCH 136/164] Cleaned up stupid logic. Add gas calculation to create2. --- mythril/laser/ethereum/instructions.py | 27 +++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py index 984d0592..80b51cd2 100644 --- a/mythril/laser/ethereum/instructions.py +++ b/mythril/laser/ethereum/instructions.py @@ -930,6 +930,14 @@ class Instruction: state.stack.append(no_of_bytes) return [global_state] + @staticmethod + def _sha3_gas_helper(global_state, length): + min_gas, max_gas = cast(Callable, OPCODE_GAS["SHA3_FUNC"])(length) + global_state.mstate.min_gas_used += min_gas + global_state.mstate.max_gas_used += max_gas + StateTransition.check_gas_usage_limit(global_state) + return global_state + @StateTransition(enable_gas=False) def sha3_(self, global_state: GlobalState) -> List[GlobalState]: """ @@ -955,10 +963,7 @@ class Instruction: state.max_gas_used += gas_tuple[1] return [global_state] - min_gas, max_gas = cast(Callable, OPCODE_GAS["SHA3_FUNC"])(length) - state.min_gas_used += min_gas - state.max_gas_used += max_gas - StateTransition.check_gas_usage_limit(global_state) + _sha3_gas_helper(global_state, length) state.mem_extend(index, length) data_list = [ @@ -1623,7 +1628,7 @@ class Instruction: @staticmethod def _create_transaction_helper( - global_state, call_value, mem_offset, mem_size, op_code + global_state, call_value, mem_offset, mem_size, op_code, create2_salt=None ): mstate = global_state.mstate @@ -1662,20 +1667,20 @@ class Instruction: origin = environment.origin contract_address = None - if op_code is "CREATE2": - salt = hex(mstate.stack.pop().value)[2:] + if create2_salt: + salt = hex(create2_salt)[2:] salt = "0" * (64 - len(salt)) + salt addr = hex(caller.value)[2:] addr = "0" * (40 - len(addr)) + addr + _sha3_gas_helper(global_state, len(code_str[:2] // 2)) + contract_address = int( get_code_hash("0xff" + addr + salt + get_code_hash(code_str)[2:])[26:], 16, ) - # TODO: calculate gas cost - transaction = ContractCreationTransaction( world_state=world_state, caller=caller, @@ -1716,10 +1721,10 @@ class Instruction: :param global_state: :return: """ - call_value, mem_offset, mem_size = global_state.mstate.pop(3) + call_value, mem_offset, mem_size, salt = global_state.mstate.pop(4) return self._create_transaction_helper( - global_state, call_value, mem_offset, mem_size, self.op_code + global_state, call_value, mem_offset, mem_size, self.op_code, salt ) @StateTransition() From 3dced432169b8f9ddab41714cca5edbc4856266e Mon Sep 17 00:00:00 2001 From: Eric N Date: Thu, 5 Sep 2019 22:39:00 -0700 Subject: [PATCH 137/164] Syntax errors --- mythril/laser/ethereum/instructions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py index 80b51cd2..fe8d4c8e 100644 --- a/mythril/laser/ethereum/instructions.py +++ b/mythril/laser/ethereum/instructions.py @@ -963,7 +963,7 @@ class Instruction: state.max_gas_used += gas_tuple[1] return [global_state] - _sha3_gas_helper(global_state, length) + Instruction._sha3_gas_helper(global_state, length) state.mem_extend(index, length) data_list = [ @@ -1674,7 +1674,7 @@ class Instruction: addr = hex(caller.value)[2:] addr = "0" * (40 - len(addr)) + addr - _sha3_gas_helper(global_state, len(code_str[:2] // 2)) + Instruction._sha3_gas_helper(global_state, len(code_str[:2] // 2)) contract_address = int( get_code_hash("0xff" + addr + salt + get_code_hash(code_str)[2:])[26:], From 988879e6322565aeeea37455b2a61276eda87f6b Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Sat, 7 Sep 2019 14:50:29 +0530 Subject: [PATCH 138/164] Mythril version 0.21.16 release --- mythril/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mythril/__version__.py b/mythril/__version__.py index 8cb19bb6..cc377277 100644 --- a/mythril/__version__.py +++ b/mythril/__version__.py @@ -4,4 +4,4 @@ This file is suitable for sourcing inside POSIX shell, e.g. bash as well as for importing into Python. """ -__version__ = "v0.21.15" +__version__ = "v0.21.16" From 7483bc80fd837b65b4e604f317032380cc1effb2 Mon Sep 17 00:00:00 2001 From: Eric N Date: Sun, 8 Sep 2019 21:33:25 -0700 Subject: [PATCH 139/164] Refactored code. --- mythril/laser/ethereum/instructions.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py index 7d21854c..ce2d2a96 100644 --- a/mythril/laser/ethereum/instructions.py +++ b/mythril/laser/ethereum/instructions.py @@ -1655,24 +1655,20 @@ class Instruction: environment = global_state.environment world_state = global_state.world_state - call_data = get_call_data( - global_state, - mem_offset, - mem_offset + mem_size, - ) - - call_data = call_data.concrete(None) + call_data = get_call_data(global_state, mem_offset, mem_offset + mem_size) - code_end = len(call_data) - for i in range(len(call_data)): - if not isinstance(call_data[i], int): + code_raw = [] + code_end = call_data.size + for i in range(call_data.size): + # Proper way to delimit init_bytecode? Seems to work. + if call_data[i].symbolic: code_end = i break + code_raw.append(call_data[i].value) - code_str = bytes.hex(bytes(call_data[0:code_end])) + code_str = bytes.hex(bytes(code_raw)) constructor_arguments = call_data[code_end:] - code = Disassembly(code_str) caller = environment.active_account.address @@ -1705,7 +1701,7 @@ class Instruction: call_value=call_value, contract_address=contract_address, ) - raise TransactionStartSignal(transaction, op_code) + raise TransactionStartSignal(transaction, op_code, global_state) @StateTransition(is_state_mutation_instruction=True) def create_(self, global_state: GlobalState) -> List[GlobalState]: From 389a2cc810da3452474f610f2f45e20a3e969764 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 9 Sep 2019 10:06:14 -0400 Subject: [PATCH 140/164] Add trial mode messaging --- mythril/mythx/__init__.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/mythril/mythx/__init__.py b/mythril/mythx/__init__.py index 3f2b9616..80d60e7d 100644 --- a/mythril/mythx/__init__.py +++ b/mythril/mythx/__init__.py @@ -1,6 +1,9 @@ +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 @@ -30,6 +33,11 @@ def analyze(contracts: List[SolidityContract], analysis_mode: str = "quick") -> password=os.environ.get("MYTHX_PASSWORD", "trial"), ) + if c.eth_address == "0x0000000000000000000000000000000000000000": + 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. @@ -61,15 +69,18 @@ def analyze(contracts: List[SolidityContract], analysis_mode: str = "quick") -> pass assert contract.creation_code, "Creation bytecode must exist." - 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, - ) + 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) From ba8cb741ca44e9fa4f19a2798fc44bacf783bd49 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 9 Sep 2019 10:23:06 -0400 Subject: [PATCH 141/164] Add variables for trial credentials --- mythril/mythx/__init__.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/mythril/mythx/__init__.py b/mythril/mythx/__init__.py index 80d60e7d..f1ebede0 100644 --- a/mythril/mythx/__init__.py +++ b/mythril/mythx/__init__.py @@ -15,6 +15,9 @@ import logging log = logging.getLogger(__name__) +TRIAL_ETH_ADDRESS = "0x0000000000000000000000000000000000000000" +TRIAL_PASSWORD = "trial" + def analyze(contracts: List[SolidityContract], analysis_mode: str = "quick") -> Report: """ @@ -27,13 +30,11 @@ def analyze(contracts: List[SolidityContract], analysis_mode: str = "quick") -> assert analysis_mode in ("quick", "full"), "analysis_mode must be 'quick' or 'full'" c = Client( - eth_address=os.environ.get( - "MYTHX_ETH_ADDRESS", "0x0000000000000000000000000000000000000000" - ), - password=os.environ.get("MYTHX_PASSWORD", "trial"), + eth_address=os.environ.get("MYTHX_ETH_ADDRESS", TRIAL_ETH_ADDRESS), + password=os.environ.get("MYTHX_PASSWORD", TRIAL_PASSWORD), ) - if c.eth_address == "0x0000000000000000000000000000000000000000": + 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." ) From 6456c86a6c1d0a046a34dc9603ef6239c19fe277 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 9 Sep 2019 17:35:39 -0400 Subject: [PATCH 142/164] Make _cache and _issues public --- mythril/analysis/modules/base.py | 14 +++----------- mythril/analysis/modules/delegatecall.py | 6 +++--- .../modules/dependence_on_predictable_vars.py | 6 +++--- mythril/analysis/modules/deprecated_ops.py | 2 +- mythril/analysis/modules/dos.py | 2 +- mythril/analysis/modules/ether_thief.py | 2 +- mythril/analysis/modules/exceptions.py | 4 ++-- mythril/analysis/modules/integer.py | 6 +++--- mythril/analysis/modules/multiple_sends.py | 6 +++--- .../modules/state_change_external_calls.py | 2 +- mythril/analysis/modules/suicide.py | 6 +++--- mythril/analysis/modules/unchecked_retval.py | 6 +++--- mythril/analysis/potential_issues.py | 2 +- 13 files changed, 28 insertions(+), 36 deletions(-) diff --git a/mythril/analysis/modules/base.py b/mythril/analysis/modules/base.py index 89c031f8..bcecf6a7 100644 --- a/mythril/analysis/modules/base.py +++ b/mythril/analysis/modules/base.py @@ -4,7 +4,6 @@ modules.""" import logging from typing import List, Set -from mythril.analysis.potential_issues import PotentialIssue from mythril.analysis.report import Issue log = logging.getLogger(__name__) @@ -36,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..0f37c660 100644 --- a/mythril/analysis/modules/delegatecall.py +++ b/mythril/analysis/modules/delegatecall.py @@ -93,12 +93,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]: 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 eda73a57..85766537 100644 --- a/mythril/analysis/modules/deprecated_ops.py +++ b/mythril/analysis/modules/deprecated_ops.py @@ -34,7 +34,7 @@ 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) 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 90028acf..13066795 100644 --- a/mythril/analysis/modules/ether_thief.py +++ b/mythril/analysis/modules/ether_thief.py @@ -61,7 +61,7 @@ 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 potential_issues = self._analyze_state(state) 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/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 33912766..a05f4ffd 100644 --- a/mythril/analysis/modules/state_change_external_calls.py +++ b/mythril/analysis/modules/state_change_external_calls.py @@ -112,7 +112,7 @@ 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) 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 index f866bbca..5fb5ccff 100644 --- a/mythril/analysis/potential_issues.py +++ b/mythril/analysis/potential_issues.py @@ -81,7 +81,7 @@ def check_potential_issues(state: GlobalState) -> None: :return: """ annotation = get_potential_issues_annotation(state) - for potential_issue in annotation.potential_issues[:]: + for potential_issue in annotation.potential_issues: try: transaction_sequence = get_transaction_sequence( state, state.mstate.constraints + potential_issue.constraints From 4342785c5df94ab4386a8f817c43bdb660c9af21 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 10 Sep 2019 11:26:47 -0400 Subject: [PATCH 143/164] Fix missed _cache and _issue names --- mythril/analysis/potential_issues.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mythril/analysis/potential_issues.py b/mythril/analysis/potential_issues.py index 5fb5ccff..99340330 100644 --- a/mythril/analysis/potential_issues.py +++ b/mythril/analysis/potential_issues.py @@ -90,8 +90,8 @@ def check_potential_issues(state: GlobalState) -> None: continue annotation.potential_issues.remove(potential_issue) - potential_issue.detector._cache.add(potential_issue.address) - potential_issue.detector._issues.append( + potential_issue.detector.cache.add(potential_issue.address) + potential_issue.detector.issues.append( Issue( contract=potential_issue.contract, function_name=potential_issue.function_name, From 3eaf917b228cf90c36427959d958de3e0f500a3d Mon Sep 17 00:00:00 2001 From: Eric N Date: Tue, 10 Sep 2019 23:51:20 -0700 Subject: [PATCH 144/164] Fixing CCT logic. Fixes CREATE with symbolic constructor arguments, but not Concrete. --- mythril/laser/ethereum/instructions.py | 162 ++++++++++++++---- .../transaction/transaction_models.py | 2 +- 2 files changed, 132 insertions(+), 32 deletions(-) diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py index ce2d2a96..e5c62f2a 100644 --- a/mythril/laser/ethereum/instructions.py +++ b/mythril/laser/ethereum/instructions.py @@ -31,6 +31,8 @@ from mythril.laser.smt import symbol_factory from mythril.disassembler.disassembly import Disassembly +from mythril.laser.ethereum.state.calldata import ConcreteCalldata, SymbolicCalldata + import mythril.laser.ethereum.util as helper from mythril.laser.ethereum import util from mythril.laser.ethereum.call import get_call_parameters, native_call, get_call_data @@ -933,14 +935,18 @@ class Instruction: state = global_state.mstate environment = global_state.environment disassembly = environment.code + calldata = global_state.environment.calldata if isinstance(global_state.current_transaction, ContractCreationTransaction): # Hacky way to ensure constructor arguments work - Pick some reasonably large size. - no_of_bytes = ( - len(disassembly.bytecode) // 2 + 0x200 - ) # space for 16 32-byte arguments - global_state.mstate.constraints.append( - global_state.environment.calldata.size == no_of_bytes - ) + no_of_bytes = len(disassembly.bytecode) // 2 + if isinstance(calldata, ConcreteCalldata): + no_of_bytes += calldata.size + else: + no_of_bytes += 0x200 # space for 16 32-byte arguments + global_state.mstate.constraints.append( + global_state.environment.calldata.size == no_of_bytes + ) + else: no_of_bytes = len(disassembly.bytecode) // 2 state.stack.append(no_of_bytes) @@ -1072,28 +1078,115 @@ class Instruction: global_state.mstate.stack.pop(), global_state.mstate.stack.pop(), ) + code = global_state.environment.code.bytecode + if code[0:2] == "0x": + code = code[2:] + code_size = len(code) // 2 - if ( - isinstance(global_state.current_transaction, ContractCreationTransaction) - and code_offset >= len(global_state.environment.code.bytecode) // 2 - ): + if isinstance(global_state.current_transaction, ContractCreationTransaction): # Treat creation code after the expected disassembly as calldata. # This is a slightly hacky way to ensure that symbolic constructor # arguments work correctly. - offset = code_offset - len(global_state.environment.code.bytecode) // 2 + offset = code_offset - code_size log.warning("Doing hacky thing offset: {} size: {}".format(offset, size)) - return self._calldata_copy_helper( - global_state, global_state.mstate, memory_offset, offset, size - ) - else: - return self._code_copy_helper( - code=global_state.environment.code.bytecode, - memory_offset=memory_offset, - code_offset=code_offset, - size=size, - op="CODECOPY", - global_state=global_state, - ) + + if isinstance(global_state.environment.calldata, SymbolicCalldata): + if code_offset >= code_size: + return self._calldata_copy_helper( + global_state, global_state.mstate, memory_offset, offset, size + ) + else: + # creation code and arguments may be in same object. + # Copy from both code and calldata appropriately. + state = global_state.mstate + environment = global_state.environment + concrete_memory_offset = helper.get_concrete_int(memory_offset) + concrete_code_offset = helper.get_concrete_int(code_offset) + concrete_size = helper.get_concrete_int(size) + + calldata = global_state.environment.calldata + calldata_size = helper.get_concrete_int(calldata.size) + + total_size = code_size + calldata_size + + if concrete_size > 0: + # borrow some logic from codecopyhelper and calldatacopyhelper + try: + state.mem_extend(concrete_memory_offset, concrete_size) + except TypeError as e: + log.debug("Memory allocation error: {}".format(e)) + state.mem_extend(concrete_memory_offset, 1) + state.memory[mstart] = global_state.new_bitvec( + "calldata_" + + str(environment.active_account.contract_name) + + "[" + + str(offset) + + ": + " + + str(concrete_size) + + "]", + 8, + ) + return [global_state] + + try: + i_data = code_size + new_memory = [] + for i in range(concrete_size): + if concrete_code_offset + i < code_size: + # copy from code + state.memory[concrete_memory_offset + i] = int( + code[ + 2 + * (concrete_code_offset + i) : 2 + * (concrete_code_offset + i + 1) + ], + 16, + ) + elif concrete_code_offset + i < total_size: + # copy from calldata + value = environment.calldata[i_data] + new_memory.append(value) + + i_data = ( + i_data + 1 + if isinstance(i_data, int) + else simplify(cast(BitVec, i_data) + 1) + ) + else: + raise IndexError + if new_memory: + for i in range(len(new_memory)): + state.memory[i + mstart] = new_memory[i] + except IndexError: + log.debug("Exception copying code to memory") + + state.memory[mstart] = global_state.new_bitvec( + "code_" + + str(environment.active_account.contract_name) + + "[" + + str(code_offset) + + ": + " + + str(concrete_size) + + "]", + 8, + ) + elif concrete_size == 0 and isinstance( + global_state.current_transaction, ContractCreationTransaction + ): + if concrete_code_offset >= total_size: + Instruction._handle_symbolic_args( + global_state, concrete_memory_offset + ) + + return [global_state] + return self._code_copy_helper( + code=global_state.environment.code.bytecode, + memory_offset=memory_offset, + code_offset=code_offset, + size=size, + op="CODECOPY", + global_state=global_state, + ) @StateTransition() def extcodesize_(self, global_state: GlobalState) -> List[GlobalState]: @@ -1657,16 +1750,24 @@ class Instruction: call_data = get_call_data(global_state, mem_offset, mem_offset + mem_size) - code_raw = [] - code_end = call_data.size - for i in range(call_data.size): - # Proper way to delimit init_bytecode? Seems to work. - if call_data[i].symbolic: + # code_raw = [] + # code_end = call_data.size + # for i in range(call_data.size): + # # Proper way to delimit init_bytecode? Seems to work. + # if call_data[i].symbolic: + # code_end = i + # break + # code_raw.append(call_data[i].value) + call_data = call_data.concrete(None) + + code_end = len(call_data) + for i in range(len(call_data)): + if not isinstance(call_data[i], int): code_end = i break - code_raw.append(call_data[i].value) - code_str = bytes.hex(bytes(code_raw)) + # code_str = bytes.hex(bytes(code_raw)) + code_str = bytes.hex(bytes(call_data[0:code_end])) constructor_arguments = call_data[code_end:] code = Disassembly(code_str) @@ -1689,7 +1790,6 @@ class Instruction: get_code_hash("0xff" + addr + salt + get_code_hash(code_str)[2:])[26:], 16, ) - transaction = ContractCreationTransaction( world_state=world_state, caller=caller, diff --git a/mythril/laser/ethereum/transaction/transaction_models.py b/mythril/laser/ethereum/transaction/transaction_models.py index 563e9e2f..3195365d 100644 --- a/mythril/laser/ethereum/transaction/transaction_models.py +++ b/mythril/laser/ethereum/transaction/transaction_models.py @@ -96,7 +96,7 @@ class BaseTransaction: self.call_data = ( call_data if isinstance(call_data, BaseCalldata) - else ConcreteCalldata(self.id, call_data) + else ConcreteCalldata(self.id, []) ) self.call_value = ( From a9755b8c67ee6a6fc5f782f3d31c0d703235585b Mon Sep 17 00:00:00 2001 From: Eric N Date: Sun, 15 Sep 2019 23:50:14 -0700 Subject: [PATCH 145/164] Modified codecopy. Debugging issue with contract function calls. --- mythril/laser/ethereum/gas.py | 2 +- mythril/laser/ethereum/instructions.py | 171 +++++++++++-------------- 2 files changed, 77 insertions(+), 96 deletions(-) diff --git a/mythril/laser/ethereum/gas.py b/mythril/laser/ethereum/gas.py index 4d8de488..0a0c0a38 100644 --- a/mythril/laser/ethereum/gas.py +++ b/mythril/laser/ethereum/gas.py @@ -180,7 +180,7 @@ OPCODE_GAS = { "LOG3": (4 * 375, 4 * 375 + 8 * 32), "LOG4": (5 * 375, 5 * 375 + 8 * 32), "CREATE": (32000, 32000), - "CREATE2": (32000, 32000), # TODO: Make the gas value is dynamic + "CREATE2": (32000, 32000), # TODO: Make the gas values dynamic "CALL": (700, 700 + 9000 + 25000), "NATIVE_COST": calculate_native_gas, "CALLCODE": (700, 700 + 9000 + 25000), diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py index e5c62f2a..903fe1ec 100644 --- a/mythril/laser/ethereum/instructions.py +++ b/mythril/laser/ethereum/instructions.py @@ -50,6 +50,7 @@ from mythril.laser.ethereum.transaction import ( MessageCallTransaction, TransactionStartSignal, ContractCreationTransaction, + get_next_transaction_id, ) from mythril.support.support_utils import get_code_hash @@ -939,13 +940,13 @@ class Instruction: if isinstance(global_state.current_transaction, ContractCreationTransaction): # Hacky way to ensure constructor arguments work - Pick some reasonably large size. no_of_bytes = len(disassembly.bytecode) // 2 - if isinstance(calldata, ConcreteCalldata): - no_of_bytes += calldata.size - else: - no_of_bytes += 0x200 # space for 16 32-byte arguments - global_state.mstate.constraints.append( - global_state.environment.calldata.size == no_of_bytes - ) + # if isinstance(calldata, ConcreteCalldata): + # no_of_bytes += calldata.size + 5000 + # else: + no_of_bytes += 0x200 # space for 16 32-byte arguments + # global_state.mstate.constraints.append( + # global_state.environment.calldata.size == no_of_bytes + # ) else: no_of_bytes = len(disassembly.bytecode) // 2 @@ -1096,7 +1097,6 @@ class Instruction: global_state, global_state.mstate, memory_offset, offset, size ) else: - # creation code and arguments may be in same object. # Copy from both code and calldata appropriately. state = global_state.mstate environment = global_state.environment @@ -1105,80 +1105,67 @@ class Instruction: concrete_size = helper.get_concrete_int(size) calldata = global_state.environment.calldata - calldata_size = helper.get_concrete_int(calldata.size) - - total_size = code_size + calldata_size - - if concrete_size > 0: - # borrow some logic from codecopyhelper and calldatacopyhelper - try: - state.mem_extend(concrete_memory_offset, concrete_size) - except TypeError as e: - log.debug("Memory allocation error: {}".format(e)) - state.mem_extend(concrete_memory_offset, 1) - state.memory[mstart] = global_state.new_bitvec( - "calldata_" - + str(environment.active_account.contract_name) - + "[" - + str(offset) - + ": + " - + str(concrete_size) - + "]", - 8, + + code_copy_offset = concrete_code_offset + code_copy_size = ( + concrete_size + if concrete_code_offset + concrete_size <= code_size + else code_size - concrete_code_offset + ) + code_copy_size = code_copy_size if code_copy_size >= 0 else 0 + + calldata_copy_offset = ( + concrete_code_offset - code_size + if concrete_code_offset - code_size > 0 + else 0 + ) + calldata_copy_size = concrete_code_offset + concrete_size - code_size + calldata_copy_size = ( + calldata_copy_size if calldata_copy_size >= 0 else 0 + ) + + try: + global_state.mstate.mem_extend( + concrete_memory_offset, concrete_size + calldata_copy_size + ) + except TypeError: + global_state.mstate.mem_extend(concrete_memory_offset, 1) + global_state.mstate.memory[ + concrete_memory_offset + ] = global_state.new_bitvec( + "code({})".format( + global_state.environment.active_account.contract_name + ), + 8, + ) + return [global_state] + + for i in range(code_copy_size + calldata_copy_size): + if i < code_copy_size: + global_state.mstate.memory[concrete_memory_offset + i] = int( + code[ + 2 + * (code_copy_offset + i) : 2 + * (code_copy_offset + i + 1) + ], + 16, ) - return [global_state] - - try: - i_data = code_size - new_memory = [] - for i in range(concrete_size): - if concrete_code_offset + i < code_size: - # copy from code - state.memory[concrete_memory_offset + i] = int( - code[ - 2 - * (concrete_code_offset + i) : 2 - * (concrete_code_offset + i + 1) - ], - 16, - ) - elif concrete_code_offset + i < total_size: - # copy from calldata - value = environment.calldata[i_data] - new_memory.append(value) - - i_data = ( - i_data + 1 - if isinstance(i_data, int) - else simplify(cast(BitVec, i_data) + 1) - ) - else: - raise IndexError - if new_memory: - for i in range(len(new_memory)): - state.memory[i + mstart] = new_memory[i] - except IndexError: - log.debug("Exception copying code to memory") - - state.memory[mstart] = global_state.new_bitvec( - "code_" - + str(environment.active_account.contract_name) - + "[" - + str(code_offset) - + ": + " - + str(concrete_size) - + "]", + elif i < code_copy_size + calldata_copy_size: + # copy from calldata + global_state.mstate.memory[ + concrete_memory_offset + i + ] = calldata[calldata_copy_offset + i] + else: + global_state.mstate.memory[ + concrete_memory_offset + i + ] = global_state.new_bitvec( + "code({})".format( + global_state.environment.active_account.contract_name + ), 8, ) - elif concrete_size == 0 and isinstance( - global_state.current_transaction, ContractCreationTransaction - ): - if concrete_code_offset >= total_size: - Instruction._handle_symbolic_args( - global_state, concrete_memory_offset - ) - return [global_state] + return self._code_copy_helper( code=global_state.environment.code.bytecode, memory_offset=memory_offset, @@ -1750,26 +1737,20 @@ class Instruction: call_data = get_call_data(global_state, mem_offset, mem_offset + mem_size) - # code_raw = [] - # code_end = call_data.size - # for i in range(call_data.size): - # # Proper way to delimit init_bytecode? Seems to work. - # if call_data[i].symbolic: - # code_end = i - # break - # code_raw.append(call_data[i].value) - call_data = call_data.concrete(None) - - code_end = len(call_data) - for i in range(len(call_data)): - if not isinstance(call_data[i], int): + code_raw = [] + code_end = call_data.size + for i in range(call_data.size): + if call_data[i].symbolic: code_end = i break + code_raw.append(call_data[i].value) - # code_str = bytes.hex(bytes(code_raw)) - code_str = bytes.hex(bytes(call_data[0:code_end])) + code_str = bytes.hex(bytes(code_raw)) - constructor_arguments = call_data[code_end:] + next_transaction_id = get_next_transaction_id() + constructor_arguments = ConcreteCalldata( + next_transaction_id, call_data[code_end:] + ) code = Disassembly(code_str) caller = environment.active_account.address @@ -1838,7 +1819,7 @@ class Instruction: @StateTransition() def create2_post(self, global_state: GlobalState) -> List[GlobalState]: - addr, call_value, mem_offset, mem_size = global_state.mstate.pop(4) + addr, call_value, mem_offset, mem_size, salt = global_state.mstate.pop(5) global_state.mstate.stack.append(addr) return [global_state] From bc0f677e87c65c091ab257baae306eaf39d29cd0 Mon Sep 17 00:00:00 2001 From: Eric N Date: Tue, 17 Sep 2019 00:02:41 -0700 Subject: [PATCH 146/164] Cleaned up code. Add create test. --- mythril/laser/ethereum/instructions.py | 51 ++++++++++------- mythril/laser/ethereum/svm.py | 16 +++--- tests/instructions/create_test.py | 77 +++++++++++++++++++------- 3 files changed, 97 insertions(+), 47 deletions(-) diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py index 903fe1ec..5d37350b 100644 --- a/mythril/laser/ethereum/instructions.py +++ b/mythril/laser/ethereum/instructions.py @@ -940,13 +940,13 @@ class Instruction: if isinstance(global_state.current_transaction, ContractCreationTransaction): # Hacky way to ensure constructor arguments work - Pick some reasonably large size. no_of_bytes = len(disassembly.bytecode) // 2 - # if isinstance(calldata, ConcreteCalldata): - # no_of_bytes += calldata.size + 5000 - # else: - no_of_bytes += 0x200 # space for 16 32-byte arguments - # global_state.mstate.constraints.append( - # global_state.environment.calldata.size == no_of_bytes - # ) + if isinstance(calldata, ConcreteCalldata): + no_of_bytes += calldata.size + else: + no_of_bytes += 0x200 # space for 16 32-byte arguments + global_state.mstate.constraints.append( + global_state.environment.calldata.size == no_of_bytes + ) else: no_of_bytes = len(disassembly.bytecode) // 2 @@ -1139,9 +1139,9 @@ class Instruction: 8, ) return [global_state] - - for i in range(code_copy_size + calldata_copy_size): + for i in range(concrete_size): if i < code_copy_size: + # copy from code global_state.mstate.memory[concrete_memory_offset + i] = int( code[ 2 @@ -1154,7 +1154,7 @@ class Instruction: # copy from calldata global_state.mstate.memory[ concrete_memory_offset + i - ] = calldata[calldata_copy_offset + i] + ] = calldata[calldata_copy_offset + i - code_copy_size] else: global_state.mstate.memory[ concrete_memory_offset + i @@ -1726,11 +1726,9 @@ class Instruction: # Not supported return [global_state] - @staticmethod def _create_transaction_helper( - global_state, call_value, mem_offset, mem_size, op_code, create2_salt=None + self, global_state, call_value, mem_offset, mem_size, create2_salt=None ): - mstate = global_state.mstate environment = global_state.environment world_state = global_state.world_state @@ -1782,7 +1780,7 @@ class Instruction: call_value=call_value, contract_address=contract_address, ) - raise TransactionStartSignal(transaction, op_code, global_state) + raise TransactionStartSignal(transaction, self.op_code, global_state) @StateTransition(is_state_mutation_instruction=True) def create_(self, global_state: GlobalState) -> List[GlobalState]: @@ -1795,13 +1793,20 @@ class Instruction: call_value, mem_offset, mem_size = global_state.mstate.pop(3) return self._create_transaction_helper( - global_state, call_value, mem_offset, mem_size, self.op_code + global_state, call_value, mem_offset, mem_size ) @StateTransition() def create_post(self, global_state: GlobalState) -> List[GlobalState]: - addr, call_value, mem_offset, mem_size = global_state.mstate.pop(4) - global_state.mstate.stack.append(addr) + call_value, mem_offset, mem_size = global_state.mstate.pop(3) + call_data = get_call_data(global_state, mem_offset, mem_offset + mem_size) + if global_state.last_return_data: + global_state.mstate.stack.append( + symbol_factory.BitVecVal(int(global_state.last_return_data, 16), 256) + ) + else: + global_state.mstate.stack.append(symbol_factory.BitVecVal(0, 256)) + return [global_state] @StateTransition(is_state_mutation_instruction=True) @@ -1814,13 +1819,19 @@ class Instruction: call_value, mem_offset, mem_size, salt = global_state.mstate.pop(4) return self._create_transaction_helper( - global_state, call_value, mem_offset, mem_size, self.op_code, salt + global_state, call_value, mem_offset, mem_size, salt ) @StateTransition() def create2_post(self, global_state: GlobalState) -> List[GlobalState]: - addr, call_value, mem_offset, mem_size, salt = global_state.mstate.pop(5) - global_state.mstate.stack.append(addr) + call_value, mem_offset, mem_size, salt = global_state.mstate.pop(4) + call_data = get_call_data(global_state, mem_offset, mem_offset + mem_size) + if global_state.last_return_data: + global_state.mstate.stack.append( + symbol_factory.BitVecVal(int(global_state.last_return_data), 256) + ) + else: + global_state.mstate.stack.append(symbol_factory.BitVecVal(0, 256)) return [global_state] @StateTransition() diff --git a/mythril/laser/ethereum/svm.py b/mythril/laser/ethereum/svm.py index fa78c840..e2b53199 100644 --- a/mythril/laser/ethereum/svm.py +++ b/mythril/laser/ethereum/svm.py @@ -380,13 +380,6 @@ class LaserEVM: :param return_data: :return: """ - if isinstance(global_state.current_transaction, ContractCreationTransaction): - # is this the proper place to put CREATE handle? - return_global_state.mstate.stack.append( - global_state.environment.active_account.address - ) - return_global_state.mstate.min_gas_used += global_state.mstate.min_gas_used - return_global_state.mstate.max_gas_used += global_state.mstate.max_gas_used return_global_state.mstate.constraints += global_state.mstate.constraints # Resume execution of the transaction initializing instruction @@ -401,6 +394,15 @@ class LaserEVM: return_global_state.environment.active_account = global_state.accounts[ return_global_state.environment.active_account.address.value ] + if isinstance( + global_state.current_transaction, ContractCreationTransaction + ): + return_global_state.mstate.min_gas_used += ( + global_state.mstate.min_gas_used + ) + return_global_state.mstate.max_gas_used += ( + global_state.mstate.max_gas_used + ) # Execute the post instruction handler new_global_states = Instruction( diff --git a/tests/instructions/create_test.py b/tests/instructions/create_test.py index 5941949d..0a97565e 100644 --- a/tests/instructions/create_test.py +++ b/tests/instructions/create_test.py @@ -6,30 +6,67 @@ from mythril.laser.ethereum.state.global_state import GlobalState from mythril.laser.ethereum.state.world_state import WorldState from mythril.laser.ethereum.instructions import Instruction from mythril.laser.ethereum.transaction.transaction_models import MessageCallTransaction - +from mythril.laser.ethereum.state.calldata import ConcreteCalldata from mythril.laser.ethereum.svm import LaserEVM - from mythril.laser.smt import symbol_factory +# contract A { +# uint256 public val = 10; +# } +contract_init_code = "6080604052600a600055348015601457600080fd5b506084806100236000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c80633c6bb43614602d575b600080fd5b60336049565b6040518082815260200191505060405180910390f35b6000548156fea265627a7a72315820d3cfe7a909450a953cbd7e47d8c17477f2609baa5208d043e965efec69d1ed9864736f6c634300050b0032" +contract_runtime_code = "6080604052348015600f57600080fd5b506004361060285760003560e01c80633c6bb43614602d575b600080fd5b60336049565b6040518082815260200191505060405180910390f35b6000548156fea265627a7a72315820d3cfe7a909450a953cbd7e47d8c17477f2609baa5208d043e965efec69d1ed9864736f6c634300050b0032" -def test_create(): - creating_contract_runtime_code = "608060405260043610601c5760003560e01c8063efc81a8c146021575b600080fd5b60276029565b005b60006040516035906056565b604051809103906000f0801580156050573d6000803e3d6000fd5b50905050565b605b806100638339019056fe6080604052348015600f57600080fd5b50603e80601d6000396000f3fe6080604052600080fdfea265627a7a72315820f4040cbd444dcd2f49f1daf46a116eb32396f1cc84a9e79e3836d4bfe84d6bca64736f6c634300050b0032a265627a7a72315820e2350a73a28ed02b4dac678f2b77a330dc512c3cce8ca53fa1c30869f443553d64736f6c634300050b0032" - created_contract_init_code = "6080604052348015600f57600080fd5b50603e80601d6000396000f3fe6080604052600080fdfea265627a7a72315820f4040cbd444dcd2f49f1daf46a116eb32396f1cc84a9e79e3836d4bfe84d6bca64736f6c634300050b0032" - created_contract_runtime_code = "6080604052600080fdfea265627a7a72315820f4040cbd444dcd2f49f1daf46a116eb32396f1cc84a9e79e3836d4bfe84d6bca64736f6c634300050b0032" - world_state = WorldState() - account = world_state.create_account(balance=1000, address=101) - account.code = Disassembly(creating_contract_runtime_code) - environment = Environment(account, None, None, None, None, None) - og_state = GlobalState( - world_state, environment, None, MachineState(gas_limit=8000000) - ) - og_state.transaction_stack.append( - (MessageCallTransaction(world_state=WorldState(), gas_limit=8000000), None) - ) +last_state = None +created_contract_account = None + + +def execute_create(): + global last_state + global created_contract_account + if not last_state and not created_contract_account: + code_raw = [] + for i in range(len(contract_init_code) // 2): + code_raw.append(int(contract_init_code[2 * i : 2 * (i + 1)], 16)) + calldata = ConcreteCalldata(0, code_raw) - laser_evm = LaserEVM() + world_state = WorldState() + account = world_state.create_account(balance=1000000, address=101) + account.code = Disassembly("60a760006000f000") + environment = Environment(account, None, calldata, None, None, None) + og_state = GlobalState( + world_state, environment, None, MachineState(gas_limit=8000000) + ) + og_state.transaction_stack.append( + (MessageCallTransaction(world_state=WorldState(), gas_limit=8000000), None) + ) - new_states, op_code = laser_evm.execute_state(og_state) - # checks + laser = LaserEVM() + states = [og_state] + last_state = og_state + for state in states: + new_states, op_code = laser.execute_state(state) + last_state = state + if op_code == "STOP": + break + states.extend(new_states) - # TODO: come up with sequence, then grab an address from stack and check that its code is created. + created_contract_address = last_state.mstate.stack[-1].value + created_contract_account = last_state.world_state.accounts[ + created_contract_address + ] + + return last_state, created_contract_account + + +def test_create_has_code(): + last_state, created_contract_account = execute_create() + assert created_contract_account.code.bytecode == contract_runtime_code + + +def test_create_has_storage(): + last_state, created_contract_account = execute_create() + storage = created_contract_account.storage + # From contract, val = 10. + assert storage[symbol_factory.BitVecVal(0, 256)] == symbol_factory.BitVecVal( + 10, 256 + ) From 5f424b62d9219d8a50aab488df0ee9a5d060e79f Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Tue, 17 Sep 2019 18:13:26 +0530 Subject: [PATCH 147/164] Only propagate code based call annotations (#1211) * Only propagate code based call annotations * Move the if condition to a better place --- .../implementations/dependency_pruner.py | 57 ++-------------- .../implementations/mutation_pruner.py | 29 ++------- .../implementations/plugin_annotations.py | 65 +++++++++++++++++++ mythril/laser/ethereum/state/global_state.py | 8 +++ mythril/laser/ethereum/svm.py | 16 +++++ 5 files changed, 98 insertions(+), 77 deletions(-) create mode 100644 mythril/laser/ethereum/plugins/implementations/plugin_annotations.py diff --git a/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py b/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py index bfff2c4d..dacc6bd8 100644 --- a/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py +++ b/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py @@ -1,7 +1,10 @@ from mythril.laser.ethereum.svm import LaserEVM from mythril.laser.ethereum.plugins.plugin import LaserPlugin from mythril.laser.ethereum.plugins.signals import PluginSkipState -from mythril.laser.ethereum.state.annotation import StateAnnotation +from mythril.laser.ethereum.plugins.implementations.plugin_annotations import ( + DependencyAnnotation, + WSDependencyAnnotation, +) from mythril.laser.ethereum.state.global_state import GlobalState from mythril.laser.ethereum.transaction.transaction_models import ( ContractCreationTransaction, @@ -9,64 +12,12 @@ from mythril.laser.ethereum.transaction.transaction_models import ( from mythril.exceptions import UnsatError from mythril.analysis import solver from typing import cast, List, Dict, Set -from copy import copy import logging log = logging.getLogger(__name__) -class DependencyAnnotation(StateAnnotation): - """Dependency Annotation - - This annotation tracks read and write access to the state during each transaction. - """ - - def __init__(self): - self.storage_loaded = [] # type: List - self.storage_written = {} # type: Dict[int, List] - self.has_call = False # type: bool - self.path = [0] # type: List - self.blocks_seen = set() # type: Set[int] - - def __copy__(self): - result = DependencyAnnotation() - result.storage_loaded = copy(self.storage_loaded) - result.storage_written = copy(self.storage_written) - result.has_call = self.has_call - result.path = copy(self.path) - result.blocks_seen = copy(self.blocks_seen) - return result - - def get_storage_write_cache(self, iteration: int): - if iteration not in self.storage_written: - self.storage_written[iteration] = [] - - return self.storage_written[iteration] - - def extend_storage_write_cache(self, iteration: int, value: object): - if iteration not in self.storage_written: - self.storage_written[iteration] = [value] - elif value not in self.storage_written[iteration]: - self.storage_written[iteration].append(value) - - -class WSDependencyAnnotation(StateAnnotation): - """Dependency Annotation for World state - - This world state annotation maintains a stack of state annotations. - It is used to transfer individual state annotations from one transaction to the next. - """ - - def __init__(self): - self.annotations_stack = [] - - def __copy__(self): - result = WSDependencyAnnotation() - result.annotations_stack = copy(self.annotations_stack) - return result - - def get_dependency_annotation(state: GlobalState) -> DependencyAnnotation: """ Returns a dependency annotation diff --git a/mythril/laser/ethereum/plugins/implementations/mutation_pruner.py b/mythril/laser/ethereum/plugins/implementations/mutation_pruner.py index fa6aef58..6bc13d9e 100644 --- a/mythril/laser/ethereum/plugins/implementations/mutation_pruner.py +++ b/mythril/laser/ethereum/plugins/implementations/mutation_pruner.py @@ -1,26 +1,16 @@ -from mythril.laser.ethereum.state.annotation import StateAnnotation -from mythril.laser.ethereum.svm import LaserEVM from mythril.laser.ethereum.plugins.signals import PluginSkipWorldState from mythril.laser.ethereum.plugins.plugin import LaserPlugin +from mythril.laser.ethereum.plugins.implementations.plugin_annotations import ( + MutationAnnotation, +) from mythril.laser.ethereum.state.global_state import GlobalState +from mythril.laser.ethereum.svm import LaserEVM from mythril.laser.ethereum.transaction.transaction_models import ( ContractCreationTransaction, ) from mythril.laser.smt import And, symbol_factory -class MutationAnnotation(StateAnnotation): - """Mutation Annotation - - This is the annotation used by the MutationPruner plugin to record mutations - """ - - @property - def persist_to_world_state(self): - # This should persist among calls, and be but as a world state annotation. - return True - - class MutationPruner(LaserPlugin): """Mutation pruner plugin @@ -47,16 +37,10 @@ class MutationPruner(LaserPlugin): @symbolic_vm.pre_hook("SSTORE") def sstore_mutator_hook(global_state: GlobalState): global_state.annotate(MutationAnnotation()) - assert len( - list(global_state.world_state.get_annotations(MutationAnnotation)) - ) @symbolic_vm.pre_hook("CALL") def call_mutator_hook(global_state: GlobalState): global_state.annotate(MutationAnnotation()) - assert len( - list(global_state.world_state.get_annotations(MutationAnnotation)) - ) @symbolic_vm.laser_hook("add_world_state") def world_state_filter_hook(global_state: GlobalState): @@ -72,8 +56,5 @@ class MutationPruner(LaserPlugin): global_state.current_transaction, ContractCreationTransaction ): return - if ( - len(list(global_state.world_state.get_annotations(MutationAnnotation))) - == 0 - ): + if len(list(global_state.get_annotations(MutationAnnotation))) == 0: raise PluginSkipWorldState diff --git a/mythril/laser/ethereum/plugins/implementations/plugin_annotations.py b/mythril/laser/ethereum/plugins/implementations/plugin_annotations.py new file mode 100644 index 00000000..209e562e --- /dev/null +++ b/mythril/laser/ethereum/plugins/implementations/plugin_annotations.py @@ -0,0 +1,65 @@ +from mythril.laser.ethereum.state.annotation import StateAnnotation + +from copy import copy +from typing import Dict, List, Set + + +class MutationAnnotation(StateAnnotation): + """Mutation Annotation + + This is the annotation used by the MutationPruner plugin to record mutations + """ + + def __init__(self): + pass + + +class DependencyAnnotation(StateAnnotation): + """Dependency Annotation + + This annotation tracks read and write access to the state during each transaction. + """ + + def __init__(self): + self.storage_loaded = [] # type: List + self.storage_written = {} # type: Dict[int, List] + self.has_call = False # type: bool + self.path = [0] # type: List + self.blocks_seen = set() # type: Set[int] + + def __copy__(self): + result = DependencyAnnotation() + result.storage_loaded = copy(self.storage_loaded) + result.storage_written = copy(self.storage_written) + result.has_call = self.has_call + result.path = copy(self.path) + result.blocks_seen = copy(self.blocks_seen) + return result + + def get_storage_write_cache(self, iteration: int): + if iteration not in self.storage_written: + self.storage_written[iteration] = [] + + return self.storage_written[iteration] + + def extend_storage_write_cache(self, iteration: int, value: object): + if iteration not in self.storage_written: + self.storage_written[iteration] = [value] + elif value not in self.storage_written[iteration]: + self.storage_written[iteration].append(value) + + +class WSDependencyAnnotation(StateAnnotation): + """Dependency Annotation for World state + + This world state annotation maintains a stack of state annotations. + It is used to transfer individual state annotations from one transaction to the next. + """ + + def __init__(self): + self.annotations_stack = [] + + def __copy__(self): + result = WSDependencyAnnotation() + result.annotations_stack = copy(self.annotations_stack) + return result diff --git a/mythril/laser/ethereum/state/global_state.py b/mythril/laser/ethereum/state/global_state.py index 0c43980b..5096516e 100644 --- a/mythril/laser/ethereum/state/global_state.py +++ b/mythril/laser/ethereum/state/global_state.py @@ -53,6 +53,14 @@ class GlobalState: self.last_return_data = last_return_data self._annotations = annotations or [] + def add_annotations(self, annotations: List[StateAnnotation]): + """ + Function used to add annotations to global state + :param annotations: + :return: + """ + self._annotations += annotations + def __copy__(self) -> "GlobalState": """ diff --git a/mythril/laser/ethereum/svm.py b/mythril/laser/ethereum/svm.py index 0c554c7c..822d1c55 100644 --- a/mythril/laser/ethereum/svm.py +++ b/mythril/laser/ethereum/svm.py @@ -12,6 +12,9 @@ from mythril.laser.ethereum.evm_exceptions import VmException from mythril.laser.ethereum.instructions import Instruction from mythril.laser.ethereum.iprof import InstructionProfiler from mythril.laser.ethereum.plugins.signals import PluginSkipWorldState, PluginSkipState +from mythril.laser.ethereum.plugins.implementations.plugin_annotations import ( + MutationAnnotation, +) from mythril.laser.ethereum.state.global_state import GlobalState from mythril.laser.ethereum.state.world_state import WorldState from mythril.laser.ethereum.strategy.basic import DepthFirstSearchStrategy @@ -354,6 +357,19 @@ class LaserEVM: # First execute the post hook for the transaction ending instruction self._execute_post_hook(op_code, [end_signal.global_state]) + # Propogate codecall based annotations + if return_global_state.get_current_instruction()["opcode"] in ( + "DELEGATECALL", + "CALLCODE", + ): + new_annotations = [ + annotation + for annotation in global_state.get_annotations( + MutationAnnotation + ) + ] + return_global_state.add_annotations(new_annotations) + new_global_states = self._end_message_call( copy(return_global_state), global_state, From 8571d301d2102208bc1668bd3121c22cce0330cb Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 17 Sep 2019 10:01:32 -0400 Subject: [PATCH 148/164] Implement user supplied assertion module --- mythril/analysis/modules/user_assertions.py | 106 ++++++++++++++++++++ mythril/laser/ethereum/instructions.py | 2 +- 2 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 mythril/analysis/modules/user_assertions.py diff --git a/mythril/analysis/modules/user_assertions.py b/mythril/analysis/modules/user_assertions.py new file mode 100644 index 00000000..0c87798f --- /dev/null +++ b/mythril/analysis/modules/user_assertions.py @@ -0,0 +1,106 @@ +"""This module contains the detection code for potentially insecure low-level +calls.""" + +from mythril.analysis.potential_issues import ( + PotentialIssue, + get_potential_issues_annotation, +) +from mythril.analysis.swc_data import ASSERT_VIOLATION +from mythril.analysis.modules.base import DetectionModule +from mythril.laser.ethereum.state.global_state import GlobalState +import logging +import eth_abi + +log = logging.getLogger(__name__) + +DESCRIPTION = """ + +Search for reachable user-supplied exceptions. +Report a warning if an log message is emitted: 'emit AssertionFailed(string)' + +""" + +assertion_failed_hash = ( + 0xB42604CB105A16C8F6DB8A41E6B00C0C1B4826465E8BC504B3EB3E88B3E6A4A0 +) + + +class UserAssertions(DetectionModule): + """This module searches for low level calls (e.g. call.value()) that + forward all gas to the callee.""" + + def __init__(self): + """""" + super().__init__( + name="External calls", + swc_id=ASSERT_VIOLATION, + description=DESCRIPTION, + entrypoint="callback", + pre_hooks=["LOG1"], + ) + + def _execute(self, state: GlobalState) -> None: + """ + + :param state: + :return: + """ + potential_issues = self._analyze_state(state) + + annotation = get_potential_issues_annotation(state) + annotation.potential_issues.extend(potential_issues) + + def _analyze_state(self, state: GlobalState): + """ + + :param state: + :return: + """ + mem_start = state.mstate.stack[-1] + size = state.mstate.stack[-2] + topic = state.mstate.stack[-3] + + if topic.symbolic: + return [] + + if topic.value != assertion_failed_hash: + return [] + + message = None + if not mem_start.symbolic and not size.symbolic: + message = eth_abi.decode_single( + "string", + bytes( + state.mstate.memory[ + mem_start.value + 32 : mem_start.value + size.value + ] + ), + ).decode("utf8") + + description_head = "A user-provided assertion failed. Make sure the user-provided assertion is correct." + if message: + description_tail = "A user-provided assertion failed with message '{}'. Make sure the user-provided assertion is correct.".format( + message + ) + else: + description_tail = "A user-provided assertion failed. Make sure the user-provided assertion is correct." + + address = state.get_current_instruction()["address"] + issue = PotentialIssue( + contract=state.environment.active_account.contract_name, + function_name=state.environment.active_function_name, + address=address, + swc_id=ASSERT_VIOLATION, + title="Assertion Failed", + bytecode=state.environment.code.bytecode, + severity="Medium", + description_head=description_head, + description_tail=description_tail, + constraints=[], + detector=self, + ) + + return [issue] + + +detector = UserAssertions() diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py index 4bd8436f..75af57e3 100644 --- a/mythril/laser/ethereum/instructions.py +++ b/mythril/laser/ethereum/instructions.py @@ -1624,7 +1624,7 @@ class Instruction: # TODO: implement me state = global_state.mstate dpth = int(self.op_code[3:]) - state.stack.pop(), state.stack.pop() + mem_start, size = state.stack.pop(), state.stack.pop() log_data = [state.stack.pop() for _ in range(dpth)] # Not supported return [global_state] From f781ce3604e11c2475e59e8f412746f9cc0067de Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 17 Sep 2019 10:11:15 -0400 Subject: [PATCH 149/164] Add user supplied assertions module to documentation --- docs/source/module-list.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/source/module-list.rst b/docs/source/module-list.rst index 11c40d02..101ff94f 100644 --- a/docs/source/module-list.rst +++ b/docs/source/module-list.rst @@ -67,3 +67,9 @@ Unchecked Retval **************** The `unchecked retval module `_ detects `SWC-104 (Unchecked Call Return Value) `_. + +**************** +Unchecked Retval +**************** + +The `user supplied assertion module `_ detects `SWC-110 (Assert Violation) `_ for user-supplied assertions. User supplied assertions should be log messages of the form: :code:`emit AssertionFailed(string)`. From 126fc26da6de0b00809dc1ae4767b501a02975fa Mon Sep 17 00:00:00 2001 From: Eric N Date: Tue, 17 Sep 2019 22:58:29 -0700 Subject: [PATCH 150/164] Cleaned up some logic. Fixed issue with VMException and freezing on staticcall. --- mythril/analysis/call_helpers.py | 2 +- mythril/laser/ethereum/instructions.py | 20 +++++++++---------- .../transaction/transaction_models.py | 2 +- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/mythril/analysis/call_helpers.py b/mythril/analysis/call_helpers.py index 270ff5af..cb6f55d7 100644 --- a/mythril/analysis/call_helpers.py +++ b/mythril/analysis/call_helpers.py @@ -18,7 +18,7 @@ def get_call_from_state(state: GlobalState) -> Union[Call, None]: op = instruction["opcode"] stack = state.mstate.stack - if op in ("CALL", "CALLCODE", "STATICCALL"): + if op in ("CALL", "CALLCODE"): gas, to, value, meminstart, meminsz, memoutstart, memoutsz = ( get_variable(stack[-1]), get_variable(stack[-2]), diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py index 5d37350b..09ea0a2b 100644 --- a/mythril/laser/ethereum/instructions.py +++ b/mythril/laser/ethereum/instructions.py @@ -1789,7 +1789,6 @@ class Instruction: :param global_state: :return: """ - call_value, mem_offset, mem_size = global_state.mstate.pop(3) return self._create_transaction_helper( @@ -1801,12 +1800,10 @@ class Instruction: call_value, mem_offset, mem_size = global_state.mstate.pop(3) call_data = get_call_data(global_state, mem_offset, mem_offset + mem_size) if global_state.last_return_data: - global_state.mstate.stack.append( - symbol_factory.BitVecVal(int(global_state.last_return_data, 16), 256) - ) + return_val = symbol_factory.BitVecVal(int(global_state.last_return_data, 16), 256) else: - global_state.mstate.stack.append(symbol_factory.BitVecVal(0, 256)) - + return_val = symbol_factory.BitVecVal(0, 256) + global_state.mstate.stack.append(return_val) return [global_state] @StateTransition(is_state_mutation_instruction=True) @@ -1827,11 +1824,10 @@ class Instruction: call_value, mem_offset, mem_size, salt = global_state.mstate.pop(4) call_data = get_call_data(global_state, mem_offset, mem_offset + mem_size) if global_state.last_return_data: - global_state.mstate.stack.append( - symbol_factory.BitVecVal(int(global_state.last_return_data), 256) - ) + return_val = symbol_factory.BitVecVal(int(global_state.last_return_data), 256) else: - global_state.mstate.stack.append(symbol_factory.BitVecVal(0, 256)) + return_val = symbol_factory.BitVecVal(0, 256) + global_state.mstate.stack.append(return_val) return [global_state] @StateTransition() @@ -2287,6 +2283,7 @@ class Instruction: ) raise TransactionStartSignal(transaction, self.op_code, global_state) + @StateTransition() def staticcall_post(self, global_state: GlobalState) -> List[GlobalState]: return self.post_handler(global_state, function_name="staticcall") @@ -2294,8 +2291,9 @@ class Instruction: instr = global_state.get_current_instruction() try: + with_value = function_name is not "staticcall" callee_address, callee_account, call_data, value, gas, memory_out_offset, memory_out_size = get_call_parameters( - global_state, self.dynamic_loader, True + global_state, self.dynamic_loader, with_value ) except ValueError as e: log.debug( diff --git a/mythril/laser/ethereum/transaction/transaction_models.py b/mythril/laser/ethereum/transaction/transaction_models.py index 3195365d..06019646 100644 --- a/mythril/laser/ethereum/transaction/transaction_models.py +++ b/mythril/laser/ethereum/transaction/transaction_models.py @@ -201,7 +201,7 @@ class ContractCreationTransaction(BaseTransaction): callee_account = world_state.create_account( 0, concrete_storage=True, creator=caller.value, address=contract_address ) - callee_account.contract_name = contract_name + callee_account.contract_name = contract_name or callee_account.contract_name # init_call_data "should" be false, but it is easier to model the calldata symbolically # and add logic in codecopy/codesize/calldatacopy/calldatasize than to model code "correctly" super().__init__( From 079b8a70da18c944ecbbb9973518a6fd46367bfc Mon Sep 17 00:00:00 2001 From: Eric N Date: Tue, 17 Sep 2019 23:00:02 -0700 Subject: [PATCH 151/164] Ran black. TODO: Fix issue with storage. --- mythril/laser/ethereum/instructions.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py index 09ea0a2b..e91862b7 100644 --- a/mythril/laser/ethereum/instructions.py +++ b/mythril/laser/ethereum/instructions.py @@ -1800,7 +1800,9 @@ class Instruction: call_value, mem_offset, mem_size = global_state.mstate.pop(3) call_data = get_call_data(global_state, mem_offset, mem_offset + mem_size) if global_state.last_return_data: - return_val = symbol_factory.BitVecVal(int(global_state.last_return_data, 16), 256) + return_val = symbol_factory.BitVecVal( + int(global_state.last_return_data, 16), 256 + ) else: return_val = symbol_factory.BitVecVal(0, 256) global_state.mstate.stack.append(return_val) @@ -1824,7 +1826,9 @@ class Instruction: call_value, mem_offset, mem_size, salt = global_state.mstate.pop(4) call_data = get_call_data(global_state, mem_offset, mem_offset + mem_size) if global_state.last_return_data: - return_val = symbol_factory.BitVecVal(int(global_state.last_return_data), 256) + return_val = symbol_factory.BitVecVal( + int(global_state.last_return_data), 256 + ) else: return_val = symbol_factory.BitVecVal(0, 256) global_state.mstate.stack.append(return_val) From 72db1237fbf64e42b042040bc8b5978b5e3fee2b Mon Sep 17 00:00:00 2001 From: Eric N Date: Tue, 17 Sep 2019 23:48:32 -0700 Subject: [PATCH 152/164] Fixed issue with staticcall. --- mythril/laser/ethereum/instructions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py index e91862b7..b7709343 100644 --- a/mythril/laser/ethereum/instructions.py +++ b/mythril/laser/ethereum/instructions.py @@ -2280,7 +2280,7 @@ class Instruction: origin=environment.origin, code=callee_account.code, caller=environment.address, - callee_account=environment.active_account, + callee_account=callee_account, call_data=call_data, call_value=value, static=True, From 581c8d7b018448a7a6fd419357ae00e48572a1a4 Mon Sep 17 00:00:00 2001 From: Eric N Date: Wed, 18 Sep 2019 13:31:23 -0700 Subject: [PATCH 153/164] Refactor code --- mythril/laser/ethereum/instructions.py | 109 ++++-------------- .../transaction/transaction_models.py | 3 + 2 files changed, 27 insertions(+), 85 deletions(-) diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py index b7709343..70467da3 100644 --- a/mythril/laser/ethereum/instructions.py +++ b/mythril/laser/ethereum/instructions.py @@ -787,7 +787,7 @@ class Instruction: return [global_state] @staticmethod - def _calldata_copy_helper(global_state, state, mstart, dstart, size): + def _calldata_copy_helper(global_state, mstate, mstart, dstart, size): environment = global_state.environment try: @@ -811,11 +811,11 @@ class Instruction: size = cast(int, size) if size > 0: try: - state.mem_extend(mstart, size) + mstate.mem_extend(mstart, size) except TypeError as e: log.debug("Memory allocation error: {}".format(e)) - state.mem_extend(mstart, 1) - state.memory[mstart] = global_state.new_bitvec( + mstate.mem_extend(mstart, 1) + mstate.memory[mstart] = global_state.new_bitvec( "calldata_" + str(environment.active_account.contract_name) + "[" @@ -840,12 +840,12 @@ class Instruction: else simplify(cast(BitVec, i_data) + 1) ) for i in range(len(new_memory)): - state.memory[i + mstart] = new_memory[i] + mstate.memory[i + mstart] = new_memory[i] except IndexError: log.debug("Exception copying calldata to memory") - state.memory[mstart] = global_state.new_bitvec( + mstate.memory[mstart] = global_state.new_bitvec( "calldata_" + str(environment.active_account.contract_name) + "[" @@ -1039,34 +1039,6 @@ class Instruction: global_state.mstate.stack.append(global_state.environment.gasprice) return [global_state] - @staticmethod - def _handle_symbolic_args( - global_state: GlobalState, concrete_memory_offset: int - ) -> None: - """ - In contract creation transaction with dynamic arguments(like arrays, maps) solidity will try to - execute CODECOPY with code size as len(with_args) - len(without_args) which in our case - would be 0, hence we are writing 10 symbol words onto the memory on the assumption that - no one would use 10 array/map arguments for constructor. - :param global_state: The global state - :param concrete_memory_offset: The memory offset on which symbols should be written - """ - no_of_words = ceil( - min(len(global_state.environment.code.bytecode) / 2, 320) / 32 - ) - global_state.mstate.mem_extend(concrete_memory_offset, 32 * no_of_words) - for i in range(no_of_words): - global_state.mstate.memory.write_word_at( - concrete_memory_offset + i * 32, - global_state.new_bitvec( - "code_{}({})".format( - concrete_memory_offset + i * 32, - global_state.environment.active_account.contract_name, - ), - 256, - ), - ) - @StateTransition() def codecopy_(self, global_state: GlobalState) -> List[GlobalState]: """ @@ -1088,17 +1060,17 @@ class Instruction: # Treat creation code after the expected disassembly as calldata. # This is a slightly hacky way to ensure that symbolic constructor # arguments work correctly. + mstate = global_state.mstate offset = code_offset - code_size - log.warning("Doing hacky thing offset: {} size: {}".format(offset, size)) + log.debug("Copying from code offset: {} with size: {}".format(offset, size)) if isinstance(global_state.environment.calldata, SymbolicCalldata): if code_offset >= code_size: return self._calldata_copy_helper( - global_state, global_state.mstate, memory_offset, offset, size + global_state, mstate, memory_offset, offset, size ) else: # Copy from both code and calldata appropriately. - state = global_state.mstate environment = global_state.environment concrete_memory_offset = helper.get_concrete_int(memory_offset) concrete_code_offset = helper.get_concrete_int(code_offset) @@ -1124,47 +1096,21 @@ class Instruction: calldata_copy_size if calldata_copy_size >= 0 else 0 ) - try: - global_state.mstate.mem_extend( - concrete_memory_offset, concrete_size + calldata_copy_size - ) - except TypeError: - global_state.mstate.mem_extend(concrete_memory_offset, 1) - global_state.mstate.memory[ - concrete_memory_offset - ] = global_state.new_bitvec( - "code({})".format( - global_state.environment.active_account.contract_name - ), - 8, - ) - return [global_state] - for i in range(concrete_size): - if i < code_copy_size: - # copy from code - global_state.mstate.memory[concrete_memory_offset + i] = int( - code[ - 2 - * (code_copy_offset + i) : 2 - * (code_copy_offset + i + 1) - ], - 16, - ) - elif i < code_copy_size + calldata_copy_size: - # copy from calldata - global_state.mstate.memory[ - concrete_memory_offset + i - ] = calldata[calldata_copy_offset + i - code_copy_size] - else: - global_state.mstate.memory[ - concrete_memory_offset + i - ] = global_state.new_bitvec( - "code({})".format( - global_state.environment.active_account.contract_name - ), - 8, - ) - return [global_state] + [global_state] = self._code_copy_helper( + code=global_state.environment.code.bytecode, + memory_offset=memory_offset, + code_offset=code_copy_offset, + size=code_copy_size, + op="CODECOPY", + global_state=global_state, + ) + return self._calldata_copy_helper( + global_state=global_state, + mstate=mstate, + mstart=memory_offset + code_copy_size, + dstart=calldata_copy_offset, + size=calldata_copy_size, + ) return self._code_copy_helper( code=global_state.environment.code.bytecode, @@ -1254,13 +1200,6 @@ class Instruction: if code[0:2] == "0x": code = code[2:] - if concrete_size == 0 and isinstance( - global_state.current_transaction, ContractCreationTransaction - ): - if concrete_code_offset >= len(code) // 2: - Instruction._handle_symbolic_args(global_state, concrete_memory_offset) - return [global_state] - for i in range(concrete_size): if 2 * (concrete_code_offset + i + 1) <= len(code): global_state.mstate.memory[concrete_memory_offset + i] = int( diff --git a/mythril/laser/ethereum/transaction/transaction_models.py b/mythril/laser/ethereum/transaction/transaction_models.py index 06019646..b36cc61d 100644 --- a/mythril/laser/ethereum/transaction/transaction_models.py +++ b/mythril/laser/ethereum/transaction/transaction_models.py @@ -198,6 +198,9 @@ class ContractCreationTransaction(BaseTransaction): contract_address=None, ) -> None: self.prev_world_state = deepcopy(world_state) + contract_address = ( + contract_address if isinstance(contract_address, int) else None + ) callee_account = world_state.create_account( 0, concrete_storage=True, creator=caller.value, address=contract_address ) From e47c35ee154a755ca5a59dcfc00f65df6b3e1c03 Mon Sep 17 00:00:00 2001 From: Eric N Date: Wed, 18 Sep 2019 14:16:36 -0700 Subject: [PATCH 154/164] Fixed mypy. --- mythril/laser/ethereum/instructions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py index 70467da3..39ea5c4c 100644 --- a/mythril/laser/ethereum/instructions.py +++ b/mythril/laser/ethereum/instructions.py @@ -1152,9 +1152,9 @@ class Instruction: @staticmethod def _code_copy_helper( code: str, - memory_offset: BitVec, - code_offset: BitVec, - size: BitVec, + memory_offset: Union[int, BitVec], + code_offset: Union[int, BitVec], + size: Union[int, BitVec], op: str, global_state: GlobalState, ) -> List[GlobalState]: From 3cab4beb768b3ff23bd6491664735815faf376af Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 19 Sep 2019 17:33:36 -0400 Subject: [PATCH 155/164] Minor improvements to user_assertions.py --- mythril/analysis/modules/user_assertions.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mythril/analysis/modules/user_assertions.py b/mythril/analysis/modules/user_assertions.py index 0c87798f..0e68475e 100644 --- a/mythril/analysis/modules/user_assertions.py +++ b/mythril/analysis/modules/user_assertions.py @@ -26,8 +26,7 @@ assertion_failed_hash = ( class UserAssertions(DetectionModule): - """This module searches for low level calls (e.g. call.value()) that - forward all gas to the callee.""" + """This module searches for user supplied exceptions: emit AssertionFailed("Error").""" def __init__(self): """""" @@ -77,7 +76,7 @@ class UserAssertions(DetectionModule): ), ).decode("utf8") - description_head = "A user-provided assertion failed. Make sure the user-provided assertion is correct." + description_head = "A user-provided assertion failed." if message: description_tail = "A user-provided assertion failed with message '{}'. Make sure the user-provided assertion is correct.".format( message From 44856621262c67161cea7a2963ed9c40416b7e84 Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 19 Sep 2019 20:19:28 -0400 Subject: [PATCH 156/164] Remove unused variables --- mythril/laser/ethereum/instructions.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py index 39ea5c4c..46111773 100644 --- a/mythril/laser/ethereum/instructions.py +++ b/mythril/laser/ethereum/instructions.py @@ -1071,13 +1071,9 @@ class Instruction: ) else: # Copy from both code and calldata appropriately. - environment = global_state.environment - concrete_memory_offset = helper.get_concrete_int(memory_offset) concrete_code_offset = helper.get_concrete_int(code_offset) concrete_size = helper.get_concrete_int(size) - calldata = global_state.environment.calldata - code_copy_offset = concrete_code_offset code_copy_size = ( concrete_size From b2bbe2568a0a60977a03c9370e876c80a9595834 Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 19 Sep 2019 20:30:08 -0400 Subject: [PATCH 157/164] Remove unused import --- mythril/laser/ethereum/instructions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py index 46111773..2f1c7f03 100644 --- a/mythril/laser/ethereum/instructions.py +++ b/mythril/laser/ethereum/instructions.py @@ -6,7 +6,6 @@ import logging from copy import copy, deepcopy from typing import cast, Callable, List, Set, Union, Tuple, Any from datetime import datetime -from math import ceil from ethereum import utils from mythril.laser.smt import ( From 2a34a5d71207b0e4a68ebab9676129c5c271f1aa Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 23 Sep 2019 12:50:58 -0400 Subject: [PATCH 158/164] Remove unused variables --- mythril/laser/ethereum/instructions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py index 75af57e3..4bd8436f 100644 --- a/mythril/laser/ethereum/instructions.py +++ b/mythril/laser/ethereum/instructions.py @@ -1624,7 +1624,7 @@ class Instruction: # TODO: implement me state = global_state.mstate dpth = int(self.op_code[3:]) - mem_start, size = state.stack.pop(), state.stack.pop() + state.stack.pop(), state.stack.pop() log_data = [state.stack.pop() for _ in range(dpth)] # Not supported return [global_state] From cb307f0d94082b88d71ea88d81ea92cceb212bf3 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 24 Sep 2019 05:30:45 -0400 Subject: [PATCH 159/164] Check delegatecall after execution finishes (#1215) --- mythril/analysis/modules/delegatecall.py | 38 ++++++++++-------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/mythril/analysis/modules/delegatecall.py b/mythril/analysis/modules/delegatecall.py index 4b97228b..aa0cae6a 100644 --- a/mythril/analysis/modules/delegatecall.py +++ b/mythril/analysis/modules/delegatecall.py @@ -1,19 +1,18 @@ """This module contains the detection code for insecure delegate call usage.""" -import json import logging -from copy import copy -from typing import List, cast, Dict +from typing import List -from mythril.analysis import solver +from mythril.analysis.potential_issues import ( + get_potential_issues_annotation, + PotentialIssue, +) from mythril.analysis.swc_data import DELEGATECALL_TO_UNTRUSTED_CONTRACT from mythril.laser.ethereum.transaction.symbolic import ATTACKER_ADDRESS from mythril.laser.ethereum.transaction.transaction_models import ( ContractCreationTransaction, ) -from mythril.analysis.report import Issue from mythril.analysis.modules.base import DetectionModule from mythril.exceptions import UnsatError -from mythril.laser.ethereum.state.annotation import StateAnnotation from mythril.laser.ethereum.state.global_state import GlobalState from mythril.laser.smt import symbol_factory, UGT @@ -41,19 +40,16 @@ class DelegateCallModule(DetectionModule): """ 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: GlobalState) -> List[Issue]: + def _analyze_state(self, state: GlobalState) -> List[PotentialIssue]: """ :param state: the current state :return: returns the issues for that corresponding state """ - op_code = state.get_current_instruction()["opcode"] - gas = state.mstate.stack[-1] to = state.mstate.stack[-2] @@ -67,16 +63,14 @@ class DelegateCallModule(DetectionModule): constraints.append(tx.caller == ATTACKER_ADDRESS) try: - transaction_sequence = solver.get_transaction_sequence( - state, state.mstate.constraints + constraints - ) - address = state.get_current_instruction()["address"] + logging.debug( - "[DELEGATECALL] Detected delegatecall to a user-supplied address : {}".format( + "[DELEGATECALL] Detected potential 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 " @@ -85,7 +79,7 @@ class DelegateCallModule(DetectionModule): ) return [ - Issue( + PotentialIssue( contract=state.environment.active_account.contract_name, function_name=state.environment.active_function_name, address=address, @@ -95,8 +89,8 @@ class DelegateCallModule(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, ) ] From bd8c3f8cd4d9c0911546393094d4a18457792a94 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 24 Sep 2019 09:13:37 -0400 Subject: [PATCH 160/164] Minor improvements to user_assertions.py --- mythril/analysis/modules/user_assertions.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/mythril/analysis/modules/user_assertions.py b/mythril/analysis/modules/user_assertions.py index 0e68475e..bef9c33a 100644 --- a/mythril/analysis/modules/user_assertions.py +++ b/mythril/analysis/modules/user_assertions.py @@ -55,14 +55,9 @@ class UserAssertions(DetectionModule): :param state: :return: """ - mem_start = state.mstate.stack[-1] - size = state.mstate.stack[-2] - topic = state.mstate.stack[-3] + topic, size, mem_start = state.mstate.stack[-3:] - if topic.symbolic: - return [] - - if topic.value != assertion_failed_hash: + if topic.symbolic or topic.value != assertion_failed_hash: return [] message = None From d8bf5021c78bf04fb71b2e47968d6387b588bab4 Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Wed, 25 Sep 2019 16:52:22 +0100 Subject: [PATCH 161/164] Add the return condition (#1216) --- mythril/analysis/modules/delegatecall.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mythril/analysis/modules/delegatecall.py b/mythril/analysis/modules/delegatecall.py index aa0cae6a..e9229468 100644 --- a/mythril/analysis/modules/delegatecall.py +++ b/mythril/analysis/modules/delegatecall.py @@ -56,6 +56,10 @@ class DelegateCallModule(DetectionModule): constraints = [ to == ATTACKER_ADDRESS, UGT(gas, symbol_factory.BitVecVal(2300, 256)), + state.new_bitvec( + "retval_{}".format(state.get_current_instruction()["address"]), 256 + ) + == 1, ] for tx in state.world_state.transaction_sequence: From 028fae6ff1f6a61cd7298f3db48b6bea9f438f9a Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Wed, 25 Sep 2019 18:44:04 +0100 Subject: [PATCH 162/164] Mythril v0.21.17 --- mythril/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mythril/__version__.py b/mythril/__version__.py index cc377277..9085271a 100644 --- a/mythril/__version__.py +++ b/mythril/__version__.py @@ -4,4 +4,4 @@ This file is suitable for sourcing inside POSIX shell, e.g. bash as well as for importing into Python. """ -__version__ = "v0.21.16" +__version__ = "v0.21.17" From 90f2de8555ddf83a233af52108b25327f5a7e7e1 Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Thu, 26 Sep 2019 22:48:10 +0100 Subject: [PATCH 163/164] Fix typehints for latest mypy release (#1221) --- mythril/analysis/modules/unchecked_retval.py | 8 ++------ mythril/laser/ethereum/state/calldata.py | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/mythril/analysis/modules/unchecked_retval.py b/mythril/analysis/modules/unchecked_retval.py index 678f329c..5aa8f005 100644 --- a/mythril/analysis/modules/unchecked_retval.py +++ b/mythril/analysis/modules/unchecked_retval.py @@ -116,13 +116,9 @@ class UncheckedRetvalModule(DetectionModule): assert state.environment.code.instruction_list[state.mstate.pc - 1][ "opcode" ] in ["CALL", "DELEGATECALL", "STATICCALL", "CALLCODE"] - retval = state.mstate.stack[-1] - # Use Typed Dict after release of mypy 0.670 and remove type ignore + return_value = state.mstate.stack[-1] retvals.append( - { # type: ignore - "address": state.instruction["address"] - 1, - "retval": retval, - } + {"address": state.instruction["address"] - 1, "retval": return_value} ) return [] diff --git a/mythril/laser/ethereum/state/calldata.py b/mythril/laser/ethereum/state/calldata.py index a8ebfa2b..f7fa0036 100644 --- a/mythril/laser/ethereum/state/calldata.py +++ b/mythril/laser/ethereum/state/calldata.py @@ -70,7 +70,7 @@ class BaseCalldata: try: current_index = ( start - if isinstance(start, Expression) + if isinstance(start, BitVec) else symbol_factory.BitVecVal(start, 256) ) parts = [] From 75838e3a8d7f7300f5174fea1bba659fa173d496 Mon Sep 17 00:00:00 2001 From: Martin Derka Date: Fri, 27 Sep 2019 16:37:41 -0400 Subject: [PATCH 164/164] Added support for solidity 0.5.x (#1219) * Added support for solidity 0.5.x * Formatting using black * Added missing requirement for tests to pass * Fixed py-solc-x version * Added packages to setup * Removed py35 from tox * Synced the solc version for the test * Updated the test smart contract to floating pragma --- mythril/ethereum/util.py | 30 ++++++++++++++++--------- mythril/mythril/mythril_disassembler.py | 19 +++++++++++----- requirements.txt | 2 ++ setup.py | 2 ++ tests/native_test.py | 2 ++ tests/native_tests.sol | 2 +- tox.ini | 2 +- 7 files changed, 42 insertions(+), 17 deletions(-) diff --git a/mythril/ethereum/util.py b/mythril/ethereum/util.py index 7c9020f5..0fd72dcb 100644 --- a/mythril/ethereum/util.py +++ b/mythril/ethereum/util.py @@ -3,6 +3,7 @@ solc integration.""" import binascii import json import os +import solcx from pathlib import Path from subprocess import PIPE, Popen @@ -117,16 +118,25 @@ def solc_exists(version): :param version: :return: """ - solc_binaries = [ - os.path.join( - os.environ.get("HOME", str(Path.home())), - ".py-solc/solc-v" + version, - "bin/solc", - ) # py-solc setup - ] - if version.startswith("0.5"): - # Temporary fix to support v0.5.x with Ubuntu PPA setup - solc_binaries.append("/usr/bin/solc") + + solc_binaries = [] + if version.startswith("0.4"): + solc_binaries = [ + os.path.join( + os.environ.get("HOME", str(Path.home())), + ".py-solc/solc-v" + version, + "bin/solc", + ) # py-solc setup + ] + else: + # we are using solc-x for the the 0.5 and higher + solc_binaries = [os.path.join(solcx.__path__[0], "bin", "solc-v" + version)] + for solc_path in solc_binaries: if os.path.exists(solc_path): return solc_path + + # Last resort is to use the system installation + default_binary = "/usr/bin/solc" + if os.path.exists(default_binary): + return default_binary diff --git a/mythril/mythril/mythril_disassembler.py b/mythril/mythril/mythril_disassembler.py index 28fa01eb..e7ef1e3b 100644 --- a/mythril/mythril/mythril_disassembler.py +++ b/mythril/mythril/mythril_disassembler.py @@ -1,10 +1,10 @@ import logging import re import solc +import solcx import os from ethereum import utils -from solc.exceptions import SolcError from typing import List, Tuple, Optional from mythril.ethereum import util from mythril.ethereum.interface.rpc.client import EthJsonRpc @@ -63,15 +63,24 @@ class MythrilDisassembler: solc_binary = os.environ.get("SOLC") or "solc" else: solc_binary = util.solc_exists(version) - if solc_binary: + if solc_binary and solc_binary != util.solc_exists( + "default_ubuntu_version" + ): log.info("Given version is already installed") else: try: - solc.install_solc("v" + version) + if version.startswith("0.4"): + solc.install_solc("v" + version) + else: + solcx.install_solc("v" + version) solc_binary = util.solc_exists(version) if not solc_binary: - raise SolcError() - except SolcError: + raise solc.exceptions.SolcError() + except solc.exceptions.SolcError: + raise CriticalError( + "There was an error when trying to install the specified solc version" + ) + except solcx.exceptions.SolcError: raise CriticalError( "There was an error when trying to install the specified solc version" ) diff --git a/requirements.txt b/requirements.txt index 1a3662a0..13b69082 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,12 +17,14 @@ mock persistent>=4.2.0 plyvel py-flags +py-solc-x==0.6.0 py-solc pytest>=3.6.0 pytest-cov pytest_mock requests>=2.22.0 rlp>=1.0.1 +semantic_version==2.8.1 transaction>=2.2.1 z3-solver>=4.8.5.0 pysha3 diff --git a/setup.py b/setup.py index dd673b04..5955ab26 100755 --- a/setup.py +++ b/setup.py @@ -30,6 +30,8 @@ REQUIRED = [ "z3-solver>=4.8.5.0", "requests>=2.22.0", "py-solc", + "py-solc-x==0.6.0", + "semantic_version==2.8.1", "plyvel", "eth_abi==1.3.0", "eth-account>=0.1.0a2,<=0.3.0", diff --git a/tests/native_test.py b/tests/native_test.py index b9fd3671..82bb36b4 100644 --- a/tests/native_test.py +++ b/tests/native_test.py @@ -76,6 +76,8 @@ class NativeTests(BaseTestCase): @staticmethod def runTest(): """""" + # The solidity version (0.5.3 at the moment) should be kept in sync with + # pragma in ./tests/native_tests.sol disassembly = SolidityContract( "./tests/native_tests.sol", solc_binary=MythrilDisassembler._init_solc_binary("0.5.3"), diff --git a/tests/native_tests.sol b/tests/native_tests.sol index f786c1bd..15b32021 100644 --- a/tests/native_tests.sol +++ b/tests/native_tests.sol @@ -1,4 +1,4 @@ -pragma solidity 0.5.0; +pragma solidity ^0.5.0; contract Caller { diff --git a/tox.ini b/tox.ini index e50f07fe..804b6791 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py35,py36 +envlist = py36 [testenv] deps =