使用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案例:在可支付函数(payable function)的循环内部使用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的实习经历非常有趣,帮助我提升了安全技能并学习了如何更好地进行审计。如果您有兴趣获得类似经验,可以申请加入我们作为区块链安全学徒。