mirror of https://github.com/crytic/slither
- Graph traversal is performed one time for all the variants (speedup) - Add better support for internal callspull/162/head
parent
bec190e877
commit
7d04cc049d
@ -0,0 +1,189 @@ |
|||||||
|
"""" |
||||||
|
Re-entrancy detection |
||||||
|
|
||||||
|
Based on heuristics, it may lead to FP and FN |
||||||
|
Iterate over all the nodes of the graph until reaching a fixpoint |
||||||
|
""" |
||||||
|
|
||||||
|
from slither.core.cfg.node import NodeType |
||||||
|
from slither.core.declarations import Function, SolidityFunction |
||||||
|
from slither.core.expressions import UnaryOperation, UnaryOperationType |
||||||
|
from slither.detectors.abstract_detector import (AbstractDetector, |
||||||
|
DetectorClassification) |
||||||
|
from slither.slithir.operations import (HighLevelCall, LowLevelCall, |
||||||
|
LibraryCall, |
||||||
|
Send, Transfer) |
||||||
|
|
||||||
|
def union_dict(d1, d2): |
||||||
|
d3 = {k: d1.get(k, []) + d2.get(k, []) for k in set(list(d1.keys()) + list(d2.keys()))} |
||||||
|
return d3 |
||||||
|
|
||||||
|
def dict_are_equal(d1, d2): |
||||||
|
if set(list(d1.keys())) != set(list(d2.keys())): |
||||||
|
return False |
||||||
|
return all(set(d1[k]) == set(d2[k]) for k in d1.keys()) |
||||||
|
|
||||||
|
class Reentrancy(AbstractDetector): |
||||||
|
# This detector is not meant to be registered |
||||||
|
# It is inherited by reentrancy variantsœ |
||||||
|
# ARGUMENT = 'reentrancy' |
||||||
|
# HELP = 'Reentrancy vulnerabilities' |
||||||
|
# IMPACT = DetectorClassification.HIGH |
||||||
|
# CONFIDENCE = DetectorClassification.HIGH |
||||||
|
|
||||||
|
KEY = 'REENTRANCY' |
||||||
|
|
||||||
|
@staticmethod |
||||||
|
def _can_callback(irs): |
||||||
|
""" |
||||||
|
Detect if the node contains a call that can |
||||||
|
be used to re-entrance |
||||||
|
|
||||||
|
Consider as valid target: |
||||||
|
- low level call |
||||||
|
- high level call |
||||||
|
|
||||||
|
Do not consider Send/Transfer as there is not enough gas |
||||||
|
""" |
||||||
|
for ir in irs: |
||||||
|
if isinstance(ir, LowLevelCall): |
||||||
|
return True |
||||||
|
if isinstance(ir, HighLevelCall) and not isinstance(ir, LibraryCall): |
||||||
|
return True |
||||||
|
return False |
||||||
|
|
||||||
|
@staticmethod |
||||||
|
def _can_send_eth(irs): |
||||||
|
""" |
||||||
|
Detect if the node can send eth |
||||||
|
""" |
||||||
|
for ir in irs: |
||||||
|
if isinstance(ir, (HighLevelCall, LowLevelCall, Transfer, Send)): |
||||||
|
if ir.call_value: |
||||||
|
return True |
||||||
|
return False |
||||||
|
|
||||||
|
def _filter_if(self, node): |
||||||
|
""" |
||||||
|
Check if the node is a condtional node where |
||||||
|
there is an external call checked |
||||||
|
Heuristic: |
||||||
|
- The call is a IF node |
||||||
|
- It contains a, external call |
||||||
|
- The condition is the negation (!) |
||||||
|
|
||||||
|
This will work only on naive implementation |
||||||
|
""" |
||||||
|
return isinstance(node.expression, UnaryOperation)\ |
||||||
|
and node.expression.type == UnaryOperationType.BANG |
||||||
|
|
||||||
|
def _explore(self, node, visited, skip_father=None): |
||||||
|
""" |
||||||
|
Explore the CFG and look for re-entrancy |
||||||
|
Heuristic: There is a re-entrancy if a state variable is written |
||||||
|
after an external call |
||||||
|
|
||||||
|
node.context will contains the external calls executed |
||||||
|
It contains the calls executed in father nodes |
||||||
|
|
||||||
|
if node.context is not empty, and variables are written, a re-entrancy is possible |
||||||
|
""" |
||||||
|
if node in visited: |
||||||
|
return |
||||||
|
|
||||||
|
visited = visited + [node] |
||||||
|
|
||||||
|
# First we add the external calls executed in previous nodes |
||||||
|
# send_eth returns the list of calls sending value |
||||||
|
# calls returns the list of calls that can callback |
||||||
|
# read returns the variable read |
||||||
|
# read_prior_calls returns the variable read prior a call |
||||||
|
fathers_context = {'send_eth':[], 'calls':[], 'read':[], 'read_prior_calls':{}} |
||||||
|
|
||||||
|
for father in node.fathers: |
||||||
|
if self.KEY in father.context: |
||||||
|
fathers_context['send_eth'] += [s for s in father.context[self.KEY]['send_eth'] if s!=skip_father] |
||||||
|
fathers_context['calls'] += [c for c in father.context[self.KEY]['calls'] if c!=skip_father] |
||||||
|
fathers_context['read'] += father.context[self.KEY]['read'] |
||||||
|
fathers_context['read_prior_calls'] = union_dict(fathers_context['read_prior_calls'], father.context[self.KEY]['read_prior_calls']) |
||||||
|
|
||||||
|
# Exclude path that dont bring further information |
||||||
|
if node in self.visited_all_paths: |
||||||
|
if all(call in self.visited_all_paths[node]['calls'] for call in fathers_context['calls']): |
||||||
|
if all(send in self.visited_all_paths[node]['send_eth'] for send in fathers_context['send_eth']): |
||||||
|
if all(read in self.visited_all_paths[node]['read'] for read in fathers_context['read']): |
||||||
|
if dict_are_equal(self.visited_all_paths[node]['read_prior_calls'], fathers_context['read_prior_calls']): |
||||||
|
return |
||||||
|
else: |
||||||
|
self.visited_all_paths[node] = {'send_eth':[], 'calls':[], 'read':[], 'read_prior_calls':{}} |
||||||
|
|
||||||
|
self.visited_all_paths[node]['send_eth'] = list(set(self.visited_all_paths[node]['send_eth'] + fathers_context['send_eth'])) |
||||||
|
self.visited_all_paths[node]['calls'] = list(set(self.visited_all_paths[node]['calls'] + fathers_context['calls'])) |
||||||
|
self.visited_all_paths[node]['read'] = list(set(self.visited_all_paths[node]['read'] + fathers_context['read'])) |
||||||
|
self.visited_all_paths[node]['read_prior_calls'] = union_dict(self.visited_all_paths[node]['read_prior_calls'], fathers_context['read_prior_calls']) |
||||||
|
|
||||||
|
node.context[self.KEY] = fathers_context |
||||||
|
|
||||||
|
state_vars_read = node.state_variables_read |
||||||
|
|
||||||
|
# All the state variables written |
||||||
|
state_vars_written = node.state_variables_written |
||||||
|
slithir_operations = [] |
||||||
|
# Add the state variables written in internal calls |
||||||
|
for internal_call in node.internal_calls: |
||||||
|
# Filter to Function, as internal_call can be a solidity call |
||||||
|
if isinstance(internal_call, Function): |
||||||
|
state_vars_written += internal_call.all_state_variables_written() |
||||||
|
state_vars_read += internal_call.all_state_variables_read() |
||||||
|
slithir_operations += internal_call.all_slithir_operations() |
||||||
|
|
||||||
|
contains_call = False |
||||||
|
node.context[self.KEY]['written'] = state_vars_written |
||||||
|
if self._can_callback(node.irs + slithir_operations): |
||||||
|
node.context[self.KEY]['calls'] = list(set(node.context[self.KEY]['calls'] + [node])) |
||||||
|
node.context[self.KEY]['read_prior_calls'][node] = list(set(node.context[self.KEY]['read_prior_calls'].get(node, []) + node.context[self.KEY]['read']+ state_vars_read)) |
||||||
|
contains_call = True |
||||||
|
if self._can_send_eth(node.irs + slithir_operations): |
||||||
|
node.context[self.KEY]['send_eth'] = list(set(node.context[self.KEY]['send_eth'] + [node])) |
||||||
|
|
||||||
|
node.context[self.KEY]['read'] = list(set(node.context[self.KEY]['read'] + state_vars_read)) |
||||||
|
|
||||||
|
sons = node.sons |
||||||
|
if contains_call and node.type in [NodeType.IF, NodeType.IFLOOP]: |
||||||
|
if self._filter_if(node): |
||||||
|
son = sons[0] |
||||||
|
self._explore(son, visited, node) |
||||||
|
sons = sons[1:] |
||||||
|
else: |
||||||
|
son = sons[1] |
||||||
|
self._explore(son, visited, node) |
||||||
|
sons = [sons[0]] |
||||||
|
|
||||||
|
|
||||||
|
for son in sons: |
||||||
|
self._explore(son, visited) |
||||||
|
|
||||||
|
def detect_reentrancy(self, contract): |
||||||
|
""" |
||||||
|
""" |
||||||
|
for function in contract.functions_and_modifiers_not_inherited: |
||||||
|
if function.is_implemented: |
||||||
|
if self.KEY in function.context: |
||||||
|
continue |
||||||
|
self._explore(function.entry_point, []) |
||||||
|
function.context[self.KEY] = True |
||||||
|
|
||||||
|
def detect(self): |
||||||
|
""" |
||||||
|
""" |
||||||
|
# if a node was already visited by another path |
||||||
|
# we will only explore it if the traversal brings |
||||||
|
# new variables written |
||||||
|
# This speedup the exploration through a light fixpoint |
||||||
|
# Its particular useful on 'complex' functions with several loops and conditions |
||||||
|
self.visited_all_paths = {} |
||||||
|
|
||||||
|
for c in self.contracts: |
||||||
|
self.detect_reentrancy(c) |
||||||
|
|
||||||
|
return [] |
Loading…
Reference in new issue