All Articles

A PART OF ANTI-VIRUS 2【3 章 AMSI プロバイダー】

本章では、アプリケーションから要求されたスキャンを行うための AMSI プロバイダーについて解説します。

本章で使用するサンプルコードは、https://github.com/microsoft/Windows-classic-samples/releases/tag/MicrosoftDocs-Samples にて iantimalwareprovider-sample.zip として配布されています。


AMSI プロバイダーのサンプルコードも、単一ファイル AmsiProvider.cpp により実装されており、 コード量も 300 行未満と、容易に実装を把握できるようになっています。

また、2 章で扱ったサンプルコードと同じく、ダウンロードしたソリューションファイルを Visual Studio で開くことで、サンプルコードをビルドできるようになります。

もくじ

AmsiProvider の構成要素

本章で扱うサンプルプロバイダー AmsiProvider は、基本的には IAntimalwareProvider インターフェースを継承する SampleAmsiProvider クラスにより実装されており、 IAntimalwareProvider インターフェースに含まれる以下の 3 つのメソッドの実際の動作を定義しています。1

  • Scan メソッド
  • DisplayName メソッド
  • CloseSession メソッド

また、SampleAmsiProvider クラス以外にも、 ビルドした DLL を AMSI プロバイダーとして登録するために必要ないくつかの操作や、 イベントを ETW トレースセッションとして出力するために使用する関数などが実装されています。

サンプルプロバイダーを実行する

今回使用するサンプルプロバイダー SampleAmsiProvider は、 システムに AMSI プロバイダーとして登録された後、 受け取ったスキャン要求に対して以下の操作を行います。

  • スキャンの開始後、取得した AMSI 属性情報を ETW トレースセッションに出力する
  • スキャン対象として受け取ったデータのすべてのバイトを XOR し、その結果を ETW トレースセッションに出力する
  • すべてのスキャン要求に対して AMSI_RESULT_NOT_DETECTED を返却する

本項では、実際にサンプルプロバイダーをビルドし、仮想マシン内で実行することで上記の動作となることを確認します。

サンプルプロバイダーを登録する

まずは、2 章と同じ手順でビルドしたファイルを仮想マシンに配置した後、 管理者権限で起動したコマンドプロンプトで以下のコマンドを実行することで AMSI プロバイダーとして登録します。

regsvr32 AmsiProvider.dll

このコマンドにより、HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\AMSI\Providers の直下に新しく SampleAmsiProvider の ID である {2E5D8A62-77F9-4F7B-A90C-2744820139B2} が登録されました。

SampleAmsiProvider の登録

さらに、HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\{2E5D8A62-77F9-4F7B-A90C-2744820139B2} には、 COM サーバーとして登録されている SampleAmsiProvider の CLSID と DLL へのパスが書き込まれたことを確認できます。

SampleAmsiProvider の登録

サンプルプロバイダーで AMSI スキャン要求を処理する

サンプルプロバイダー SampleAmsiProvider をシステムに登録できたので、 登録した SampleAmsiProvider の動作を確認します。

このサンプルプロバイダーでは、GUID {00604c86-2d25-46d6-b814-cd149bfdf0b3} にて ETW トレースログの出力を行っています。

そのため、この GUID と対応するイベントを取得することで、SampleAmsiProvider の動作を確認できます。


1 章で紹介した通り、ETW トレースログは logman や Xperf などのツールで取得できますが、 本章では Windows WDK に含まれる traceview.exe を使用します。

traceview.exe を使用すると、GUI 上で簡単に ETW トレースログの取得と参照を行うことができるので便利です。

WDK をインストール済みの環境の場合、C:\Program Files (x86)\Windows Kits\10\bin\<バージョン>\x64\traceview.exe を実行することで traceview.exe を起動できます。2


traceview.exe を起動したら、SampleAmsiProvider のイベントトレースを取得するため、 GUID {00604c86-2d25-46d6-b814-cd149bfdf0b3} を指定して新しいトレースセッションを開始します。

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

この GUID は、SampleAmsiProvider のコード内で以下の通り定義されています。

// Define a trace logging provider: 
// 00604c86-2d25-46d6-b814-cd149bfdf0b3
TRACELOGGING_DEFINE_PROVIDER(
  g_traceLoggingProvider, "SampleAmsiProvider",
  (0x00604c86, 0x2d25, 0x46d6, 0xb8, 0x14, 0xcd, 0x14, 0x9b, 0xfd, 0xf0, 0xb3)
);

