Merge pull request #2156 from crytic/new-detectors

Release new detectors
pull/2164/head
Feist Josselin 1 year ago committed by GitHub
commit bc79beec5e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      slither/detectors/all_detectors.py
  2. 91
      slither/detectors/assembly/incorrect_return.py
  3. 68
      slither/detectors/assembly/return_instead_of_leave.py
  4. 93
      slither/detectors/operations/incorrect_exp.py
  5. 123
      slither/detectors/statements/return_bomb.py
  6. 69
      slither/detectors/statements/tautological_compare.py
  7. 9
      tests/e2e/detectors/snapshots/detectors__detector_IncorrectOperatorExponentiation_0_7_6_incorrect_exp_sol__0.txt
  8. 4
      tests/e2e/detectors/snapshots/detectors__detector_IncorrectReturn_0_8_10_incorrect_return_sol__0.txt
  9. 5
      tests/e2e/detectors/snapshots/detectors__detector_ReturnBomb_0_8_20_return_bomb_sol__0.txt
  10. 2
      tests/e2e/detectors/snapshots/detectors__detector_ReturnInsteadOfLeave_0_8_10_incorrect_return_sol__0.txt
  11. 3
      tests/e2e/detectors/snapshots/detectors__detector_TautologicalCompare_0_8_20_compare_sol__0.txt
  12. 30
      tests/e2e/detectors/test_data/incorrect-exp/0.7.6/incorrect_exp.sol
  13. BIN
      tests/e2e/detectors/test_data/incorrect-exp/0.7.6/incorrect_exp.sol-0.7.6.zip
  14. 36
      tests/e2e/detectors/test_data/incorrect-return/0.8.10/incorrect_return.sol
  15. BIN
      tests/e2e/detectors/test_data/incorrect-return/0.8.10/incorrect_return.sol-0.8.10.zip
  16. 57
      tests/e2e/detectors/test_data/return-bomb/0.8.20/return_bomb.sol
  17. BIN
      tests/e2e/detectors/test_data/return-bomb/0.8.20/return_bomb.sol-0.8.20.zip
  18. 8
      tests/e2e/detectors/test_data/return-leave/0.8.10/incorrect_return.sol
  19. BIN
      tests/e2e/detectors/test_data/return-leave/0.8.10/incorrect_return.sol-0.8.10.zip
  20. 6
      tests/e2e/detectors/test_data/tautological-compare/0.8.20/compare.sol
  21. BIN
      tests/e2e/detectors/test_data/tautological-compare/0.8.20/compare.sol-0.8.20.zip
  22. 25
      tests/e2e/detectors/test_detectors.py
  23. 8
      tests/e2e/solc_parsing/test_ast_parsing.py

@ -92,3 +92,8 @@ from .functions.cyclomatic_complexity import CyclomaticComplexity
from .operations.cache_array_length import CacheArrayLength
from .statements.incorrect_using_for import IncorrectUsingFor
from .operations.encode_packed import EncodePackedCollision
from .assembly.incorrect_return import IncorrectReturn
from .assembly.return_instead_of_leave import ReturnInsteadOfLeave
from .operations.incorrect_exp import IncorrectOperatorExponentiation
from .statements.tautological_compare import TautologicalCompare
from .statements.return_bomb import ReturnBomb

