CTF 选手效率利器:使用 Phalcon Fork 来学习 Ethernaut CTF 2024
2024-04-12 18:22
BlockSec
2024-04-12 18:22
订阅此专栏
收藏此文章

Preface

目前 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]。

Space Bank

通过查看 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]。

Wombo Combo

这道题目的原型是 2023 年 12 月发生的 TIME Token 攻击事件,我们可以在这里 [7]查看其中一笔原始攻击交易。该漏洞的成因是 ERC-2771 与 Muticall 在实现上存在不兼容问题,OpenZeppelin 在其官方博客 [8]中对此漏洞做了详细说明。

回到题目本身,我们知道 OpenZeppelin 在 5.0.1 与 4.9.4 后通过在 Muticall 合约中引入 context 修复了该问题,而题目中使用的合约版本是 v4.4.1,因此我们可以构造如下调用链:

  • Forwarder.execute(multicall(bytes[])) -> Staking.multicall(bytes[]) -> delegatecall(maliciousCalldata) -> Execute as Victim

其中 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。

XYZ

这是一道较复杂的 DeFi 相关题目,其原型是 2023 年 11 月发生的 Raft.fi 协议攻击事件,在这里 [10]可以查看我们当时对该事件的报告。

简单来说,攻击者利用了合约中的清算逻辑,通过捐赠的方式操控了抵押代币的 storedIndex,又由于抵押代币在铸造时存在精度损失问题(向上取整),攻击者仅需数量为 1 的标的代币即可铸造 1 份额的抵押代币。重复该过程即可放大协议的损失。

这道题目对原始的利用场景进行了简化,在部署题目的交易中,预置了一个可以被清算的仓位。题目要求最终 0xcafebabe 地址的 XYZ 代币余额等于 250000000118,这意味着我们需要在原有基础上再放大抵押代币的 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]你可以找到测试攻击合约的全部交易并尝试调试他们。

Conclusion

通过 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

【免责声明】市场有风险,投资需谨慎。本文不构成投资建议,用户应考虑本文中的任何意见、观点或结论是否符合其特定状况。据此投资,责任自负。

BlockSec
数据请求中
查看更多

推荐专栏

数据请求中
在 App 打开