SampleAmsiProvider のイベントトレース取得を開始したら、 2 章で使用したサンプルプログラム AmsiStream を実行して AMSI スキャン要求を行います。

その結果、SampleAmsiProvider による複数のイベント出力が行われることを、Trace View の GUI 上で確認することができます。

SampleAmsiProvider のトレースログを取得する

まず最初に記録されたのは、"Loaded" というテキストのみが含まれるイベントでした。

{
  "meta": {
    "provider": "SampleAmsiProvider",
    "event": "Loaded",
    /* 省略 */
  }
}

続いて、"requestNumber": 1 という情報と共に、テキスト "Scan Start" を含むイベントが記録されていました。

これは、SampleAmsiProvider によりスキャンが開始されたことを示すイベントであり、 requestNumber には SampleAmsiProvider で管理されている、要求されたスキャンの回数を示す情報が記録されます。

{
  "requestNumber": 1,
  "meta": {
    "provider": "SampleAmsiProvider",
    "event": "Scan Start",
    /* 省略 */
  }
}

スキャンが開始すると、スキャン対象の AMSI 属性情報に関するイベントが記録されます。

以下のイベントでは、サンプルプログラム AmsiStream から返された AMSI_ATTRIBUTE_APP_NAMEAMSI_ATTRIBUTE_CONTENT_ADDRESS などの各情報が記録されています。

{
  "requestNumber": 1,
  "App Name": "Contoso Script Engine v3.4.9999.0",
  "Content Name": "Sample content.txt",
  "Content Size": 13,
  "Session": 0,
  "Content Address": "0x7FF79C43FBF8",
  "meta": {
    "provider": "SampleAmsiProvider",
    "event": "Attributes",
    /* 省略 */
  }
}

最後に、AMSI スキャンにより受け取ったデータをバイトごとに XOR した結果を result として出力しています。

{
  "requestNumber": 1,
  "result": 44,
  "meta": {
    "provider": "SampleAmsiProvider",
    "event": "Memory xor",
    /* 省略 */
  }
}

この結果は、確かにサンプルプログラム AmsiStream がメモリスキャン時に使用する文字列 Hello, world の各バイトを XOR した値と一致することを以下の Python スクリプトで確認できます。

scan_conten = "Hello, world"
result = 0

for byte_data in scan_conten:
    result ^= ord(byte_data)

print(result) # 44

これで、サンプルプロバイダー SampleAmsiProvider が AMSI スキャン要求を受け取り、コンテンツの処理を行っていることを確認することができました。

AMSI プロバイダーの実装

サンプルプロバイダー SampleAmsiProvider の動作を確認できたので、詳しい実装を解説していきます。

以下のステップは、システムに登録されたサンプルプロバイダー SampleAmsiProvider の動作を簡略化したものです。

  1. システムに登録されている AMSI プロバイダーの DLL が AMSI によりロードされる
  2. DLLMain 関数内にて ETW プロバイダーの登録や COM コンポーネント初期化の処理を行う
  3. クライアントアプリケーションからの要求を受けて Scan メソッドを実行する
  4. クライアントアプリケーションから AMSI 属性情報を取得し、ETW トレースログとして出力する
  5. クライアントアプリケーションから読み込んだスキャンデータの各バイトを XOR した結果を ETW トレースログ出力する

本章では、IAntimalwareProvider インターフェースを継承する SampleAmsiProvider クラスの実装を中心に、 サンプルプロバイダーの実行コードを追っていくことにします。

なお、amsi.dll にて実装されているインターフェースがシステムに登録されている AMSI プロバイダーの DLL をロードする際の動作については、 公開されているサンプルコードの実装には含まれないため本書では扱いません。

しかし、上記のような動作については、本書でも参考情報として使用している Evading EDR の 10 章で詳しく紹介されているため、 ご興味があれば一読いただくことをおすすめします。3

SampleAmsiProvider クラスの実装

本章で解説するサンプルプロバイダーの中心となる SampleAmsiProvider クラスは以下のコードで実装されています。