@ -0,0 +1,91 @@
from typing import List, Optional
from slither.core.declarations import SolidityFunction, Function
from slither.detectors.abstract_detector import (
AbstractDetector,
DetectorClassification,
DETECTOR_INFO,
)
from slither.slithir.operations import SolidityCall
from slither.utils.output import Output
def _assembly_node(function: Function) -> Optional[SolidityCall]:
"""
Check if there is a node that use return in assembly
Args:
function:
Returns:
"""
for ir in function.all_slithir_operations():
if isinstance(ir, SolidityCall) and ir.function == SolidityFunction(
"return(uint256,uint256)"
):
return ir
return None
class IncorrectReturn(AbstractDetector):
"""
Check for cases where a return(a,b) is used in an assembly function
"""
ARGUMENT = "incorrect-return"
HELP = "If a `return` is incorrectly used in assembly mode."
IMPACT = DetectorClassification.HIGH
CONFIDENCE = DetectorClassification.MEDIUM
WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-assembly-return"
WIKI_TITLE = "Incorrect return in assembly"
WIKI_DESCRIPTION = "Detect if `return` in an assembly block halts unexpectedly the execution."
WIKI_EXPLOIT_SCENARIO = """
```solidity
contract C {
function f() internal returns (uint a, uint b) {
assembly {
return (5, 6)
}
}
function g() returns (bool){
f();
return true;
}
}
```
The return statement in `f` will cause execution in `g` to halt.
The function will return 6 bytes starting from offset 5, instead of returning a boolean."""
WIKI_RECOMMENDATION = "Use the `leave` statement."
# pylint: disable=too-many-nested-blocks
def _detect(self) -> List[Output]:
results: List[Output] = []
for c in self.contracts:
for f in c.functions_and_modifiers_declared:
for node in f.nodes:
if node.sons:
for function_called in node.internal_calls:
if isinstance(function_called, Function):
found = _assembly_node(function_called)
if found:
info: DETECTOR_INFO = [
f,
" calls ",
function_called,
" which halt the execution ",
found.node,
"\n",
]
json = self.generate_result(info)
results.append(json)
return results

@ -0,0 +1,68 @@
from typing import List
from slither.core.declarations import SolidityFunction, Function
from slither.detectors.abstract_detector import (
AbstractDetector,
DetectorClassification,
DETECTOR_INFO,
)
from slither.slithir.operations import SolidityCall
from slither.utils.output import Output
class ReturnInsteadOfLeave(AbstractDetector):
"""
Check for cases where a return(a,b) is used in an assembly function that also returns two variables
"""
ARGUMENT = "return-leave"
HELP = "If a `return` is used instead of a `leave`."
IMPACT = DetectorClassification.HIGH
CONFIDENCE = DetectorClassification.MEDIUM
WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-assembly-return"
WIKI_TITLE = "Return instead of leave in assembly"
WIKI_DESCRIPTION = "Detect if a `return` is used where a `leave` should be used."
WIKI_EXPLOIT_SCENARIO = """
```solidity
contract C {
function f() internal returns (uint a, uint b) {
assembly {
return (5, 6)
}
}
}
```
The function will halt the execution, instead of returning a two uint."""
WIKI_RECOMMENDATION = "Use the `leave` statement."
def _check_function(self, f: Function) -> List[Output]:
results: List[Output] = []
for node in f.nodes:
for ir in node.irs:
if isinstance(ir, SolidityCall) and ir.function == SolidityFunction(
"return(uint256,uint256)"
):
info: DETECTOR_INFO = [f, " contains an incorrect call to return: ", node, "\n"]
json = self.generate_result(info)
results.append(json)
return results
def _detect(self) -> List[Output]:
results: List[Output] = []
for c in self.contracts:
for f in c.functions_declared:
if (
len(f.returns) == 2
and f.contains_assembly
and f.visibility not in ["public", "external"]
):
results += self._check_function(f)
return results

