本文へ移動

TECHFUND
Media

Hi AUDIT
2024/4/2

スマートコントラクトをバイトコードからアセンブルバック

著者 Ravikiran Betha

この記事は、スマートコントラクトのコードをバイトコードからリバース・エンジニアリングし、バイトコード内の関数を実行してみるという簡単な演習である。

この記事の主な焦点は、バイトコードの読み解き方について理解を深めることである。Etherscanにデプロイされたコントラクトのバイトコードを見ることから始めることもできるが、それは本質的にCTFのようなものであり、はるかに難しい。

よりシンプルな検証プロセスとして、私が取ったアプローチは、Remix上でSolidityでシンプルなスマートコントラクトを構築し、バイトコードを生成することだ。バイトコードを手に入れたら、Solidityコードの理解をどのように構築し直すかに集中し始める。この練習は、基本的な理解を深め、Etherscanにデプロイされたコントラクトのバイトコードを探索する準備を整えるのに役立つだろう。

堅実な契約

// SPDX-License-Identifier: GPL-3.0

pragma solidity 0.8.25;

contract SimpleContract {
    mapping(address => uint256) public myMap;

    function get(address _addr) public view returns (uint256) {
        return myMap[_addr];
    }

    function set(address _addr, uint256 _i) public {
        myMap[_addr] = _i;
    }

    function remove(address _addr) public {
        delete myMap[_addr];
    }
}

上記のコントラクトは、アドレスに対してuint値を格納するマップを持っている。それぞれマッピングを設定、取得、削除する3つの関数がある。

分解

逆アセンブルとは、16進文字列をもう少し理解しやすいものにデコードするプロセスである。このプロセスでは、16進数の命令を見て、フローを構築し直す。幸運なことに、私たちにはそれを助けるツールがある。デコンパイラ

バイトコードの生成

バイトコードを生成するために、コードをremixのファイルにコピーしてコンパイルした。artifactsの下に、コンパイルの結果としてjsonファイルが生成される。

ファイルの中身を見ると、2つのバイトコードがある。

1) バイトコード

6080604052348015600e575f80fd5b5061031e8061001c5f395ff3fe608060405234801561000f575f80fd5b506004361061004a575f3560e01c806327cbd5291461004e57806329092d0e1461007e5780633825d8281461009a578063c2bc2efc146100b6575b5f80fd5b61006860048036038101906100639190610224565b6100e6565b6040516100759190610267565b60405180910390f35b61009860048036038101906100939190610224565b6100fa565b005b6100b460048036038101906100af91906102aa565b61013c565b005b6100d060048036038101906100cb9190610224565b610181565b6040516100dd9190610267565b60405180910390f35b5f602052805f5260405f205f915090505481565b5f808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f905550565b805f808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f20819055505050565b5f805f8373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f20549050919050565b5f80fd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f6101f3826101ca565b9050919050565b610203816101e9565b811461020d575f80fd5b50565b5f8135905061021e816101fa565b92915050565b5f60208284031215610239576102386101c6565b5b5f61024684828501610210565b91505092915050565b5f819050919050565b6102618161024f565b82525050565b5f60208201905061027a5f830184610258565b92915050565b6102898161024f565b8114610293575f80fd5b50565b5f813590506102a481610280565b92915050565b5f80604083850312156102c0576102bf6101c6565b5b5f6102cd85828601610210565b92505060206102de85828601610296565b915050925092905056fea2646970667358221220e5d9bd33d378443e6559d0b55a41a52833081937ceeefb7aaf24e5ea8862eb9564736f6c63430008190033

2) デプロイ済みバイトコード

608060405234801561000f575f80fd5b506004361061004a575f3560e01c806327cbd5291461004e57806329092d0e1461007e5780633825d8281461009a578063c2bc2efc146100b6575b5f80fd5b61006860048036038101906100639190610224565b6100e6565b6040516100759190610267565b60405180910390f35b61009860048036038101906100939190610224565b6100fa565b005b6100b460048036038101906100af91906102aa565b61013c565b005b6100d060048036038101906100cb9190610224565b610181565b6040516100dd9190610267565b60405180910390f35b5f602052805f5260405f205f915090505481565b5f808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f905550565b805f808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f20819055505050565b5f805f8373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f20549050919050565b5f80fd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f6101f3826101ca565b9050919050565b610203816101e9565b811461020d575f80fd5b50565b5f8135905061021e816101fa565b92915050565b5f60208284031215610239576102386101c6565b5b5f61024684828501610210565b91505092915050565b5f819050919050565b6102618161024f565b82525050565b5f60208201905061027a5f830184610258565b92915050565b6102898161024f565b8114610293575f80fd5b50565b5f813590506102a481610280565b92915050565b5f80604083850312156102c0576102bf6101c6565b5b5f6102cd85828601610210565b92505060206102de85828601610296565b915050925092905056fea2646970667358221220e5d9bd33d378443e6559d0b55a41a52833081937ceeefb7aaf24e5ea8862eb9564736f6c63430008190033

バイトコードは、deployedBytecodeの上にいくつかのコードを追加しているため、deployedBytecodeよりも長い。

この2つのコードを観察すると、バイトコードに追加された命令として、以下のようなものがあることがわかる。

6080604052348015600e575f80fd5b5061031e8061001c5f395ff3fe

