mirror of https://github.com/ConsenSys/mythril
commit
3313d1f9b1
@ -1,91 +0,0 @@ |
|||||||
from mythril.ether import util |
|
||||||
from mythril.rpc.client import EthJsonRpc |
|
||||||
from mythril.ether.contractstorage import get_persistent_storage |
|
||||||
from mythril.disassembler.disassembly import Disassembly |
|
||||||
from ethereum.abi import encode_abi |
|
||||||
import re |
|
||||||
import os |
|
||||||
|
|
||||||
|
|
||||||
# Discover contract functions that write the sender address, or an address passed as an argument, to storage. |
|
||||||
# Needs testrpc running on port 8546 |
|
||||||
|
|
||||||
# testrpc --port 8546 --gasLimit 0xFFFFFF --account 0x0b6f3fd29ca0e570faf9d0bb8945858b9c337cd2a2ff89d65013eec412a4a811,500000000000000000000 --account 0x2194ac1cd3b9ca6cccc1a90aa2c6f944994b80bb50c82b973adce7f288734d5c,500000000000000000000 |
|
||||||
|
|
||||||
|
|
||||||
addr_knupper= "0xe2beffc4bc7ebb9eae43d59d2b555749d9ce7c54" |
|
||||||
addr_schnupper = "0xadc2f8617191ff60a36c3c136170cc69c03e64cd" |
|
||||||
|
|
||||||
contract_storage = get_persistent_storage(os.path.join(os.path.expanduser('~'), ".mythril")) |
|
||||||
testrpc = EthJsonRpc("localhost", 8546) |
|
||||||
|
|
||||||
testargs1 = [ |
|
||||||
([], []), |
|
||||||
(['address'], [addr_schnupper]), |
|
||||||
(['address', 'uint256'], [addr_schnupper, 1 ]), |
|
||||||
(['address', 'uint256', 'uint256'], [addr_schnupper, 1, 1]), |
|
||||||
(['address[]'], [[addr_schnupper]]), |
|
||||||
(['address[]', 'uint256'], [[addr_schnupper], 1 ]), |
|
||||||
(['address[]', 'uint256', 'uint256'], [[addr_schnupper], 1, 1]), |
|
||||||
] |
|
||||||
|
|
||||||
|
|
||||||
def testCase(contract_addr, function_selector, arg_types, args): |
|
||||||
|
|
||||||
if re.match(r'^UNK_0x', function_selector): |
|
||||||
args = encode_abi(['address'], [addr_schnupper]) |
|
||||||
data= function_selector[4:] + args.hex() |
|
||||||
else: |
|
||||||
data = util.encode_calldata(function_selector, arg_types, args) |
|
||||||
|
|
||||||
tx = testrpc.eth_sendTransaction(to_address=contract_addr, from_address=addr_schnupper, gas=5000000, value=0, data=data) |
|
||||||
|
|
||||||
trace = testrpc.traceTransaction(tx) |
|
||||||
|
|
||||||
if trace: |
|
||||||
for t in trace['structLogs']: |
|
||||||
if t['op'] == 'SSTORE': |
|
||||||
if addr_schnupper[2:] in t['stack'][-2]: |
|
||||||
return True |
|
||||||
|
|
||||||
return False |
|
||||||
|
|
||||||
|
|
||||||
def testDynamic(contract_hash, contract, addresses, balances): |
|
||||||
|
|
||||||
ret = testrpc.eth_sendTransaction(from_address=addr_knupper, gas=5000000, value=0, data=contract.creation_code) |
|
||||||
receipt = testrpc.eth_getTransactionReceipt(ret) |
|
||||||
contract_addr = receipt['contractAddress'] |
|
||||||
|
|
||||||
try: |
|
||||||
disas = Disassembly(contract.code) |
|
||||||
except: |
|
||||||
return |
|
||||||
|
|
||||||
found = False |
|
||||||
|
|
||||||
for function_selector in disas.func_to_addr: |
|
||||||
|
|
||||||
try: |
|
||||||
for t in testargs1: |
|
||||||
if(testCase(contract_addr, function_selector, t[0], t[1])): |
|
||||||
print("Possible write!") |
|
||||||
print("Contract hash: " + contract_hash) |
|
||||||
print("Selector: " + function_selector) |
|
||||||
print("Input data: " + str(t[1])) |
|
||||||
|
|
||||||
for i in range(0, len(addresses)): |
|
||||||
print("Address: " + addresses[i] + ", balance: " + str(balances[i])) |
|
||||||
|
|
||||||
found = True |
|
||||||
break |
|
||||||
|
|
||||||
if found: |
|
||||||
break |
|
||||||
except: |
|
||||||
break |
|
||||||
|
|
||||||
|
|
||||||
print("Searching " +str(len(list(contract_storage.contracts))) + " contracts...") |
|
||||||
|
|
||||||
contract_storage.search("code#PUSH#", testDynamic) # Returns all contracts |
|
@ -1,72 +0,0 @@ |
|||||||
from mythril.ether import evm |
|
||||||
from mythril.ether.contractstorage import get_persistent_storage |
|
||||||
from mythril.disassembler.disassembly import Disassembly |
|
||||||
from mythril.rpc.client import EthJsonRpc |
|
||||||
from mythril.disassembler.callgraph import generate_callgraph |
|
||||||
import os |
|
||||||
|
|
||||||
|
|
||||||
contract_storage = get_persistent_storage() |
|
||||||
contract_keys = list(contract_storage.contracts) |
|
||||||
homestead = EthJsonRpc() |
|
||||||
|
|
||||||
# Iterate over all contracts in the database |
|
||||||
|
|
||||||
for k in contract_keys: |
|
||||||
|
|
||||||
contract = contract_storage.contracts[k] |
|
||||||
|
|
||||||
# Run each contract in the PyEthereum EVM trace to check whether DELEGATECALL is reached |
|
||||||
# To execute the fallback function, we don't provide any input data |
|
||||||
|
|
||||||
ret = evm.trace(contract.code) |
|
||||||
|
|
||||||
if 'DELEGATECALL' in ret: |
|
||||||
|
|
||||||
print("DELEGATECALL in fallback function: Contract 0x" + k.hex() + " deployed at: ") |
|
||||||
|
|
||||||
instance_list = contract_storage.instance_lists[k] |
|
||||||
|
|
||||||
for i in range(1, len(instance_list.addresses)): |
|
||||||
print("Address: " + instance_list.addresses[i] + ", balance: " + str(instance_list.balances[i])) |
|
||||||
|
|
||||||
# contract.get_xrefs() should contain the delegateCall() target (library contract) |
|
||||||
|
|
||||||
print("Referenced contracts:") |
|
||||||
|
|
||||||
xrefs = contract.get_xrefs() |
|
||||||
|
|
||||||
print ("\n".join(xrefs)) |
|
||||||
|
|
||||||
''' |
|
||||||
from here on are many different options! |
|
||||||
|
|
||||||
In this example, we'll only check one of the refernced contracts contains the initWallet() function |
|
||||||
If it does, we save the disassembly and callgraph for further analysis |
|
||||||
|
|
||||||
''' |
|
||||||
|
|
||||||
for xref in xrefs: |
|
||||||
code = homestead.eth_getCode(xref) |
|
||||||
|
|
||||||
disassembly = Disassembly(code) |
|
||||||
|
|
||||||
if contract.matches_expression("func#initWallet(address[],uint256,uint256)#"): |
|
||||||
print ("initWallet() in referenced library contract: " + xref) |
|
||||||
|
|
||||||
# Save list of contracts that forward calls to this library contract |
|
||||||
|
|
||||||
cwd = os.getcwd() |
|
||||||
|
|
||||||
with open("contracts_calling_" + xref + ".txt", "w") as f: |
|
||||||
addresses = contract_storage.instance_lists[k].addresses |
|
||||||
|
|
||||||
f.write("\n".join(addresses)) |
|
||||||
|
|
||||||
easm = disassembly.get_easm() |
|
||||||
|
|
||||||
with open("library_" + xref + ".easm", "w") as f: |
|
||||||
f.write(easm) |
|
||||||
|
|
||||||
generate_callgraph(disassembly, os.path.join(cwd, "library_" + xref)) |
|
||||||
|
|
@ -1,85 +0,0 @@ |
|||||||
//sol Wallet |
|
||||||
// Multi-sig, daily-limited account proxy/wallet. |
|
||||||
// @authors: |
|
||||||
// Gav Wood <g@ethdev.com> |
|
||||||
// inheritable "property" contract that enables methods to be protected by requiring the acquiescence of either a |
|
||||||
// single, or, crucially, each of a number of, designated owners. |
|
||||||
// usage: |
|
||||||
// use modifiers onlyowner (just own owned) or onlymanyowners(hash), whereby the same hash must be provided by |
|
||||||
// some number (specified in constructor) of the set of owners (specified in the constructor, modifiable) before the |
|
||||||
// interior is executed. |
|
||||||
|
|
||||||
pragma solidity ^0.4.9; |
|
||||||
|
|
||||||
contract WalletEvents { |
|
||||||
// EVENTS |
|
||||||
|
|
||||||
// this contract only has six types of events: it can accept a confirmation, in which case |
|
||||||
// we record owner and operation (hash) alongside it. |
|
||||||
event Confirmation(address owner, bytes32 operation); |
|
||||||
event Revoke(address owner, bytes32 operation); |
|
||||||
|
|
||||||
// some others are in the case of an owner changing. |
|
||||||
event OwnerChanged(address oldOwner, address newOwner); |
|
||||||
event OwnerAdded(address newOwner); |
|
||||||
event OwnerRemoved(address oldOwner); |
|
||||||
|
|
||||||
// the last one is emitted if the required signatures change |
|
||||||
event RequirementChanged(uint newRequirement); |
|
||||||
|
|
||||||
// Funds has arrived into the wallet (record how much). |
|
||||||
event Deposit(address _from, uint value); |
|
||||||
// Single transaction going out of the wallet (record who signed for it, how much, and to whom it's going). |
|
||||||
event SingleTransact(address owner, uint value, address to, bytes data, address created); |
|
||||||
// Multi-sig transaction going out of the wallet (record who signed for it last, the operation hash, how much, and to whom it's going). |
|
||||||
event MultiTransact(address owner, bytes32 operation, uint value, address to, bytes data, address created); |
|
||||||
// Confirmation still needed for a transaction. |
|
||||||
event ConfirmationNeeded(bytes32 operation, address initiator, uint value, address to, bytes data); |
|
||||||
} |
|
||||||
|
|
||||||
contract Wallet is WalletEvents { |
|
||||||
|
|
||||||
// METHODS |
|
||||||
|
|
||||||
// gets called when no other function matches |
|
||||||
function() payable { |
|
||||||
// just being sent some cash? |
|
||||||
if (msg.value > 0) |
|
||||||
Deposit(msg.sender, msg.value); |
|
||||||
else if (msg.data.length > 0) |
|
||||||
_walletLibrary.delegatecall(msg.data); |
|
||||||
} |
|
||||||
|
|
||||||
// Gets an owner by 0-indexed position (using numOwners as the count) |
|
||||||
function getOwner(uint ownerIndex) constant returns (address) { |
|
||||||
return address(m_owners[ownerIndex + 1]); |
|
||||||
} |
|
||||||
|
|
||||||
// As return statement unavailable in fallback, explicit the method here |
|
||||||
|
|
||||||
function hasConfirmed(bytes32 _operation, address _owner) external constant returns (bool) { |
|
||||||
return _walletLibrary.delegatecall(msg.data); |
|
||||||
} |
|
||||||
|
|
||||||
function isOwner(address _addr) constant returns (bool) { |
|
||||||
return _walletLibrary.delegatecall(msg.data); |
|
||||||
} |
|
||||||
|
|
||||||
// FIELDS |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
address constant _walletLibrary = 0x1111111111111111111111111111111111111111; |
|
||||||
|
|
||||||
// the number of owners that must confirm the same operation before it is run. |
|
||||||
uint public m_required; |
|
||||||
// pointer used to find a free slot in m_owners |
|
||||||
uint public m_numOwners; |
|
||||||
|
|
||||||
uint public m_dailyLimit; |
|
||||||
uint public m_spentToday; |
|
||||||
uint public m_lastDay; |
|
||||||
|
|
||||||
// list of owners |
|
||||||
uint[256] m_owners; |
|
||||||
} |
|
@ -1,357 +0,0 @@ |
|||||||
pragma solidity ^0.4.9; |
|
||||||
|
|
||||||
contract WalletLibrary { |
|
||||||
// EVENTS |
|
||||||
|
|
||||||
// this contract only has six types of events: it can accept a confirmation, in which case |
|
||||||
// we record owner and operation (hash) alongside it. |
|
||||||
event Confirmation(address owner, bytes32 operation); |
|
||||||
event Revoke(address owner, bytes32 operation); |
|
||||||
|
|
||||||
// some others are in the case of an owner changing. |
|
||||||
event OwnerChanged(address oldOwner, address newOwner); |
|
||||||
event OwnerAdded(address newOwner); |
|
||||||
event OwnerRemoved(address oldOwner); |
|
||||||
|
|
||||||
// the last one is emitted if the required signatures change |
|
||||||
event RequirementChanged(uint newRequirement); |
|
||||||
|
|
||||||
// Funds has arrived into the wallet (record how much). |
|
||||||
event Deposit(address _from, uint value); |
|
||||||
// Single transaction going out of the wallet (record who signed for it, how much, and to whom it's going). |
|
||||||
event SingleTransact(address owner, uint value, address to, bytes data, address created); |
|
||||||
// Multi-sig transaction going out of the wallet (record who signed for it last, the operation hash, how much, and to whom it's going). |
|
||||||
event MultiTransact(address owner, bytes32 operation, uint value, address to, bytes data, address created); |
|
||||||
// Confirmation still needed for a transaction. |
|
||||||
event ConfirmationNeeded(bytes32 operation, address initiator, uint value, address to, bytes data); |
|
||||||
|
|
||||||
|
|
||||||
// TYPES |
|
||||||
|
|
||||||
// struct for the status of a pending operation. |
|
||||||
struct PendingState { |
|
||||||
uint yetNeeded; |
|
||||||
uint ownersDone; |
|
||||||
uint index; |
|
||||||
} |
|
||||||
|
|
||||||
// Transaction structure to remember details of transaction lest it need be saved for a later call. |
|
||||||
struct Transaction { |
|
||||||
address to; |
|
||||||
uint value; |
|
||||||
bytes data; |
|
||||||
} |
|
||||||
|
|
||||||
// MODIFIERS |
|
||||||
|
|
||||||
// simple single-sig function modifier. |
|
||||||
modifier onlyowner { |
|
||||||
if (isOwner(msg.sender)) |
|
||||||
_; |
|
||||||
} |
|
||||||
// multi-sig function modifier: the operation must have an intrinsic hash in order |
|
||||||
// that later attempts can be realised as the same underlying operation and |
|
||||||
// thus count as confirmations. |
|
||||||
modifier onlymanyowners(bytes32 _operation) { |
|
||||||
if (confirmAndCheck(_operation)) |
|
||||||
_; |
|
||||||
} |
|
||||||
|
|
||||||
// METHODS |
|
||||||
|
|
||||||
// gets called when no other function matches |
|
||||||
function() payable { |
|
||||||
// just being sent some cash? |
|
||||||
if (msg.value > 0) |
|
||||||
Deposit(msg.sender, msg.value); |
|
||||||
} |
|
||||||
|
|
||||||
// constructor is given number of sigs required to do protected "onlymanyowners" transactions |
|
||||||
// as well as the selection of addresses capable of confirming them. |
|
||||||
function initMultiowned(address[] _owners, uint _required) { |
|
||||||
m_numOwners = _owners.length + 1; |
|
||||||
m_owners[1] = uint(msg.sender); |
|
||||||
m_ownerIndex[uint(msg.sender)] = 1; |
|
||||||
for (uint i = 0; i < _owners.length; ++i) |
|
||||||
{ |
|
||||||
m_owners[2 + i] = uint(_owners[i]); |
|
||||||
m_ownerIndex[uint(_owners[i])] = 2 + i; |
|
||||||
} |
|
||||||
m_required = _required; |
|
||||||
} |
|
||||||
|
|
||||||
// Revokes a prior confirmation of the given operation |
|
||||||
function revoke(bytes32 _operation) external { |
|
||||||
uint ownerIndex = m_ownerIndex[uint(msg.sender)]; |
|
||||||
// make sure they're an owner |
|
||||||
if (ownerIndex == 0) return; |
|
||||||
uint ownerIndexBit = 2**ownerIndex; |
|
||||||
var pending = m_pending[_operation]; |
|
||||||
if (pending.ownersDone & ownerIndexBit > 0) { |
|
||||||
pending.yetNeeded++; |
|
||||||
pending.ownersDone -= ownerIndexBit; |
|
||||||
Revoke(msg.sender, _operation); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Replaces an owner `_from` with another `_to`. |
|
||||||
function changeOwner(address _from, address _to) onlymanyowners(sha3(msg.data)) external { |
|
||||||
if (isOwner(_to)) return; |
|
||||||
uint ownerIndex = m_ownerIndex[uint(_from)]; |
|
||||||
if (ownerIndex == 0) return; |
|
||||||
|
|
||||||
clearPending(); |
|
||||||
m_owners[ownerIndex] = uint(_to); |
|
||||||
m_ownerIndex[uint(_from)] = 0; |
|
||||||
m_ownerIndex[uint(_to)] = ownerIndex; |
|
||||||
OwnerChanged(_from, _to); |
|
||||||
} |
|
||||||
|
|
||||||
function addOwner(address _owner) onlymanyowners(sha3(msg.data)) external { |
|
||||||
if (isOwner(_owner)) return; |
|
||||||
|
|
||||||
clearPending(); |
|
||||||
if (m_numOwners >= c_maxOwners) |
|
||||||
reorganizeOwners(); |
|
||||||
if (m_numOwners >= c_maxOwners) |
|
||||||
return; |
|
||||||
m_numOwners++; |
|
||||||
m_owners[m_numOwners] = uint(_owner); |
|
||||||
m_ownerIndex[uint(_owner)] = m_numOwners; |
|
||||||
OwnerAdded(_owner); |
|
||||||
} |
|
||||||
|
|
||||||
function removeOwner(address _owner) onlymanyowners(sha3(msg.data)) external { |
|
||||||
uint ownerIndex = m_ownerIndex[uint(_owner)]; |
|
||||||
if (ownerIndex == 0) return; |
|
||||||
if (m_required > m_numOwners - 1) return; |
|
||||||
|
|
||||||
m_owners[ownerIndex] = 0; |
|
||||||
m_ownerIndex[uint(_owner)] = 0; |
|
||||||
clearPending(); |
|
||||||
reorganizeOwners(); //make sure m_numOwner is equal to the number of owners and always points to the optimal free slot |
|
||||||
OwnerRemoved(_owner); |
|
||||||
} |
|
||||||
|
|
||||||
function changeRequirement(uint _newRequired) onlymanyowners(sha3(msg.data)) external { |
|
||||||
if (_newRequired > m_numOwners) return; |
|
||||||
m_required = _newRequired; |
|
||||||
clearPending(); |
|
||||||
RequirementChanged(_newRequired); |
|
||||||
} |
|
||||||
|
|
||||||
// Gets an owner by 0-indexed position (using numOwners as the count) |
|
||||||
function getOwner(uint ownerIndex) external constant returns (address) { |
|
||||||
return address(m_owners[ownerIndex + 1]); |
|
||||||
} |
|
||||||
|
|
||||||
function isOwner(address _addr) constant returns (bool) { |
|
||||||
return m_ownerIndex[uint(_addr)] > 0; |
|
||||||
} |
|
||||||
|
|
||||||
function hasConfirmed(bytes32 _operation, address _owner) external constant returns (bool) { |
|
||||||
var pending = m_pending[_operation]; |
|
||||||
uint ownerIndex = m_ownerIndex[uint(_owner)]; |
|
||||||
|
|
||||||
// make sure they're an owner |
|
||||||
if (ownerIndex == 0) return false; |
|
||||||
|
|
||||||
// determine the bit to set for this owner. |
|
||||||
uint ownerIndexBit = 2**ownerIndex; |
|
||||||
return !(pending.ownersDone & ownerIndexBit == 0); |
|
||||||
} |
|
||||||
|
|
||||||
// constructor - stores initial daily limit and records the present day's index. |
|
||||||
function initDaylimit(uint _limit) { |
|
||||||
m_dailyLimit = _limit; |
|
||||||
m_lastDay = today(); |
|
||||||
} |
|
||||||
// (re)sets the daily limit. needs many of the owners to confirm. doesn't alter the amount already spent today. |
|
||||||
function setDailyLimit(uint _newLimit) onlymanyowners(sha3(msg.data)) external { |
|
||||||
m_dailyLimit = _newLimit; |
|
||||||
} |
|
||||||
// resets the amount already spent today. needs many of the owners to confirm. |
|
||||||
function resetSpentToday() onlymanyowners(sha3(msg.data)) external { |
|
||||||
m_spentToday = 0; |
|
||||||
} |
|
||||||
|
|
||||||
// constructor - just pass on the owner array to the multiowned and |
|
||||||
// the limit to daylimit |
|
||||||
function initWallet(address[] _owners, uint _required, uint _daylimit) { |
|
||||||
initDaylimit(_daylimit); |
|
||||||
initMultiowned(_owners, _required); |
|
||||||
} |
|
||||||
|
|
||||||
// kills the contract sending everything to `_to`. |
|
||||||
function kill(address _to) onlymanyowners(sha3(msg.data)) external { |
|
||||||
suicide(_to); |
|
||||||
} |
|
||||||
|
|
||||||
// Outside-visible transact entry point. Executes transaction immediately if below daily spend limit. |
|
||||||
// If not, goes into multisig process. We provide a hash on return to allow the sender to provide |
|
||||||
// shortcuts for the other confirmations (allowing them to avoid replicating the _to, _value |
|
||||||
// and _data arguments). They still get the option of using them if they want, anyways. |
|
||||||
function execute(address _to, uint _value, bytes _data) external onlyowner returns (bytes32 o_hash) { |
|
||||||
// first, take the opportunity to check that we're under the daily limit. |
|
||||||
if ((_data.length == 0 && underLimit(_value)) || m_required == 1) { |
|
||||||
// yes - just execute the call. |
|
||||||
address created; |
|
||||||
if (_to == 0) { |
|
||||||
created = create(_value, _data); |
|
||||||
} else { |
|
||||||
if (!_to.call.value(_value)(_data)) |
|
||||||
throw; |
|
||||||
} |
|
||||||
SingleTransact(msg.sender, _value, _to, _data, created); |
|
||||||
} else { |
|
||||||
// determine our operation hash. |
|
||||||
o_hash = sha3(msg.data, block.number); |
|
||||||
// store if it's new |
|
||||||
if (m_txs[o_hash].to == 0 && m_txs[o_hash].value == 0 && m_txs[o_hash].data.length == 0) { |
|
||||||
m_txs[o_hash].to = _to; |
|
||||||
m_txs[o_hash].value = _value; |
|
||||||
m_txs[o_hash].data = _data; |
|
||||||
} |
|
||||||
if (!confirm(o_hash)) { |
|
||||||
ConfirmationNeeded(o_hash, msg.sender, _value, _to, _data); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
function create(uint _value, bytes _code) internal returns (address o_addr) { |
|
||||||
assembly { |
|
||||||
o_addr := create(_value, add(_code, 0x20), mload(_code)) |
|
||||||
// jumpi(invalidJumpLabel, iszero(extcodesize(o_addr))) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// confirm a transaction through just the hash. we use the previous transactions map, m_txs, in order |
|
||||||
// to determine the body of the transaction from the hash provided. |
|
||||||
function confirm(bytes32 _h) onlymanyowners(_h) returns (bool o_success) { |
|
||||||
if (m_txs[_h].to != 0 || m_txs[_h].value != 0 || m_txs[_h].data.length != 0) { |
|
||||||
address created; |
|
||||||
if (m_txs[_h].to == 0) { |
|
||||||
created = create(m_txs[_h].value, m_txs[_h].data); |
|
||||||
} else { |
|
||||||
if (!m_txs[_h].to.call.value(m_txs[_h].value)(m_txs[_h].data)) |
|
||||||
throw; |
|
||||||
} |
|
||||||
|
|
||||||
MultiTransact(msg.sender, _h, m_txs[_h].value, m_txs[_h].to, m_txs[_h].data, created); |
|
||||||
delete m_txs[_h]; |
|
||||||
return true; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// INTERNAL METHODS |
|
||||||
|
|
||||||
function confirmAndCheck(bytes32 _operation) internal returns (bool) { |
|
||||||
// determine what index the present sender is: |
|
||||||
uint ownerIndex = m_ownerIndex[uint(msg.sender)]; |
|
||||||
// make sure they're an owner |
|
||||||
if (ownerIndex == 0) return; |
|
||||||
|
|
||||||
var pending = m_pending[_operation]; |
|
||||||
// if we're not yet working on this operation, switch over and reset the confirmation status. |
|
||||||
if (pending.yetNeeded == 0) { |
|
||||||
// reset count of confirmations needed. |
|
||||||
pending.yetNeeded = m_required; |
|
||||||
// reset which owners have confirmed (none) - set our bitmap to 0. |
|
||||||
pending.ownersDone = 0; |
|
||||||
pending.index = m_pendingIndex.length++; |
|
||||||
m_pendingIndex[pending.index] = _operation; |
|
||||||
} |
|
||||||
// determine the bit to set for this owner. |
|
||||||
uint ownerIndexBit = 2**ownerIndex; |
|
||||||
// make sure we (the message sender) haven't confirmed this operation previously. |
|
||||||
if (pending.ownersDone & ownerIndexBit == 0) { |
|
||||||
Confirmation(msg.sender, _operation); |
|
||||||
// ok - check if count is enough to go ahead. |
|
||||||
if (pending.yetNeeded <= 1) { |
|
||||||
// enough confirmations: reset and run interior. |
|
||||||
delete m_pendingIndex[m_pending[_operation].index]; |
|
||||||
delete m_pending[_operation]; |
|
||||||
return true; |
|
||||||
} |
|
||||||
else |
|
||||||
{ |
|
||||||
// not enough: record that this owner in particular confirmed. |
|
||||||
pending.yetNeeded--; |
|
||||||
pending.ownersDone |= ownerIndexBit; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
function reorganizeOwners() private { |
|
||||||
uint free = 1; |
|
||||||
while (free < m_numOwners) |
|
||||||
{ |
|
||||||
while (free < m_numOwners && m_owners[free] != 0) free++; |
|
||||||
while (m_numOwners > 1 && m_owners[m_numOwners] == 0) m_numOwners--; |
|
||||||
if (free < m_numOwners && m_owners[m_numOwners] != 0 && m_owners[free] == 0) |
|
||||||
{ |
|
||||||
m_owners[free] = m_owners[m_numOwners]; |
|
||||||
m_ownerIndex[m_owners[free]] = free; |
|
||||||
m_owners[m_numOwners] = 0; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// checks to see if there is at least `_value` left from the daily limit today. if there is, subtracts it and |
|
||||||
// returns true. otherwise just returns false. |
|
||||||
function underLimit(uint _value) internal onlyowner returns (bool) { |
|
||||||
// reset the spend limit if we're on a different day to last time. |
|
||||||
if (today() > m_lastDay) { |
|
||||||
m_spentToday = 0; |
|
||||||
m_lastDay = today(); |
|
||||||
} |
|
||||||
// check to see if there's enough left - if so, subtract and return true. |
|
||||||
// overflow protection // dailyLimit check |
|
||||||
if (m_spentToday + _value >= m_spentToday && m_spentToday + _value <= m_dailyLimit) { |
|
||||||
m_spentToday += _value; |
|
||||||
return true; |
|
||||||
} |
|
||||||
return false; |
|
||||||
} |
|
||||||
|
|
||||||
// determines today's index. |
|
||||||
function today() private constant returns (uint) { return now / 1 days; } |
|
||||||
|
|
||||||
function clearPending() internal { |
|
||||||
uint length = m_pendingIndex.length; |
|
||||||
|
|
||||||
for (uint i = 0; i < length; ++i) { |
|
||||||
delete m_txs[m_pendingIndex[i]]; |
|
||||||
|
|
||||||
if (m_pendingIndex[i] != 0) |
|
||||||
delete m_pending[m_pendingIndex[i]]; |
|
||||||
} |
|
||||||
|
|
||||||
delete m_pendingIndex; |
|
||||||
} |
|
||||||
|
|
||||||
// FIELDS |
|
||||||
address constant _walletLibrary = 0xcafecafecafecafecafecafecafecafecafecafe; |
|
||||||
|
|
||||||
// the number of owners that must confirm the same operation before it is run. |
|
||||||
uint public m_required; |
|
||||||
// pointer used to find a free slot in m_owners |
|
||||||
uint public m_numOwners; |
|
||||||
|
|
||||||
uint public m_dailyLimit; |
|
||||||
uint public m_spentToday; |
|
||||||
uint public m_lastDay; |
|
||||||
|
|
||||||
// list of owners |
|
||||||
uint[256] m_owners; |
|
||||||
|
|
||||||
uint constant c_maxOwners = 250; |
|
||||||
// index on the list of owners to allow reverse lookup |
|
||||||
mapping(uint => uint) m_ownerIndex; |
|
||||||
// the ongoing operations. |
|
||||||
mapping(bytes32 => PendingState) m_pending; |
|
||||||
bytes32[] m_pendingIndex; |
|
||||||
|
|
||||||
// pending transactions we have at present. |
|
||||||
mapping (bytes32 => Transaction) m_txs; |
|
||||||
} |
|
@ -0,0 +1,73 @@ |
|||||||
|
from z3 import * |
||||||
|
from mythril.analysis.ops import * |
||||||
|
from mythril.analysis.report import Issue |
||||||
|
import re |
||||||
|
import logging |
||||||
|
|
||||||
|
|
||||||
|
''' |
||||||
|
MODULE DESCRIPTION: |
||||||
|
|
||||||
|
Check for call.value()() to an untrusted address |
||||||
|
''' |
||||||
|
|
||||||
|
def execute(statespace): |
||||||
|
|
||||||
|
logging.debug("Executing module: CALL_TO_DYNAMIC_WITH_GAS") |
||||||
|
|
||||||
|
issues = [] |
||||||
|
|
||||||
|
for call in statespace.calls: |
||||||
|
|
||||||
|
if (call.type == "CALL"): |
||||||
|
|
||||||
|
logging.debug("[CALL_TO_DYNAMIC_WITH_GAS] Call to: " + str(call.to) + ", value " + str(call.value) + ", gas = " + str(call.gas)) |
||||||
|
|
||||||
|
if (call.to.type == VarType.SYMBOLIC and (call.gas.type == VarType.CONCRETE and call.gas.val > 2300) or (call.gas.type == VarType.SYMBOLIC and "2300" not in str(call.gas))): |
||||||
|
|
||||||
|
description = "The function " + call.node.function_name + " contains a function call to " |
||||||
|
|
||||||
|
target = str(call.to) |
||||||
|
is_valid = False |
||||||
|
|
||||||
|
if ("calldata" in target or "caller" in target): |
||||||
|
|
||||||
|
if ("calldata" in target): |
||||||
|
description += "an address provided as a function argument. " |
||||||
|
else: |
||||||
|
description += "the address of the transaction sender. " |
||||||
|
|
||||||
|
is_valid = True |
||||||
|
else: |
||||||
|
m = re.search(r'storage_([a-z0-9_&^]+)', str(call.to)) |
||||||
|
|
||||||
|
if (m): |
||||||
|
index = m.group(1) |
||||||
|
|
||||||
|
try: |
||||||
|
|
||||||
|
for s in statespace.sstors[index]: |
||||||
|
|
||||||
|
if s.tainted: |
||||||
|
|
||||||
|
description += \ |
||||||
|
"an address found at storage position " + str(index) + ".\n" + \ |
||||||
|
"This storage position can be written to by calling the function '" + s.node.function_name + "'.\n" \ |
||||||
|
"Verify that the contract address cannot be set by untrusted users.\n" |
||||||
|
|
||||||
|
is_valid = True |
||||||
|
break |
||||||
|
|
||||||
|
except KeyError: |
||||||
|
logging.debug("[CALL_TO_DYNAMIC_WITH_GAS] No storage writes to index " + str(index)) |
||||||
|
continue |
||||||
|
|
||||||
|
if is_valid: |
||||||
|
|
||||||
|
description += "The available gas is forwarded to the called contract. Make sure that the logic of the calling contract is not adversely affected if the called contract misbehaves (e.g. reentrancy)." |
||||||
|
|
||||||
|
issue = Issue(call.node.module_name, call.node.function_name, call.addr, "CALL with gas to dynamic address", "Warning", description) |
||||||
|
|
||||||
|
issues.append(issue) |
||||||
|
|
||||||
|
return issues |
@ -0,0 +1,53 @@ |
|||||||
|
from z3 import * |
||||||
|
import re |
||||||
|
from mythril.analysis.ops import * |
||||||
|
from mythril.analysis.report import Issue |
||||||
|
import logging |
||||||
|
|
||||||
|
|
||||||
|
''' |
||||||
|
MODULE DESCRIPTION: |
||||||
|
|
||||||
|
Check for invocations of delegatecall(msg.data) in the fallback function. |
||||||
|
''' |
||||||
|
|
||||||
|
def execute(statespace): |
||||||
|
|
||||||
|
logging.debug("Executing module: DELEGATECALL_FORWARD") |
||||||
|
|
||||||
|
issues = [] |
||||||
|
visited = [] |
||||||
|
|
||||||
|
for call in statespace.calls: |
||||||
|
|
||||||
|
# Only needs to be checked once per call instructions (essentially just static analysis) |
||||||
|
|
||||||
|
if call.addr in visited: |
||||||
|
continue |
||||||
|
else: |
||||||
|
visited.append(call.addr) |
||||||
|
|
||||||
|
if (call.type == "DELEGATECALL") and (call.node.function_name == "main"): |
||||||
|
|
||||||
|
stack = call.state.stack |
||||||
|
|
||||||
|
meminstart = get_variable(stack[-3]) |
||||||
|
|
||||||
|
if meminstart.type == VarType.CONCRETE: |
||||||
|
|
||||||
|
if (re.search(r'calldata.*_0', str(call.state.memory[meminstart.val]))): |
||||||
|
|
||||||
|
issue = Issue(call.node.module_name, call.node.function_name, call.addr, "CALLDATA forwarded with delegatecall()", "Informational") |
||||||
|
|
||||||
|
issue.description = \ |
||||||
|
"This contract forwards its calldata via DELEGATECALL in its fallback function. " \ |
||||||
|
"This means that any function in the called contract can be executed. Note that the callee contract will have access to the storage of the calling contract.\n" |
||||||
|
|
||||||
|
if (call.to.type == VarType.CONCRETE): |
||||||
|
issue.description += ("DELEGATECALL target: " + hex(call.to.val)) |
||||||
|
else: |
||||||
|
issue.description += "DELEGATECALL target: " + str(call.to) |
||||||
|
|
||||||
|
issues.append(issue) |
||||||
|
|
||||||
|
return issues |
@ -0,0 +1,67 @@ |
|||||||
|
from z3 import * |
||||||
|
from mythril.analysis.ops import * |
||||||
|
from mythril.analysis.report import Issue |
||||||
|
import re |
||||||
|
import logging |
||||||
|
|
||||||
|
|
||||||
|
''' |
||||||
|
MODULE DESCRIPTION: |
||||||
|
|
||||||
|
Check for invocations of delegatecall/callcode to a user-supplied address |
||||||
|
''' |
||||||
|
|
||||||
|
def execute(statespace): |
||||||
|
|
||||||
|
logging.debug("Executing module: DELEGATECALL_TO_DYNAMIC") |
||||||
|
|
||||||
|
issues = [] |
||||||
|
|
||||||
|
for call in statespace.calls: |
||||||
|
|
||||||
|
if (call.type == "DELEGATECALL" or call.type == "CALLCODE"): |
||||||
|
|
||||||
|
if (call.to.type == VarType.SYMBOLIC): |
||||||
|
|
||||||
|
if ("calldata" in str(call.to)): |
||||||
|
issue = Issue(call.node.module_name, call.node.function_name, call.addr, call.type + " to dynamic address") |
||||||
|
|
||||||
|
issue.description = \ |
||||||
|
"The function " + call.node.function_name + " delegates execution to a contract address obtained from calldata.\n" \ |
||||||
|
"Recipient address: " + str(call.to) |
||||||
|
|
||||||
|
issues.append(issue) |
||||||
|
else: |
||||||
|
m = re.search(r'storage_([a-z0-9_&^]+)', str(call.to)) |
||||||
|
|
||||||
|
if (m): |
||||||
|
index = m.group(1) |
||||||
|
logging.debug("DELEGATECALL to contract address in storage") |
||||||
|
|
||||||
|
try: |
||||||
|
|
||||||
|
for s in statespace.sstors[index]: |
||||||
|
|
||||||
|
if s.tainted: |
||||||
|
issue = Issue(call.type + " to dynamic address in storage", "Warning") |
||||||
|
issue.description = \ |
||||||
|
"The function " + call.node.function_name + " in contract '" + call.node.module_name + " delegates execution to a contract address stored in a state variable. " \ |
||||||
|
"There is a check on storage index " + str(index) + ". This storage index can be written to by calling the function '" + s.node.function_name + "'.\n" \ |
||||||
|
"Make sure that the contract address cannot be set by untrusted users." |
||||||
|
issues.append(issue) |
||||||
|
break |
||||||
|
|
||||||
|
except KeyError: |
||||||
|
logging.debug("[ETHER_SEND] No storage writes to index " + str(index)) |
||||||
|
|
||||||
|
else: |
||||||
|
|
||||||
|
issue = Issue(call.node.module_name, call.node.function_name, call.addr, "DELEGATECALL to dynamic address", "Informational") |
||||||
|
|
||||||
|
issue.description = \ |
||||||
|
"The function " + call.node.function_name + " in contract '" + call.node.module_name + " delegates execution to a contract with a dynamic address." \ |
||||||
|
"To address:" + str(call.to) |
||||||
|
|
||||||
|
issues.append(issue) |
||||||
|
|
||||||
|
return issues |
@ -0,0 +1,133 @@ |
|||||||
|
from z3 import * |
||||||
|
from mythril.analysis.ops import * |
||||||
|
from mythril.analysis import solver |
||||||
|
from mythril.analysis.report import Issue |
||||||
|
from mythril.exceptions import UnsatError |
||||||
|
import re |
||||||
|
import logging |
||||||
|
|
||||||
|
|
||||||
|
''' |
||||||
|
MODULE DESCRIPTION: |
||||||
|
|
||||||
|
Check for CALLs that send >0 Ether to either the transaction sender, or to an address provided as a function argument. |
||||||
|
If msg.sender is checked against a value in storage, check whether that storage index is tainted (i.e. there's an unconstrained write |
||||||
|
to that index). |
||||||
|
''' |
||||||
|
|
||||||
|
def execute(statespace): |
||||||
|
|
||||||
|
logging.debug("Executing module: ETHER_SEND") |
||||||
|
|
||||||
|
issues = [] |
||||||
|
|
||||||
|
for call in statespace.calls: |
||||||
|
|
||||||
|
if ("callvalue" in str(call.value)): |
||||||
|
logging.debug("[ETHER_SEND] Skipping refund function") |
||||||
|
continue |
||||||
|
|
||||||
|
# We're only interested in calls that send Ether |
||||||
|
|
||||||
|
if call.value.type == VarType.CONCRETE: |
||||||
|
if call.value.val == 0: |
||||||
|
continue |
||||||
|
|
||||||
|
interesting = False |
||||||
|
|
||||||
|
description = "In the function '" + call.node.function_name +"' " |
||||||
|
|
||||||
|
if re.search(r'caller', str(call.to)): |
||||||
|
description += "a non-zero amount of Ether is sent to msg.sender.\n" |
||||||
|
interesting = True |
||||||
|
|
||||||
|
elif re.search(r'calldata', str(call.to)): |
||||||
|
description += "a non-zero amount of Ether is sent to an address taken from function arguments.\n" |
||||||
|
interesting = True |
||||||
|
|
||||||
|
else: |
||||||
|
m = re.search(r'storage_([a-z0-9_&^]+)', str(call.to)) |
||||||
|
|
||||||
|
if (m): |
||||||
|
|
||||||
|
idx = m.group(1) |
||||||
|
|
||||||
|
try: |
||||||
|
|
||||||
|
for s in statespace.sstors[idx]: |
||||||
|
|
||||||
|
if s.tainted: |
||||||
|
description += "a non-zero amount of Ether is sent to an address taken from storage slot " + str(idx) + "." \ |
||||||
|
" This storage slot can be written to by calling the function '" + s.node.function_name + "'.\n" |
||||||
|
interesting = True |
||||||
|
continue |
||||||
|
|
||||||
|
except KeyError: |
||||||
|
logging.debug("[ETHER_SEND] No storage writes to index " + str(idx)) |
||||||
|
break |
||||||
|
|
||||||
|
|
||||||
|
if interesting: |
||||||
|
|
||||||
|
description += "Call value is " + str(call.value) + ".\n" |
||||||
|
|
||||||
|
node = call.node |
||||||
|
|
||||||
|
can_solve = True |
||||||
|
constrained = False |
||||||
|
|
||||||
|
index = 0 |
||||||
|
|
||||||
|
while(can_solve and index < len(node.constraints)): |
||||||
|
|
||||||
|
constraint = node.constraints[index] |
||||||
|
index += 1 |
||||||
|
logging.debug("[ETHER_SEND] Constraint: " + str(constraint)) |
||||||
|
|
||||||
|
m = re.search(r'storage_([a-z0-9_&^]+)', str(constraint)) |
||||||
|
|
||||||
|
overwrite = False |
||||||
|
|
||||||
|
if (m): |
||||||
|
|
||||||
|
constrained = True |
||||||
|
idx = m.group(1) |
||||||
|
|
||||||
|
func = statespace.find_storage_write(idx) |
||||||
|
|
||||||
|
if (func): |
||||||
|
description += "\nThere is a check on storage index " + str(index) + ". This storage slot can be written to by calling the function '" + func + "'." |
||||||
|
overwrite = True |
||||||
|
else: |
||||||
|
logging.debug("[ETHER_SEND] No storage writes to index " + str(index)) |
||||||
|
can_solve = False |
||||||
|
break |
||||||
|
|
||||||
|
# CALLER may also be constrained to hardcoded address. I.e. 'caller' and some integer |
||||||
|
|
||||||
|
elif (re.search(r"caller", str(constraint)) and re.search(r'[0-9]{20}', str(constraint))): |
||||||
|
constrained = True |
||||||
|
can_solve = False |
||||||
|
break |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if not constrained: |
||||||
|
description += "It seems that this function can be called without restrictions." |
||||||
|
|
||||||
|
if can_solve: |
||||||
|
|
||||||
|
try: |
||||||
|
model = solver.get_model(node.constraints) |
||||||
|
logging.debug("[ETHER_SEND] MODEL: " + str(model)) |
||||||
|
|
||||||
|
for d in model.decls(): |
||||||
|
logging.debug("[ETHER_SEND] main model: %s = 0x%x" % (d.name(), model[d].as_long())) |
||||||
|
|
||||||
|
issue = Issue(call.node.module_name, call.node.function_name, call.addr, "Ether send", "Warning", description) |
||||||
|
issues.append(issue) |
||||||
|
|
||||||
|
except UnsatError: |
||||||
|
logging.debug("[ETHER_SEND] no model found") |
||||||
|
|
||||||
|
return issues |
@ -0,0 +1,76 @@ |
|||||||
|
from z3 import * |
||||||
|
from mythril.analysis import solver |
||||||
|
from mythril.analysis.ops import * |
||||||
|
from mythril.analysis.report import Issue |
||||||
|
from mythril.exceptions import UnsatError |
||||||
|
import re |
||||||
|
import logging |
||||||
|
|
||||||
|
|
||||||
|
''' |
||||||
|
MODULE DESCRIPTION: |
||||||
|
|
||||||
|
Check for integer underflows. |
||||||
|
For every SUB instruction, check if there's a possible state where op1 > op0. |
||||||
|
''' |
||||||
|
|
||||||
|
def execute(statespace): |
||||||
|
|
||||||
|
logging.debug("Executing module: INTEGER_UNDERFLOW") |
||||||
|
|
||||||
|
issues = [] |
||||||
|
|
||||||
|
for k in statespace.nodes: |
||||||
|
node = statespace.nodes[k] |
||||||
|
|
||||||
|
for instruction in node.instruction_list: |
||||||
|
|
||||||
|
if(instruction['opcode'] == "SUB"): |
||||||
|
|
||||||
|
stack = node.states[instruction['address']].stack |
||||||
|
|
||||||
|
op0 = stack[-1] |
||||||
|
op1 = stack[-2] |
||||||
|
|
||||||
|
constraints = copy.deepcopy(node.constraints) |
||||||
|
|
||||||
|
if type(op0) == int and type(op1) == int: |
||||||
|
continue |
||||||
|
|
||||||
|
if (re.search(r'calldatasize_', str(op0))) \ |
||||||
|
or (re.search(r'256\*.*If\(1', str(op0), re.DOTALL) or re.search(r'256\*.*If\(1', str(op1), re.DOTALL)) \ |
||||||
|
or (re.search(r'32 \+.*calldata', str(op0), re.DOTALL) or re.search(r'32 \+.*calldata', str(op1), re.DOTALL)): |
||||||
|
|
||||||
|
# Filter for patterns that contain possible (but apparently non-exploitable) Integer underflows. |
||||||
|
|
||||||
|
# Pattern 1: (96 + calldatasize_MAIN) - (96), where (96 + calldatasize_MAIN) would underflow if calldatasize is very large. |
||||||
|
# Pattern 2: (256*If(1 & storage_0 == 0, 1, 0)) - 1, this would underlow if storage_0 = 0 |
||||||
|
|
||||||
|
# Both seem to be standard compiler outputs that exist in many contracts. |
||||||
|
|
||||||
|
continue |
||||||
|
|
||||||
|
logging.debug("[INTEGER_UNDERFLOW] Checking SUB " + str(op0) + ", " + str(op1) + " at address " + str(instruction['address'])) |
||||||
|
|
||||||
|
constraints.append(UGT(op1,op0)) |
||||||
|
|
||||||
|
try: |
||||||
|
|
||||||
|
model = solver.get_model(constraints) |
||||||
|
|
||||||
|
issue = Issue(node.module_name, node.function_name, instruction['address'], "Integer Underflow", "Warning") |
||||||
|
|
||||||
|
issue.description = "A possible integer underflow exists in the function " + node.function_name + ".\n" \ |
||||||
|
"The SUB instruction at address " + str(instruction['address']) + " may result in a value < 0." |
||||||
|
|
||||||
|
issue.debug = "(" + str(op0) + ") - (" + str(op1) + ").]" |
||||||
|
|
||||||
|
issues.append(issue) |
||||||
|
|
||||||
|
for d in model.decls(): |
||||||
|
logging.debug("[INTEGER_UNDERFLOW] model: %s = 0x%x" % (d.name(), model[d].as_long())) |
||||||
|
|
||||||
|
except UnsatError: |
||||||
|
logging.debug("[INTEGER_UNDERFLOW] no model found") |
||||||
|
|
||||||
|
return issues |
@ -0,0 +1,31 @@ |
|||||||
|
from mythril.analysis.report import Issue |
||||||
|
import re |
||||||
|
import logging |
||||||
|
|
||||||
|
|
||||||
|
''' |
||||||
|
MODULE DESCRIPTION: |
||||||
|
|
||||||
|
Check for constraints on tx.origin (i.e., access to some functionality is restricted to a specific origin). |
||||||
|
''' |
||||||
|
|
||||||
|
def execute(statespace): |
||||||
|
|
||||||
|
logging.debug("Executing module: DEPRECIATED OPCODES") |
||||||
|
|
||||||
|
issues = [] |
||||||
|
|
||||||
|
for k in statespace.nodes: |
||||||
|
node = statespace.nodes[k] |
||||||
|
|
||||||
|
for instruction in node.instruction_list: |
||||||
|
|
||||||
|
if(instruction['opcode'] == "ORIGIN"): |
||||||
|
|
||||||
|
issue = Issue(node.module_name, node.function_name, None, "Use of tx.origin", "Warning", \ |
||||||
|
"Function " + node.function_name + " retrieves the transaction origin (tx.origin) using the ORIGIN opcode. Use tx.sender instead.\nSee also: https://solidity.readthedocs.io/en/develop/security-considerations.html#tx-origin" |
||||||
|
) |
||||||
|
|
||||||
|
issues.append(issue) |
||||||
|
|
||||||
|
return issues |
@ -0,0 +1,83 @@ |
|||||||
|
from z3 import * |
||||||
|
import re |
||||||
|
from mythril.analysis.ops import * |
||||||
|
from mythril.analysis.report import Issue |
||||||
|
import logging |
||||||
|
from laser.ethereum import helper |
||||||
|
|
||||||
|
|
||||||
|
''' |
||||||
|
MODULE DESCRIPTION: |
||||||
|
|
||||||
|
Test whether CALL return value is checked. |
||||||
|
|
||||||
|
For direct calls, the Solidity compiler auto-generates this check. E.g.: |
||||||
|
|
||||||
|
Alice c = Alice(address); |
||||||
|
c.ping(42); |
||||||
|
|
||||||
|
Here the CALL will be followed by IZSERO(retval), if retval = ZERO then state is reverted. |
||||||
|
|
||||||
|
For low-level-calls this check is omitted. E.g.: |
||||||
|
|
||||||
|
c.call.value(0)(bytes4(sha3("ping(uint256)")),1); |
||||||
|
|
||||||
|
''' |
||||||
|
|
||||||
|
def execute(statespace): |
||||||
|
|
||||||
|
logging.debug("Executing module: UNCHECKED_RETVAL") |
||||||
|
|
||||||
|
issues = [] |
||||||
|
visited = [] |
||||||
|
|
||||||
|
for call in statespace.calls: |
||||||
|
|
||||||
|
# Only needs to be checked once per call instructions (it's essentially just static analysis) |
||||||
|
|
||||||
|
if call.addr in visited: |
||||||
|
continue |
||||||
|
else: |
||||||
|
visited.append(call.addr) |
||||||
|
|
||||||
|
# The instructions executed in each node (basic block) are saved in node.instruction_list, e.g.: |
||||||
|
# [{address: "132", opcode: "CALL"}, {address: "133", opcode: "ISZERO"}] |
||||||
|
|
||||||
|
start_index = helper.get_instruction_index(call.node.instruction_list, call.addr) + 1 |
||||||
|
|
||||||
|
retval_checked = False |
||||||
|
|
||||||
|
# ISZERO retval should be found within the next few instructions. |
||||||
|
|
||||||
|
for i in range(0, 10): |
||||||
|
|
||||||
|
try: |
||||||
|
instr = call.node.instruction_list[start_index + i] |
||||||
|
except IndexError: |
||||||
|
break |
||||||
|
|
||||||
|
if (instr['opcode'] == 'ISZERO' and re.search(r'retval', str(call.node.states[instr['address']].stack[-1]))): |
||||||
|
retval_checked = True |
||||||
|
break |
||||||
|
|
||||||
|
if not retval_checked: |
||||||
|
|
||||||
|
issue = Issue(call.node.module_name, call.node.function_name, call.addr, "Unchecked CALL return value") |
||||||
|
|
||||||
|
if (call.to.type == VarType.CONCRETE): |
||||||
|
receiver = hex(call.to.val) |
||||||
|
elif (re.search(r"caller", str(call.to))): |
||||||
|
receiver = "msg.sender" |
||||||
|
elif (re.search(r"storage", str(call.to))): |
||||||
|
receiver = "an address obtained from storage" |
||||||
|
else: |
||||||
|
receiver = str(call.to) |
||||||
|
|
||||||
|
|
||||||
|
issue.description = \ |
||||||
|
"The function " + call.node.function_name + " contains a call to " + receiver + ".\n" \ |
||||||
|
"The return value of this call is not checked. Note that the function will continue to execute with a return value of '0' if the called contract throws." |
||||||
|
|
||||||
|
issues.append(issue) |
||||||
|
|
||||||
|
return issues |
@ -0,0 +1,112 @@ |
|||||||
|
from z3 import * |
||||||
|
from mythril.analysis import solver |
||||||
|
from mythril.analysis.ops import * |
||||||
|
from mythril.analysis.report import Issue |
||||||
|
from mythril.exceptions import UnsatError |
||||||
|
import re |
||||||
|
import logging |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
''' |
||||||
|
MODULE DESCRIPTION: |
||||||
|
|
||||||
|
Check for SUICIDE instructions that either can be reached by anyone, or where msg.sender is checked against a tainted storage index |
||||||
|
(i.e. there's a write to that index is unconstrained by msg.sender). |
||||||
|
''' |
||||||
|
|
||||||
|
def execute(statespace): |
||||||
|
|
||||||
|
logging.debug("Executing module: UNCHECKED_SUICIDE") |
||||||
|
|
||||||
|
issues = [] |
||||||
|
|
||||||
|
for k in statespace.nodes: |
||||||
|
node = statespace.nodes[k] |
||||||
|
|
||||||
|
for instruction in node.instruction_list: |
||||||
|
|
||||||
|
if(instruction['opcode'] == "SUICIDE"): |
||||||
|
|
||||||
|
logging.debug("[UNCHECKED_SUICIDE] suicide in function " + node.function_name) |
||||||
|
|
||||||
|
description = "The function " + node.function_name + " executes the SUICIDE instruction." |
||||||
|
|
||||||
|
state = node.states[instruction['address']] |
||||||
|
to = state.stack.pop() |
||||||
|
|
||||||
|
if ("caller" in str(to)): |
||||||
|
description += "\nThe remaining Ether is sent to the caller's address.\n" |
||||||
|
elif ("storage" in str(to)): |
||||||
|
description += "\nThe remaining Ether is sent to a stored address\n" |
||||||
|
elif ("calldata" in str(to)): |
||||||
|
description += "\nThe remaining Ether is sent to an address provided as a function argument." |
||||||
|
elif (type(to) == BitVecNumRef): |
||||||
|
description += "\nThe remaining Ether is sent to: " + hex(to.as_long()) |
||||||
|
else: |
||||||
|
description += "\nThe remaining Ether is sent to: " + str(to) + "\n" |
||||||
|
|
||||||
|
constrained = False |
||||||
|
can_solve = True |
||||||
|
|
||||||
|
index = 0 |
||||||
|
|
||||||
|
while(can_solve and index < len(node.constraints)): |
||||||
|
|
||||||
|
constraint = node.constraints[index] |
||||||
|
index += 1 |
||||||
|
|
||||||
|
m = re.search(r'storage_([a-z0-9_&^]+)', str(constraint)) |
||||||
|
|
||||||
|
overwrite = False |
||||||
|
|
||||||
|
if (m): |
||||||
|
constrained = True |
||||||
|
index = m.group(1) |
||||||
|
|
||||||
|
try: |
||||||
|
|
||||||
|
for s in statespace.sstors[index]: |
||||||
|
|
||||||
|
if s.tainted: |
||||||
|
description += "\nThere is a check on storage index " + str(index) + ". This storage index can be written to by calling the function '" + s.node.function_name + "'." |
||||||
|
break |
||||||
|
|
||||||
|
if not overwrite: |
||||||
|
logging.debug("[UNCHECKED_SUICIDE] No storage writes to index " + str(index)) |
||||||
|
can_solve = False |
||||||
|
break |
||||||
|
|
||||||
|
except KeyError: |
||||||
|
logging.debug("[UNCHECKED_SUICIDE] No storage writes to index " + str(index)) |
||||||
|
can_solve = False |
||||||
|
break |
||||||
|
|
||||||
|
|
||||||
|
# CALLER may also be constrained to hardcoded address. I.e. 'caller' and some integer |
||||||
|
|
||||||
|
elif (re.search(r"caller", str(constraint)) and re.search(r'[0-9]{20}', str(constraint))): |
||||||
|
can_solve = False |
||||||
|
break |
||||||
|
|
||||||
|
|
||||||
|
if not constrained: |
||||||
|
description += "\nIt seems that this function can be called without restrictions." |
||||||
|
|
||||||
|
if can_solve: |
||||||
|
|
||||||
|
try: |
||||||
|
model = solver.get_model(node.constraints) |
||||||
|
logging.debug("[UNCHECKED_SUICIDE] MODEL: " + str(model)) |
||||||
|
|
||||||
|
|
||||||
|
for d in model.decls(): |
||||||
|
logging.debug("[UNCHECKED_SUICIDE] main model: %s = 0x%x" % (d.name(), model[d].as_long())) |
||||||
|
|
||||||
|
issue = Issue(node.module_name, node.function_name, instruction['address'], "Unchecked SUICIDE", "Warning", description) |
||||||
|
issues.append(issue) |
||||||
|
|
||||||
|
except UnsatError: |
||||||
|
logging.debug("[UNCHECKED_SUICIDE] no model found") |
||||||
|
|
||||||
|
return issues |
@ -0,0 +1,123 @@ |
|||||||
|
import re |
||||||
|
from z3 import * |
||||||
|
from mythril.analysis.ops import * |
||||||
|
from mythril.analysis import solver |
||||||
|
from mythril.analysis.report import Issue |
||||||
|
from mythril.exceptions import UnsatError |
||||||
|
import logging |
||||||
|
|
||||||
|
''' |
||||||
|
MODULE DESCRIPTION: |
||||||
|
|
||||||
|
Check for CALLs that send >0 Ether as a result of computation based on predictable state variables such as |
||||||
|
block.coinbase, block.gaslimit, block.timestamp, block.number |
||||||
|
|
||||||
|
TODO: |
||||||
|
- block.blockhash(block.number-1) |
||||||
|
- block.blockhash(some_block_past_256_blocks_from_now)==0 |
||||||
|
- external source of random numbers (e.g. Oraclize) |
||||||
|
''' |
||||||
|
|
||||||
|
|
||||||
|
def execute(statespace): |
||||||
|
|
||||||
|
logging.debug("Executing module: WEAK_RANDOM") |
||||||
|
|
||||||
|
issues = [] |
||||||
|
|
||||||
|
for call in statespace.calls: |
||||||
|
|
||||||
|
if ("callvalue" in str(call.value)): |
||||||
|
logging.debug("[WEAK_RANDOM] Skipping refund function") |
||||||
|
continue |
||||||
|
|
||||||
|
# We're only interested in calls that send Ether |
||||||
|
|
||||||
|
if call.value.type == VarType.CONCRETE: |
||||||
|
if call.value.val == 0: |
||||||
|
continue |
||||||
|
|
||||||
|
description = "In the function '" + call.node.function_name + "' " |
||||||
|
description += "the following predictable state variables are used to determine Ether recipient:\n" |
||||||
|
|
||||||
|
# First check: look for predictable state variables in node & call recipient constraints |
||||||
|
|
||||||
|
vars = ["coinbase", "gaslimit", "timestamp", "number"] |
||||||
|
|
||||||
|
found = [] |
||||||
|
for var in vars: |
||||||
|
for constraint in call.node.constraints + [call.to]: |
||||||
|
if var in str(constraint): |
||||||
|
found.append(var) |
||||||
|
|
||||||
|
if len(found): |
||||||
|
for item in found: |
||||||
|
description += "- block.{}\n".format(item) |
||||||
|
if solve(call): |
||||||
|
issue = Issue(call.node.module_name, call.node.function_name, call.addr, "Weak random", "Warning", |
||||||
|
description) |
||||||
|
issues.append(issue) |
||||||
|
|
||||||
|
# Second check: blockhash |
||||||
|
|
||||||
|
for constraint in call.node.constraints + [call.to]: |
||||||
|
if "blockhash" in str(constraint): |
||||||
|
description = "In the function '" + call.node.function_name + "' " |
||||||
|
if "number" in str(constraint): |
||||||
|
m = re.search('blockhash\w+(\s\-\s(\d+))*', str(constraint)) |
||||||
|
if m and solve(call): |
||||||
|
|
||||||
|
found = m.group(1) |
||||||
|
|
||||||
|
if found: # block.blockhash(block.number - N) |
||||||
|
description += "predictable expression 'block.blockhash(block.number - " + m.group(2) + \ |
||||||
|
")' is used to determine Ether recipient" |
||||||
|
if int(m.group(2)) > 255: |
||||||
|
description += ", this expression will always be equal to zero." |
||||||
|
elif "storage" in str(constraint): # block.blockhash(block.number - storage_0) |
||||||
|
description += "predictable expression 'block.blockhash(block.number - " + \ |
||||||
|
"some_storage_var)' is used to determine Ether recipient" |
||||||
|
else: # block.blockhash(block.number) |
||||||
|
description += "predictable expression 'block.blockhash(block.number)'" + \ |
||||||
|
" is used to determine Ether recipient" |
||||||
|
description += ", this expression will always be equal to zero." |
||||||
|
|
||||||
|
issue = Issue(call.node.module_name, call.node.function_name, call.addr, "Weak random", |
||||||
|
"Warning", description) |
||||||
|
issues.append(issue) |
||||||
|
break |
||||||
|
else: |
||||||
|
r = re.search(r'storage_([a-z0-9_&^]+)', str(constraint)) |
||||||
|
if r: # block.blockhash(storage_0) |
||||||
|
|
||||||
|
''' |
||||||
|
We actually can do better here by adding a constraint blockhash_block_storage_0 == 0 |
||||||
|
and checking model satisfiability. When this is done, severity can be raised |
||||||
|
from 'Informational' to 'Warning'. |
||||||
|
Checking that storage at given index can be tainted is not necessary, since it usually contains |
||||||
|
block.number of the 'commit' transaction in commit-reveal workflow. |
||||||
|
''' |
||||||
|
|
||||||
|
index = r.group(1) |
||||||
|
if index and solve(call): |
||||||
|
description += 'block.blockhash() is calculated using a value from storage ' \ |
||||||
|
'at index {}'.format(index) |
||||||
|
issue = Issue(call.node.module_name, call.node.function_name, call.addr, "Weak random", |
||||||
|
"Informational", description) |
||||||
|
issues.append(issue) |
||||||
|
break |
||||||
|
return issues |
||||||
|
|
||||||
|
|
||||||
|
def solve(call): |
||||||
|
try: |
||||||
|
model = solver.get_model(call.node.constraints) |
||||||
|
logging.debug("[WEAK_RANDOM] MODEL: " + str(model)) |
||||||
|
|
||||||
|
for d in model.decls(): |
||||||
|
logging.debug("[WEAK_RANDOM] main model: %s = 0x%x" % (d.name(), model[d].as_long())) |
||||||
|
return True |
||||||
|
|
||||||
|
except UnsatError: |
||||||
|
logging.debug("[WEAK_RANDOM] no model found") |
||||||
|
return False |
@ -0,0 +1,61 @@ |
|||||||
|
from z3 import * |
||||||
|
from enum import Enum |
||||||
|
from laser.ethereum import helper |
||||||
|
|
||||||
|
|
||||||
|
class VarType(Enum): |
||||||
|
SYMBOLIC = 1 |
||||||
|
CONCRETE = 2 |
||||||
|
|
||||||
|
|
||||||
|
class Variable: |
||||||
|
|
||||||
|
def __init__(self, val, _type): |
||||||
|
self.val = val |
||||||
|
self.type = _type |
||||||
|
|
||||||
|
def __str__(self): |
||||||
|
return str(self.val) |
||||||
|
|
||||||
|
|
||||||
|
class Op: |
||||||
|
|
||||||
|
def __init__(self, node, addr): |
||||||
|
self.node = node |
||||||
|
self.addr = addr |
||||||
|
self.state = node.states[addr] |
||||||
|
|
||||||
|
|
||||||
|
class Call(Op): |
||||||
|
|
||||||
|
def __init__(self, node, addr, _type, to, gas, value = Variable(0, VarType.CONCRETE), data = None): |
||||||
|
|
||||||
|
super().__init__(node, addr) |
||||||
|
self.to = to |
||||||
|
self.gas = gas |
||||||
|
self.type = _type |
||||||
|
self.value = value |
||||||
|
self.data = data |
||||||
|
|
||||||
|
class Suicide(Op): |
||||||
|
|
||||||
|
def __init__(self, node, addr, call_type, to, value): |
||||||
|
super().__init__(node, addr) |
||||||
|
self.to = to |
||||||
|
|
||||||
|
class SStore(Op): |
||||||
|
|
||||||
|
def __init__(self, node, addr, value): |
||||||
|
super().__init__(node, addr) |
||||||
|
self.value = value |
||||||
|
self.tainted = False |
||||||
|
|
||||||
|
|
||||||
|
def get_variable(i): |
||||||
|
try: |
||||||
|
return Variable(helper.get_concrete_int(i), VarType.CONCRETE) |
||||||
|
except AttributeError: |
||||||
|
return Variable(simplify(i), VarType.SYMBOLIC) |
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,59 @@ |
|||||||
|
import hashlib |
||||||
|
|
||||||
|
class Issue: |
||||||
|
|
||||||
|
def __init__(self, contract, function, pc, title, _type="Informational", description="", debug=""): |
||||||
|
|
||||||
|
self.title = title |
||||||
|
self.contract = contract |
||||||
|
self.function = function |
||||||
|
self.pc = pc |
||||||
|
self.description = description |
||||||
|
self.type = _type |
||||||
|
self.debug = debug |
||||||
|
self.code = None |
||||||
|
|
||||||
|
|
||||||
|
def as_dict(self): |
||||||
|
|
||||||
|
return {'title': self.title, 'description':self.description, 'type': self.type} |
||||||
|
|
||||||
|
|
||||||
|
class Report: |
||||||
|
|
||||||
|
def __init__(self): |
||||||
|
self.issues = {} |
||||||
|
pass |
||||||
|
|
||||||
|
def append_issue(self, issue): |
||||||
|
m = hashlib.md5() |
||||||
|
m.update((issue.contract + str(issue.pc) + issue.title).encode('utf-8')) |
||||||
|
self.issues[m.digest()] = issue |
||||||
|
|
||||||
|
def as_text(self): |
||||||
|
text = "" |
||||||
|
|
||||||
|
for key, issue in self.issues.items(): |
||||||
|
|
||||||
|
text += "==== " + issue.title + " ====\n" |
||||||
|
text += "Type: " + issue.type + "\n" |
||||||
|
|
||||||
|
if len(issue.contract): |
||||||
|
text += "Contract: " + issue.contract + "\n" |
||||||
|
else: |
||||||
|
text += "Contract: Unknown\n" |
||||||
|
|
||||||
|
text += "Function name: " + issue.function + "\n" |
||||||
|
text += "PC address: " + str(issue.pc) + "\n" |
||||||
|
|
||||||
|
text += issue.description + "\n--------------------\n" |
||||||
|
|
||||||
|
if issue.code: |
||||||
|
text += "Affected code:\n\n" + issue.code + "\n--------------------\n" |
||||||
|
|
||||||
|
if len(issue.debug): |
||||||
|
text += "++++ Debugging info ++++\n" + issue.debug + "\n" |
||||||
|
|
||||||
|
text+="\n" |
||||||
|
|
||||||
|
return text |
@ -0,0 +1,29 @@ |
|||||||
|
from mythril.analysis.report import Report |
||||||
|
from mythril.analysis import modules |
||||||
|
import pkgutil |
||||||
|
import logging |
||||||
|
|
||||||
|
|
||||||
|
def fire_lasers(statespace): |
||||||
|
|
||||||
|
issues = [] |
||||||
|
_modules = [] |
||||||
|
|
||||||
|
for loader, name, is_pkg in pkgutil.walk_packages(modules.__path__): |
||||||
|
_modules.append(loader.find_module(name).load_module(name)) |
||||||
|
|
||||||
|
logging.info("Starting analysis") |
||||||
|
|
||||||
|
for module in _modules: |
||||||
|
logging.info("Executing " + str(module)) |
||||||
|
issues += module.execute(statespace) |
||||||
|
|
||||||
|
report = Report() |
||||||
|
|
||||||
|
if (len(issues)): |
||||||
|
|
||||||
|
for i in range(0, len(issues)): |
||||||
|
report.append_issue(issues[i]) |
||||||
|
|
||||||
|
|
||||||
|
return report |
@ -0,0 +1,17 @@ |
|||||||
|
from z3 import * |
||||||
|
from mythril.exceptions import UnsatError |
||||||
|
import logging |
||||||
|
|
||||||
|
def get_model(constraints): |
||||||
|
s = Solver() |
||||||
|
s.set("timeout", 2000) |
||||||
|
|
||||||
|
for constraint in constraints: |
||||||
|
s.add(constraint) |
||||||
|
|
||||||
|
if (s.check() == sat): |
||||||
|
|
||||||
|
return s.model() |
||||||
|
|
||||||
|
else: |
||||||
|
raise UnsatError |
@ -0,0 +1,133 @@ |
|||||||
|
from mythril.analysis import solver |
||||||
|
from mythril.exceptions import UnsatError |
||||||
|
from laser.ethereum import svm |
||||||
|
from .ops import * |
||||||
|
import logging |
||||||
|
|
||||||
|
|
||||||
|
class SStorTaintStatus(Enum): |
||||||
|
TAINTED = 1 |
||||||
|
UNTAINTED = 2 |
||||||
|
|
||||||
|
|
||||||
|
class StateSpace: |
||||||
|
|
||||||
|
''' |
||||||
|
Symbolic EVM wrapper |
||||||
|
''' |
||||||
|
|
||||||
|
def __init__(self, contracts, dynloader = None, max_depth = 12): |
||||||
|
|
||||||
|
# Convert ETHContract objects to LASER SVM "modules" |
||||||
|
|
||||||
|
modules = {} |
||||||
|
|
||||||
|
for contract in contracts: |
||||||
|
modules[contract.address] = contract.as_dict() |
||||||
|
|
||||||
|
self.svm = svm.SVM(modules, dynamic_loader=dynloader, max_depth=max_depth) |
||||||
|
|
||||||
|
self.svm.sym_exec(contracts[0].address) |
||||||
|
|
||||||
|
self.modules = modules |
||||||
|
self.nodes = self.svm.nodes |
||||||
|
self.edges = self.svm.edges |
||||||
|
|
||||||
|
# Analysis |
||||||
|
|
||||||
|
self.calls = [] |
||||||
|
self.suicides = [] |
||||||
|
self.sstors = {} |
||||||
|
|
||||||
|
self.sstor_taint_cache = [] |
||||||
|
|
||||||
|
|
||||||
|
for key in self.svm.nodes: |
||||||
|
|
||||||
|
for instruction in self.nodes[key].instruction_list: |
||||||
|
|
||||||
|
op = instruction['opcode'] |
||||||
|
|
||||||
|
if op in ('CALL', 'CALLCODE', 'DELEGATECALL', 'STATICCALL'): |
||||||
|
stack = copy.deepcopy(self.svm.nodes[key].states[instruction['address']].stack) |
||||||
|
|
||||||
|
if op in ('CALL', 'CALLCODE'): |
||||||
|
gas, to, value, meminstart, meminsz, memoutstart, memoutsz = \ |
||||||
|
get_variable(stack.pop()), get_variable(stack.pop()), get_variable(stack.pop()), get_variable(stack.pop()), get_variable(stack.pop()), get_variable(stack.pop()), get_variable(stack.pop()) |
||||||
|
|
||||||
|
if (to.type == VarType.CONCRETE): |
||||||
|
if (to.val < 5): |
||||||
|
# ignore prebuilts |
||||||
|
continue |
||||||
|
|
||||||
|
if (meminstart.type == VarType.CONCRETE and meminsz.type == VarType.CONCRETE): |
||||||
|
self.calls.append(Call(self.nodes[key], instruction['address'], op, to, gas, value, self.svm.nodes[key].states[instruction['address']].memory[meminstart.val:meminsz.val*4])) |
||||||
|
else: |
||||||
|
self.calls.append(Call(self.nodes[key], instruction['address'], op, to, gas, value)) |
||||||
|
else: |
||||||
|
gas, to, meminstart, meminsz, memoutstart, memoutsz = \ |
||||||
|
get_variable(stack.pop()), get_variable(stack.pop()), get_variable(stack.pop()), get_variable(stack.pop()), get_variable(stack.pop()), get_variable(stack.pop()) |
||||||
|
|
||||||
|
self.calls.append(Call(self.nodes[key], instruction['address'], op, to, gas)) |
||||||
|
|
||||||
|
elif op == 'SSTORE': |
||||||
|
stack = copy.deepcopy(self.svm.nodes[key].states[instruction['address']].stack) |
||||||
|
|
||||||
|
index, value = stack.pop(), stack.pop() |
||||||
|
|
||||||
|
try: |
||||||
|
self.sstors[str(index)].append(SStore(self.nodes[key], instruction['address'], value)) |
||||||
|
except KeyError: |
||||||
|
self.sstors[str(index)] = [SStore(self.nodes[key], instruction['address'], value)] |
||||||
|
|
||||||
|
# self.sstor_analysis() |
||||||
|
|
||||||
|
|
||||||
|
''' |
||||||
|
def sstor_analysis(self): |
||||||
|
|
||||||
|
logging.info("Analyzing storage operations...") |
||||||
|
|
||||||
|
for index in self.sstors: |
||||||
|
for s in self.sstors[index]: |
||||||
|
|
||||||
|
# 'Taint' every 'store' instruction that is reachable without any constraint on msg.sender |
||||||
|
|
||||||
|
taint = True |
||||||
|
|
||||||
|
for constraint in s.node.constraints: |
||||||
|
if ("caller" in str(constraint)): |
||||||
|
taint = False |
||||||
|
break |
||||||
|
|
||||||
|
if taint: |
||||||
|
s.tainted = True |
||||||
|
|
||||||
|
try: |
||||||
|
solver.get_model(s.node.constraints) |
||||||
|
s.tainted = True |
||||||
|
except UnsatError: |
||||||
|
s.tainted = False |
||||||
|
''' |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def find_storage_write(self, index): |
||||||
|
|
||||||
|
# Find a an unconstrained SSTOR that writes to storage index "index" |
||||||
|
|
||||||
|
try: |
||||||
|
for s in self.sstors[index]: |
||||||
|
taint = True |
||||||
|
|
||||||
|
for constraint in s.node.constraints: |
||||||
|
if ("caller" in str(constraint)): |
||||||
|
taint = False |
||||||
|
break |
||||||
|
|
||||||
|
return s.node.function_name |
||||||
|
|
||||||
|
return None |
||||||
|
except KeyError: |
||||||
|
return None |
||||||
|
|
File diff suppressed because one or more lines are too long
@ -1,2 +1,5 @@ |
|||||||
class CompilerError(Exception): |
class CompilerError(Exception): |
||||||
|
pass |
||||||
|
|
||||||
|
class UnsatError(Exception): |
||||||
pass |
pass |
@ -0,0 +1,41 @@ |
|||||||
|
import re |
||||||
|
from ethereum import utils |
||||||
|
|
||||||
|
def add_signatures_from_file(file, sigs={}): |
||||||
|
|
||||||
|
funcs = [] |
||||||
|
|
||||||
|
with open(file, encoding="utf-8") as f: |
||||||
|
for line in f: |
||||||
|
|
||||||
|
m = re.search(r'function\s+(.*\))', line) |
||||||
|
|
||||||
|
if m: |
||||||
|
funcs.append(m.group(1)) |
||||||
|
|
||||||
|
for f in funcs: |
||||||
|
|
||||||
|
m = re.search(r'^([A-Za-z0-9_]+)', f) |
||||||
|
|
||||||
|
if (m): |
||||||
|
|
||||||
|
signature = m.group(1) |
||||||
|
|
||||||
|
m = re.search(r'\((.*)\)', f) |
||||||
|
|
||||||
|
_args = m.group(1).split(",") |
||||||
|
|
||||||
|
types = [] |
||||||
|
|
||||||
|
for arg in _args: |
||||||
|
|
||||||
|
_type = arg.lstrip().split(" ")[0] |
||||||
|
if _type == "uint": |
||||||
|
_type = "uint256" |
||||||
|
|
||||||
|
types.append(_type) |
||||||
|
|
||||||
|
typelist = ",".join(types) |
||||||
|
signature += "(" + typelist + ")" |
||||||
|
|
||||||
|
sigs["0x" + utils.sha3(signature)[:4].hex()] = signature |
@ -0,0 +1,87 @@ |
|||||||
|
import os |
||||||
|
import re |
||||||
|
import sys |
||||||
|
import json |
||||||
|
from mythril.ether import util |
||||||
|
from mythril.ether.ethcontract import ETHContract |
||||||
|
from mythril.analysis.security import fire_lasers |
||||||
|
from mythril.analysis.symbolic import StateSpace |
||||||
|
from laser.ethereum import helper |
||||||
|
|
||||||
|
|
||||||
|
def analyze_truffle_project(): |
||||||
|
|
||||||
|
project_root = os.getcwd() |
||||||
|
|
||||||
|
build_dir = os.path.join(project_root, "build", "contracts") |
||||||
|
|
||||||
|
files = os.listdir(build_dir) |
||||||
|
|
||||||
|
for filename in files: |
||||||
|
|
||||||
|
if re.match(r'.*\.json$', filename) and filename != "Migrations.json": |
||||||
|
|
||||||
|
with open(os.path.join(build_dir, filename)) as cf: |
||||||
|
contractdata = json.load(cf) |
||||||
|
|
||||||
|
try: |
||||||
|
name = contractdata['contractName'] |
||||||
|
bytecode = contractdata['deployedBytecode'] |
||||||
|
except: |
||||||
|
print("Unable to parse contract data. Please use Truffle 4 to compile your project.") |
||||||
|
sys.exit() |
||||||
|
|
||||||
|
|
||||||
|
if (len(bytecode) < 4): |
||||||
|
continue |
||||||
|
|
||||||
|
ethcontract= ETHContract(bytecode, name=name, address = util.get_indexed_address(0)) |
||||||
|
|
||||||
|
contracts = [ethcontract] |
||||||
|
|
||||||
|
states = StateSpace(contracts, max_depth = 10) |
||||||
|
report = fire_lasers(states) |
||||||
|
|
||||||
|
# augment with source code |
||||||
|
|
||||||
|
disassembly = ethcontract.get_disassembly() |
||||||
|
source = contractdata['source'] |
||||||
|
|
||||||
|
deployedSourceMap = contractdata['deployedSourceMap'].split(";") |
||||||
|
|
||||||
|
mappings = [] |
||||||
|
i = 0 |
||||||
|
|
||||||
|
while(i < len(deployedSourceMap)): |
||||||
|
|
||||||
|
m = re.search(r"^(\d+):*(\d+)", deployedSourceMap[i]) |
||||||
|
|
||||||
|
if (m): |
||||||
|
offset = m.group(1) |
||||||
|
length = m.group(2) |
||||||
|
else: |
||||||
|
m = re.search(r"^:(\d+)", deployedSourceMap[i]) |
||||||
|
|
||||||
|
if m: |
||||||
|
length = m.group(1) |
||||||
|
|
||||||
|
mappings.append((int(offset), int(length))) |
||||||
|
|
||||||
|
i += 1 |
||||||
|
|
||||||
|
for key, issue in report.issues.items(): |
||||||
|
|
||||||
|
index = helper.get_instruction_index(disassembly.instruction_list, issue.pc) |
||||||
|
|
||||||
|
if index: |
||||||
|
issue.code_start = mappings[index][0] |
||||||
|
issue.code_length = mappings[index][1] |
||||||
|
issue.code = source[mappings[index][0]: mappings[index][0] + mappings[index][1]] |
||||||
|
|
||||||
|
|
||||||
|
if len(report.issues): |
||||||
|
print("Analysis result for " + name + ":\n" + report.as_text()) |
||||||
|
else: |
||||||
|
print("Analysis result for " + name + ": No issues found.") |
||||||
|
|
||||||
|
|
@ -0,0 +1,19 @@ |
|||||||
|
# Smart Contract Security Issues |
||||||
|
|
||||||
|
| Issue | Description | Mythril Detection Module(s) | References | |
||||||
|
|------:|-------------|------------|----------| |
||||||
|
|Unprotected functions| Critical functions such as sends with non-zero value or suicide() calls are callable by anyone, or msg.sender is compared against an address in storage that can be written to. E.g. Parity wallet bugs. | [unchecked_suicide](mythril/analysis/modules/unchecked_suicide.py), [ether_send](mythril/analysis/modules/ether_send.py) | | |
||||||
|
|Missing check on CALL return value| | [unchecked_retval](mythril/analysis/modules/unchecked_retval.py) | [Handle errors in external calls](https://consensys.github.io/smart-contract-best-practices/recommendations/#use-caution-when-making-external-calls) | |
||||||
|
|Re-entrancy| | [call to untrusted contract with gas](mythril/analysis/modules/call_to_dynamic_with_gas.py) | | |
||||||
|
|Multiple sends in a single transaction| External calls can fail accidentally or deliberately. Avoid combining multiple send() calls in a single transaction. | | [Favor pull over push for external calls](https://consensys.github.io/smart-contract-best-practices/recommendations/#favor-pull-over-push-for-external-calls) | |
||||||
|
|Function call to untrusted contract| | [call to untrusted contract with gas](mythril/analysis/modules/call_to_dynamic_with_gas.py) | | |
||||||
|
|Delegatecall or callcode to untrusted contract| | [delegatecall_forward](mythril/analysis/modules/delegatecall_forward.py), [delegatecall_to_dynamic.py](mythril/analysis/modules/delegatecall_to_dynamic.py) | | |
||||||
|
|Integer overflow/underflow| | [integer_underflow](mythril/analysis/modules/integer_underflow.py) | | |
||||||
|
|Timestamp dependence| | | | |
||||||
|
|Payable transaction does not revert in case of failure | | | | |
||||||
|
|Call depth attack| | | | |
||||||
|
|Use of `tx.origin`| | [tx_origin](mythril/analysis/modules/tx_origin.py) | [Solidity documentation](https://solidity.readthedocs.io/en/develop/security-considerations.html#tx-origin), [Avoid using tx.origin](https://consensys.github.io/smart-contract-best-practices/recommendations/#avoid-using-txorigin) | |
||||||
|
|Type confusion| | | | |
||||||
|
|Predictable RNG| | | [weak_random](mythril/analysis/modules/weak_random.py) | |
||||||
|
|Transaction order dependence| | | | | |
||||||
|
|Information exposure| | | | |
@ -0,0 +1 @@ |
|||||||
|
{"0x07f9f7ba": "StandardBounties(address)", "0x8c590917": "contribute(uint256,uint256)", "0x626a413a": "activateBounty(uint256,uint256)", "0x1e688c14": "fulfillBounty(uint256,string)", "0x41ac5dd0": "updateFulfillment(uint256,uint256,string)", "0xd9583497": "acceptFulfillment(uint256,uint256)", "0x16b57509": "killBounty(uint256)", "0x2d1fdef6": "extendDeadline(uint256,uint256)", "0x5d19606e": "transferIssuer(uint256,address)", "0xd6c0ceab": "changeBountyDeadline(uint256,uint256)", "0xf3d3402a": "changeBountyData(uint256,string)", "0x452ccadb": "changeBountyFulfillmentAmount(uint256,uint256)", "0xcdad6576": "changeBountyArbiter(uint256,address)", "0x992a3e75": "changeBountyPaysTokens(uint256,bool,address)", "0x422d4cd6": "increasePayout(uint256,uint256,uint256)", "0xb94b0a3a": "getFulfillment(uint256,uint256)", "0xee8c4bbf": "getBounty(uint256)", "0x86647bac": "getBountyArbiter(uint256)", "0xa60745aa": "getBountyData(uint256)", "0x19dba3d2": "getBountyToken(uint256)", "0x3278ba2f": "getNumBounties()", "0xfbe334f8": "getNumFulfillments(uint256)", "0xdb3b6263": "transitionToState(uint256,BountyStages)", "0x4e3b52fe": "metaCoin()", "0x412664ae": "sendToken(address,uint256)", "0x56885cd8": "crowdfunding()", "0x6c343ffe": "withdrawfunds()", "0xe8b5e51f": "invest()", "0xaa3288f4": "getBalance())", "0xc11a4b47": "Origin()", "0xf2fde38b": "transferOwnership(address)", "0x00362a95": "donate(address)", "0x70a08231": "balanceOf(address)", "0x2e1a7d4d": "withdraw(uint256)", "0x6241bfd1": "Token(uint256)", "0xa3210e87": "sendeth(address,uint256)", "0xcd38aa87": "chooseWinner()", "0xd6d22fa4": "MetaCoin()", "0x90b98a11": "sendCoin(address,uint256)", "0x7bd703e8": "getBalanceInEth(address)", "0xf8b2cb4f": "getBalance(address)", "0xa360b26f": "Migrations()", "0xfdacd576": "setCompleted(uint256)", "0x0900f010": "upgrade(address)", "0xcae9ca51": "approveAndCall(address,uint256,bytes)", "0xa9059cbb": "transfer(address,uint256)", "0x23b872dd": "transferFrom(address,address,uint256)", "0x095ea7b3": "approve(address,uint256)", "0xdd62ed3e": "allowance(address,address)", "0x525f8a5c": "setSaleStartTime(uint256)", "0xd132391a": "setSaleEndTime(uint256)", "0x0a0cd8c8": "setupDone()", "0xd7bb99ba": "contribute()", "0xf0349d5f": "setupStages()", "0x2a4f6533": "createTokenContract())", "0x42a6b21a": "getContributionLimit(address)", "0x1a787915": "startConditions(bytes32)", "0xf3fde261": "onTransition(bytes32)", "0x27816235": "onSaleEnded()", "0x091cde0b": "DisbursementHandler(address)", "0xf3fef3a3": "withdraw(address,uint256)", "0x4bc9fdc2": "calcMaxWithdraw()", "0xc9e61599": "createTarget())", "0x200094e0": "deployContract())", "0x5a048d78": "claim(Target)", "0x16ae6b67": "checkInvariant())", "0x2aa5ed61": "DayLimit(uint256)", "0xe7dde9a3": "_setDailyLimit(uint256)", "0x4a4c82c6": "_resetSpentToday()", "0x180aadb7": "underLimit(uint256)", "0x9d4468ff": "today())", "0x19045a25": "recover(bytes32,bytes)", "0xe92dfb23": "LimitBalance(uint256)", "0xd73dd623": "increaseApproval(address,uint256)", "0x66188463": "decreaseApproval(address,uint256)", "0xabaf5880": "Crowdsale(uint256,uint256,uint256,address)", "0xec8ac4d8": "buyTokens(address)", "0x9d735286": "forwardFunds()", "0x605120cf": "validPurchase())", "0x6e42787f": "hasEnded())", "0xe5c46944": "MultiSigWallet(address[],uint256)", "0x7065cb48": "addOwner(address)", "0x173825d9": "removeOwner(address)", "0xe20056e6": "replaceOwner(address,address)", "0xba51a6df": "changeRequirement(uint256)", "0xc6427474": "submitTransaction(address,uint256,bytes)", "0xc01a8c84": "confirmTransaction(uint256)", "0x20ea8d86": "revokeConfirmation(uint256)", "0xee22610b": "executeTransaction(uint256)", "0x784547a7": "isConfirmed(uint256)", "0xec096f8d": "addTransaction(address,uint256,bytes)", "0x8b51d13f": "getConfirmationCount(uint256)", "0x54741525": "getTransactionCount(bool,bool)", "0xa0e67e2b": "getOwners()", "0xb5dc40c3": "getConfirmations(uint256)", "0xa8abe69a": "getTransactionIds(uint256,uint256,bool,bool)"} |
@ -0,0 +1,34 @@ |
|||||||
|
contract Crowdfunding { |
||||||
|
|
||||||
|
mapping(address => uint) public balances; |
||||||
|
address public owner; |
||||||
|
uint256 INVEST_MIN = 1 ether; |
||||||
|
uint256 INVEST_MAX = 10 ether; |
||||||
|
|
||||||
|
modifier onlyOwner() { |
||||||
|
require(msg.sender == owner); |
||||||
|
_; |
||||||
|
} |
||||||
|
|
||||||
|
function crowdfunding() { |
||||||
|
owner = msg.sender; |
||||||
|
} |
||||||
|
|
||||||
|
function withdrawfunds() onlyOwner { |
||||||
|
msg.sender.transfer(this.balance); |
||||||
|
} |
||||||
|
|
||||||
|
function invest() public payable { |
||||||
|
require(msg.value > INVEST_MIN && msg.value < INVEST_MAX); |
||||||
|
|
||||||
|
balances[msg.sender] += msg.value; |
||||||
|
} |
||||||
|
|
||||||
|
function getBalance() public constant returns (uint) { |
||||||
|
return balances[msg.sender]; |
||||||
|
} |
||||||
|
|
||||||
|
function() public payable { |
||||||
|
invest(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,35 @@ |
|||||||
|
contract Origin { |
||||||
|
address public owner; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* @dev The Ownable constructor sets the original `owner` of the contract to the sender |
||||||
|
* account. |
||||||
|
*/ |
||||||
|
function Origin() { |
||||||
|
owner = msg.sender; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* @dev Throws if called by any account other than the owner. |
||||||
|
*/ |
||||||
|
modifier onlyOwner() { |
||||||
|
if (tx.origin != owner) { |
||||||
|
throw; |
||||||
|
} |
||||||
|
_; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* @dev Allows the current owner to transfer control of the contract to a newOwner. |
||||||
|
* @param newOwner The address to transfer ownership to. |
||||||
|
*/ |
||||||
|
function transferOwnership(address newOwner) onlyOwner { |
||||||
|
if (newOwner != address(0)) { |
||||||
|
owner = newOwner; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,23 @@ |
|||||||
|
contract Reentrancy { |
||||||
|
|
||||||
|
mapping(address => uint) public balances; |
||||||
|
|
||||||
|
function donate(address _to) public payable { |
||||||
|
balances[_to] += msg.value; |
||||||
|
} |
||||||
|
|
||||||
|
function balanceOf(address _who) public constant returns (uint balance) { |
||||||
|
return balances[_who]; |
||||||
|
} |
||||||
|
|
||||||
|
function withdraw(uint _amount) public { |
||||||
|
if(balances[msg.sender] >= _amount) { |
||||||
|
if(msg.sender.call.value(_amount)()) { |
||||||
|
_amount; |
||||||
|
} |
||||||
|
balances[msg.sender] -= _amount; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function() payable {} |
||||||
|
} |
@ -0,0 +1,20 @@ |
|||||||
|
contract Under { |
||||||
|
|
||||||
|
mapping(address => uint) balances; |
||||||
|
uint public totalSupply; |
||||||
|
|
||||||
|
function Token(uint _initialSupply) { |
||||||
|
balances[msg.sender] = totalSupply = _initialSupply; |
||||||
|
} |
||||||
|
|
||||||
|
function sendeth(address _to, uint _value) public returns (bool) { |
||||||
|
require(balances[msg.sender] - _value >= 0); |
||||||
|
balances[msg.sender] -= _value; |
||||||
|
balances[_to] += _value; |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
function balanceOf(address _owner) public constant returns (uint balance) { |
||||||
|
return balances[_owner]; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,49 @@ |
|||||||
|
pragma solidity ^0.4.16; |
||||||
|
|
||||||
|
contract WeakRandom { |
||||||
|
struct Contestant { |
||||||
|
address addr; |
||||||
|
uint gameId; |
||||||
|
} |
||||||
|
|
||||||
|
uint public constant prize = 2.5 ether; |
||||||
|
uint public constant totalTickets = 50; |
||||||
|
uint public constant pricePerTicket = prize / totalTickets; |
||||||
|
|
||||||
|
uint public gameId = 1; |
||||||
|
uint public nextTicket = 0; |
||||||
|
mapping (uint => Contestant) public contestants; |
||||||
|
|
||||||
|
function () payable public { |
||||||
|
uint moneySent = msg.value; |
||||||
|
|
||||||
|
while (moneySent >= pricePerTicket && nextTicket < totalTickets) { |
||||||
|
uint currTicket = nextTicket++; |
||||||
|
contestants[currTicket] = Contestant(msg.sender, gameId); |
||||||
|
moneySent -= pricePerTicket; |
||||||
|
} |
||||||
|
|
||||||
|
if (nextTicket == totalTickets) { |
||||||
|
chooseWinner(); |
||||||
|
} |
||||||
|
|
||||||
|
// Send back leftover money |
||||||
|
if (moneySent > 0) { |
||||||
|
msg.sender.transfer(moneySent); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function chooseWinner() private { |
||||||
|
address seed1 = contestants[uint(block.coinbase) % totalTickets].addr; |
||||||
|
address seed2 = contestants[uint(msg.sender) % totalTickets].addr; |
||||||
|
uint seed3 = block.difficulty; |
||||||
|
bytes32 randHash = keccak256(seed1, seed2, seed3); |
||||||
|
|
||||||
|
uint winningNumber = uint(randHash) % totalTickets; |
||||||
|
address winningAddress = contestants[winningNumber].addr; |
||||||
|
|
||||||
|
gameId++; |
||||||
|
nextTicket = 0; |
||||||
|
winningAddress.transfer(prize); |
||||||
|
} |
||||||
|
} |
File diff suppressed because one or more lines are too long
@ -1,19 +1,18 @@ |
|||||||
import unittest |
import unittest |
||||||
from mythril.disassembler.callgraph import generate_callgraph |
from mythril.analysis.symbolic import StateSpace |
||||||
from mythril.disassembler.disassembly import Disassembly |
from mythril.analysis.callgraph import generate_graph |
||||||
from laser.ethereum import svm |
from mythril.ether.ethcontract import ETHContract |
||||||
|
|
||||||
|
|
||||||
class SVMTestCase(unittest.TestCase): |
class SVMTestCase(unittest.TestCase): |
||||||
|
|
||||||
def runTest(self): |
def runTest(self): |
||||||
|
|
||||||
modules = {} |
code = "0x60606040525b603c5b60006010603e565b9050593681016040523660008237602060003683856040603f5a0204f41560545760206000f35bfe5b50565b005b73c3b2ae46792547a96b9f84405e36d0e07edcd05c5b905600a165627a7a7230582062a884f947232ada573f95940cce9c8bfb7e4e14e21df5af4e884941afb55e590029" |
||||||
modules['0x0000000000000000000000000000000000000000'] = {'name': 'metaCoin', 'address': '0x0000000000000000000000000000000000000000', 'creation_code': '', 'code': '60606040526004361061004c576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806327e235e314610051578063412664ae1461009e575b600080fd5b341561005c57600080fd5b610088600480803573ffffffffffffffffffffffffffffffffffffffff169060200190919050506100f8565b6040518082815260200191505060405180910390f35b34156100a957600080fd5b6100de600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091908035906020019091905050610110565b604051808215151515815260200191505060405180910390f35b60006020528060005260406000206000915090505481565b6000816000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054101561016157600090506101fe565b816000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282540392505081905550816000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282540192505081905550600090505b929150505600a165627a7a72305820fd4fa106da498514e90965a45ffecc1da53a0cd8bb7a7135910f8612245a46370029'} |
|
||||||
modules['0x0000000000000000000000000000000000000000']['disassembly'] = Disassembly(modules['0x0000000000000000000000000000000000000000']['code']) |
|
||||||
|
|
||||||
_svm = svm.SVM(modules) |
contract = ETHContract(code) |
||||||
|
statespace = StateSpace([contract]) |
||||||
|
|
||||||
html = generate_callgraph(_svm, '0x0000000000000000000000000000000000000000', False) |
html = generate_graph(statespace) |
||||||
|
|
||||||
self.assertTrue("var nodes = [\n{id: \'metaCoin:" in html) |
self.assertTrue("0 PUSH1 0x60\\n2 PUSH1 0x40\\n4 MSTORE\\n5 JUMPDEST" in html) |
||||||
|
Loading…
Reference in new issue