ソフトウェア・インテグリティ

 

Defensics SDKを用いてシリアルポート・ファザーを構築する方法

Defensics SDKを利用すれば、カスタム・プロトコルのファジングテストが可能になります。以下では、Defensics SDK APIを使用してカスタム・インジェクタを作成する方法をご紹介します。

SerialPortFuzzer.jpg

ファジングテストは決して悪い発想ではありません。不正な入力や予期しない入力で実装をテストしていなければ、第三者がシステムを実行するだけで弱点を悪用できる可能性があります。ファジングテスト(またはファジング)を行うことで、潜在的なセキュリティ上の問題が発見されるとともに、システムの全体的な堅牢性が向上する場合もあります。

Synopsys Defensics®は、プロトコルの詳細なモデルに基づいてテストケースを作成するジェネレーショナル・ファザーです。Defensicsには、一般的なネットワーク・プロトコルおよびファイル・プロトコルのファジングテスト・ソリューションとしてすぐに使える、250を超える事前構築済みのテスト・スイートが搭載されています。テスト・スイートの広範なリストに含まれていないプロトコルの実装は安全であるとはいえません。

最近のアプリケーションは、通常、1つのシステムとして連携して動作するさまざまなコードベースで構成されています。たとえば、1つのアプリケーションに組み込みのIoTコード、モバイル・アプリ・コード、サーバー側のAPIとシステムが含まれる場合があり、これらすべてが悪意のあるアクターにとって広範なアタックサーフェスとなります。悪意のあるアクターがIoTボード上のセンサーのはんだを除去して配線し、センサー通信プロトコルをファジングすることで、不正な入力によってホスト・プロセッサ上でコードを実行できるかどうかを調べ、不正な形式のデータでサーバー側のAPIが呼び出されるかどうかを確認する可能性があります。

Defensics SDKの概要

Defensics SDKを使用すると、一般的でない、カスタマイズした、または独自開発のプロトコルおよびファイル形式パーサー用のテスト・スイートを開発できます。Defensics SDKで作成されたテスト・スイートは、すべてのビルド済みテスト・スイートと同様にDefensics GUIに表示されます。必要な作業はデータモデルの指定だけです。データモデルは、プロトコルを機械で読み取り可能な形で表現したものです。Defensicsはデータモデルを使用して送信メッセージを作成し、受信メッセージを解析します。そのため、すべてのテストケースはデータモデルとメッセージ・シーケンスに基づき、Defensicsのジェネレーショナル・テストケース・エンジンによって自動生成されます。

Defensics SDKのセットアップ方法およびデータモデルの作成に便利なSDK PCAPウィザードの使い方の詳細については、Defensics SDKのセットアップ方法と使用方法に関するチュートリアルをご覧ください。

Defensics SDKの機能に関する次の記事も参考にしてください。

インジェクタの役割

インジェクタは、テスト・ターゲットにテストケースを配信する役割を果たします。インジェクタは、配信チャネルの初期化を実行し、チャネルからのデータを送受信し、テストケースの終了時に配信チャネルを閉じます。

Defensics SDKには、ファイルをエクスポートするためのTCP/IP、HTTP、TLS、UDP、SCTP、WebSocket、イーサネット、GATT、RFCOMM用インジェクタが組み込まれています。そのため、プロトコルがTCP/IP接続で実行されている場合は、組み込みのTCP/IPインジェクタを使用できるように構成するだけで済みます。

次の例は、組み込みのTCPインジェクタを初期化してテスト・シーケンスで使用する方法を示しています。

Injector io = tools.injector().tcp(“127.0.0.1”, 1234);

tools.setSequence(

  io.send(myProtocolReq),

  io.receive(myProtocolResp));

 

TCPインジェクタの最初のパラメータはターゲット・ホスト・アドレスで、2番目のパラメータはターゲット・ポートです。インジェクタは、TCPソケットからメッセージを送受信するために使用されます。また、Defensics SDKはTCPソケットを介して通信する特殊なプロトコル用に、組み込みインジェクタをカスタマイズできるカスタムTCPインジェクタ・チャネルを備えています。

テスト・ターゲットがDefensics SDKの組み込みインジェクタをサポートしていない場合、次の2つの選択肢があります。

  1. Linuxでsocatなどのツールを使用して、組み込みのインジェクタ・トラフィックをトンネリングする
  2. Defensics SDK APIを使用してカスタム・インジェクタを作成する

