All Articles

A PART OF ANTI-VIRUS 2【4 章 サンプルプログラムのカスタマイズ】

本章では、2 章と 3 章で解説したサンプルプログラムのカスタマイズを通して、 より AMSI スキャンに対する理解を深めることに挑戦します。

具体的に、本章ではサンプルプログラムに対してそれぞれ以下のカスタマイズを行います。

  • サンプルプロバイダー

    • 要求されたスキャンコンテンツを出力する
    • 簡易的なコンテンツスキャナーを実装し、動作ブロックを行う
  • サンプルプログラム

    • 入力として受け取った文字列をメモリコンテンツとして保存し、AMSI によりスキャンする

なお、カスタマイズ後のサンプルコードについては、下記リポジトリにて公開しております。


https://github.com/kash1064/AMSI-Samples

もくじ

AMSI プロバイダーのカスタマイズ

まずは、3 章で解説した AMSI プロバイダーのサンプルコードをカスタマイズします。

既に解説した通り、AMSI プロバイダーのサンプルは実際には要求されたデータのスキャンは行わず、 すべての要求に対して AMSI_RESULT_NOT_DETECTED を返却します。

そのため、今回はサンプルプロバイダーに簡易的なスキャナーを実装し、 コンテンツを不正と判定する機能を追加していきます。

サンプルプロバイダーにスキャン機能を追加する

3 章で確認した通り、SampleAmsiProvider がスキャンコンテンツが保存されたメモリアドレス(contentAddress)を取得できた場合には、 Scan メソッド内の以下のコード部分にて CalculateBufferXor 関数を呼び出してメモリ内のデータをバイトごとに XOR した結果をイベントに書き込む操作を行います。

// The data to scan is provided in the form of a memory buffer.
auto result = CalculateBufferXor(
                contentAddress, 
                contentSize
              );

TraceLoggingWrite(g_traceLoggingProvider, "Memory xor",
   TraceLoggingValue(requestNumber),
   TraceLoggingValue(result));

今回は、上記のコードをすべてコメントアウトし、 新たに作成した MemoryContentScan 関数を呼び出すように書き換えました。

*result = MemoryContentScan(contentAddress, contentSize, appName.Get());

MemoryContentScan 関数は、以下の通りスキャン対象のデータが含まれるバッファとそのサイズ、 また AMSI スキャン要求を行ったアプリケーションから取得した属性情報である AMSI_ATTRIBUTE_APP_NAME の 3 つを引数に取ります。

この関数の中では、スキャン要求がサンプルプログラム AmsiStream からのものであるか、 もしくは PowerShell からのものであるかを確認し、処理を分岐します。

AMSI_RESULT MemoryContentScan(
  _In_ LPCBYTE buffer, 
  _In_ ULONGLONG size, 
  _In_ PCWSTR appName)
{

    if (wcsncmp(
          appName, 
          L"Contoso Script Engine", 
          wcslen(L"Contoso Script Engine")
        ) == 0) 
    {
        /* サンプルプログラムからのスキャン要求を処理 */
    }
    else if (wcsncmp(
              appName, 
              L"PowerShell",
              wcslen(L"PowerShell")
            ) == 0) 
    {
        /* PowerShell からのスキャン要求を処理 */
    }

    return AMSI_RESULT_NOT_DETECTED;

}

スキャンの要求元がサンプルプログラム AmsiStream である場合には、 バッファ内から取り出したスキャン対象の ASCII 文字列を ETW トレースログとして出力した後、 データ内に Malicious もしくは Clean のいずれかの文字列が含まれるかをスキャンします。

LPSTR ascii_text = (LPSTR)malloc(
                            strlen((const char*)buffer) + 1
                          );
if (!ascii_text) return AMSI_RESULT_NOT_DETECTED;
memcpy(ascii_text, buffer, strlen((const char*)buffer));
ascii_text[strlen((const char*)buffer)] = '\0';

TraceLoggingWrite(
  g_traceLoggingProvider, 
  "Scan Contoso Script Engine Content", 
  TraceLoggingString(ascii_text, "Buffer")
);

static const char malicious[] = "Malicious";
static const char clean[] = "Clean";
size_t len_malicious = strlen(malicious);
size_t len_clean = strlen(clean);

for (size_t i = 0; i <= strlen(ascii_text); i++) {
  if (i <= strlen(ascii_text) - len_malicious) {
    if (memcmp(
          &ascii_text[i], 
          malicious, 
          len_malicious
        ) == 0)
    {
      return AMSI_RESULT_DETECTED;
    }
  }

  if (i <= strlen(ascii_text) - len_clean) {
    if (memcmp(&ascii_text[i], clean, len_clean) == 0) {
      return AMSI_RESULT_CLEAN;
    }
  }
}

ここで、データ内に文字列 Malicious が含まれる場合には、 サンプルプロバイダーはスキャン結果として AMSI_RESULT_DETECTED を返します。

一方で、データ内に文字列 Clean が含まれる場合、 サンプルプロバイダーはスキャン結果として AMSI_RESULT_CLEAN を返します。

また、上記のどちらの文字列も含まれない場合には、AMSI プロバイダーは最終的に AMSI_RESULT_NOT_DETECTED を結果として返します。


もし、スキャンの要求元が PowerShell である場合には、 サンプルプロバイダーは受け取ったスキャンコンテンツを ETW トレースログに出力した後、 Malicious-Script または Clean-Script のいずれかのワイド文字列が含まれるかをスキャンします。

TraceLoggingWrite(
  g_traceLoggingProvider, 
  "Scan PowerShell Content", 
  TraceLoggingCountedWideString(
    (PCWSTR)buffer,
    wcslen((PCWSTR)buffer), 
    "Buffer"
  )
);

