All Articles

A PART OF ANTI-VIRUS 2【2 章 AMSI クライアントインターフェース】

本章では、公開されているサンプルコードを使用して、AMSI スキャン要求を行うクライアントアプリケーションの実装について解説します。

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


このサンプルプログラム AmsiStream は、単一ファイル AmsiStream.cpp のみで実装されています。

AmsiStream.cpp は 300 行程度のプログラムですので、比較的容易に実装を把握することができます。

ダウンロードした ZIP ファイルを解凍し、AmsiStream.sln ファイルを Visual Studio で開くことでサンプルコードをビルドできるようになります。

もくじ

AmsiStream の構成要素

今回使用するサンプルプログラム AmsiStream は、 主に以下のクラスにより実装されています。

  • CAmsiStreamBase クラス
  • CStreamScanner クラス
  • CAmsiFileStream クラス
  • CAmsiMemoryStream クラス

以下に、各クラスの概要をまとめます。

CAmsiStreamBase クラス

CAmsiStreamBase クラスは、CAmsiFileStream クラスと CAmsiMemoryStream クラスに継承される基底クラスであり、以下のメソッドが定義されています。

  • SetContentName メソッド
  • BaseGetAttribute メソッド
  • CopyAttribute メソッド

SetContentName メソッドでは、スキャンするファイルの名前などの情報をメモリに保存するメソッドです。

また、BaseGetAttribute メソッドと CopyAttribute メソッドは、 後述する Attribute(属性) と呼ばれる AMSI スキャンにおいて重要な要素を処理する機能を定義しています。

これらのメソッドは、スキャン対象がファイルの場合に使用する CAmsiFileStream クラスと、 スキャン対象がメモリ内のデータの場合に使用する CAmsiMemoryStream クラスの両方で使用されます。

CStreamScanner クラス

CStreamScanner クラスは、amsi.dll で実装されている AMSI の COM オブジェクトを作成し、直接的に AMSI スキャン要求を行う機能を持つクラスです。

このクラスでは、以下の 2 つのメソッドが定義されています。

  • Initialize メソッド
  • ScanStream メソッド

これらのメソッドのうち、ScanStream メソッドが AMSI の Scan メソッドを使用してコンテンツのスキャンを行い、その結果を表示する役割を担っています。

なお、今回使用するサンプルプログラムは PowerShell などのように何らかのコードを実行する機能は持たないため、 AMSI スキャンを実施した結果は、コンソール上に表示されるだけの動作となります。

CAmsiFileStream クラス

CAmsiFileStream クラスは、AmsiStream プログラムにおいてファイルのスキャン要求を行う場合に使用するクラスです。

このクラスは、COM コンポーネントを実装するために使用できる Windows Runtime C++ Template Library(WRL) を利用して実装されています。

このクラスでは、以下の 3 つのメソッドが定義されています。

  • RuntimeClassInitialize メソッド
  • GetAttribute メソッド
  • Read メソッド

これらのメソッドのうち、RuntimeClassInitialize メソッドは MakeAndInitialize 関数を使用して WRL クラスのオブジェクトを初期化するために使用する関数です。1

CAmsiFileStream クラスは、エントリポイントの wmain 関数から直接呼び出される ScanArguments 関数内で MakeAndInitialize 関数により初期化されます。


他の 2 つのメソッドである GetAttribute メソッドと Read メソッドは、 それぞれ IAmsiStream インターフェースのメソッドとして宣言されている処理を実装しています。

IAmsiStream::GetAttribute メソッドはストリームから要求された属性を返す役割を持ち、 IAmsiStream::Read メソッドは AMSI プロバイダーからの要求に対して、指定されたバッファいっぱいのコンテンツを返す役割を持ちます。

CAmsiFileStream クラスの場合、スキャン対象がファイルになるので、 Read メソッドはスキャン対象のファイルから読み出したデータを返すように実装されています。

CAmsiMemoryStream クラス

CAmsiFileStream クラスは、AmsiStream プログラムにおいてメモリコンテンツのスキャン要求を行う場合に使用するクラスです。

このクラスも、COM コンポーネントを実装するために使用できる Windows Runtime C++ Template Library(WRL) を利用して実装されており、 CAmsiFileStream クラスと同じく以下の 3 つのメソッドが定義されています。

  • RuntimeClassInitialize メソッド
  • GetAttribute メソッド
  • Read メソッド

これらのメソッドの用途はいずれも CAmsiFileStream クラスと同じですが、 それぞれファイルではなくメモリコンテンツをスキャンする場合に使用できるように異なる処理が行われます。

