Table of Contents

    前回の記事では、bitcoindのテストベッドを設定する方法をご紹介しました。そのときは、2つのコンテナ、fleurとviktorを作成し、2つのbitcoindインスタンス間の通信を設定しました。

    この記事では、ビットコイン・ネットワーク・プロトコルのデータモデルを作成し、Defensics® SDKを用いてbitcoindに対してファジングを実行する方法を説明します。

    ソース:

    ビットコインメッセージのヘッダーモデル

    私はまず、ピアが自分自身をアナウンスするために別のピアに送信する最初のメッセージであるバージョンメッセージからファジングを始めました。前回の記事のWiresharkのイメージでご紹介したように、ピアがバージョンメッセージを送信すると、応答としてバージョンメッセージ(version)とverackが返されます。

    ビットコイン・ネットワーク・プロトコルについては、次のWebサイトで詳しく説明されています。
    https://bitcoin.org/en/developer-reference#p2p-network

    各ビットコインメッセージには、次の4つのフィールドで構成される共通ヘッダーがあります。

    • ネットワーク(mainnet、testnet、regtest)を識別するマジックナンバー
    • コマンド文字列
    • ペイロード長
    • ペイロードチェックサム

    ペイロード長とチェックサムは、メッセージペイロードに基づいて計算されます。

    次に、Defensics SDK BNF形式でビットコインヘッダーを表す1つの方法を示します。

    octet = 0x00-0xff

    command-chooser = !corr:target ( # Message name

        .version-name: ‘version’ 5(octet) 

      | .verack-name: ‘verack’ 6(octet)

      | .any: 12(octet)  

    )

    bitcoin-header = (

        .magic: 0xfabfb5da # Magic number for regtest 

        .command: command-chooser

        .size: !length32:target 0x00000000-0xffffffff 

        .checksum: !sha256x2:target 0x00000000-0xffffffff

    ビットコインヘッダーの基本構造はマジックナンバー、コマンド、長さ、チェックサムであることがわかります。また、長さとチェックサムには具体的なルールがあります。

    ルールはJavaコードで設定しますが、ここで指定している内容(!length32:targetなど)は、length32という名前のルールがあり、結果がヘッダーのサイズフィールドに格納されることを示しています。

    ヘッダー内のコマンド文字列を後で対応するメッセージペイロードと照合するために相関ルールを使用します。ここでは、command-chooserを使用して、使用可能なメッセージコマンドの1つを選択します。versionとverackが指定されていますが、ビットコインプロトコルのより詳細なモデルでは、指定する項目を簡単に増やすことができます。

    相関ルールを使用して、次に示すように、メッセージコマンドごとに1つずつ、数多いペイロード選択肢のうち1つを選択します。

    bitcoin-payload = !corr:source (

        .version-payload: version-payload 

      | .verack-payload: verack-payload

      | .any-payload: any-payload 

    )

    (未定義のルールを使用して)これを設定すると、完全なビットコインメッセージの一般的な定義が設定されます。

    bitcoin-message = @corr @length32 @sha256x2 ( 

      .header: bitcoin-header

      .payload: !length32:source !sha256x2:source bitcoin-payload 

    )

    @corr @length32 @sha256x2の指定は、3つの名前付きルールを使用していることを示し、そのソースとターゲットの指定は、ルールが適用される場所を示します。

    後で、ルールの定義方法を見てみましょう。

    ビットコインのバージョン・メッセージ・モデル

    バージョン・メッセージ・ペイロードの定義も単純ですが、ここでは各フィールドについての詳細には触れません。たとえば、各フィールドに対してより具体的なモデルを作成し、より具体的な変則を適用すれば、ターゲットの徹底したテストに役立ちます。

    version-payload = (

      .version: (0x7f110100 | 4(octet))

      .services: (0x0904000000000000 | 8(octet))

      .timestamp: (0x6ff27d5f00000000 | 8(octet)) 

      .addr-recvservices: (0x0100000000000000 | 8(octet))

      .addr-recvipaddress: (0x00000000000000000000000000000000 | 16(octet))

      .addr-recvport: 0x0000 – 0xffff

      .addr-transservices: (0x0904000000000000 | 8(octet)) 

      .addr-transipaddress: (0x00000000000000000000000000000000 | 16(octet))

      .addr-transport: 0x0000 – 0xffff

      .nonce: (0xcf7990b352cb105e | 8(octet))

      .user-agentbytes: (0x10 | 0x00-0xff) 

      .user-agent: (‘/Satoshi:0.20.1/’ | 0..255(octet))

      .start-height: (0x65000000 | 4(octet))

      .relay: (0x01 | octet)

    )

    Defensics SDKにモデルを取り込む

    BNFで規定された定義は、Defensics SDKに簡単に取り込むことができます。たとえば、すべてのBNFをresources/model.bnf ファイルに定義するとします。テストスイートでは、これらの定義の取り込みは簡単です。

    public void build(BuilderTools tools) throws Exception {

        ElementFactory factory = tools.factory();

        // ルールの設定…

        factory.readTypes(tools.resources().getPathToResource(“model.bnf”));

    定義が取り込まれると、ヘッダーでコマンド名を選択することで、特定のビットコインメッセージを組み立てることができます。関連するペイロードの選択は相関ルールで行います。たとえば、バージョンメッセージを作成してメッセージシーケンスで使用できるようにする方法を次に示します。

    MessageElement version = tools.factory().getType(“bitcoin-message”);

        version.find().mandatory(“version-name”).element().select();

        tools.messages().message(“version”, version).finish();

    課題に対処する

    今のところ、これらのモデリングはいずれも注目に値するテストケースになっていません。特に、ペイロードサイズ・フィールドとペイロードチェックサム・フィールドが正確ではありません。これはおそらく、これらのフィールドがbitcoindで検査される最初のフィールドであることが原因で、該当のテストケースはすぐに破棄されます。

    信頼に足るテストケースで最大限に適確なテストを行うには、サイズフィールドとチェックサムフィールドが正確である必要があります。

    Defensics SDKでは、特定のフィールドが特定の方法で動作する必要があるケースにはルールが使用されます。

    相関と長さは最も単純なルールで、Defensics SDKに組み込まれているルールで実現できます。定義は、BNF定義がreadTypes()で読み込まれる前に行われます。

    RuleFactory rf = tools.rule();

    rf.correlate(“corr”);

    rf.length(“length32”).format(“int-lsb-32bit”);

    最後の行では、length32という名前の長さルールを作成し、結果を32ビット整数としてフォーマットし、最下位ビットを先頭にします。

    チェックサムは組み込みルールで対処できないため、もう少し難しくなります。ビットコインプロトコルのチェックサムは、ペイロードのSHA256ダイジェスト値のSHA256ダイジェスト値の最下位4バイトです。これはタイプミスではありません。まず、ペイロードのSHA256ダイジェスト値を計算します。その後、そのダイジェスト値のSHA256を計算します。次に、結果の最下位4バイトを受け取り、チェックサムに使用します。

    以下のようにDefensics SDKでカスタムルールを定義しました。

    package com.example.sdk; 

    import java.security.*;

    import java.util.Arrays;

    import com.synopsys.defensics.api.message.*;

    import com.synopsys.defensics.api.message.rule.CustomChecksum;

    public class SHA256x2 implements CustomChecksum {

      @Override

      public byte[] calculate(SDKEngine engine, byte[] data) {

        MessageDigest mDigest; 

        try {

          mDigest = MessageDigest.getInstance(“SHA-256”);

        } catch (NoSuchAlgorithmException e) {

          throw new IllegalStateException(e); 

        }

        byte[] shaTheFirst = mDigest.digest(data);

        byte[] shaTheSecond = mDigest.digest(shaTheFirst);

        return Arrays.copyOfRange(shaTheSecond, 0, 4); 

      }

    }

    このルールを組み込むには、カスタムルールをインスタンス化する必要があります。

    rf.checksum(“sha256x2”, new SHA256x2());

    この場合も、sha256x2ルールの定義はBNFが読み込まれる前に行う必要があります。

    ここでは実際にジェネレーショナルファジングの値を利用しています。ペイロードの一部が認識できないほど変則的な場合でも、データモデルで定義したルールによって、ヘッダーサイズとチェックサムのフィールドが正しく設定されます。bitcoindターゲットに配信されたテストケースは精査され、サイズとチェックサムの検証が完了した後、さらに解析コードに渡されます。

    全体を合わせる

    この記事では、ソースコード全体ではなく、特に重要な部分のみを紹介しました。ソースコード全体が揃ったら、Defensicsでテストスイートを読み込み、他のDefensics テストスイートと同様に使用することができます。

    Defensicsがテストベッドの仮想マシンと同じネットワーク上にある場合、ターゲットのIPアドレスとポート番号をDefensicsに通知するだけです。ポート18444を使用すると、fleurコンテナにマッピングされます。

    デフォルトでは、Defensicsは暗黙的なTCPインストルメンテーションを使用します。つまり、TCPポートを開き続けることができる限り、ターゲットは正常であると仮定します。bitcoindを停止すれば、Defensicsはポートを開くことができなくなり、エラーフラグが設定されます。

    テスト中に詳細情報が必要になった場合は、次のコマンドを使用してbitcoindのデバッグログへの出力を確認します。ファジングテストと同様に、通常はメッセージの組み合わせが表示されます。

    bitcoindは受け取ったテストケースを通知する場合もあれば、苦情を申し立てる場合もあります。ログの監視は、テストケースがターゲットによって受信され、処理されていることを確認するのに適した方法です。

    root@fleur:~# tail -f ~/.bitcoin/regtest/debug.log

    2020-11-10T14:34:34Z connection from 172.17.0.1:56228 accepted 

    2020-11-10T14:34:34Z received: version (87 bytes) peer=2088

    2020-11-10T14:34:34Z ProcessMessages(version, 87 bytes): Exception ‘CDataStream::read(): end of data: iostream error’ (NSt8ios_base7failureB5cxx11E) caught 

    2020-11-10T14:34:34Z ProcessMessages(version, 87 bytes) FAILED peer=2088

    2020-11-10T14:34:34Z socket closed for peer=2088 

    2020-11-10T14:34:34Z disconnecting peer=2088

    2020-11-10T14:34:34Z Cleared nodestate for peer=2088 

    最大限に効果的なテストを行うには、-whitelistオプションを使用してbitcoindに組み込まれている保護を無効にする必要があります。

    今後の指針

    Defensics SDKを利用したビットコインプロトコルのファジングを楽しんでいただければ幸いです。Defensics SDKがあらゆる種類のソフトウェアに対してジェネレーショナルファジングの威力を発揮するしくみをご紹介しました。

    bitcoindの個別のケースでは、このテストをさらに次のように発展させることができます。

    • データモデルを改善する。たとえば、バージョンメッセージ内のフィールドは無作為に指定されますが、プロトコル仕様を使用すれば、より「実態」に近付けることができます。
    • ビットコインプロトコルで他のメッセージをモデル化し、テストする。
    • 故障監視を改善する。たとえば、ASANを使用してbitcoindターゲットを実行し、メモリーエラーを捕捉するようにすると、DefensicsのAgent Instrumentation Frameworkとうまく連携します。

    謝辞

    本稿を見直し、コードを見事に改善してくれたDefensics R&DチームのAleksis KauppinenとJanne Ruotsalainenに感謝します。

    ビットコインのファジングの詳細に興味がある方は