目前 Web3 的 CTF 赛题部署大都依赖于容器化的基础设施,比较常用且得到社区广泛认可的包括 solidctf [1] 与 paradigm-ctf-infrastructure [2]。后者的优势是对 Foundry 有很好地支持,对于多合约赛题的部署可以通过 foundry script
自动化进行,极大地提高了赛题的部署效率。
而对于选手来说,尽管目前的题目环境支持本地使用工具集如 Foundry 进行调试,但由于缺少相应交易浏览器的支持,查看链上交易的具体细节仍要通过命令行交互。对于复杂的交易,控制台能展示的信息是有限且不够直观的。
Phalcon Fork [3]是专门为 Web3 开发者与安全研究人员设计的综合分析工具。通过 RPC 接入指定 Fork,用户可以使用 Phalcon Explorer 浏览并调试交易;同时,Fork 还内部集成了类似 Etherscan 的区块链浏览器 Phalcon Scan,用户可以轻松地查看分叉链上的地址与交易详情。
本文将针对 Ethernaut CTF 2024 中的部分题目,演示 Phalcon Fork 如何让选手的解题过程如虎添翼。
在此感谢 OpenZeppelin 提供的高质量赛题。本文中所使用的附件都已上传 Github [4]。
通过查看 Challenge
合约,我们可以得知该题目最终的目的是调用 explodeSpaceBank
函数,绕过一系列检查,最终实现将 exploded
置为 true
。
使用我们提供的 deploy.sh
[5]脚本部署该题目后,可以在对应的 Fork 上查看已经部署的合约信息:
通过解读 SpaceBank
合约的源码,不难看出我们需要调用 flashLoan
函数并重入 deposit
函数来触发 EmergencyAlarms
的自增并设法通过 _emergencyAlarmProtocol
函数中的检查。同时,通过这种重入的手法也可以达到掏空合约中全部 SpaceToken 的目的。
function deposit(uint256 amount, bytes calldata data) external _emergencyAlarms(data) {
require(token.transferFrom(msg.sender, address(this), amount), "Transfer failed");
balances[msg.sender] += amount;
}
function flashLoan(uint256 amount, address flashLoanReceiver) external {
uint256 initialBalance = token.balanceOf(address(this));
require(initialBalance >= amount, "Not enough liquidity");
// Transfer loan amount to the receiver
require(token.transfer(flashLoanReceiver, amount), "Transfer failed");
// Execute custom logic in the receiver's contract
entered = true;
(bool success, bytes memory result) =
flashLoanReceiver.call(abi.encodeWithSignature("executeFlashLoan(uint256)", amount)); // -> Here we can re-enter deposit function
if (success == false) revert(string(result));
entered = false;
uint256 fee = amount / 1000; // 0.1% fee
uint256 currentBalance = token.balanceOf(address(this));
require(currentBalance >= initialBalance + fee, "Loan not repaid with fee");
}
真正棘手的地方在于第 97 至 101 行,在 EmergencyAlarms
为 2 时,合约将使用 CREATE2
创建新合约,并在检查余额增长后,将新合约地址赋值给 storage 变量 _createdAddress
。而在第 117 行,代码又会检查该地址的代码长度是否为 0。幸运的是,我们有 SELFDESTRUCT
可以绕过该检查。
关于本题的详细解法请参考官方提供的题解 [6]。
这道题目的原型是 2023 年 12 月发生的 TIME Token 攻击事件,我们可以在这里 [7]查看其中一笔原始攻击交易。该漏洞的成因是 ERC-2771 与 Muticall 在实现上存在不兼容问题,OpenZeppelin 在其官方博客 [8]中对此漏洞做了详细说明。
回到题目本身,我们知道 OpenZeppelin 在 5.0.1 与 4.9.4 后通过在 Muticall
合约中引入 context
修复了该问题,而题目中使用的合约版本是 v4.4.1,因此我们可以构造如下调用链:
其中 Victim 是由 _msgSender()
函数解析 maliciousCalldata 的最后 20 字节得到的。
部署题目后,可以在 Fork Scan [9] 上查看 Staking
合约的历史交易:
通过查看交易,可以得知奖励间隔 duration
被设置为 20,而关键的 storge 变量 rewardRate
仍未初始化。我们可以使用上面提供的调用链,用下面的代码构造 maliciousCalldata,绕过 onlyOwner()
的检查:
bytes[] memory maliciousCalldata = new bytes[](2);
maliciousCalldata[0] = abi.encodeWithSignature(
"setRewardsDuration(uint256)",
uint256(1), // minimal duration
owner
);
maliciousCalldata[1] = abi.encodeWithSignature(
"notifyRewardAmount(uint256)",
uint256(1128120030438127299645800), // amazing number
owner
);
成功调整 duration
与 rewardRate
后,正常地进行质押并获取奖励即可获得大额奖励代币,将其转账给 0x123 地址即可获得 flag。
这是一道较复杂的 DeFi 相关题目,其原型是 2023 年 11 月发生的 Raft.fi 协议攻击事件,在这里 [10]可以查看我们当时对该事件的报告。
简单来说,攻击者利用了合约中的清算逻辑,通过捐赠的方式操控了抵押代币的 storedIndex
,又由于抵押代币在铸造时存在精度损失问题(向上取整),攻击者仅需数量为 1 的标的代币即可铸造 1 份额的抵押代币。重复该过程即可放大协议的损失。
这道题目对原始的利用场景进行了简化,在部署题目的交易中,预置了一个可以被清算的仓位。题目要求最终 0xcafebabe 地址的 XYZ 代币余额等于 signal
值,让 1 份额的抵押代币借出更多的 XYZ 代币。
function mint(address to, uint256 amount) external onlyManager {
_mint(to, amount.divUp(signal));
}
function setSignal(uint256 backingAmount) external onlyManager {
uint256 supply = ERC20.totalSupply();
uint256 newSignal = (backingAmount == 0 && supply == 0) ? ProtocolMath.ONE : backingAmount.divUp(supply);
signal = newSignal;
}
分析 Manager
合约源码,要触发 ERC20Signal
合约的 setSignal
逻辑需要用户对不健康的仓位进行清算,为了放大 signal
的值,我们可以通过捐赠的方式使得 backingAmount
增大。之后,反复利用 mint
函数的精度损失问题,可以在持有大量 XYZ 代币的同时撤出之前捐赠的所有 sETH。
在 Fork 中部署测试攻击合约并发送交易后,我们可以在区块链浏览器中查看该交易 [11],为了更方便地调试,我们可以使用如下命令对测试攻击合约进行验证:
forge verify-contract <address> <path>:<contract> --etherscan-api-key <phalcon access key> --verifier-url "https://api.phalcon.xyz/api/<phalcon rpc id>" --rpc-url <phalcon rpc url>
在交易浏览器中调试该交易,可以清晰地看到抵押代币 XYZ-sETH-c 的 setSignal
函数已经按照预期被成功调用:
而且由于 Fork Scan 上已经验证过该合约的源码,我们还可以步入测试攻击合约调试,具体地依照交易执行路径验证每步操作是否达到预期效果。在这里 [12]你可以找到测试攻击合约的全部交易并尝试调试他们。
通过 Phalcon Fork 创建私有测试网并部署 CTF 题目,选手便可以使用内部集成的区块链浏览器与交易浏览器帮助解题。创建的 Fork 可以通过 RPC 访问且兼容多种开发测试框架,选手可以在更真实的环境下享受他们的 CTF 比赛。
[1] https://github.com/chainflag/solidctf[2] https://github.com/paradigmxyz/paradigm-ctf-infrastructure[3] https://blocksec.com/fork[4] https://github.com/blocksecteam/Ethernaut2024phalcon[5] https://github.com/blocksecteam/Ethernaut2024phalcon/blob/main/deploy.sh[6] https://github.com/OpenZeppelin/ctf-2024/blob/main/spacebank/README.md[7] https://explorer.phalcon.xyz/tx/eth/0xecdd111a60debfadc6533de30fb7f55dc5ceed01dfadd30e4a7ebdb416d2f6b6[8] https://blog.openzeppelin.com/arbitrary-address-spoofing-vulnerability-erc2771context-multicall-public-disclosure[9] https://app.blocksec.com/fork/scan/forkd6cc482d0fdf4683bd41ffb820a69157[10] https://twitter.com/BlockSecTeam/status/1723229393529835972[11] https://app.blocksec.com/fork/scan/fork0805fbb57ae1463bbcf15103d443861f/tx/0x8859559011967f7f714eb4658836add26300cccf4e4d5e769c339645d12bf763[12] https://app.blocksec.com/fork/scan/fork_0805fbb57ae1463bbcf15103d443861f/address/0x24ecc5e6eaa700368b8fac259d3fbd045f695a08
【免责声明】市场有风险,投资需谨慎。本文不构成投资建议,用户应考虑本文中的任何意见、观点或结论是否符合其特定状况。据此投资,责任自负。