ファジングテストは決して悪い発想ではありません。不正な入力や予期しない入力で実装をテストしていなければ、第三者がシステムを実行するだけで弱点を悪用できる可能性があります。ファジングテスト(またはファジング)を行うことで、潜在的なセキュリティ上の問題が発見されるとともに、システムの全体的な堅牢性が向上する場合もあります。
Synopsys Defensics®は、プロトコルの詳細なモデルに基づいてテストケースを作成するジェネレーショナルファザーです。Defensicsには、一般的なネットワークプロトコルおよびファイルプロトコルのファジングテスト・ソリューションとしてすぐに使える、250を超える事前構築済みのテストスイートが搭載されています。テストスイートの広範なリストに含まれていないプロトコルの実装は安全であるとはいえません。
最近のアプリケーションは、通常、1つのシステムとして連携して動作するさまざまなコードベースで構成されています。たとえば、1つのアプリケーションに組み込みのIoTコード、モバイルアプリ・コード、サーバー側のAPIとシステムが含まれる場合があり、これらすべてが悪意のあるアクターにとって広範なアタックサーフェスとなります。悪意のあるアクターがIoTボード上のセンサーのはんだを除去して配線し、センサー通信プロトコルをファジングすることで、不正な入力によってホスト・プロセッサ上でコードを実行できるかどうかを調べ、不正な形式のデータでサーバー側のAPIが呼び出されるかどうかを確認する可能性があります。
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つの選択肢があります。
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クラスパスを拡張する
追加したライブラリ(この場合は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に表示される
この例を見れば、速度やフロー制御など、あらゆる一般的なシリアルポート構成パラメーターに関する設定が作成されていることがわかります。ホスト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コマンドを送信して、その内容を復唱するという方法が考えられます。
この有効なケースの例で送信されるデータは”echo Hello World!” コマンドです。このデータモデルでは、コマンドに3つの引数があります。
アノマリは、この構造に基づいて自動的に生成されます。一例として、最初のコマンド引数を$PATH環境変数に置き換えています。
有効なケースの応答は想定どおり “Hello World!”です。
このサンプルのアノマリに対する応答は、Linuxユーザーにとっては妥当でも、プロトコルユーザーにとっては想定外である可能性があります。
Defensicsはプロトコルのファジングテストに最適な高性能ツールです。カスタムプロトコルの実装に対して初めてDefensicsを実行したときは、コード内に見つかったエラーに驚かれることでしょう。Defensicsで記述されたカスタム・プロトコル・テストスイートはエラーの発見を支援し、シーケンスエディターを使用すれば、相互運用性テストのための有効なテストケースを手軽に作成できます。
シノプシスのDefensics SDKでは、カスタム配信チャネルを使用している場合でもカスタム・プロトコルのファジングテストが可能です。シリアルポートの例を見れば、カスタムインジェクタをテストスイートに追加することは複雑な作業ではないことがお分かりでしょう。