2番目の選択肢の例として、この記事ではシリアルポート・インジェクタを使用してプロトコル・テスト・スイートを作成する方法を紹介します。

プロジェクトのセットアップ

組み込み機器では、オンボード・チップ間またはホストPCとボード間の通信の一般的なインターフェイスはUART/SPI/I2Cです。Defensicsテスト・スイートを実行するPCでは、組み込みシステム・ソフトウェア開発の一般的なデバッグ・ツールであるUSB UARTケーブル(USBシリアルポート・ケーブルとも呼ばれる)を使用できます。ケーブルがPCに接続されている場合、オペレーティング・システムのシリアルポートとして表示されます。

Defensics SDKの主なプログラミング言語はJavaです。インジェクタ・メソッドを実装するJavaライブラリがある場合は、ライブラリをテスト・スイートにバンドルするだけです。シリアルポートの場合、JavaライブラリのjSerialComm(https://github.com/Fazecast/jSerialComm)でプラットフォームに依存しないシリアルポート・アクセスが可能になります。別の言語で書かれたインジェクタ・ライブラリがある場合は、テスト・スイートのJavaコードとネイティブ・コード間のJavaネイティブ・インターフェイス・マッピングも作成する必要があります。

プロジェクトは、ホストPC上でWindows 10を実行し、USB UARTケーブルで接続されたRaspberry Piコンソールをテストするようにセットアップしています。この例では、テスト・スイートはシリアル通信自体ではなく、シリアル接続の背後にあるプロトコルをファジングしています。

Defensics SDKの作業環境とプロジェクトは、SDK配布パッケージにある開発者ガイドに従って設定されています。このパッケージには、このサンプル・プロジェクトの完全なソースコードと他のSDKのサンプルも含まれています。

インジェクタの初期化

外部ライブラリを使用する場合は、最初にテスト・スイート・プロジェクトにライブラリを追加します。.jarファイルをコピーしてsdk.jarと同じレベルにある<プロジェクトフォルダー>/libフォルダー配下に貼り付け、gradleの依存関係を更新してライブラリを追加します。開発者モードでテストランナーを使用する場合は、テスト・スイートの読み込み時に指定されたクラスパス変数にライブラリを追加する必要があります。それ以外の場合、テストランナーは追加されたライブラリのクラスを意識しません。

図1:開発者モードのテストランナーで外部ライブラリを使用するようにJavaクラスパスを拡張する

Javaクラスパスの拡張 | シノプシス

追加したライブラリ(この場合はjSerialComm)のAPIがプロジェクトで使用可能な場合、テストの実行を開始する前に、使用するインジェクタを設定できます。Defensics SDKを使用すれば新しいインジェクタ設定の追加は簡単で、定義された設定は自動的にコマンドラインとDefensicsモニターGUIの両方で使用できるようになります。

デフォルトでは、設定値がDefensics GUIで変更されると、テスト・スイートのデータモデルが再読み込みされます。この機能は、設定値によってテストケースの内容が変更され、データモデルに影響を与える場合に必要です。インジェクタの設定がデータモデルに影響しない場合は、以下のコードに示すように、no-reload(再読み込みなし)の設定を定義できます。no-reloadの設定値はモデルの再読み込みなしで変更できます。

tools.settings()

  .addChoiceSetting(“–data-bits”,

                    “Number of data bits”,

                    tools.settings().createChoice(“7”, “7 bit”),

                    tools.settings().createChoice(“8”, “8 bit”).setDefault())

  .setGroup(GROUP_SERIAL_PORT)

  .noReload();

 

次の例では、データビット選択の設定を作成しています。GUIには、”serial port”という名前の独自の設定グループの下に2つのオプション(7ビット・モードまたは8ビット・モード)を含むドロップダウン・リストの形式で表示されます。デフォルトは8ビット・モードです。コマンドラインから、–data-bits 7パラメーターを使用してデフォルト値を変更できます。

図2: 作成した設定は自動的にDefensics GUIに表示される

DefensicsのGUI | シノプシス

この例を見れば、速度やフロー制御など、あらゆる一般的なシリアルポート構成パラメーターに関する設定が作成されていることがわかります。ホストPC上で使用可能なシリアルポートのリストを作成する場合、使用するライブラリはjSerialCommのみです。

SerialPort[] ports = SerialPort.getCommPorts();

 

これらのポートは選択肢の設定にマッピングされます。

ArrayList<FuzzSettingChoice> portChoices = new ArrayList<>(ports.length);

for (SerialPort port : ports) {

  portChoices.add(

    tools.settings().createChoice(

      port.getSystemPortName().replace(“.”, “-“),

      port.getDescriptivePortName()));

}

tools.settings().addChoiceSetting(“–com-port”,

                                  “Port”,

                                  portChoices.toArray(new FuzzSettingChoice[0]))

                 .setGroup(GROUP_SERIAL_PORT)

          .noReload();

 

インジェクタに必要なすべての設定を定義すれば、配信チャネルを作成できます。

カスタム・インジェクタの作成

カスタム・インジェクタは組み込みインジェクタと同様に動作します。カスタム・インジェクタを作成するには、SDKで定義されたCustomChannelインターフェイスを実装します。インターフェイスは単純です。

void close(InjectorEngine engine) // チャネルを閉じる

void open(InjectorEngine engine)  // チャネルを開く

void receive(InjectorEngine engine, Message message) // チャネルからメッセージを受信する

void send(InjectorEngine engine, Message message) // チャネルにメッセージを送信する

 

これらのメソッドはすべて、テストの実行中に自動的に呼び出されます。

open()メソッドでユーザー設定を読み込んでインジェクタを設定します。たとえば、data-bitsの設定を読み取ってserialPortオブジェクトに割り当てることができます。

String value = getEngine().settings().getSettingValue(“—data-bits”);

serialPort.setNumDataBits(Integer.parseInt(value));

 

設定が完了したら、シリアルポート接続を開くことができます。

Public void open(InjectorEngine engine) throws IOException {

    inBuffer = new ByteArrayOutputStream(INPUT_BUFFER_DEFAULT_SIZE_BYTES);

    initWithUserSettings(engine.getSdkEngine());

    initWithControlSettings(engine);

    if (serialPort != null) {

      if (serialPort.isOpen()) {

        throw new EngineException(

            “Serial Port ( “ + serialPort.getSystemPortName() + “ ) already in use!”);

      }

      serialPort.openPort();

    }

  }

 

テストケースが終了した後、次のopen()の呼び出しを処理できる状態に戻すことによって、シリアルポートを閉じることができます。

  Public void close(InjectorEngine engine) {

    inBuffer.reset();

    if (serialPort != null) {

      serialPort.closePort();

    }

    serialPort = null;

  }

 

open()メソッドとclose()メソッドは、テストの実行を開始するときだけでなく、テストケースごとに呼び出されます。send()receive()の呼び出しは、シーケンスファイルの<send>と<recv>の定義と1対1対応します。

send()メソッドで、メッセージからのデータをインジェクタ・チャネルに設定する必要があります。データはテストケースから取得し、アノマリや大量のオーバーフローまたはアンダーフローが存在する可能性があるため、内容に関する前提を設けずにデータをチャネルに送信する必要があります。データサイズを制限する必要がある場合は、ここで処理するのではなく、モデルを構成して制限を設定してください。TestCaseConfigの例をご覧ください。

public void send(InjectorEngine engine, Message message) throws IOException {

    MessageElement element = message.getRoot();

    byte[] bytes = element.encode();

    if (serialPort.isOpen()) {

      serialPort.writeBytes(bytes, bytes.length);

      if (transmitEnds.length > 0) {

        serialPort.writeBytes(transmitEnds, transmitEnds.length);

      }

    }

  }

 

データ受信の処理はもう少し複雑です。プロトコルによっては、一定のバイト数または特殊な送信終了マークが受信されるまで、データの読み取りをタイムアウトまたは非ブロッキング読み取りでブロックしておくことが可能です。受信したデータは、シーケンス・ファイルのrecvタグの型として定義されたMessageElementと完全に一致する必要があります。データが型と一致しない場合、予期しないメッセージのハンドラが自動的に呼び出されます。予期しないメッセージのハンドラは、順不同で受信できるプロトコル内の一般的なメッセージを処理します。

シリアルポートの例では、データの最初のバイトのブロッキング待機とタイムアウトが指定されています。最初のバイトが受信された後、ユーザー定義の行末文字が受信されるまでデータが読み取られます。

  public void receive(InjectorEngine engine, Message message) throws IOException {

    engine.getSdkEngine().log().out(“CustomChannel receive()”);

    int numRead;

    int endMark = -1;

    byte[] readBuffer = new byte[INPUT_BUFFER_DEFAULT_SIZE_BYTES];

    // 指定の行末文字を受信するまで、または読み取りタイムアウトになるまで読み取る

    do {

      numRead = serialPort.readBytes(readBuffer, readBuffer.length);

      if (numRead > 0) {

        inBuffer.write(readBuffer, 0, numRead);

        if (receiveEnds.length > 0) {

          endMark = indexOf(inBuffer.toByteArray(), receiveEnds);

        }

      }

    } while (numRead > 0 && endMark == -1);

    // 受信データなし

    if (inBuffer.size() == 0) {

      throw new EngineException(“Timeout! No data received.”);

    }

    // 受信したデータを処理する

    byte[] data = inBuffer.toByteArray();

    inBuffer.reset();

    // 送信終了マークが見つかった場合

    if (endMark > 0) {

      message.getRoot().assignData(Arrays.copyOfRange(data, 0, endMark));

      // 受信したデータを終了マークの後に保持する

      if (data.length > (endMark + receiveEnds.length + 1)) {

        inBuffer.write(Arrays.copyOfRange(data, endMark + receiveEnds.length, data.length));

      }

    } else {

      // 終了マークなし。すべてのデータを処理する

      message.getRoot().assignData(data);

    }

  }

実行例

すべてのテスト・スイートの実行は、GUIまたはコマンドラインからテスト・スイートを読み込むときにbuild()で開始します。シリアルポートの例では、メソッド呼び出しの中で、前に示した設定とカスタム・インジェクタを作成します。

  public void build(BuilderTools tools) throws Exception {

    // データモデルを読み取る

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

    // 設定を作成する

    SerialPortSettings serialSettings = new SerialPortSettings(tools);

    // メッセージを作成する

    createMessages(tools);

    // ioを作成する

    CustomInjector io = tools.injector().custom(new SerialPortInjector());

    // テスト・シーケンスを作成する

    tools.buildSequence(io)

        .createSequencesFrom(

            tools.resources().getPathToResource(serialSettings.getSequenceFile().getValue()));

    // 予期しないメッセージのハンドラを作成および設定する

    MessageElement unexpected = tools.buildSequence(io)

        .messagesFromFile(tools.resources()

            .getPathToResource(serialSettings.getUnexpectedSequenceFile().getValue()));

    io.setUnexpectedMessageHandler(unexpected)

        .maxReadMessages(100)

        .debug(true);

    // 帯域幅が制限されているため、最大オーバーフローを制限する

    tools.testCaseConfig().maximumOverflowLength(512); // バイト数

  }

 

テスト・スイートがGUIに表示されるようになったので、ユーザーはGUIの設定からインジェクターを構成できます。正しい設定が選択されていれば、テスト・スイートとターゲット・デバイスの相互運用性をテストできます。相互運用性は、シーケンス・ファイルでテスト・シーケンスとして定義した有効なテストケースで検証されます。

シリアルポートの例では、テストケースの実行ログを確認すれば、カスタム・チャネル・メソッドがどのように呼び出されるかが分かります。

図3:有効なケースとしては、Raspberry Piのシリアル・コンソールにechoコマンドを送信して、その内容を復唱するという方法が考えられます。

Raspberry Piシリアル・コンソールの.png

この有効なケースの例で送信されるデータは”echo Hello World!” コマンドです。このデータモデルでは、コマンドに3つの引数があります。

アノマリ | シノプシスアノマリは、この構造に基づいて自動的に生成されます。一例として、最初のコマンド引数を$PATH環境変数に置き換えています。

アノマリを含むコマンド | シノプシス

有効なケースの応答は想定どおり “Hello World!”です。

アノマリに対する応答 | シノプシス

このサンプルのアノマリに対する応答は、Linuxユーザーにとっては妥当でも、プロトコル・ユーザーにとっては想定外である可能性があります。

シェルコマンドの応答 | シノプシス

ファジングテストを効率化するDefensics SDK

Defensicsはプロトコルのファジングテストに最適な高性能ツールです。カスタム・プロトコルの実装に対して初めてDefensicsを実行したときは、コード内に見つかったエラーに驚かれることでしょう。Defensicsで記述されたカスタム・プロトコル・テスト・スイートはエラーの発見を支援し、シーケンス・エディターを使用すれば、相互運用性テストのための有効なテストケースを手軽に作成できます。

シノプシスのDefensics SDKでは、カスタム配信チャネルを使用している場合でもカスタム・プロトコルのファジングテストが可能です。シリアルポートの例を見れば、カスタム・インジェクタをテスト・スイートに追加することは複雑な作業ではないことがお分かりでしょう。