@ -0,0 +1,93 @@
"""
Module detecting incorrect operator usage for exponentiation where bitwise xor '^' is used instead of '**'
"""
from typing import Tuple, List, Union
from slither.core.cfg.node import Node
from slither.core.declarations import Contract, Function
from slither.detectors.abstract_detector import (
AbstractDetector,
DetectorClassification,
DETECTOR_INFO,
)
from slither.slithir.operations import Binary, BinaryType, Operation
from slither.slithir.utils.utils import RVALUE
from slither.slithir.variables.constant import Constant
from slither.utils.output import Output
def _is_constant_candidate(var: Union[RVALUE, Function]) -> bool:
"""
Check if the variable is a constant.
Do not consider variable that are expressed with hexadecimal.
Something like 2^0xf is likely to be a correct bitwise operator
:param var:
:return:
"""
return isinstance(var, Constant) and not var.original_value.startswith("0x")
def _is_bitwise_xor_on_constant(ir: Operation) -> bool:
return (
isinstance(ir, Binary)
and ir.type == BinaryType.CARET
and (_is_constant_candidate(ir.variable_left) or _is_constant_candidate(ir.variable_right))
)
def _detect_incorrect_operator(contract: Contract) -> List[Tuple[Function, Node]]:
ret: List[Tuple[Function, Node]] = []
f: Function
for f in contract.functions + contract.modifiers: # type:ignore
# Heuristic: look for binary expressions with ^ operator where at least one of the operands is a constant, and
# the constant is not in hex, because hex typically is used with bitwise xor and not exponentiation
nodes = [node for node in f.nodes for ir in node.irs if _is_bitwise_xor_on_constant(ir)]
for node in nodes:
ret.append((f, node))
return ret
# pylint: disable=too-few-public-methods
class IncorrectOperatorExponentiation(AbstractDetector):
"""
Incorrect operator usage of bitwise xor mistaking it for exponentiation
"""
ARGUMENT = "incorrect-exp"
HELP = "Incorrect exponentiation"
IMPACT = DetectorClassification.HIGH
CONFIDENCE = DetectorClassification.MEDIUM
WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-exponentiation"
WIKI_TITLE = "Incorrect exponentiation"
WIKI_DESCRIPTION = "Detect use of bitwise `xor ^` instead of exponential `**`"
WIKI_EXPLOIT_SCENARIO = """
```solidity
contract Bug{
uint UINT_MAX = 2^256 - 1;
...
}
```
Alice deploys a contract in which `UINT_MAX` incorrectly uses `^` operator instead of `**` for exponentiation"""
WIKI_RECOMMENDATION = "Use the correct operator `**` for exponentiation."
def _detect(self) -> List[Output]:
"""Detect the incorrect operator usage for exponentiation where bitwise xor ^ is used instead of **
Returns:
list: (function, node)
"""
results: List[Output] = []
for c in self.compilation_unit.contracts_derived:
res = _detect_incorrect_operator(c)
for (func, node) in res:
info: DETECTOR_INFO = [
func,
" has bitwise-xor operator ^ instead of the exponentiation operator **: \n",
]
info += ["\t - ", node, "\n"]
results.append(self.generate_result(info))
return results