例えば、Read メソッドはファイルから読み出したコンテンツではなく、 ハードコードされてサンプルプログラムのメモリ内にロードされているデータを返すように実装されています。

サンプルプログラムを実行する

サンプルプログラムをビルドする

サンプルプログラム AmsiStream の詳しい実装を確認する前に、 まずはサンプルコードをビルドしてプログラムを実行してみることにします。


サンプルコードのビルドのため、ダウンロードした iamsistream-sample.zip を解凍した後、 ソリューションファイル AmsiStream.sln を Visual Studio で開きます。(本書では Visual Studio 2022 を使用します。)

ソリューションファイルの起動時に以下のようなプロジェクトの再ターゲットに関するウインドウが表示された場合は、デフォルトの設定のまま [OK] をクリックして問題ありません。

プロジェクトの再ターゲット


Visual Studio でソリューションファイルを開いたら、画面上部のメニューから [ソリューションのリビルド] をクリックしてサンプルコードをビルドできます。

後でデバッグを行うため、ビルド構成は [Debug] を選択したままにしておきます。

サンプルコードをビルドする

ビルドが正常に完了すると、.\iamsistream-sample\x64\Debug の直下に AmsiStream.exe と AmsiStream.pdb の 2 つのファイルが作成されたことを確認できます。

サンプルプログラムでメモリコンテンツをスキャンする

ビルドした AmsiStream.exe をコマンドライン引数無しで実行すると、以下のようにスキャンが行われたことを示す情報がコマンドライン上に出力されます。

AMSI でメモリスキャンを実行する

この時、サンプルプログラム AmsiStream はメモリ内に保存されている文字列 Hello, world を AMSI によりスキャンしています。

実際に AMSI により文字列 Hello, world がスキャンされているか否かについては、1 章に記載した手順で ETW イベントトレースを取得することで確認することができます。

ただし、1 章で使用した解析ツール Get-AMSIEvent を使用すると、[Text.Encoding]::Unicode.GetString($_.Properties[7].Value) により スキャンコンテンツを UTF-16 LE でエンコードしてしまうので、文字列 Hello, world がスキャンされたことを確認することができません。3

Get-AMSIEvent の出力結果

そこで、今回は標準の Get-WinEvent コマンドレットを使用して ETL ファイルからスキャン対象の AMSI コンテンツを確認してみます。


AMSI によりスキャンされたデータを確認するため、まずは保存された AMSITrace.etl を以下のコマンドで $Events として取得します。

$Events = Get-WinEvent -Path "$($env:USERPROFILE)\Downloads\AMSITrace.etl" -Oldest

上記のコマンドで取得したイベントは EventRecord クラスのオブジェクトとして保存されました。4

保存されたイベントの確認

続いて、このイベントの中から AMSI でスキャンされたコンテンツを抽出します。

1 章で使用した解析ツール Get-AMSIEvent では、Get-WinEvent コマンドレットで取得したイベントの Properties[4].Value を ContentName、 および、Properties[7].Value を AMSIContent として取り出しています。

そこで、取得したイベントに対して以下のコマンドをそれぞれ実行することで、ContentName が Sample content.txt であり、 かつスキャンされたデータが確かに Hello, world であることを確認できました。

# ContentName
$Events[2].Properties[4].Value

# AMSIContent
[Text.Encoding]::UTF8.GetString($Events[2].Properties[7].Value)

AMSI でスキャンされたコンテンツ

サンプルプログラムでファイルをスキャンする

続いて、サンプルプログラム AmsiStream によるファイルのスキャンを試します。

サンプルプログラム AmsiStream では、実行時のコマンドライン引数にファイルパスを指定した場合、 メモリ内のデータではなくファイルに保存されているデータを AMSI でスキャンできます。

以下は、AMSI 検知テスト用の文字列 AMSI Test Sample:7e72c3ce-861b-4339-8740-0ac1484c1386 が保存された sample1.txt と、 無害なテキストが保存された sample2.txt を同時に AmsiStream でスキャンした場合の結果です。

この実行結果から、テスト検知用の文字列を含むファイルである sample1.txt のみが、AMSI スキャンによりマルウェアと判定されたことがわかります。

AMSI でファイルのスキャンを実行する

AMSI クライアントインターフェースの実装

サンプルプログラム AmsiStream の動作を確認できたので、詳しい実装を解説していきます。