class
  DECLSPEC_UUID("2E5D8A62-77F9-4F7B-A90C-2744820139B2")
  SampleAmsiProvider : \
    public RuntimeClass<RuntimeClassFlags<ClassicCom>,
           IAntimalwareProvider,
           FtmBase>
{
public:
  IFACEMETHOD(Scan)(_In_ IAmsiStream* stream,
                    _Out_ AMSI_RESULT* result) override;
  IFACEMETHOD_(void, CloseSession) \
                   (_In_ ULONGLONG session) override;
  IFACEMETHOD(DisplayName) \
                   (_Outptr_ LPWSTR* displayName) override;

private:
  // We assign each Scan request 
  // a unique number for logging purposes.
  LONG m_requestNumber = 0;
};

上記のコードの通り、SampleAmsiProvider クラスは IAntimalwareProvider インターフェースを継承する COM コンポーネントとして定義されており、 IAntimalwareProvider インターフェースに必要な 3 つのメソッド(Scan、CloseSession、DisplayName)をオーバーライドしています。

また、先ほど確認したサンプルプロバイダーの CLSID である {2E5D8A62-77F9-4F7B-A90C-2744820139B2}

なお、プライベートメンバとして定義されている m_requestNumber は、ETW トレースログに requestNumber として記録されるスキャン要求のカウンターとして使用されます。


SampleAmsiProvider クラスでオーバーライドされる Scan メソッドは、IAntimalwareProvider インターフェースの定義通り以下の引数を受け取り、 スキャンの結果(AMSI_RESULT) を返すメソッドです。4

HRESULT SampleAmsiProvider::Scan(
          _In_ IAmsiStream* stream,
          _Out_ AMSI_RESULT* result
        )

このメソッドが呼び出されると、まず始めに InterlockedIncrement 関数により インクリメントされた m_requestNumber の値と共に、スキャンの開始を ETW トレースログに記録します。

LONG requestNumber = InterlockedIncrement(&m_requestNumber);

TraceLoggingWrite(g_traceLoggingProvider,
                  "Scan Start",
                  TraceLoggingValue(requestNumber)
                );

続いて、GetStringAttribute 関数と GetFixedSizeAttribute 関数により、 クライアントアプリケーションから各種 AMSI 属性情報を取得します。

AMSI 属性情報の取得に使用している GetStringAttribute 関数と GetFixedSizeAttribute 関数の実装については後述しますが、 いずれも IAmsiStream インターフェースの GetAttribute メソッドを使用してクライアントアプリケーションから AMSI 属性情報を取得するラッパー関数として動作します。

auto appName = GetStringAttribute(
                 stream,
                 AMSI_ATTRIBUTE_APP_NAME
               );

auto contentName = GetStringAttribute(
                    stream,
                    AMSI_ATTRIBUTE_CONTENT_NAME
                  );

auto contentSize = GetFixedSizeAttribute<ULONGLONG>(
                     stream, 
                     AMSI_ATTRIBUTE_CONTENT_SIZE
                   );

auto session = GetFixedSizeAttribute<ULONGLONG>(
                 stream, 
                 AMSI_ATTRIBUTE_SESSION
               );

auto contentAddress = GetFixedSizeAttribute<PBYTE>(
                        stream,
                        AMSI_ATTRIBUTE_CONTENT_ADDRESS
                      );

上記の処理で取得した AMSI 属性情報は、以下のコード部分にて ETW トレースログとして出力されます。

TraceLoggingWrite(
  g_traceLoggingProvider,"Attributes",
  TraceLoggingValue(requestNumber),
  TraceLoggingWideString(appName.Get(),"App Name"),
  TraceLoggingWideString(contentName.Get(),"Content Name"),
  TraceLoggingUInt64(contentSize,"Content Size"),
  TraceLoggingUInt64(session,"Session"),
  TraceLoggingPointer(contentAddress,"Content Address")
);

続いて、SampleAmsiProvider は要求されたスキャンコンテンツの処理を行います。

この時の動作は、AMSI 属性情報 AMSI_ATTRIBUTE_CONTENT_ADDRESS と対応するスキャンコンテンツが保存されたメモリアドレスを取得できたかどうかによって変化します。

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));

サンプルプロバイダー SampleAmsiProvider の場合は実際のマルウェアスキャンは行わないため、 スキャンコンテンツに対して行われる処理は CalculateBufferXor 関数で実装されています。