上記のバイトコードには、初期コードとデプロイされたバイトコードが含まれている。コンストラクタは一度しか実行されないので、初期コードを保存する必要はなく、したがってそれはdeployedBytecodeの一部ではない。以上から、コントラクトがデプロイされるとバイトコードが送信され、そのうちのdeployedBytecodeだけがブロックチェーンに保存されることがわかる。

初期化コード

[00]

PUSH1

80

[02]

PUSH1

40

[04]

MSTORE

[05]

CALLVALUE

[06]

DUP1

[07]

ISZERO

[08]

PUSH1

0e

[0a]

JUMPI

[0b]

PUSH0

[0c]

DUP1

[0d]

REVERT

[0e]

JUMPDEST

[0f]

POP

[10]

PUSH2

031e

[13]

DUP1

[14]

PUSH2

001c

[17]

PUSH0

[18]

CODECOPY

[19]

PUSH0

[1a]

RETURN

[1b]

INVALID

上記のinitコードでは、コントラクトの初期値が設定され、デプロイされる。

デプロイされたバイトコード

デプロイされたコードはブロックチェーンに保存される。ここで重要なのは、コントラクトのインターフェースは、コントラクトが公開するパブリック関数と外部関数だけで構成されるということだ。プライベート関数や内部関数は見えない。

つまり、EVMがどのようにトランザクションをルーティングするかに焦点を当て、コントラクトのファンクションセレクタを理解する。EVMは常に0番目の命令から開始し、命令を流れる。

命令セットの中から、JUMP、JUMPI、JUMPDEST、RETURN、STOPの各オプコードに注目する必要がある。EVMがコードを実行するとき、例外なくトップダウンで実行する。コードへの他のエントリー・ポイントはない。JUMPとJUMPIが実行するのは、常に先頭からジャンプすることである。JUMPはスタックから最上位の値を取り出し、その場所に実行を移す。移動先の場所にはJUMPDESTオペコードが含まれていなければならない。JUMPIは、条件付きという点で少し異なる。スタックの2番目の項目が0であれば、ジャンプせずに続行する。

STOPはコントラクトの実行を完全に停止し、RETURNは実行を停止するがEVMメモリーからデータを戻す。

関数シグネチャ

逆アセンブルに入る前に、solidityの関数は関数シグネチャに基づいて4バイトにエンコードされていることを理解しましょう。エンコードされた関数はそれぞれ4バイトなので、バイトコードを見て、バイトコードから関数を抽出できるかどうか見てみましょう。

バイトコードをEVM Playgroundに貼り付け、PUSH4命令を探します。

PUSH4のインスタンスは以下の4つ。

27cbd529
29092d0e
3825d828
c2bc2efc

上記の各関数ハッシュの関数シグネチャを取得してみよう。関数シグネチャを取得するには、foundryのキャストツールを使用します。

以下の関数に興味がある。

a) remove(address)

b) set(address, uint256)

c) get(address)

コールデータ

calldataはエンコードされた16進数の塊で、コントラクトのどの関数を呼び出したいのか、その引数やデータについての情報を含んでいる。

1) コールセット機能

以下のパラメータでset関数を呼び出してみよう。

パックされた入力データは、以下の情報でパックされなければならない。

a) エンコードされた関数シグネチャ

set(address, uint256) : 0x3825d828

b) パラメータ

address : 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2

uint256  : 25

セット・ファンクション用にエンコードされたコール・データ・パックを構築する。

0x3825d828000000000000000000000000ab8483f64d9c6d1ecf9b849ae677dd3315835cb20000000000000000000000000000000000000000000000000000000000000019

この値をcall-dataで渡し、プレイグラウンドのEVMストレージの状態遷移を観察する。

ストレージのスロットに19という値が表示されるはずである。19は25の16進数表現であり、正確である。

2) Get 関数を呼び出す

それではget関数を呼び出してみよう。

パックされた入力データは、以下の情報でパックされなければならない。

a) エンコードされた関数シグネチャー

set(address, uint256) : 0xc2bc2efc

b) パラメーター

address: ab8483f64d9c6d1ecf9b849ae677dd3315835cb2

上記と同様に、get関数用にエンコードされたコールデータは以下のようになる。

0xc2bc2efc000000000000000000000000ab8483f64d9c6d1ecf9b849ae677dd3315835cb2

このパックされたコールデータを渡し、EVMプレイグラウンドの状態変化を観察する。

概要:

この演習では、スマート・コントラクトのバイトコードに含まれる機能を読み取る能力にとって重要な、いくつかの重要な概念を学んだ。

a) 生成されるバイトコードは2つあり、1つは主にコンストラクター呼び出しである初期化用、もう1つはデプロイされるバイトコードである。

b) EVMは常に0番目の命令から始まり、JUMP、STOP、REVERT、RETURNを使用して流れる。

c) パブリック関数と外部関数だけがインターフェースの一部であるため、push4オペコードを使ってコントラクトへの潜在的なエントリー・ポイントを特定できる。

d) 関数のシグネチャを見て、calldataを構築することを学びました。

さらに理解を深めるための次のステップは

a) 関数のロジックを理解するために、命令のシーケンスを確認することによって、関数のフローに注釈を付ける。

b) 関数呼び出しのライフサイクルにおいて、メモリポインタ、スタック、メモリ、ストレージがどのように使用されるかに注目する。

Hi AUDIT

Hi AUDITは、ハイスピード・ハイクオリティ・ベストプライスのセキュリティ監査サービスです。WEB3スタートアップ向けの自動監査SaaSとコンサルティングを提供しています。
View More