以下のステップは、サンプルプログラム AmsiStream の動作を簡略化したものです。

  1. エントリポイントである wmain 関数が呼び出され、CoInitializeEx 関数による初期化を行う
  2. 初期化に成功した場合、コマンドライン引数とともに ScanArguments 関数を呼び出す
  3. ScanArguments 関数にて、初めに CStreamScanner クラスの初期化を行う
  4. ファイルパスを指定するためのコマンドライン引数が存在している場合には CAmsiFileStream クラス、存在していない場合には CAmsiMemoryStream クラスのオブジェクトを初期化する
  5. ScanStream メソッドを使用してファイルもしくはメモリコンテンツのスキャンを行う

本章では、上記の実行ステップに沿ってプログラムの実行コードを追っていくことにします。

エントリポイントの実装

エントリポイントとなる wmain 関数は、以下のように小さな関数として実装されています。

int __cdecl wmain(_In_ int argc, _In_reads_(argc) WCHAR **argv)
{
  HRESULT hr = \
    CoInitializeEx(nullptr, COINIT_MULTITHREADED);

  if (SUCCEEDED(hr)) {
    hr = ScanArguments(argc, argv);
    CoUninitialize();
  }
  wprintf(L"Leaving with hr = 0x%x\n", hr);
  return 0;
}

この中ではまず、CoInitializeEx 関数を呼び出して COM ライブラリの初期化を行います。5

その後、各種クラスの初期化やスキャン要求を行う ScanArguments 関数がコマンドライン引数とともに呼び出され、 スキャンの完了後に CoUninitialize 関数によりリソースが解放されます。

ScanArguments 関数の実装

ScanArguments 関数は、以下のように wmain 関数からコマンドライン引数を受け取って呼び出されます。

HRESULT ScanArguments(_In_ int argc, _In_reads_(argc) wchar_t** argv)

この ScanArguments 関数内ではまず、CStreamScanner クラスのオブジェクトである scanner の初期化を行います。

CStreamScanner クラスは、前述した通り AMSI の Scan メソッドを使用してコンテンツのスキャンを行い、 その結果を表示する役割を担う ScanStream メソッドを定義しているクラスです。

CStreamScanner scanner;
HRESULT hr = scanner.Initialize();
if (FAILED(hr))
{
  return hr;
}

scanner の初期化の完了後、ScanArguments 関数ではコマンドライン引数にファイルパスが含まれるか否かによって、 CAmsiFileStream クラス、もしくは CAmsiMemoryStream クラスの初期化を行います。

コマンドライン引数無しでプログラムを実行した場合、 サンプルプログラム AmsiStream はメモリコンテンツをスキャンするために CAmsiMemoryStream クラスの初期化を行います。

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

ComPtr<IAmsiStream> stream;
hr = MakeAndInitialize<CAmsiMemoryStream>(&stream);
if (FAILED(hr)) {
   return hr;
}

上記のコードでは、Windows ランタイムクラスを初期化するための MakeAndInitialize 関数を使用して、 CAmsiMemoryStream クラスのオブジェクト stream を初期化しています。6

そして、CAmsiMemoryStream クラスのオブジェクトの初期化が完了した後、 scanner.ScanStream(stream.Get()) によりスキャン要求を実施しています。

hr = scanner.ScanStream(stream.Get());
if (FAILED(hr))
{
   return hr;
}

CAmsiMemoryStream クラスは MakeAndInitialize<CAmsiMemoryStream>(&stream) により Windows ランタイムクラス ComPtr<IAmsiStream> として初期化されているため、 stream.Get() により Microsoft::WRL::ComPtr<IAmsiStream>::Get が呼び出され、この ComPtr と紐づくインターフェース(IAmsiStream インターフェース)へのポインタが返されます。7

scanner.ScanStream() により実行されるスキャン動作については、CStreamScanner::ScanStream メソッドの項で後述します。


続いて、コードを少し戻し、サンプルプログラムがコマンドライン引数により指定されたファイルパスと共に実行された場合の動作を確認します。

サンプルプログラムがファイルパスを引数として実行された場合、ScanArguments 関数は CAmsiMemoryStream クラスではなく CAmsiFileStream クラスの初期化を行います。

CAmsiFileStream クラスの初期化を行う際には、コマンドライン引数から取得したファイルパスが引数として渡されます。

また、CAmsiMemoryStream クラスを使用する場合と同様、stream.Get() で取得したインターフェースへのポインタを使用して scanner.ScanStream() を呼び出しスキャンを実行しています。