以下の通り、CalculateBufferXor 関数は単純にバッファ内のデータを 1 バイトずつ取り出して XOR した結果を返す処理のみを行います。

BYTE CalculateBufferXor(
  _In_ LPCBYTE buffer,
   _In_ ULONGLONG size
) {
  BYTE value = 0;
  for (ULONGLONG i = 0; i < size; i++)
  {
    value ^= buffer[i];
  }
  return value;
}

なお、実際の AMSI プロバイダーの場合は、CalculateBufferXor 関数の代わりにインストールされている アンチマルウェア製品のスキャンエンジンを使用してバッファ内のデータのスキャンを行います。


一方、以下は SampleAmsiProvider がスキャンコンテンツが保存されたメモリアドレス(contentAddress)を取得できなかった場合に実行されるコード部分です。

スキャンコンテンツを CalculateBufferXor 関数で XOR した結果を ETW トレースログに書き込む点については共通ですが、 処理するデータについては IAmsiStream インターフェースの Read メソッドを呼び出すことで取得している点が異なります。

// Provided as a stream. Read it stream a chunk at a time.
BYTE cumulativeXor = 0;
BYTE chunk[1024];
ULONG readSize;

for (ULONGLONG position = 0;
     position < contentSize;
     position += readSize
    ) 
{
  HRESULT hr = stream->Read(
                position, 
                sizeof(chunk), 
                chunk, 
                &readSize
              );

  if (SUCCEEDED(hr))
  {
    cumulativeXor ^= CalculateBufferXor(chunk, readSize);
    TraceLoggingWrite(g_traceLoggingProvider,"Read chunk",
      TraceLoggingValue(requestNumber),
      TraceLoggingValue(position),
      TraceLoggingValue(readSize),
      TraceLoggingValue(cumulativeXor));
  }
  else
  {
    TraceLoggingWrite(g_traceLoggingProvider,"Read failed",
      TraceLoggingValue(requestNumber),
      TraceLoggingValue(position),
      TraceLoggingValue(hr));
    break;
  }
}

Scan メソッドと同じく SampleAmsiProvider でオーバーライドされている DisplayName メソッドは以下の通りシンプルに実装されています。5

このメソッドは、要求に対して AMSI プロバイダー名を返すように実装されています。

HRESULT SampleAmsiProvider::DisplayName(
  _Outptr_ LPWSTR *displayName
) {
  *displayName = const_cast<LPWSTR>(L"Sample AMSI Provider");
  return S_OK;
}

上記のメソッドは、2 章で解説したサンプルプログラムでは以下のように使用されており、 Scan メソッドのアウトプットとして受け取った AMSI プロバイダーの名前の取得を行っています。

PWSTR name;
hr = provider->DisplayName(&name);

if (SUCCEEDED(hr)) {
  wprintf(L"Provider display name: %s\n",
          name
         );
 CoTaskMemFree(name);
}

最後の CloseSession メソッドは、"Close session" という文字列を ETW トレースログに出力するだけのメソッドとして実装されています。

void SampleAmsiProvider::CloseSession(_In_ ULONGLONG session)
{
  TraceLoggingWrite(
    g_traceLoggingProvider,
    "Close session",
    TraceLoggingValue(session)
  );
}

実際には、CloseSession メソッドは指定の AMSI セッションを閉じるためのメソッドとして定義されているため、 プロバイダーの実装によっては何らかのリソースの解放などを担う可能性があります。6

GetStringAttribute 関数の実装

GetStringAttribute 関数は、前項で解説した Scan メソッドの中で、 AMSI_ATTRIBUTE_APP_NAMEAMSI_ATTRIBUTE_CONTENT_NAME の属性情報の取得に使用されていた関数です。

この関数は、以下の通りスキャンメソッドが受け取る IAmsiStream インターフェースのオブジェクトと、 取得したい AMSI 属性情報を指定する値を引数として取ります。

HeapMemPtr<wchar_t> GetStringAttribute(
  _In_ IAmsiStream* stream,
  _In_ AMSI_ATTRIBUTE attribute
)

なお、戻り値として指定されている HeapMemPtr は、ヒープの割り当てと解放を行うためにサンプルプロバイダー内で定義されているクラスです。

HeapMemPtr では、API 関数 HeapAlloc を使用したメモリの割り当てを行います。


GetStringAttribute 関数は、以下の通り実装されています。

HeapMemPtr<wchar_t> result;