@ -0,0 +1,123 @@
from typing import List
from slither.core.cfg.node import Node
from slither.core.declarations import Contract
from slither.core.declarations.function import Function
from slither.core.solidity_types import Type
from slither.detectors.abstract_detector import (
AbstractDetector,
DetectorClassification,
DETECTOR_INFO,
)
from slither.slithir.operations import LowLevelCall, HighLevelCall
from slither.analyses.data_dependency.data_dependency import is_tainted
from slither.utils.output import Output
class ReturnBomb(AbstractDetector):
ARGUMENT = "return-bomb"
HELP = "A low level callee may consume all callers gas unexpectedly."
IMPACT = DetectorClassification.LOW
CONFIDENCE = DetectorClassification.MEDIUM
WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#return-bomb"
WIKI_TITLE = "Return Bomb"
WIKI_DESCRIPTION = "A low level callee may consume all callers gas unexpectedly."
WIKI_EXPLOIT_SCENARIO = """
```solidity
//Modified from https://github.com/nomad-xyz/ExcessivelySafeCall
contract BadGuy {
function youveActivateMyTrapCard() external pure returns (bytes memory) {
assembly{
revert(0, 1000000)
}
}
}
contract Mark {
function oops(address badGuy) public{
bool success;
bytes memory ret;
// Mark pays a lot of gas for this copy
//(success, ret) = badGuy.call{gas:10000}(
(success, ret) = badGuy.call(
abi.encodeWithSelector(
BadGuy.youveActivateMyTrapCard.selector
)
);
// Mark may OOG here, preventing local state changes
//importantCleanup();
}
}
```
After Mark calls BadGuy bytes are copied from returndata to memory, the memory expansion cost is paid. This means that when using a standard solidity call, the callee can "returnbomb" the caller, imposing an arbitrary gas cost.
Callee unexpectedly makes the caller OOG.
"""
WIKI_RECOMMENDATION = "Avoid unlimited implicit decoding of returndata."
@staticmethod
def is_dynamic_type(ty: Type) -> bool:
# ty.is_dynamic ?
name = str(ty)
if "[]" in name or name in ("bytes", "string"):
return True
return False
def get_nodes_for_function(self, function: Function, contract: Contract) -> List[Node]:
nodes = []
for node in function.nodes:
for ir in node.irs:
if isinstance(ir, (HighLevelCall, LowLevelCall)):
if not is_tainted(ir.destination, contract): # type:ignore
# Only interested if the target address is controlled/tainted
continue
if isinstance(ir, HighLevelCall) and isinstance(ir.function, Function):
# in normal highlevel calls return bombs are _possible_
# if the return type is dynamic and the caller tries to copy and decode large data
has_dyn = False
if ir.function.return_type:
has_dyn = any(
self.is_dynamic_type(ty) for ty in ir.function.return_type
)
if not has_dyn:
continue
# If a gas budget was specified then the
# user may not know about the return bomb
if ir.call_gas is None:
# if a gas budget was NOT specified then the caller
# may already suspect the call may spend all gas?
continue
nodes.append(node)
# TODO: check that there is some state change after the call
return nodes
def _detect(self) -> List[Output]:
results = []
for contract in self.compilation_unit.contracts:
for function in contract.functions_declared:
nodes = self.get_nodes_for_function(function, contract)
if nodes:
info: DETECTOR_INFO = [
function,
" tries to limit the gas of an external call that controls implicit decoding\n",
]
for node in sorted(nodes, key=lambda x: x.node_id):
info += ["\t", node, "\n"]
res = self.generate_result(info)
results.append(res)
return results

@ -0,0 +1,69 @@
from typing import List
from slither.detectors.abstract_detector import (
AbstractDetector,
DetectorClassification,
DETECTOR_INFO,
)
from slither.slithir.operations import (
Binary,
BinaryType,
)
from slither.core.declarations import Function
from slither.utils.output import Output
class TautologicalCompare(AbstractDetector):
"""
Same variable comparison detector
"""
ARGUMENT = "tautological-compare"
HELP = "Comparing a variable to itself always returns true or false, depending on comparison"
IMPACT = DetectorClassification.MEDIUM
CONFIDENCE = DetectorClassification.HIGH
WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#tautological-compare"
WIKI_TITLE = "Tautological compare"
WIKI_DESCRIPTION = "A variable compared to itself is probably an error as it will always return `true` for `==`, `>=`, `<=` and always `false` for `<`, `>` and `!=`."
WIKI_EXPLOIT_SCENARIO = """
```solidity
function check(uint a) external returns(bool){
return (a >= a);
}
```
`check` always return true."""
WIKI_RECOMMENDATION = "Remove comparison or compare to different value."
def _check_function(self, f: Function) -> List[Output]:
affected_nodes = set()
for node in f.nodes:
for ir in node.irs:
if isinstance(ir, Binary):
if ir.type in [
BinaryType.GREATER,
BinaryType.GREATER_EQUAL,
BinaryType.LESS,
BinaryType.LESS_EQUAL,
BinaryType.EQUAL,
BinaryType.NOT_EQUAL,
]:
if ir.variable_left == ir.variable_right:
affected_nodes.add(node)
results = []
for n in affected_nodes:
info: DETECTOR_INFO = [f, " compares a variable to itself:\n\t", n, "\n"]
res = self.generate_result(info)
results.append(res)
return results
def _detect(self):
results = []
for f in self.compilation_unit.functions_and_modifiers:
results.extend(self._check_function(f))
return results