// Scan the files passed on the command line.
for (int i = 1; i < argc; i++)
{
  LPWSTR fileName = argv[i];

  wprintf(L"Creating stream object with file name: %s\n",
          fileName
        );
  ComPtr<IAmsiStream> stream;
  hr = MakeAndInitialize<CAmsiFileStream>(&stream,
                                          fileName
                                         );
  if (FAILED(hr)) {
    return hr;
  }

  hr = scanner.ScanStream(stream.Get());
  if (FAILED(hr))
  {
    return hr;
  }
}

CStreamScanner クラスの実装

CStreamScanner クラスは、サンプルプログラムにてメモリコンテンツの AMSI スキャン要求を行うために使用するクラスです。

このクラスでは、以下の 2 つのメソッドが定義されています。

  • Initialize メソッド
  • ScanStream メソッド

Initialize メソッドは、ScanArguments 関数内で scanner.Initialize() として呼び出される初期化用のメソッドであり、以下の通り定義されています。

HRESULT Initialize()
{
  return CoCreateInstance(
    __uuidof(CAntimalware),
    nullptr,
    CLSCTX_INPROC_SERVER,
    IID_PPV_ARGS(&m_antimalware));
}

この Initialize メソッドの中で呼び出されている CoCreateInstance 関数は、指定した CLSID と紐づくクラスのオブジェクトを作成して初期化します。8

この中で引数に使用されている CAntimalware は、ヘッダファイル amsi.h で以下の通り定義されている CLSID です。

そのため、この CLSID と紐づくクラスのオブジェクトを作成して初期化することで、 サンプルプログラムからの AMSI スキャン要求を行うことができるようになります。


ちなみに、この CLSID と対応する COM クラスは %windir%\system32\amsi.dll に含まれています。

class DECLSPEC_UUID("fdb00e52-a214-4aa1-8fba-4357bb0072ec") CAntimalware;

また、CoCreateInstance 関数で作成されたインターフェースのポインタは ComPtr<IAntimalware> m_antimalware; として定義されているプライベートメンバである m_antimalware に保存されます。

なお、__uuidof オペレーターや IID_PPV_ARGS マクロについては本書のスコープからは外れるので詳しくは扱いませんが、 いずれもより効果的に COM を使用するプログラムを実装するために使用しています。9


CStreamScanner クラスで定義されているもう 1 つのメソッドである ScanStream メソッドは、 IAmsiStream インターフェイスのオブジェクトを引数 stream として受け取り、 IAntimalware インターフェースの Scan メソッドを呼び出して AMSI スキャン要求を行います。

このメソッドは以下の通り実装されています。

HRESULT ScanStream(_In_ IAmsiStream* stream)
{
  wprintf(L"Calling antimalware->Scan() ...\n");

  ComPtr<IAntimalwareProvider> provider;
  AMSI_RESULT r;
  HRESULT hr = m_antimalware->Scan(stream, &r, &provider);
  if (FAILED(hr)) {
    return hr;
  }

  wprintf(L"Scan result is %u. IsMalware: %d\n",
           r,
           AmsiResultIsMalware(r)
         );

  if (provider) {
      PWSTR name;
      hr = provider->DisplayName(&name);
      if (SUCCEEDED(hr)) {
        wprintf(L"Provider display name: %s\n",
                 name
               );
        CoTaskMemFree(name);
      }
      else
      {
        wprintf(L"DisplayName failed with 0x%x", hr);
      }
  }

  return S_OK;
}

以下のコードでは、Initialize メソッドにより初期化した IAntimalware インターフェースのポインタが保存されている m_antimalware を使用し、 引数として受け取った stream と共に Scan メソッドを呼び出しています。

ComPtr<IAntimalwareProvider> provider;
AMSI_RESULT r;
HRESULT hr = m_antimalware->Scan(stream, &r, &provider);
if (FAILED(hr)) {
  return hr;
}

この時呼び出される IAntimalware インターフェースの Scan メソッドは以下の通り 3 つの引数を取ります。10

HRESULT Scan(
  [in]  IAmsiStream          *stream,
  [out] AMSI_RESULT          *result,
  [out] IAntimalwareProvider **provider
);

第 1 引数には、IAmsiStream インターフェイスのオブジェクトである stream を使用します。

また、第 2 引数には、スキャン結果(AMSI_RESULT)を受け取る出力先のアドレスを使用します。

そして、第 3 引数では、アンチマルウェア製品のプロバイダー情報を示す IAntimalwareProvider インターフェースのオブジェクトを受け取ります。


AMSI スキャンが完了すると、ScanStream メソッドは受け取ったスキャン結果をコンソールに出力します。

