本文会涉及到slither中几类call的区别,slither遍历node时的常用的递归框架,以及将这两类知识应用到批量函数调用风险的检测中。
本文会涉及到slither中几类call的区别,slither遍历node时的常用的递归
框架,以及将这两类知识应用到批量函数调用风险的检测中。
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>
这是遍历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),则对该节点的子节点递归调用
,同时给该节点设置上处理标志。
批量函数调用,指在一个函数块中(如一个循环中)进行批量的函数调用,可能导致DOS攻击。举个例子,如下面的代码中,在for 循环中调用了transfer函数,产生的风险就是如果循环中有一个transfer失败的话,会导致整个tx回退。
所以如果有恶意用户利用此漏洞的话,可以通过使用自己
可以控制的某个函数失败从而导致所有
用户的调用失败。
contract CallsInLoop{
address[] destinations;
constructor(address[] newDestinations) public{
destinations = newDestinations;
}
function bad() external{
for (uint i=0; i < destinations.length; i++){
destinations[i].transfer(i);
}
}
}
简单来说,对个node节点的处理时,如果node节点是STARTLOOP
,并且node中的ir存在函数调用的情况,就认为存在批量函数调用的风险。
代码逻辑:
call_in_loop
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
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!