11.slither检测器之三——批量函数调用检测

  • 小驹
  • 更新于 2023-07-03 16:15
  • 阅读 2014

本文会涉及到slither中几类call的区别,slither遍历node时的常用的递归框架,以及将这两类知识应用到批量函数调用风险的检测中。

11.slither检测器之三——批量函数调用检测

本文会涉及到slither中几类call的区别,slither遍历node时的常用的递归框架,以及将这两类知识应用到批量函数调用风险的检测中。

1.前置知识

1.1 几类call的区别

slither中有个基础的Call类别在(slither.slithir.operations下的call文件中),HighLevelCall、HighLevelCall、Transfer、Send这4个类都是继承自Call类,LibraryCall函数调用是一种高级调用,所以是继承自HighLevelCall。

在判断各类的函数调用时,要注意区分这几种Call的区别。

<aside> 😜 在这个例子中。 1. 学习对节点的遍历的递归调用的方法。 2. 各类Call的区别。 3. 学习 node,ir的区别。node是对应着一句源代码,ir代表当前行源代码生成的中间代码。通过node对象的all_slithir_operations 属性可以得到当前node的所有的ir,如果当前ir是一种InternalCall类型的话,使用ir.function可以得到该内部调用对应的Function对象,通过该Function对象的entry_point就得到了内部调用的入口点。 4. node对象的sons属性,可以返回node对象所有的子节点。

</aside>

2.2 Node节点的递归框架

这是遍历node节点时最常用的框架结构。

遍历每个合约,遍历合约的每个函数对象,从每个函数对象的入口点开始进入检测函数。检测函数的架构通常如下,通常使用递归的方式调用:

def _detect_xxx(
    node: Node,
    explored: Set[Node],
    written: Dict[Variable, Node],
    ret: List[Tuple[Variable, Node, Node]],
):

node: 当前正在检测的节点。
explored: 已经遍历过的节点,通常为Set类型或者List类型。
written: 不一定存在这个参数,可能用来存储中间变量。
ret: 返回结果。

# 如果这个节点处理过,就直接跳过
if node in explored:
        return
# 处理当前节点
explored.add(node)
………………………………………………………………
这里对当前节点的不同的检测逻辑
………………………………………………………………

# 递归处理子节点
for son in node.sons:
        _detect_xxxx(son, explored, dict(written), ret)

这段代码中,使用ret保存函数返回的节点的列表。使用explored变量来保存该节点是否已经处理过的标志。在对每个节点的遍历中,如果该节点没有被处理过(也就是explored中的标志为false),则对该节点的子节点递归调用,同时给该节点设置上处理标志。

2.批量函数调用检测

2.1 风险描述

批量函数调用,指在一个函数块中(如一个循环中)进行批量的函数调用,可能导致DOS攻击。举个例子,如下面的代码中,在for 循环中调用了transfer函数,产生的风险就是如果循环中有一个transfer失败的话,会导致整个tx回退。

所以如果有恶意用户利用此漏洞的话,可以通过使用自己可以控制的某个函数失败从而导致所有用户的调用失败。

contract CallsInLoop{

    address[] destinations;

    constructor(address[] newDestinations) public{
        destinations = newDestinations;
    }

    function bad() external{
        for (uint i=0; i &lt; destinations.length; i++){
            destinations[i].transfer(i);
        }
    }

}

2.2 检测逻辑

简单来说,对个node节点的处理时,如果node节点是STARTLOOP,并且node中的ir存在函数调用的情况,就认为存在批量函数调用的风险。

代码逻辑:

  1. 遍历所有的合约c,调用detect_call_in_loop(c)
  2. 对每个合约中的遍历函数,如果函数没有实现代码,就直接跳过。
  3. 对于有实现的函数,调用call_in_loop
  4. call_in_loop使用了第1章中说的node节点的递归框架。各参数如下:

    node: 函数的入口点

    in_loop_counter:记录循环的层数

    visited:Node列表,记录遍历过的Node节点。

    ret: Node列表,保存存在缺陷的Node列表。

    简单来说,对个node节点的处理时,如果node节点是STARTLOOP,并且node中的ir存在函数调用的情况,就认为存在批量函数调用的风险。

对call_in_loop的分析,函数起始

from slither.slithir.operations import (
    HighLevelCall,
    LibraryCall,
    LowLevelCall,
    Send,
    Transfer,
    InternalCall,
)

# 判断node是否已经分析过,没分析过才继续分析并将该node加入已分析列表。分析过的话直接跳过。
if node in visited:
        return
    # shared visited
    visited.append(node)

# 如果进入到loop中,对应的loop的计数进行增减
if node.type == NodeType.STARTLOOP:
        in_loop_counter += 1
    elif node.type == NodeType.ENDLOOP:
        in_loop_counter -= 1

# 如果在loop中,对node的所有的ir进行遍历,如果ir对应的对象进行处理。
# 如果是是LowLevelCall、HighLevelCall, Send, Transfer的实例,并且不是LibraryCall(调用函数)的话,就加到结果列表中。
# 如果是InternalCall的话,就通过ir.function.entry_point取得内部调用的入口点,递归调用。
if in_loop_counter > 0:
        for ir in node.all_slithir_operations():
            if isinstance(ir, (LowLevelCall, HighLevelCall, Send, Transfer)):
                if isinstance(ir, LibraryCall):
                    continue
                ret.append(ir.node) // 这里发现存在call调用,而且是在loop循环中的。
            if isinstance(ir, (InternalCall)):
                call_in_loop(ir.function.entry_point, in_loop_counter, visited, ret)
# 将node所有的子项,进行遍历
for son in node.sons:
        call_in_loop(son, in_loop_counter, visited, ret)

遍历合约的所有函数代码版本

def detect_call_in_loop(contract: Contract) -> List[Node]:
    ret: List[Node] = []
    for f in contract.functions_entry_points:
        if f.is_implemented:
            call_in_loop(f.entry_point, 0, [], ret)

    return ret

遍历所有Node的代码


def call_in_loop(node: Node, in_loop_counter: int, visited: List[Node], ret: List[Node]) -> None:
    if node in visited:
        return
    # shared visited
    visited.append(node)

    if node.type == NodeType.STARTLOOP:
        in_loop_counter += 1
    elif node.type == NodeType.ENDLOOP:
        in_loop_counter -= 1

    if in_loop_counter > 0:
        for ir in node.all_slithir_operations():
            if isinstance(ir, (LowLevelCall, HighLevelCall, Send, Transfer)):
                if isinstance(ir, LibraryCall):
                    continue
                ret.append(ir.node)
            if isinstance(ir, (InternalCall)):
                call_in_loop(ir.function.entry_point, in_loop_counter, visited, ret)

    for son in node.sons:
        call_in_loop(son, in_loop_counter, visited, ret)

完整的检测代码参考slither中的calls_in_loop

点赞 1
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
小驹
小驹
0xcD46...3461
weixin: xiaoju521区块链安全分析,欢迎私信沟通交流