ここで使用している AmsiResultIsMalware マクロは、受け取ったスキャン結果(AMSI_RESULT)から、 対象のコンテンツをブロックすべきかどうかを判断します。11

wprintf(L"Scan result is %u. IsMalware: %d\n",
         r,
         AmsiResultIsMalware(r)
       );

このマクロは、公開されているヘッダファイル amsi.h の中で AMSI_RESULT と共に以下のように定義されており、 スキャン結果の値が AMSI_RESULT_DETECTED = 32768 以上の値の場合に True を返すことがわかります。

enum AMSI_RESULT
{
    AMSI_RESULT_CLEAN	= 0,
    AMSI_RESULT_NOT_DETECTED	= 1,
    AMSI_RESULT_BLOCKED_BY_ADMIN_START	= 0x4000,
    AMSI_RESULT_BLOCKED_BY_ADMIN_END	= 0x4fff,
    AMSI_RESULT_DETECTED	= 32768
} 	AMSI_RESULT;

#define AmsiResultIsMalware(r) ((r) >= AMSI_RESULT_DETECTED)

つまり、AMSI プロバイダーとして登録されるアンチマルウェア製品は、 スキャンしたコンテンツをマルウェアと判定する場合には スキャン結果を AMSI_RESULT_DETECTED = 32768 以上の値として返す必要があります。

登録されている AMSI プロバイダーがスキャン結果を返却する仕組みについては 3 章で後述します。

CAmsiStreamBase クラスの実装

CStreamScanner クラスの ScanStream メソッドは、 CAmsiFileStream クラスと CAmsiMemoryStream クラスのメソッドから メモリコンテンツやファイルデータのスキャンを行うために呼び出されます。

本項では、これらのクラスの実装を解説する前に、2 つのクラスに継承される基底クラスである CAmsiStreamBase クラスの実装を解説します。


前述の通り、CAmsiStreamBase クラスには以下の 3 つのメソッドが定義されています。

  • SetContentName メソッド
  • BaseGetAttribute メソッド
  • CopyAttribute メソッド

SetContentName メソッドは、以下の通り定義されており、 スキャンするファイルの名前などの情報をメモリに保存します。

HRESULT SetContentName(_In_ PCWSTR name)
{
    m_contentName = _wcsdup(name);
    return m_contentName ? S_OK : E_OUTOFMEMORY;
}

例えば、CAmsiFileStream クラスのメソッドから呼び出される場合には、SetContentName(fileName) のようにファイル名を含む文字列が引数として与えられます。

また、CAmsiMemoryStream クラスのメソッドから呼び出される場合には、SetContentName(L"Sample content.txt") と、ハードコードされている文字列が引数として与えられます。

ここで保存される m_contentName は、あとで AMSI プロバイダーから属性情報 AMSI_ATTRIBUTE_CONTENT_NAME を要求された際にクライアントアプリケーションが返却する値としてに使用されます。


次に、CAmsiStreamBase クラスで実装されている BaseGetAttribute メソッドを解説します。

このサンプルプログラムにおいて、BaseGetAttribute メソッドは 要求された AMSI 属性情報を返すために使用されるメソッドである IAmsiStream インターフェースの GetAttribute メソッドから呼び出される形で利用されます。13

この BaseGetAttribute メソッドでは、引数として要求された AMSI 属性の情報を受け取り、 その値に応じて異なる属性情報を出力先のバッファに書き込みます。

HRESULT BaseGetAttribute(
  _In_ AMSI_ATTRIBUTE attribute,
  _In_ ULONG bufferSize,
  _Out_writes_bytes_to_(bufferSize, *actualSize) PBYTE buffer,
  _Out_ ULONG* actualSize)
  //
  // Return Values:
  //   S_OK: SUCCESS
  //   E_NOTIMPL: attribute not supported
  //   E_NOT_SUFFICIENT_BUFFER: need a larger buffer, required size in *retSize
  //   E_INVALIDARG: bad arguments
  //   E_NOT_VALID_STATE: object not initialized
  //

この時に要求される、AMSI スキャン時に使用する属性情報には以下の種類があります。

AMSI スキャン時に使用する属性情報

また、このサンプルプログラムの中では、要求された属性に対してそれぞれ以下の値を返します。

サンプルプログラムが返す属性情報


以下が、BaseGetAttribute メソッドにて各属性情報をバッファに書き込むためのコード部分です。

AMSI_ATTRIBUTE_CONTENT_ADDRESS は、メモリコンテンツのスキャン時にのみ使用されるため、 BaseGetAttribute メソッドではなく CAmsiMemoryStream クラスの GetAttribute メソッド内で定義されています。