static const wchar_t malicious[] = L"Malicious-Script";
static const wchar_t clean[] = L"Clean-Script";
size_t len_malicious = wcslen(malicious);
size_t len_clean = wcslen(clean);

for (size_t i = 0; i <= size; i++) {
  if (i <= size - len_malicious) {
    if (wcsncmp(
         (PCWSTR)&buffer[i],
         malicious, 
         len_malicious
        ) == 0) 
    {
        return AMSI_RESULT_DETECTED;
    }
  }

  if (i <= size - len_clean) {
    if (wcsncmp(
         (PCWSTR)&buffer[i], 
         clean, 
         len_clean
        ) == 0) 
    {
        return AMSI_RESULT_CLEAN;
    }
  }
}

スキャンの結果は要求元がサンプルプログラムの場合と同様に評価され、 ここで、データ内にワイド文字列 Malicious-Script が含まれる場合には AMSI_RESULT_DETECTED が、 ワイド文字列 Clean-Script が含まれる場合には AMSI_RESULT_CLEAN がスキャンの結果として返されます。

サンプルプログラムのカスタマイズ

次に、2 章で解説したサンプルプログラム AmsiStream に任意の文字列を入力する機能を追加します。

サンプルプログラムに入力機能を追加する

2 章で解説した通り、サンプルプログラム AmsiStream では実行時にコマンドライン引数を指定しない場合、 グローバル定数 SampleStream としてハードコードされた文字列を AMSI によりスキャンします。

そこで、まずはグローバル変数 SampleStream を以下のように再定義し、 後で標準入力から受け取った文字列を保存するバッファとして使用します。

#define BUF_SIZE 256
char SampleStream[BUF_SIZE];

次に、ScanArguments 関数内で CAmsiMemoryStream クラスの初期化を行う直前に、 標準入力から受け取った文字列をグローバル変数 SampleStream に書き込むコードを追加します。

// Scan a single memory stream.
wprintf(L"Creating memory stream object\n");
wprintf(L"Please input for memory stream scan\n");

if (fgets(SampleStream, BUF_SIZE, stdin) != NULL) {
  SampleStream[strcspn(SampleStream, "\n")] = '\0';
}

printf("Input text: %s\n", SampleStream);

これにより、標準入力から受け取った任意の文字列をスキャンすることができるようになりました。

カスタマイズしたプログラムを実行する

次に、カスタマイズしたサンプルプログラムとサンプルプロバイダーをそれぞれ再ビルドし、 仮想マシンにインストールすることで動作確認を行います。

もし、すでにサンプルプロバイダーを AMSI プロバイダーとして登録済みの場合は、 事前に以下のコマンドでサンプルプロバイダーのアンロードを行った後に、DLL ファイルを差し替える必要があります。

regsvr32 /u AmsiProvider.dll

再ビルドした AmsiProvider.dll を仮想マシンに配置したら、 3 章と同じく管理者権限で起動したコマンドプロンプトで以下のコマンドを実行し、 更新したサンプルプロバイダーをシステムに登録します。

regsvr32 AmsiProvider.dll

次に、こちらも 3 章と同じ手順で Windows WDK に含まれる traceview.exe を起動し、 サンプルプロバイダーの GUID である {00604c86-2d25-46d6-b814-cd149bfdf0b3} を指定して新しいトレースセッションを開始します。

ETW プロバイダーの GUID を指定する

すべての準備が完了したら、まずは再ビルドしたサンプルプログラム AmsiStream を起動し、 標準入力から受け取った任意の文字列をスキャン対象に指定できること、 また、文字列 Malicious を含むテキストがサンプルプロバイダーにより検知されることを確認できます。

文字列 Malicious のスキャン結果

さらに、traceview.exe で取得した上記のスキャン時の ETW トレースログ上でも、 確かにサンプルプロバイダーが Test: Malicious content という文字列をスキャンしたことを確認できます。

{
  "Buffer": "Test: Malicious content",
  "meta": {
    "provider": "SampleAmsiProvider",
    "event": "Scan Contoso Script Engine Content",
    /* 省略 */
  }
}

続けて、以下の PowerShell コマンドを実行した場合にもサンプルプロバイダーによる検知が行われることを確認します。

$testString = "Sample Provider: " + "Malicious" + "-" + "Script"
Invoke-Expression $testString

上記のコマンドを実行した場合、文字列 Malicious-Script は分割により難読化されているため、 1 行目のコマンド実行はサンプルプロバイダーによる検知は回避されます。

しかし、2 行目の Invoke-Expression $testString にて分割された文字列を結合した上で評価する際には、 サンプルプロバイダーにより不正なワイド文字列 Malicious-Script が検知され、コマンドの実行が AMSI によりブロックされることを確認できます。

PowerShell コマンドをサンプルプロバイダーでブロックする

また、この時にサンプルプロバイダーの出力する ETW トレースログを確認すると、 PowerShell からサンプルプロバイダーが不正なワイド文字列 Malicious-Script を受け取るまでの一連のスキャンコンテンツを追跡できます。

PowerShell コマンドブロック時のトレースログ

4 章のまとめ

本章では、これまでに解説したサンプルコードのカスタマイズを行い、 サンプルプロバイダーとサンプルプログラムに以下の機能を追加しました。

  • サンプルプロバイダー

    • 要求されたスキャンコンテンツを出力する
    • 簡易的なコンテンツスキャナーを実装し、動作ブロックを行う
  • サンプルプログラム

    • 入力として受け取った文字列をメモリコンテンツとして保存し、AMSI によりスキャンする

最後の 5 章では、AMSI が実際のプロダクトにどのように統合されているかについて、 OSS としてソースコードが公開されている PowerShell を例に解説します。

本書のもくじ