ULONG allocSize;
ULONG actualSize;
if (
  stream->GetAttribute(
            attribute, 
            0, 
            nullptr, 
            &allocSize
          ) == E_NOT_SUFFICIENT_BUFFER &&
  SUCCEEDED(result.Alloc(allocSize)) &&
  SUCCEEDED(stream->GetAttribute(
            attribute, 
            allocSize, 
            reinterpret_cast<PBYTE>(result.Get()), 
            &actualSize
           )
          ) && actualSize <= allocSize)
{
  return result;
}
return HeapMemPtr<wchar_t>();

上記のコード内では、要求に必要なメモリサイズを特定するために、 まずあえて dataSize と対応する第 2 引数に 0 を指定した上で GetAttribute メソッドの呼び出しを行い、 その結果が E_NOT_SUFFICIENT_BUFFER エラーとなることを確認しています。

stream->GetAttribute(
         attribute, 
         0, 
         nullptr, 
         &allocSize
       ) == E_NOT_SUFFICIENT_BUFFER

これは、2 章の「図2.10: GetAttribute メソッドのパラメータ情報」に記載の通り、 GetAttribute メソッドが E_NOT_SUFFICIENT_BUFFER を返す場合に、 第 4 引数として受け取る retData に要求に必要なバイト数が書き込まれることを利用しています。


上記のように GetAttribute メソッドで要求に必要なサイズを確認したら、 HeapMemPtr<wchar_t> のオブジェクトである result を使用し、 result.Alloc(allocSize) にて結果を取得するために必要なメモリ領域の割り当てを行います。

その後、再度以下のように GetAttribute メソッドを呼び出すことで、 result.Get() で取得した出力先のバッファにて要求した AMSI 属性情報を受け取ります。

stream->GetAttribute(
          attribute, 
          allocSize, 
          reinterpret_cast<PBYTE>(result.Get()), 
          &actualSize
        )

GetFixedSizeAttribute 関数の実装

前項で解説した GetStringAttribute 関数とは異なり、GetFixedSizeAttribute 関数は AMSI_ATTRIBUTE_CONTENT_SIZEAMSI_ATTRIBUTE_SESSION および AMSI_ATTRIBUTE_CONTENT_ADDRESS のように、取得する情報のサイズが固定されている AMSI 属性情報の取得を行います。

そのため、関数の実装は GetStringAttribute 関数と比較してよりシンプルであり、 単純に GetAttribute メソッドにて指定した属性の値をバッファで受け取る関数となっています。

template<typename T>
T GetFixedSizeAttribute(
  _In_ IAmsiStream* stream,
  _In_ AMSI_ATTRIBUTE attribute
) {
    T result;

    ULONG actualSize;
    if (SUCCEEDED(stream->GetAttribute(
                   attribute, 
                   sizeof(T), 
                   reinterpret_cast<PBYTE>(&result), 
                   &actualSize
                 )
                ) &&
        actualSize == sizeof(T))
    {
        return result;
    }
    return T();
}

3 章のまとめ

本章では、サンプルプロバイダー AmsiProvider が AMSI を使用して 要求されたスキャンコンテンツを受け取り、またそれを処理する仕組みを解説しました。

サンプルプロバイダーの実装から、AMSI を通して要求されたスキャンを行うためには、以下の実装が必要であることがわかりました。

  • システムに AMSI プロバイダー を登録する
  • IAntimalwareProvider インターフェースにて定義された各メソッドをオーバーライドする
  • AMSI を通して Scan 要求を受けた際に、各種 AMSI 属性情報とスキャンコンテンツの取得を行う
  • 取得したスキャンコンテンツに対して、アンチマルウェアスキャンなどの任意の処理行い、結果(AMSI_RESULT) を返す

なお、本章で解説したサンプルプロバイダー AmsiProvider は、すべてのスキャン要求に対して AMSI_RESULT_NOT_DETECTED を返却してしまうため、 実際に作成した AMSI プロバイダーの結果によりコンテンツがブロックされる動作を確認することができませんでした。

そこで、次の 4 章では、本書で解説したサンプルコードをカスタマイズすることで、 自身が登録した AMSI プロバイダーによりコンテンツがブロックされることを確認するとともに、 より AMSI スキャンの動作に対する理解を深めることに挑戦します。

本書のもくじ