*actualSize = 0;

switch (attribute)
{
  case AMSI_ATTRIBUTE_CONTENT_SIZE:
    return CopyAttribute(&m_contentSize, 
                          sizeof(m_contentSize), 
                          bufferSize, 
                          buffer, 
                          actualSize
                        );

  case AMSI_ATTRIBUTE_CONTENT_NAME:
    return CopyAttribute(m_contentName, 
                         (wcslen(m_contentName) + 1) \
                                       * sizeof(WCHAR),
                         bufferSize, buffer,
                         actualSize
                        );

  case AMSI_ATTRIBUTE_APP_NAME:
    return CopyAttribute(AppName, 
                         sizeof(AppName),
                         bufferSize,
                         buffer,
                         actualSize
                        );

  case AMSI_ATTRIBUTE_SESSION:
    // no session for file stream
    constexpr HAMSISESSION session = nullptr;
    return CopyAttribute(&session,
                          sizeof(session),
                          bufferSize,
                          buffer,
                          actualSize
                        );
}

return E_NOTIMPL; // unsupported attribute

上記のコード部分を見るとわかる通り、実際に要求された AMSI 属性情報と対応する値をバッファに書き込む操作は、 同じく CAmsiStreamBase クラスのメンバである CopyAttribute メソッドで行われます。

このメソッドの実装は非常にシンプルで、引数として受け取ったデータから、 バッファのサイズのチェックと memcpy_s 関数によるデータの書き込み操作を行います。14

HRESULT CopyAttribute(
  _In_ const void* resultData,
  _In_ size_t resultSize,
  _In_ ULONG bufferSize,
  _Out_writes_bytes_to_(bufferSize, *actualSize) \
                                          PBYTE buffer,
  _Out_ ULONG* actualSize)
{
  *actualSize = (ULONG)resultSize;
  if (bufferSize < resultSize)
  {
    return E_NOT_SUFFICIENT_BUFFER;
  }
  memcpy_s(buffer, bufferSize, resultData, resultSize);
  return S_OK;
}

CAmsiMemoryStream クラスの実装

CAmsiMemoryStream クラスは、メモリコンテンツのスキャンを行う場合に使用されるクラスです。

このクラスは AMSI の仕組みにおいて属性情報やスキャンコンテンツの取得に使用される IAmsiStream インターフェースの実装として利用されます。15

また、前項で解説した基底クラス CAmsiStreamBase を継承しています。

class CAmsiMemoryStream : public RuntimeClass<RuntimeClassFlags<ClassicCom>, IAmsiStream>, CAmsiStreamBase

この CAmsiMemoryStream クラスは IAmsiStream インターフェースの実際の実装であるため、 GetAttribute メソッドと Read メソッドの 2 つのメソッドが定義されています。

IAmsiStream インターフェースの GetAttribute メソッドは、スキャン時に要求された AMSI 属性情報と対応する値を返す必要のあるメソッドとして定義されています。16

GetAttribute メソッドが要求する各引数の用途は以下の通りです。

GetAttribute メソッドのパラメータ情報


CAmsiMemoryStream クラスにおいてこのメソッドは以下の通り定義されており、 AMSI_ATTRIBUTE_CONTENT_ADDRESS 以外の属性に関する要求を受け取った場合には、 前項で確認した基底クラスの BaseGetAttribute メソッドに処理を任せるように実装されています。

また、要求された属性が AMSI_ATTRIBUTE_CONTENT_ADDRESS の場合には、 グローバル定数 SampleStream として定義されているスキャン対象の文字列のアドレスを返します。

STDMETHOD(GetAttribute)(
  _In_ AMSI_ATTRIBUTE attribute,
  _In_ ULONG bufferSize,
  _Out_writes_bytes_to_(bufferSize, *actualSize) \
                                        PBYTE buffer,
  _Out_ ULONG* actualSize)
{
  HRESULT hr = BaseGetAttribute(attribute, 
                                bufferSize,
                                buffer, 
                                actualSize
                              );
  if (hr == E_NOTIMPL)
  {
    switch (attribute)
    {
    case AMSI_ATTRIBUTE_CONTENT_ADDRESS:
      const void* contentAddress = SampleStream;
      hr = CopyAttribute(&contentAddress,
                          sizeof(contentAddress),
                          bufferSize, 
                          buffer, 
                          actualSize
                        );
    }
  }
  return hr;
}

次に、Read メソッドの実装を確認します。

