使用Slither检测MISO和Opyn的msg.value重复利用漏洞

本文详细介绍了如何利用Slither工具检测智能合约中的msg.value重复利用漏洞,包括Opyn和MISO两个实际案例的技术分析、漏洞原理说明以及Slither检测器的实现逻辑和使用方法。

使用Slither检测MISO和Opyn的msg.value重用漏洞

2021年8月18日,samczsun报告了SushiSwap MISO智能合约中的一个关键漏洞,该漏洞使约3.5亿美元(10.9万ETH)面临风险。这个问题与2020年8月对Opyn代码库进行的攻击类似。

在报告发布时,我正在Trail of Bits完成我的区块链安全实习,在那里我深入了解了Slither的功能。我立即思考是否可能为这些漏洞创建Slither检测器。答案是肯定的!今天我们发布两个新的开源检测器:msg-value-loop和delegatecall-loop,分别可以检测Opyn和MISO漏洞。

循环中的msg.value?请避免!

MISO和Opyn漏洞的根本结果是相同的:多次重复使用相同的msg.value金额。区别在于Opyn中显式使用msg.value,而MISO中隐式使用(通过delegatecall)。

让我们看一个简单的示例来演示这个漏洞。

在Opyn的情况下,msg.value在可支付函数内的循环中使用。如果使用多个接收者调用addBalances(),即使只发送了一个接收者对应的ETH,相同的msg.value也会为每个接收者重复使用。

1
2
3
4
5
6
7
8
contract C {
  mapping (address => uint256) balances;                
  function addBalances(address[] memory receivers) public payable {
    for (uint256 i = 0; i < receivers.length; i++) {
      balances[receivers[i]] += msg.value;
    }
  }
}

在MISO的情况下,漏洞来源是可支付函数内循环中的delegatecall,该函数还调用了另一个可支付函数。Delegatecall调用函数时保持当前合约的上下文、发送者和值。这是对MISO漏洞的简化解释;更多细节建议阅读samczsun的博客文章。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
contract C {
  mapping (address => uint256) balances;
 
  function addBalance(address a) public payable {
    balances[a] += msg.value;
  }         
 
  function addBalances(address[] memory receivers) public payable {
    for (uint256 i = 0; i < receivers.length; i++) {
      address(this).delegatecall(abi.encodeWithSignature("addBalance(address)", receivers[i]));
    }
  }
}

Slither的新检测器

通过运行Slither来检测循环中的delegatecall和msg.value调用,我们得到以下结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ slither --detect delegatecall-loop Delegatecall.sol
C.addBalances(address[]) (delegatecall.sol#10-15) has delegatecall inside a loop in a payable function: address(this).delegatecall(abi.encodeWithSignature(addBalance(address),receivers[i])) (delegatecall.sol#12)
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation/#payable-functions-using-delegatecall-inside-a-loop
Delegatecall.sol analyzed (1 contracts with 1 detectors), 1 result(s) found
 
$ slither --detect msg-value-loop Msgvalue.sol
C.addBalances(address[]) (msgvalue.sol#7-12) use msg.value in a loop:
balances[receivers[i]] += msg.value (msgvalue.sol#9)
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation/#msgvalue-inside-a-loop
Msgvalue.sol analyzed (1 contracts with 1 detectors), 1 result(s) found

这两个检测器使用相同的逻辑实现,利用CFG表示和Slither的中间语言表示SlithIR。检测器遍历合约的可支付函数节点,并检查当前节点是否进入或退出循环。现在,SlithIR来帮助我们。两个检测器的实现有所不同,它们遍历节点的SlithIR操作;msg-value-loop检查当前操作是否读取msg.value,而delegatecall-loop检查当前操作是否为delegatecall。

让我们在发现存在漏洞的智能合约上尝试这些检测器。

Opyn

1
2
3
4
$ slither 0x951D51bAeFb72319d9FBE941E1615938d89ABfe2 --detect msg-value-loop
OptionsContract._exercise(uint256,address) (crytic-export/etherscan-contracts/0x951D51bAeFb72319d9FBE941E1615938d89ABfe2-oToken.sol#1816-1899) use msg.value in a loop: require(bool,string)(msg.value == amtUnderlyingToPay,Incorrect msg.value) (crytic-export/etherscan-contracts/0x951D51bAeFb72319d9FBE941E1615938d89ABfe2-oToken.sol#1875)
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation/#msgvalue-inside-a-loop
0x951D51bAeFb72319d9FBE941E1615938d89ABfe2 analyzed (13 contracts with 1 detectors), 1 result(s) found

SushiSwap的MISO

1
2
3
4
$ slither 0x4c4564a1FE775D97297F9e3Dc2e762e0Ed5Dda0e --detect delegatecall-loop
BaseBoringBatchable.batch(bytes[],bool) (contracts/Utils/BoringBatchable.sol#35-44) has delegatecall inside a loop in a payable function: (success,result) = address(this).delegatecall(calls[i]) (contracts/Utils/BoringBatchable.sol#39)
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation/#payable-functions-using-delegatecall-inside-a-loop
0x4c4564a1FE775D97297F9e3Dc2e762e0Ed5Dda0e analyzed (21 contracts with 1 detectors), 1 result(s) found

结论

总之,Slither是预防智能合约安全漏洞的强大工具,可以通过创建新的检测器来扩展其功能。如果您想了解更多关于此工具的信息,请查看我们的"构建安全智能合约"指南。

我在Trail of Bits的实习经历很有趣,帮助我提高了安全技能,并学会了如何更好地进行审计。如果您有兴趣获得类似的体验,可以申请加入我们作为区块链安全学徒。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计