今回は、前回の AMSI の概要と動作について に続いて、AMSI COM インターフェースを使用して AMSI スキャン要求を行うアプリケーションとその実装についてまとめます。
もくじ
カスタムアプリケーションから AMSI スキャン要求を発行する
アプリケーションに AMSI インターフェースを統合する方法は、大きく分けて AMSI Win32 API を利用する方法と、AMSI COM インターフェースを使用する方法の 2 種類があるようです。
参考:開発者の対象者とサンプル コード - Win32 apps | Microsoft Learn
前項で確認した PowerShell では AMSI Win32 API を利用しているので、今回は AMSI COM インターフェースを使用した実装を試してみようと思います。
AMSI COM インターフェースを使用したアプリケーションの作成には、公式のサンプルコードが参考になります。
このサンプルコードは、以下のリポジトリから amsistream-sample.zip をダウンロードすることで取得できます。
参考:Release MicrosoftDocs-Samples · microsoft/Windows-classic-samples
このサンプルプログラムは、AMSI COM インターフェースを使用してメモリ内のデータとファイルのコンテンツスキャンを行うプログラムです。
コードは 300 行程度の単一ファイルで実装されており、比較的読みやすいと思います。
まずは、このサンプルコードについて main 関数から順に読んでいくことにします。
main 関数
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;
}
main 関数ではまず始めに、CoInitializeEx 関数による COM ライブラリの初期化を行います。
参考:COM ライブラリの初期化 - Win32 apps | Microsoft Learn
参考:CoInitializeEx 関数 (combaseapi.h) - Win32 apps | Microsoft Learn
この関数は COM を使用する Windows プログラムで COM ライブラリを初期化するために呼び出す必要がある関数で、通常は COM ライブラリを使用するスレッドごとに 1 回だけ呼び出されるようです。
CoInitializeEx 関数の最初の引数は予約済みのため NULL である必要があり、2 つめの引数はプログラムの使用するスレッドモデルの指定に使用します。(apartment thread もしくは multithreaded)
このサンプルでは、マルチスレッドモデルを使用する COINIT_MULTITHREADED
を引数としています。
COM ライブラリの初期化後、ScanArguments(argc, argv)
の後に CoUninitialize 関数が呼び出されています。
この関数は、スレッドが終了する前に初期化の解除のために実行する必要があります。
ScanArguments 関数の呼び出し
COM ライブラリの初期化後、コマンドライン引数を引数として ScanArguments 関数が呼び出されます。
HRESULT ScanArguments(_In_ int argc, _In_reads_(argc) wchar_t** argv)
{
CStreamScanner scanner;
HRESULT hr = scanner.Initialize();
if (FAILED(hr))
{
return hr;
}
if (argc < 2)
{
// Scan a single memory stream.
wprintf(L"Creating memory stream object\n");
ComPtr<IAmsiStream> stream;
hr = MakeAndInitialize<CAmsiMemoryStream>(&stream);
if (FAILED(hr)) {
return hr;
}
hr = scanner.ScanStream(stream.Get());
if (FAILED(hr))
{
return hr;
}
}
else
{
// 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;
}
}
}
return S_OK;
}
この中ではまず、CStreamScanner クラスのインスタンス scanner を定義し、Initialize メソッドによる初期化を行います。
CStreamScanner scanner;
HRESULT hr = scanner.Initialize();
CStreamScanner クラスの初期化
CStreamScanner クラスは以下のように定義されており、Initialize メソッドでは CoCreateInstance によるオブジェクトの初期化を行っています。
参考:CoCreateInstance 関数 (combaseapi.h) - Win32 apps | Microsoft Learn
オブジェクトを作成するクラスを指定する CLSID には、amsi.h で定義されている値を使用して __uuidof(CAntimalware)
として CoCreateInstance 関数に渡されます。
また、オブジェクトとの通信に使用するインターフェイス識別子(IID) は、ComPtr<IAntimalware> m_antimalware;
として定義されているメンバに保存されます。
class CStreamScanner
{
public:
HRESULT Initialize()
{
return CoCreateInstance(
__uuidof(CAntimalware),
nullptr,
CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(&m_antimalware));
}
/* 省略 */
private:
ComPtr<IAntimalware> m_antimalware;
};
メモリストリームのスキャン
CStreamScanner クラスの初期化後、ScanArguments 関数ではコマンドライン引数の有無によって、事前定義されたメモリストリームかコマンドライン引数として受け取ったファイルをスキャンするかを決定します。
コマンドライン引数が存在しない場合には以下のコードが実行されます。
if (argc < 2)
{
// Scan a single memory stream.
wprintf(L"Creating memory stream object\n");
ComPtr<IAmsiStream> stream;
hr = MakeAndInitialize<CAmsiMemoryStream>(&stream);
if (FAILED(hr)) {
return hr;
}
hr = scanner.ScanStream(stream.Get());
if (FAILED(hr))
{
return hr;
}
}
このコードではまず、ComPtr<IAmsiStream> stream;
によって IAmsiStream インターフェースポインタを宣言しています。
参考:IAmsiStream (amsi.h) - Win32 apps | Microsoft Learn
参考:ComPtr クラス | Microsoft Learn
続けて、MakeAndInitialize<CAmsiMemoryStream>(&stream);
にて、MakeAndInitialize 関数により Windows ランタイム C++ テンプレート ライブラリ (WRL) クラスである CAmsiMemoryStream の初期化を行っています。
参考:MakeAndInitialize 関数 | Microsoft Learn
参考:方法: WRL コンポーネントを直接インスタンス化する | Microsoft Learn
WRL については正直理解があいまいですが、今回のサンプルの CAmsiMemoryStream は、クラシック COM コンポーネントを作成するために実装されている WRL クラスのようです。
参考:方法: WRL を使用して従来の COM コンポーネントを作成する | Microsoft Learn
CAmsiMemoryStream クラス
WRL クラスの CAmsiMemoryStream は、IAmsiStream インターフェースの実装に使用されます。
このクラスは、RuntimeClass<RuntimeClassFlags<ClassicCom>, IAmsiStream>
にて WRL を使用して IAmsiStream と対応するインターフェースを作成しているようです。
また、別途定義されている共通の処理を提供する CAmsiStreamBase クラスも継承しています。
class CAmsiMemoryStream : public RuntimeClass<RuntimeClassFlags<ClassicCom>, IAmsiStream>, CAmsiStreamBase
{
public:
HRESULT RuntimeClassInitialize()
{
m_contentSize = sizeof(SampleStream);
return SetContentName(L"Sample content.txt");
}
// IAmsiStream
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;
}
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;
}
};
まず、RuntimeClassInitialize 関数は MakeAndInitialize 関数テンプレートを使用してオブジェクトを作成する場合に初期化を行う関数とのことです。
参考:RuntimeClass クラス | Microsoft Learn
今回のサンプルでは、グローバル変数として定義されている文字列 SampleStream を、別途 CAmsiStreamBase で定義されている SetContentName によりメモリ内に保存する操作を行っています。
const char SampleStream[] = "Hello, world";
HRESULT RuntimeClassInitialize()
{
m_contentSize = sizeof(SampleStream);
return SetContentName(L"Sample content.txt");
}
以降のコードでは、IAmsiStream インターフェースの実装を行っています。
STDMETHOD というのは、「仮想関数でHRESULTを返す__stdcallされる関数」として定義するマクロらしいです。
参考:IAmsiStream (amsi.h) - Win32 apps | Microsoft Learn
CAmsiMemoryStream のクラスでは、GetAttribute と Read のメソッドはそれぞれ上記の通り定義されています。
GetAttribute メソッドは、基底クラス CAmsiStreamBase の BaseGetAttribute を使用し、AMSI スキャンに関する属性の処理を行います。
この中では、attribute や buffer などのチェックを行った上で、最終的に基底クラスで定義されている CopyAttribute メソッドの結果を返します。
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;
}
CopyAttribute メソッドは、引数として受け取ったバッファやサイズなどをチェックし、memcpy_s 関数を使って受け取ったデータのコピーを行います。
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 のクラスで定義されているもう一方のメソッドである 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;
}
ScanStream メソッドの呼び出し
コンテンツのデータをコピーした後、scanner.ScanStream(stream.Get());
にて CStreamScanner クラスの ScanStream メソッドに stream.Get()
で取得した IAmsiStream インスタンスへのポインタを渡します。
この中では、初期化済みの IAntimalware インターフェースの Scan メソッドを呼び出しています。
参考:IAntimalware::Scan (amsi.h) - Win32 apps | Microsoft Learn
class CStreamScanner
{
public:
HRESULT Initialize()
{
return CoCreateInstance(
__uuidof(CAntimalware),
nullptr,
CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(&m_antimalware));
}
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;
}
private:
ComPtr<IAntimalware> m_antimalware;
};
スキャンの結果は、AMSI_RESULT 型の変数 r に保存されます。
また、この AMSI_RESULT 型の変数を AmsiResultIsMalware マクロに渡すことでコンテンツをブロックする必要があるかどうかを返すことができます。
参考:AmsiResultIsMalware マクロ (amsi.h) - Win32 apps | Microsoft Learn
さらに、IAntimalware インターフェースの Scan メソッドが返す IAntimalwareProvider インターフェースのインスタンス provider からマルウェア対策プロバイダーの名前を表示しています。
参考:IAntimalwareProvider::D isplayName (amsi.h) - Win32 apps | Microsoft Learn
実際にマルウェア検出が行われた場合には、以下のように変数 name にプロバイダー名である Microsoft Defender Antivirus
が、そして変数 r に AMSI_RESULT_DETECTED
という検出結果が含まれることを確認できます。
ファイルストリームのスキャン
サンプルプログラムをコマンドライン引数無しで実行した場合、前項までに確認したメモリスキャンが行われます。
一方で、コマンドライン引数にファイルパスが含まれている場合、CAmsiFileStream クラスを初期化してファイルのスキャンを行います。
else
{
// 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;
}
}
}
スキャン対象のファイルパスを指定してサンプルプログラムを実行した場合、コマンドライン引数からファイルパスを取得していることをデバッガで確認できます。
CAmsiFileStream クラス
CAmsiFileStream クラスは、CAmsiMemoryStream クラスと同じく、RuntimeClass<RuntimeClassFlags<ClassicCom>, IAmsiStream>
にて WRL を使用して IAmsiStream と対応するインターフェースを作成しています。
また、別途定義されている共通の処理を提供する CAmsiStreamBase クラスも継承しています。
MakeAndInitialize 関数テンプレートを使用してオブジェクトを作成する場合に初期化を行う RuntimeClassInitialize 関数では、メモリスキャン時とは異なり、ファイル名を引数として呼び出されます。
この中では、CreateFileW で取得したスキャン対象ファイルのファイルハンドルを WRL の FileHandle 型の変数 m_fileHandle として保存しています。
m_fileHandle.Attach(CreateFileW(fileName,
GENERIC_READ, // dwDesiredAccess
0, // dwShareMode
nullptr, // lpSecurityAttributes
OPEN_EXISTING, // dwCreationDisposition
FILE_ATTRIBUTE_NORMAL, // dwFlagsAndAttributes
nullptr)); // hTemplateFile
if (!m_fileHandle.IsValid())
{
hr = HRESULT_FROM_WIN32(GetLastError());
wprintf(L"Unable to open file %s, hr = 0x%x\n", fileName, hr);
return hr;
}
さらに、このファイルのファイルサイズを取得して初期化を完了します。
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 クラスの初期化が完了すると、mcontentName にスキャン対象のファイルパスが、mfileHandle にそのファイルのファイルハンドルを割り当てたメンバが登録されていることを確認できます。
また、CAmsiFileStream クラス内でも、CAmsiMemoryStream クラスと同じく IAmsiStream インターフェース用の GetAttribute と Read メソッドが定義されます。
CAmsiFileStream クラスの GetAttribute メソッドは、CAmsiMemoryStream クラスとは異なり単に基底クラスの BaseGetAttribute メソッドを呼び出すだけのようです。
また、Read メソッドでは m_fileHandle から取得したファイルハンドルを使用して、ファイルデータを buffer に保存します。
// IAmsiStream
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);
}
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);
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;
}
return S_OK;
}
最後に ScanStream メソッドを呼び出すことで、メモリスキャン時と同様にファイルコンテンツのスキャンを行います。
まとめ
今回は AMSI インターフェースを使用してスキャン要求を行うカスタムアプリケーションのサンプルコードを読んでみました。
これで、独自に AMSI を利用したマルウェアスキャンを行うプログラムを作成できるようになりそうです。