IAmsiStream インターフェースの Read メソッドは、受け取ったバッファがいっぱいになるまでスキャン対象のコンテンツを返す必要のあるメソッドとして定義されています。17

このメソッドが受け取る引数の用途は以下の通りです。

Read メソッドのパラメータ情報


以下は、CAmsiMemoryStream クラスにおける Read メソッドの実際の実装です。

STDMETHOD(Read)(
    _In_ ULONGLONG position,
    _In_ ULONG size,
    _Out_writes_bytes_to_(size, *readSize) PBYTE buffer,
    _Out_ ULONG* readSize)
{
  wprintf(L"Read() called with: position = %I64u, size = %u\n",
          position,
          size
        );

  *readSize = 0;
  if (position >= m_contentSize)
  {
    wprintf(L"Reading beyond end of stream\n");
    return HRESULT_FROM_WIN32(ERROR_HANDLE_EOF);
  }

  if (size > m_contentSize - position) {
    size = static_cast<ULONG>(m_contentSize - position);
  }

  *readSize = size;
  memcpy_s(buffer, size, SampleStream + position, size);
  return S_OK;
}

上記のコードの通り、CAmsiMemoryStream クラスを使用する場合にはグローバル定数 SampleStream として定義されているスキャン対象の文字列を memcpy_s 関数でバッファに書き込む操作を行うことで、IAmsiStream インターフェースの Read メソッドの要件を満たしています。

CAmsiFileStream クラスの実装

最後に、AMSI によりファイルコンテンツのスキャンを行う CAmsiFileStream クラスの実装を解説します。

class CAmsiFileStream : public RuntimeClass<RuntimeClassFlags<ClassicCom>, IAmsiStream>, CAmsiStreamBase

CAmsiFileStream クラスは CAmsiMemoryStream と同じく、 AMSI の仕組みにおいて属性情報やスキャンコンテンツの取得に使用される IAmsiStream インターフェースの実装として利用されます。


このクラスの初期化時にはまず、スキャン対象のファイルパスを文字列として保存した fileName が 基底クラスの SetContentName メソッドにより m_contentName としてコピーされます。

HRESULT hr = S_OK;
hr = SetContentName(fileName);
if (FAILED(hr)) 
{ 
  return hr; 
}

続いて、CreateFileW 関数によりスキャン対象のファイルに対する読み取りアクセス用のファイルハンドルを作成し、 それをプライベートメンバ m_fileHandle にアタッチしています。

この m_fileHandle とは、WRL で定義されているファイルハンドルを扱うための FileHandleTraits のオブジェクトであり、ファイルハンドルを安全に管理するために利用されます。

m_fileHandle.Attach(
  CreateFileW(
    fileName,
    GENERIC_READ,         // dwDesiredAccess
    0,                    // dwShareMode
    nullptr,              // lpSecurityAttributes
    OPEN_EXISTING,        // dwCreationDisposition
    FILE_ATTRIBUTE_NORMAL,// dwFlagsAndAttributes
    nullptr
  )
);                        // hTemplateFile

スキャン対象のファイルのファイルハンドルを取得したら、 GetFileSizeEx 関数により対象ファイルのサイズを取得し、 ULONGLONG 型の変数 m_contentSize として保存します。

これは、前述の通り AMSI 属性 AMSI_ATTRIBUTE_CONTENT_SIZE として返される値は ULONGLONG 型で表現されると定義されているためです。

LARGE_INTEGER fileSize;
if (!GetFileSizeEx(m_fileHandle.Get(), &fileSize))
{
  hr = HRESULT_FROM_WIN32(GetLastError());
  wprintf(L"GetFileSizeEx failed with 0x%x\n", hr);
  return hr;
}

m_contentSize = (ULONGLONG)fileSize.QuadPart;

次に、CAmsiFileStream クラスの GetAttribute メソッドを確認します。

CAmsiFileStream クラスでは CAmsiMemoryStream クラスとは異なり、 基底クラスの BaseGetAttribute メソッドに完全に依存する形で GetAttribute メソッドが定義されています。

STDMETHOD(GetAttribute)(
      _In_ AMSI_ATTRIBUTE attribute,
_In_ ULONG bufferSize,
_Out_writes_bytes_to_(bufferSize, *actualSize) PBYTE buffer,
_Out_ ULONG* actualSize)
{
  return BaseGetAttribute(attribute, bufferSize, buffer, actualSize);
}

最後に、CAmsiFileStream クラスの Read メソッドを確認します。

