本文介绍了如何使用Paradigm的CTF框架创建以太坊智能合约的CTF挑战。文章详细讲解了从智能合约的漏洞设计、框架的设置、Docker环境的搭建到进行远程测试的每个步骤,适合想要了解区块链CTF的开发者与安全研究人员。
Zellic 最近赞助了 CSAW 2022↗。 CSAW 是一个每年在纽约布鲁克林由 NYU 主办的网络安全活动。多年来,它已成为美国/加拿大 CTF 场景的中心。每年,CSAW CTF 都吸引着顶级的高中和大学 CTF 玩家。事实上,我的联合创始人 Jazzy 和我实际上是在 CSAW 2017 上相遇的——在那儿我们随后组成了一个团队,最终成为了 perfect\ blue↗。因此,我们把 CSAW’22 视为回馈 CTF 社区的机会。
作为赞助商,我们有机会为 CTF 贡献一个挑战。我决定创建一个简单的以太坊智能合约挑战。这个挑战将围绕着利用一个脆弱的智能合约进行。然而,CTF 玩家需要能够与智能合约进行互动,而不将他们的解决方案或利用方式泄露给其他玩家。这是创建区块链 CTF 挑战时的一个常见问题。
幸运的是,samczsun↗ 和 Paradigm 其余团队开发了一个出色的框架,用于按需部署和托管这类 CTF 挑战。它通过为每个参与者按需部署私有的分叉区块链实例(使用 Anvil)来解决这个问题。以下是实际操作的样子:
$ nc ctf.example.xyz 31337
1 - 启动新实例
2 - 杀死实例
3 - 获取旗帜
action? 1
票据请输入: ticket
你的私有区块链已部署
将在 30 分钟后自动终止
以下是一些有用的信息
uuid: 12341234-12341234-12341234
rpc 端点: http://ctf.example.xyz:8545/12341234-12341234-12341234
私钥: 0xabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd
设置合约: 0x12341234123412341234
这个框架易于使用,尽管文档并不是非常详尽。希望这篇文章能提供一个关于如何为你自己的 CTF 设置框架的实际示例。
在我们深入使用 CTF 框架之前,让我们快速讨论一下智能合约本身。这将有助于之后提供有用的上下文。
智能合约是一个简单的保险库,允许用户存入和提取 USDC 和 DAI。然而,在提取时,如果保险库所需代币的余额不足,它会在 Uniswap 上进行交换,以获得更多的代币。
contract Chal {
// ...
function withdrawDAI(uint amountOut) public {
require(balanceOf[msg.sender] >= amountOut);
balanceOf[msg.sender] -= amountOut;
if (dai.balanceOf(address(this)) < amountOut*10e12) {
address[] memory path;
path = new address[](2);
path[0] = USDC;
path[1] = DAI;
uint[] memory amounts = router.swapExactTokensForTokens(
usdc.balanceOf(address(this)), 0, path, address(this), block.timestamp
);
}
dai.transfer(msg.sender, amountOut*10e12);
}
// ...
}
这里的问题是缺少滑点检查。攻击者可以通过使合约重复进行不利价格的交易轻松地耗尽合约。例如,这可以通过价格操纵来完成。攻击者从中获利,从而造成价格影响。
为了利用这个合约,攻击者需要 USDC 和 DAI。因此,我们为 CTF 玩家提供的测试网应为主网的分叉,以便为他们提供通常的 DeFi 设施(Uniswap、WETH 等)。
我们的挑战合约(Chal.sol)由一个设置合约进行部署和设置。以下代码可以为我们链上合约如何与 CTF 框架相连接提供一个思路,因此请不要忽略它:
pragma solidity ^0.8.13;
import "./Chal.sol";
contract Setup {
Chal public immutable TARGET; // 玩家将攻击的合约
// 硬编码常量(与主网相同)
address private constant UNISWAP_V2_ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D;
address private constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address private constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
address private constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
IUniswapV2Router private router = IUniswapV2Router(UNISWAP_V2_ROUTER);
IERC20 private dai = IERC20(DAI);
IERC20 private usdc = IERC20(USDC);
IWETH9 private weth = IWETH9(WETH);
// 用于跟踪玩家是否已解决挑战
uint private initialBalance;
constructor() payable {
// 部署受害者合约
TARGET = new Chal();
// 我们的设置合约将由我们的 CTF 框架中的设置脚本调用,并提供 100 个以太。
// 仔细检查是否提供了 100 个以太。
//
// 这也让 CTF 玩家了解设置合约调用时传入的内容,从而消除了猜测问题。
//
// 我们不会与玩家共享设置脚本,但我们会将该设置合约作为挑战分发的一部分提供给他们。
require(msg.value == 100 ether);
// 我们希望_INITIALLY_ 在保险库中持有一些 DAI 和 USDC,供玩家盗取。
// 使用各种 DeFi 原语将 100 个以太转换为 USDC 和 DAI。
weth.deposit{ value: 100 ether }();
weth.approve(address(router), type(uint256).max);
dai.approve(address(router), type(uint256).max);
usdc.approve(address(router), type(uint256).max);
address[] memory path;
path = new address[](2);
// 在 uniswap 上用一半的初始 eth 交换为 usdc
path[0] = WETH; path[1] = USDC;
amounts = router.swapExactTokensForTokens(
50 ether, 0, path, address(TARGET), block.timestamp
);
// 在 uniswap 上用一半的初始 eth 交换为 dai
path[0] = WETH; path[1] = DAI;
amounts = router.swapExactTokensForTokens(
50 ether, 0, path, address(TARGET), block.timestamp
);
initialBalance = curTargetBalance();
}
// 辅助函数
function curTargetBalance() public view returns (uint) {
return usdc.balanceOf(address(TARGET)) + dai.balanceOf(address(TARGET))/10e12;
}
// 我们的挑战在 CTF 框架中将调用此函数来检查玩家是否已解决挑战。
function isSolved() public view returns (bool) {
return curTargetBalance() < (initialBalance / 10);
}
}
在开发挑战的智能合约时,我使用了 Foundry↗ 进行本地测试。很容易通过只需进行 Foundry 测试来测试我自己对挑战的解决方案。
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../src/Exploit.sol";
contract CSAWTest is Test {
Setup chal;
function setUp() public {
chal = new Setup{value: 100 ether}();
}
function testExploit() public {
Exploit exploit = new Exploit{value: 100 ether}(chal);
require(chal.isSolved());
}
}
首先,我只是在 DigitalOcean 创建了一个新的虚拟服务器(对我来说,是一个新的滴水)。我超额配置了服务器,因为便宜(只会持续 48 小时),并且意味着我不必太担心玩家过载它。
我还在 Quicknode 中创建了一个新项目,以便拥有可以使用的 RPC 端点。Alchemy/Infura 等也可以。
在这所有过程中,我都是以 root 身份完成的。我不在乎,因为这只是一个单用途的虚拟机,并将在 CTF 结束后被丢弃。
根据 Paradigm CTF 2022\ repo↗ 中的说明,安装依赖项:
每个挑战都基于 这个 Docker\ image。克隆这个 repo,并将该目录拿走。
$ git clone https://github.com/paradigmxyz/paradigm-ctf-infrastructure
$ mv paradigm-ctf-infrastructure/images/eth-challenge-base my-chal
$ cd my-chal
将以下内容添加到 Dockerfile 的末尾(取自 Paradigm\ CTF 中的一个挑战\ ↗):
COPY deploy/ /home/ctf/
COPY contracts /tmp/contracts
RUN true \
&& cd /tmp \
&& /root/.foundry/bin/forge build --out /home/ctf/compiled \
&& rm -rf /tmp/contracts \
&& true
创建一个名为 contracts 的目录。你的合约将在此目录中存放。
$ mkdir contracts
还要创建一个名为 deploy 的目录,并将该内容存放在 deploy/chal.py 中。这是你的设置脚本,用于部署你的设置合约。
import json
from pathlib import Path
import eth_sandbox
from web3 import Web3
def deploy(web3: Web3, deployer_address: str, player_address: str) -> str:
rcpt = eth_sandbox.sendTransaction(web3, {
"from": deployer_address,
"value": Web3.toWei(100, 'ether'), # 我们的设置合约期望 100 个以太。因此让我们给它 100 个以太。
"data": json.loads(Path("compiled/Setup.sol/Setup.json").read_text())["bytecode"]["object"],
})
return rcpt.contractAddress
eth_sandbox.run_launcher([\
eth_sandbox.new_launch_instance_action(deploy),\
eth_sandbox.new_kill_instance_action(),\
eth_sandbox.new_get_flag_action() # 此实现按 Setup 合约调用 isSolved()\
])
现在你可以这样构建 Docker 镜像:
$ docker buildx build --platform linux/amd64 -t mytag .
注意 mytag 可以是你想要的任何名称。并且 ' .' 只是指当前 Dockerfile 所在的目录。
现在,你可以用以下命令运行用于我们挑战的 Docker 镜像:
## 根据需要进行调整
IMAGE=mytag
PORT=31337
HTTP_PORT=8545
exec docker run \
-e "PORT=$PORT" \
-e "HTTP_PORT=$HTTP_PORT" \
-e "ETH_RPC_URL=$ETH_RPC_URL" \
-e "FLAG=$FLAG" \
-e "PUBLIC_IP=$PUBLIC_IP" \
-p "$PORT:$PORT" \
-p "$HTTP_PORT:$HTTP_PORT" \
"$IMAGE"
现在你应该有一个可以工作的 CTF 挑战。你可以通过 netcat 连接到挑战正在监听的端口(在这个例子中,端口 31337),你应该会被 CTF 框架迎接。
设置环境变量 SECRET_KEY 为随机、私密值非常重要。否则,参与者可以直接向 Anvil 实例进行 RPC 调用,破解挑战。有关更多信息,请查看 server.py 和 auth.py 的实现。
感谢 rkm0959↗ 的 Super Guesser 指出这一点!
所有优秀的 CTF 都必须通过真实解决脚本对其挑战进行健康检查。这对于质量控制和 CTF 期间的存活性极为重要。
我们可以使用 Forge 测试我们实时的远程环境,用于测试攻击合约:
RPC_URL="http://1.3.3.7:8545/f2e36d63-78fa-4e55-9319-ac072868497d" \
PRIVATE_KEY="0xf42b8f8e5cbb128b54327182c5399c01dc90a0349239a86963aa7c94a2e1c4db" \
SETUP_CONTRACT="0xa56B24969e7f742e4EF721d5FD647896F0758A48" \
forge create Exploit.sol:Exploit --rpc-url $RPC_URL --private-key $PRIVATE_KEY --constructor-args $SETUP_CONTRACT --value 100ether
请注意,这并未实施与启动的 netcat 服务器的任何交互(即,监听在端口 31337 上的那个)。理想情况下,你还应该有一个脚本来与其通信,启动新的实例并解析通信。
在 Paradigm CTF 中,他们使用“票据”系统来防止滥用/DoS。他们通过为 CTF 网站(即 ctf.paradigm.xyz、CTFd、等等)中的每个帐户分配一个票据来实现。然后,每个票据一次只能部署一个区块链实例。这意味着你需要为 CTF 注册多个帐户,可能需要通过类似 Captcha 的方式进行。
将其连接到 CTF 框架的方法是调用某个网络端点以检查票据是否有效。该代码位于 eth_sandbox/launcher.py 中:
def check_ticket(ticket: str) -> Ticket:
if ENV == "dev":
return Ticket(challenge_id=CHALLENGE_ID, team_id="team")
ticket_info = requests.get(
f"https://us-central1-paradigm-ctf-2022.cloudfunctions.net/checkTicket?ticket={ticket}"
).json()
if ticket_info["status"] != "VALID":
return None
return Ticket(
challenge_id=ticket_info["challengeId"], team_id=ticket_info["teamId"]
)
因此,你需要将此硬编码 URL 替换为你自己的端点。
对于我来说,我不想费心,所以我完全用一些通用的 CTF PoW 替换了这个功能。这纯粹是作为一个说明性的例子:
def check_ticket(ticket: str) -> Ticket:
if len(ticket) > 100 or len(ticket) < 8:
print('无效的票据长度')
return None
if not all(c in 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' for c in ticket):
print('票据必须是字母数字')
return None
m = hashlib.sha256()
m.update(ticket.encode('ascii'))
digest1 = m.digest()
m = hashlib.sha256()
m.update(digest1 + ticket.encode('ascii'))
if not m.hexdigest().startswith('0000000'):
print('PoW:sha256(sha256(票据) + 票据) 必须以 0000000 开头')
print('(摘要是 ' + m.hexdigest() + ')')
return None
print('此票据是你的团队密钥。请不要分享!')
return Ticket(challenge_id=CHALLENGE_ID, team_id=ticket)
请注意,这个 PoW 没有随机数。这样玩家只需解决 PoW 一次,之后可以重复使用他们的秘密票据,这样可以提升用户体验。在这个基本方案中,没有什么可以阻止玩家并行处理多个 PoW 解决器,从而获得大量有效票据以进行 DoS 攻击。
如果你想的话,可以攻击一些附加的带内注册/速率限制机制(按 IP 地址,Captcha 等)。我不觉得需要,所以这留给读者作为练习。
在这篇博文中,我们探讨了 Paradigm 的 CTF 框架如何轻松托管智能合约 CTF 挑战。我们希望这能帮助任何制作 CTF 挑战的人!
最后,本文中使用的所有代码均可在 这里↗ 公开获取。
Zellic 专注于保护新兴技术。我们的安全研究人员在最有价值的目标中发现了漏洞,从财富 500 强到 DeFi 巨头。
开发人员、创始人和投资者信任我们的安全评估,以快速、自信地发布,且没有关键漏洞。凭借我们在实际攻击安全研究方面的背景,我们发现了其他人所遗漏的内容。
联系我们↗ 进行一次比其他审计更好的审计。真正的审计,而不是橡皮图章。
- 原文链接: zellic.io/blog/how-to-cr...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!