@ -0,0 +1,9 @@
Test.bad1() (tests/e2e/detectors/test_data/incorrect-exp/0.7.6/incorrect_exp.sol#9-12) has bitwise-xor operator ^ instead of the exponentiation operator **:
- UINT_MAX = 2 ^ 256 - 1 (tests/e2e/detectors/test_data/incorrect-exp/0.7.6/incorrect_exp.sol#10)
Test.bad0(uint256) (tests/e2e/detectors/test_data/incorrect-exp/0.7.6/incorrect_exp.sol#5-7) has bitwise-xor operator ^ instead of the exponentiation operator **:
- a ^ 2 (tests/e2e/detectors/test_data/incorrect-exp/0.7.6/incorrect_exp.sol#6)
Derived.slitherConstructorVariables() (tests/e2e/detectors/test_data/incorrect-exp/0.7.6/incorrect_exp.sol#30) has bitwise-xor operator ^ instead of the exponentiation operator **:
- my_var = 2 ^ 256 - 1 (tests/e2e/detectors/test_data/incorrect-exp/0.7.6/incorrect_exp.sol#3)

@ -0,0 +1,4 @@
C.bad1() (tests/e2e/detectors/test_data/incorrect-return/0.8.10/incorrect_return.sol#21-24) calls C.indirect() (tests/e2e/detectors/test_data/incorrect-return/0.8.10/incorrect_return.sol#17-19) which halt the execution return(uint256,uint256)(5,6) (tests/e2e/detectors/test_data/incorrect-return/0.8.10/incorrect_return.sol#4)
C.bad0() (tests/e2e/detectors/test_data/incorrect-return/0.8.10/incorrect_return.sol#8-11) calls C.internal_return() (tests/e2e/detectors/test_data/incorrect-return/0.8.10/incorrect_return.sol#2-6) which halt the execution return(uint256,uint256)(5,6) (tests/e2e/detectors/test_data/incorrect-return/0.8.10/incorrect_return.sol#4)

@ -0,0 +1,5 @@
Mark.oops(address) (tests/e2e/detectors/test_data/return-bomb/0.8.20/return_bomb.sol#31-55) tries to limit the gas of an external call that controls implicit decoding
ret1 = BadGuy(badGuy).fbad{gas: 2000}() (tests/e2e/detectors/test_data/return-bomb/0.8.20/return_bomb.sol#42)
(x,str) = BadGuy(badGuy).fbad1{gas: 2000}() (tests/e2e/detectors/test_data/return-bomb/0.8.20/return_bomb.sol#44)
(success,ret) = badGuy.call{gas: 10000}(abi.encodeWithSelector(BadGuy.llbad.selector)) (tests/e2e/detectors/test_data/return-bomb/0.8.20/return_bomb.sol#47-51)

@ -0,0 +1,2 @@
C.f() (tests/e2e/detectors/test_data/return-leave/0.8.10/incorrect_return.sol#3-7) contains an incorrect call to return: return(uint256,uint256)(5,6) (tests/e2e/detectors/test_data/return-leave/0.8.10/incorrect_return.sol#5)

@ -0,0 +1,3 @@
A.check(uint256) (tests/e2e/detectors/test_data/tautological-compare/0.8.20/compare.sol#3-5) compares a variable to itself:
(a >= a) (tests/e2e/detectors/test_data/tautological-compare/0.8.20/compare.sol#4)

@ -0,0 +1,30 @@
contract Test {
uint my_var = 2 ^ 256-1;
function bad0(uint a) internal returns (uint) {
return a^2;
}
function bad1() internal returns (uint) {
uint UINT_MAX = 2^256-1;
return UINT_MAX;
}
/* Correct exponentiation operator */
function good0(uint a) internal returns (uint) {
return a**2;
}
/* Neither operand is a constant */
function good1(uint a) internal returns (uint) {
return a^a;
}
/* The constant operand 0xff in hex typically means bitwise xor */
function good2(uint a) internal returns (uint) {
return a^0xff;
}
}
contract Derived is Test {}

@ -0,0 +1,36 @@
contract C {
function internal_return() internal{
assembly {
return (5, 6)
}
}
function bad0() public returns (bool){
internal_return();
return true;
}
function indirect2() internal {
internal_return();
}
function indirect() internal {
indirect2();
}
function bad1() public returns (bool){
indirect();
return true;
}
function good0() public{
// Dont report if there is no following operation
internal_return();
}
function good1() public returns (uint a, uint b){
assembly {
return (5, 6)
}
}
}

@ -0,0 +1,57 @@
contract BadGuy {
function llbad() external pure returns (bytes memory) {
assembly{
revert(0, 1000000)
}
}
function fgood() external payable returns (uint){
assembly{
return(0, 1000000)
}
}
function fbad() external payable returns (uint[] memory){
assembly{
return(0, 1000000)
}
}
function fbad1() external payable returns (uint, string memory){
assembly{
return(0, 1000000)
}
}
}
contract Mark {
function oops(address badGuy) public{
bool success;
string memory str;
bytes memory ret;
uint x;
uint[] memory ret1;
x = BadGuy(badGuy).fgood{gas:2000}();
ret1 = BadGuy(badGuy).fbad(); //good (no gas specified)
ret1 = BadGuy(badGuy).fbad{gas:2000}();
(x, str) = BadGuy(badGuy).fbad1{gas:2000}();
// Mark pays a lot of gas for this copy 😬😬😬
(success, ret) = badGuy.call{gas:10000}(
abi.encodeWithSelector(
BadGuy.llbad.selector
)
);
// Mark may OOG here, preventing local state changes
//importantCleanup();
}
}

@ -0,0 +1,8 @@
contract C {
function f() internal returns (uint a, uint b){
assembly {
return (5, 6)
}
}
}

@ -0,0 +1,6 @@
contract A{
function check(uint a) external returns(bool){
return (a >= a);
}
}

@ -1654,6 +1654,31 @@ ALL_TESTS = [
"encode_packed_collision.sol",
"0.7.6",
),
Test(
all_detectors.IncorrectReturn,
"incorrect_return.sol",
"0.8.10",
),
Test(
all_detectors.ReturnInsteadOfLeave,
"incorrect_return.sol",
"0.8.10",
),
Test(
all_detectors.IncorrectOperatorExponentiation,
"incorrect_exp.sol",
"0.7.6",
),
Test(
all_detectors.TautologicalCompare,
"compare.sol",
"0.8.20",
),
Test(
all_detectors.ReturnBomb,
"return_bomb.sol",
"0.8.20",
),
]
GENERIC_PATH = "/GENERIC_PATH"

@ -4,13 +4,13 @@ import re
import sys
from pathlib import Path
from typing import List, Dict, Tuple
from packaging.version import parse as parse_version
import pytest
from crytic_compile import CryticCompile, save_to_zip
from crytic_compile.utils.zip import load_from_zip
from deepdiff import DeepDiff
from packaging.version import parse as parse_version
from solc_select.solc_select import install_artifacts as install_solc_versions
from solc_select.solc_select import installed_versions as get_installed_solc_versions
from crytic_compile import CryticCompile, save_to_zip
from crytic_compile.utils.zip import load_from_zip
from slither import Slither
from slither.printers.guidance.echidna import Echidna

Loading…
Cancel
Save