CAmsiMemoryStream クラスと同じく、CAmsiFileStream クラスの場合も Read メソッドは IAmsiStream インターフェースの実際の実装であり、 受け取ったバッファがいっぱいになるまでスキャン対象のコンテンツを返す必要のあるメソッドとして定義されています。

OVERLAPPED o = {};
o.Offset = LODWORD(position);
o.OffsetHigh = HIDWORD(position);

if (!ReadFile(m_fileHandle.Get(),
              buffer,
              size,
              readSize,
              &o
            )
    )
{
  HRESULT hr = HRESULT_FROM_WIN32(GetLastError());
  wprintf(L"ReadFile failed with 0x%x\n", hr);
  return hr;
}

ただし、上記のコード部分の通り、CAmsiMemoryStream クラスの場合とは違い、 スキャン対象のファイルから ReadFile 関数で読み出したデータを出力先のバッファに書き込む点に大きな違いがあることがわかります。

2 章のまとめ

本章では、サンプルプログラム AmsiStream が AMSI を使用して メモリコンテンツとファイルコンテンツのスキャン要求を行う仕組みを解説しました。

サンプルプログラムの実装から、クライアントアプリケーションに AMSI を統合してコンテンツスキャンを行うためには、 端的に以下の操作を行うインターフェースを実装すればよいことがわかりました。

  • IAmsiStream インターフェースのオブジェクトを定義し、各 AMSI 属性情報やスキャンするコンテンツを返すための GetAttribute メソッドと Read メソッドを実装する
  • amsi.dll で定義されている COM クラスのオブジェクト(IAntimalware インターフェース)を作成し、Scan メソッドを呼び出して AMSI スキャンを要求する
  • Scan メソッドの結果(AMSI_RESULT) を元に、コンテンツのブロックなどの処理を行う

次の 3 章では、IAntimalware インターフェースの Scan メソッドの呼び出し後に、 システムに登録されている AMSI プロバイダーがどのようにコンテンツの取得やスキャンを行うのかを解説します。

本書のもくじ


  1. RuntimeClass Class https://learn.microsoft.com/ja-jp/cpp/cppcx/wrl/runtimeclass-class

  2. IAmsiStream interface https://learn.microsoft.com/ja-jp/windows/win32/api/amsi/nn-amsi-iamsistream

  3. Encoding.Unicode Property https://learn.microsoft.com/ja-jp/dotnet/api/system.text.encoding.unicode

  4. EventRecord Class https://learn.microsoft.com/ja-jp/dotnet/api/system.diagnostics.eventing.reader.eventrecord

  5. CoInitializeEx function https://learn.microsoft.com/ja-jp/windows/win32/api/combaseapi/nf-combaseapi-coinitializeex

  6. MakeAndInitialize Function https://learn.microsoft.com/ja-jp/cpp/cppcx/wrl/makeandinitialize-function

  7. ComPtr Class https://learn.microsoft.com/ja-jp/cpp/cppcx/wrl/comptr-class

  8. CoCreateInstance function https://learn.microsoft.com/ja-jp/windows/win32/api/combaseapi/nf-combaseapi-cocreateinstance

  9. COM Coding Practices https://learn.microsoft.com/ja-jp/windows/win32/learnwin32/com-coding-practices

  10. IAntimalware::Scan method https://learn.microsoft.com/ja-jp/windows/win32/api/amsi/nf-amsi-iantimalware-scan

  11. AmsiResultIsMalware macro https://learn.microsoft.com/en-us/windows/win32/api/amsi/nf-amsi-amsiresultismalware

  12. AMSIRESULT enumeration [https://learn.microsoft.com/ja-jp/windows/win32/api/amsi/ne-amsi-amsiresult](https://learn.microsoft.com/ja-jp/windows/win32/api/amsi/ne-amsi-amsi_result)

  13. IAmsiStream::GetAttribute method https://learn.microsoft.com/ja-jp/windows/win32/api/amsi/nf-amsi-iamsistream-getattribute

  14. memcpy_s https://learn.microsoft.com/ja-jp/cpp/c-runtime-library/reference/memcpy-s-wmemcpy-s

  15. IAmsiStream interface https://learn.microsoft.com/ja-jp/windows/win32/api/amsi/nn-amsi-iamsistream

  16. IAmsiStream::GetAttribute method https://learn.microsoft.com/ja-jp/windows/win32/api/amsi/nf-amsi-iamsistream-getattribute

  17. IAmsiStream::Read method https://learn.microsoft.com/ja-jp/windows/win32/api/amsi/nf-amsi-iamsistream-read