环境:Paradigm CTF 2021 使用 Docker 创建隔离的容器,作为各个队伍的的比赛环境,不至互相影响
为了公平,容器内使用 ganache-cli 克隆主链得到私链,比赛在私链上进行,因此队伍间无法看到他人的解题过程
所有题目都是对应智能合约 Setup.sol 的实例,其构造函数执行出题逻辑,公有函数 function solve() public; 测试题目是否成功解答
一道题目可以有多个 setup contract 实例,每次请求出题都会得到重新克隆的私链,多条私链将会共存,因此队伍内成员间可以同时解题,不会影响
每次请求出题,除了返回 setup contract 之外,还会返回 uuid,请求通关时需要输入,表示通关哪个实例
ganache-cli 监听在随机端口上,不与外界互通,因此无法将其作为 Web3 Provider 节点提交解题交易
容器启动时会创建代理服务,默认监听在 8545 端口上,与外界互通,参赛者必须将解题交易提交至代理服务,由其转发给私链
代理服务遵循 Ethereum JSON-RPC Specification,只是请求时 header 中必须包括 X-Auth-Key 字段,作为鉴权;它是个随机数,每个容器唯一,其他队伍无法猜出
pragma solidity 0.5.12;
contract ERC20Like {
function transfer(address dst, uint qty) public returns (bool);
function transferFrom(address src, address dst, uint qty) public returns (bool);
function approve(address dst, uint qty) public returns (bool);
function balanceOf(address who) public view returns (uint);
}
contract TokenModule {
function deposit(ERC20Like token, address from, uint amount) public {
token.transferFrom(from, address(this), amount);
}
function withdraw(ERC20Like token, address to, uint amount) public {
token.transfer(to, amount);
}
}
contract Wallet {
address public owner = msg.sender;
mapping(address => bool) _allowed;
mapping(address => bool) _operators;
modifier onlyOwner {
require(msg.sender == owner);
_;
}
modifier onlyOwnerOrOperators {
require(msg.sender == owner || _operators[msg.sender]);
_;
}
function allowModule(address module) public onlyOwner {
_allowed[module] = true;
}
function disallowModule(address module) public onlyOwner {
_allowed[module] = false;
}
function addOperator(address operator) public onlyOwner {
_operators[owner] = true;
}
function removeOperator(address operator) public onlyOwner {
_operators[owner] = false;
}
function execModule(address module, bytes memory data) public onlyOwnerOrOperators {
require(_allowed[module], "execModule/not-allowed");
(bool ok, bytes memory res) = module.delegatecall(data);
require(ok, string(res));
}
}
pragma solidity 0.5.12;
import "./Wallet.sol";
contract WETH9 is ERC20Like {
function deposit() public payable;
}
contract Setup {
WETH9 public constant WETH = WETH9(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
uint public constant WANT = 50 ether;
Wallet public wallet;
constructor() public payable {
require(msg.value == WANT);
address tokenModule = address(new TokenModule());
wallet = new Wallet();
wallet.allowModule(tokenModule);
WETH.deposit.value(msg.value)();
WETH.approve(address(wallet), uint(-1));
wallet.execModule(tokenModule, abi.encodeWithSelector(TokenModule(0x00).deposit.selector, WETH, address(this), msg.value));
}
function isSolved() public view returns (bool) {
return WETH.balanceOf(address(this)) == WANT;
}
}
审题:使目标合约拥有 50 WETH,提供的帐户始终以5000 ETH开始,如果设置合同有50(W)ETH,则挑战得到解决。
pragma solidity 0.5.12;
import "public/Setup.sol";
contract Exploit {
WETH9 public constant WETH = WETH9(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
constructor(Setup setup) public payable {
WETH.deposit.value(50 ether)();
WETH.transfer(address(setup), 50 ether);
}
}
启发:不一定所有题目都要用到高级的漏洞挖掘技巧,本题考察的还是基本的逻辑能力。