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.
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
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.
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.
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.
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ı
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 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
Linkleri görebilmek için kayıt olmanız gerekmektedir
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
Linkleri görebilmek için kayıt olmanız gerekmektedir
Çalıştırma Kanıtı
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: