Tutorial Process Management (Kernel-Mode) #1

Opcode

Level 28
Content Creator
Joined
Aug 17, 2017
Messages
1,733
#1
Hello everyone.


Introduction

This thread will be about process termination (kernel-mode), and #1 is used in the title because I may do follow-ups (not just about process termination) in the future, in which I will use #2, #3, and so forth.

Today we will briefly look into how we can terminate a process from within a kernel-mode device driver. As you may already know, in user-mode it is common to use the OpenProcess API which is provided to us through KERNEL32.DLL, and that once this is called, it will lead to the NtOpenProcess stub which is provided to use through NTDLL.DLL. At this point, a system call is performed and kernel-mode routines will handle the operations for us, and provide back the status code which represents if the operation was successful or not (however it is more than just a boolean for example because the data-type for NTSTATUS is LONG and this allows support for many different status codes - therefore if an error occurs, a comparison can be made to obtain more details about why the operation failed if necessary).

In kernel-mode, we don't have the OpenProcess function from KERNEL32.DLL. The reason for this is because in kernel-mode, we do not have access to the Win32 API, period. In kernel-mode you have to rely on the kernel-mode routines (exported ntoskrnl.exe routines, and potentially non-exported ntoskrnl.exe functions if you are capable of locating the addresses and know how to work such functions). The alternate to OpenProcess is NtOpenProcess, which means in kernel-mode we will use the Zw* variant (ZwOpenProcess). In case you're wondering why there is both Zw* and Nt* versions of the Native API routines (the ones NTDLL supports performing system calls for so they can be invoked from user-mode API calls) and the differences between them, it is because the Zw* version performs other operations when called in kernel-mode; in user-mode it does not matter whether you use Zw* or Nt* because the bytes at the stubs will be identical (unlike in kernel-mode).

In kernel-mode you could do the following:
- Call ZwOpenProcess to open a handle to the target process.
- Use the opened process handle for an operation like termination (e.g. by calling ZwTerminateProcess and using the process handle for this call).

However, you don't actually always need to open a handle in kernel-mode. Using the above example where termination for a process is required, who says you need to use ZwOpenProcess at all? There are kernel-mode only routines which are available to us due to being exported by ntoskrnl.exe, but cannot be invoked from user-mode because they do not have an entry in the System Service Dispatch Table (SSDT) thus stopping NTDLL.DLL from supporting a system call for such functions.

In kernel-mode you can do the following instead:
- PsLookupProcessByProcessId
- KeAttachProcess
- ZwTerminateProcess
- KeDetachProcess

Both scenarios will work fine, but I prefer the second method.

In both scenarios, you'll need a Process Identifier (Process ID -> PID) so you can target a specific process. In user-mode you can easily obtain a Process ID by providing a Process Name thanks to Win32 APIs such as CreateToolhelp32Snapshot (KERNEL32), Process32Next, etc. What about in kernel-mode? The truth is that when such Win32 API functions are called, a Native API function called NtQuerySystemInformation is called. This function takes in different class IDs to represent what the task is, and SystemProcessInformation (class ID of 5) is appropriate for process enumeration.

I've already written an example for using NtQuerySystemInformation from user-mode for process enumeration, but it won't be compatible with kernel-mode by default. You can find it here: Tutorial - NtQuerySystemInformation and Process IDs

The example in the thread above can easily be ported to kernel-mode by changing a few things, such as using ExAllocatePool/WithTag instead of NtAllocateVirtualMemory. ExAllocatePool/WithTag is used for local memory allocation in kernel-mode, and typically functions like VirtualAlloc are used in user-mode (however all such functions lead to NtAllocateVirtualMemory being called). ExAllocatePool/WithTag are not available in user-mode, kernel-mode only.

For process enumeration in kernel-mode, here are the broken-down steps on what is required to do it:
1. Calculate the length for the buffer (memory which will be allocated so the enumeration results can be stored within it). -> ZwQuerySystemInformation passing NULL parameters for the second and third -> get the ULONG length back.
2. Memory allocation for the buffer (via ExAllocatePool/WithTag).
3. Re-call ZwQuerySystemInformation to actually store the results this time.
4. Enumerate through the processes in the stored results within the SYSTEM_PROCESS_INFORMATION structure (pointer to it -> *)
5. Clean-up by freeing the memory with ExFreePool/WithTag when you're done with the buffer memory.

For actually obtaining the Process ID -> use structure entry for the PID.

SYSTEM_THREAD_INFORMATION
Code:
typedef struct _SYSTEM_THREAD_INFORMATION {
    LARGE_INTEGER KernelTime;
    LARGE_INTEGER UserTime;
    LARGE_INTEGER CreateTime;
    ULONG WaitTime;
    PVOID StartAddress;
    CLIENT_ID ClientId;
    LONG Priority;
    LONG BasePriority;
    ULONG ContextSwitches;
    ULONG ThreadState;
    ULONG WaitReason;
}SYSTEM_THREAD_INFORMATION, *PSYSTEM_THREAD_INFORMATION;
SYSTEM_PROCESS_INFORMATION
Code:
typedef struct _SYSTEM_PROCESS_INFORMATION {
    ULONG NextEntryOffset;
    ULONG NumberOfThreads;
    LARGE_INTEGER WorkingSetPrivateSize;
    ULONG HardFaultCount;
    ULONG NumberOfThreadsHighWatermark;
    ULONGLONG CycleTime;
    LARGE_INTEGER CreateTime;
    LARGE_INTEGER UserTime;
    LARGE_INTEGER KernelTime;
    UNICODE_STRING ImageName;
    LONG BasePriority;
    PVOID UniqueProcessId;
    PVOID InheritedFromUniqueProcessId;
    ULONG HandleCount;
    ULONG SessionId;
    ULONG_PTR UniqueProcessKey;
    ULONG_PTR PeakVirtualSize;
    ULONG_PTR VirtualSize;
    ULONG PageFaultCount;
    ULONG_PTR PeakWorkingSetSize;
    ULONG_PTR WorkingSetSize;
    ULONG_PTR QuotaPeakPagedPoolUsage;
    ULONG_PTR QuotaPagedPoolUsage;
    ULONG_PTR QuotaPeakNonPagedPoolUsage;
    ULONG_PTR QuotaNonPagedPoolUsage;
    ULONG_PTR PagefileUsage;
    ULONG_PTR PeakPagefileUsage;
    ULONG_PTR PrivatePageCount;
    LARGE_INTEGER ReadOperationCount;
    LARGE_INTEGER WriteOperationCount;
    LARGE_INTEGER OtherOperationCount;
    LARGE_INTEGER ReadTransferCount;
    LARGE_INTEGER WriteTransferCount;
    LARGE_INTEGER OtherTransferCount;
    SYSTEM_THREAD_INFORMATION ProcessThreads[1];
}SYSTEM_PROCESS_INFORMATION, *PSYSTEM_PROCESS_INFORMATION;
SYSTEM_INFORMATION_CLASS
Code:
typedef enum _SYSTEM_INFORMATION_CLASS {
    SystemBasicInformation = 0,
    SystemProcessorInformation = 1,
    SystemPerformanceInformation = 2,
    SystemTimeOfDayInformation = 3,
    SystemPathInformation = 4,
    SystemProcessInformation = 5,
    SystemCallCountInformation = 6,
    SystemDeviceInformation = 7,
    SystemProcessorPerformanceInformation = 8,
    SystemFlagsInformation = 9,
    SystemCallTimeInformation = 10,
    SystemModuleInformation = 11,
    SystemLocksInformation = 12,
    SystemStackTraceInformation = 13,
    SystemPagedPoolInformation = 14,
    SystemNonPagedPoolInformation = 15,
    SystemHandleInformation = 16,
    SystemObjectInformation = 17,
    SystemPageFileInformation = 18,
    SystemVdmInstemulInformation = 19,
    SystemVdmBopInformation = 20,
    SystemFileCacheInformation = 21,
    SystemPoolTagInformation = 22,
    SystemInterruptInformation = 23,
    SystemDpcBehaviorInformation = 24,
    SystemFullMemoryInformation = 25,
    SystemLoadGdiDriverInformation = 26,
    SystemUnloadGdiDriverInformation = 27,
    SystemTimeAdjustmentInformation = 28,
    SystemSummaryMemoryInformation = 29,
    SystemMirrorMemoryInformation = 30,
    SystemPerformanceTraceInformation = 31,
    SystemObsolete0 = 32,
    SystemExceptionInformation = 33,
    SystemCrashDumpStateInformation = 34,
    SystemKernelDebuggerInformation = 35,
    SystemContextSwitchInformation = 36,
    SystemRegistryQuotaInformation = 37,
    SystemExtendedServiceTableInformation = 38,
    SystemPrioritySeparation = 39,
    SystemVerifierAddDriverInformation = 40,
    SystemVerifierRemoveDriverInformation = 41,
    SystemProcessorIdleInformation = 42,
    SystemLegacyDriverInformation = 43,
    SystemCurrentTimeZoneInformation = 44,
    SystemLookasideInformation = 45,
    SystemTimeSlipNotification = 46,
    SystemSessionCreate = 47,
    SystemSessionDetach = 48,
    SystemSessionInformation = 49,
    SystemRangeStartInformation = 50,
    SystemVerifierInformation = 51,
    SystemVerifierThunkExtend = 52,
    SystemSessionProcessInformation = 53,
    SystemLoadGdiDriverInSystemSpace = 54,
    SystemNumaProcessorMap = 55,
    SystemPrefetcherInformation = 56,
    SystemExtendedProcessInformation = 57,
    SystemRecommendedSharedDataAlignment = 58,
    SystemComPlusPackage = 59,
    SystemNumaAvailableMemory = 60,
    SystemProcessorPowerInformation = 61,
    SystemEmulationBasicInformation = 62,
    SystemEmulationProcessorInformation = 63,
    SystemExtendedHandleInformation = 64,
    SystemLostDelayedWriteInformation = 65,
    SystemBigPoolInformation = 66,
    SystemSessionPoolTagInformation = 67,
    SystemSessionMappedViewInformation = 68,
    SystemHotpatchInformation = 69,
    SystemObjectSecurityMode = 70,
    SystemWatchdogTimerHandler = 71,
    SystemWatchdogTimerInformation = 72,
    SystemLogicalProcessorInformation = 73,
    SystemWow64SharedInformationObsolete = 74,
    SystemRegisterFirmwareTableInformationHandler = 75,
    SystemFirmwareTableInformation = 76,
    SystemModuleInformationEx = 77,
    SystemVerifierTriageInformation = 78,
    SystemSuperfetchInformation = 79,
    SystemMemoryListInformation = 80,
    SystemFileCacheInformationEx = 81,
    SystemThreadPriorityClientIdInformation = 82,
    SystemProcessorIdleCycleTimeInformation = 83,
    SystemVerifierCancellationInformation = 84,
    SystemProcessorPowerInformationEx = 85,
    SystemRefTraceInformation = 86,
    SystemSpecialPoolInformation = 87,
    SystemProcessIdInformation = 88,
    SystemErrorPortInformation = 89,
    SystemBootEnvironmentInformation = 90,
    SystemHypervisorInformation = 91,
    SystemVerifierInformationEx = 92,
    SystemTimeZoneInformation = 93,
    SystemImageFileExecutionOptionsInformation = 94,
    SystemCoverageInformation = 95,
    SystemPrefetchPatchInformation = 96,
    SystemVerifierFaultsInformation = 97,
    SystemSystemPartitionInformation = 98,
    SystemSystemDiskInformation = 99,
    SystemProcessorPerformanceDistribution = 100,
    SystemNumaProximityNodeInformation = 101,
    SystemDynamicTimeZoneInformation = 102,
    SystemCodeIntegrityInformation = 103,
    SystemProcessorMicrocodeUpdateInformation = 104,
    SystemProcessorBrandString = 105,
    SystemVirtualAddressInformation = 106,
    SystemLogicalProcessorAndGroupInformation = 107,
    SystemProcessorCycleTimeInformation = 108,
    SystemStoreInformation = 109,
    SystemRegistryAppendString = 110,
    SystemAitSamplingValue = 111,
    SystemVhdBootInformation = 112,
    SystemCpuQuotaInformation = 113,
    SystemNativeBasicInformation = 114,
    SystemErrorPortTimeouts = 115,
    SystemLowPriorityIoInformation = 116,
    SystemBootEntropyInformation = 117,
    SystemVerifierCountersInformation = 118,
    SystemPagedPoolInformationEx = 119,
    SystemSystemPtesInformationEx = 120,
    SystemNodeDistanceInformation = 121,
    SystemAcpiAuditInformation = 122,
    SystemBasicPerformanceInformation = 123,
    SystemQueryPerformanceCounterInformation = 124,
    SystemSessionBigPoolInformation = 125,
    SystemBootGraphicsInformation = 126,
    SystemScrubPhysicalMemoryInformation = 127,
    SystemBadPageInformation = 128,
    SystemProcessorProfileControlArea = 129,
    SystemCombinePhysicalMemoryInformation = 130,
    SystemEntropyInterruptTimingInformation = 131,
    SystemConsoleInformation = 132,
    SystemPlatformBinaryInformation = 133,
    SystemThrottleNotificationInformation = 134,
    SystemPolicyInformation = 134,
    SystemHypervisorProcessorCountInformation = 135,
    SystemDeviceDataInformation = 136,
    SystemDeviceDataEnumerationInformation = 137,
    SystemMemoryTopologyInformation = 138,
    SystemMemoryChannelInformation = 139,
    SystemBootLogoInformation = 140,
    SystemProcessorPerformanceInformationEx = 141,
    SystemSpare0 = 142,
    SystemSecureBootPolicyInformation = 143,
    SystemPageFileInformationEx = 144,
    SystemSecureBootInformation = 145,
    SystemEntropyInterruptTimingRawInformation = 146,
    SystemPortableWorkspaceEfiLauncherInformation = 147,
    SystemFullProcessInformation = 148,
    SystemKernelDebuggerInformationEx = 149,
    SystemBootMetadataInformation = 150,
    SystemSoftRebootInformation = 151,
    SystemElamCertificateInformation = 152,
    SystemOfflineDumpConfigInformation = 153,
    SystemProcessorFeaturesInformation = 154,
    SystemRegistryReconciliationInformation = 155,
    SystemEdidInformation = 156,
    SystemManufacturingInformation = 157,
    SystemEnergyEstimationConfigInformation = 158,
    SystemHypervisorDetailInformation = 159,
    SystemProcessorCycleStatsInformation = 160,
    SystemVmGenerationCountInformation = 161,
    SystemTrustedPlatformModuleInformation = 162,
    SystemKernelDebuggerFlags = 163,
    SystemCodeIntegrityPolicyInformation = 164,
    SystemIsolatedUserModeInformation = 165,
    SystemHardwareSecurityTestInterfaceResultsInformation = 166,
    SystemSingleModuleInformation = 167,
    SystemAllowedCpuSetsInformation = 168,
    SystemDmaProtectionInformation = 169,
    SystemInterruptCpuSetsInformation = 170,
    SystemSecureBootPolicyFullInformation = 171,
    SystemCodeIntegrityPolicyFullInformation = 172,
    SystemAffinitizedInterruptProcessorInformation = 173,
    SystemRootSiloInformation = 174,
    SystemCpuSetInformation = 175,
    SystemCpuSetTagInformation = 176,
    MaxSystemInfoClass = 177
} SYSTEM_INFORMATION_CLASS;
For the record, when ZwOpenProcess is called, it will lead to another function called ObOpenObjectByPointer being called. Depending on the circumstances, that function can be used to open up a handle to a process without being caught up by any potential registered kernel-mode callbacks for ObRegisterCallbacks (used to enforce handle access checks -> restrict access rights to handles targeting a specific process, typically in Anti-Virus software as a self-protection mechanism).


Source code example


I've created demonstration code on how you can find the Process ID of a process given its image name (e.g. "notepad.exe") thanks to ZwQuerySystemInformation, and then proceed to terminate this process using: PsLookupProcessByProcessId; KeAttachProcess; ZwTerminateProcess; and KeDetachProcess.

driver.h
Code:
#pragma once
#include <ntifs.h>

#include "ntdef.h"
#include "ntexport.h"
#include "processmm.h"
ntdef.h
Code:
#pragma once
#include "driver.h"

typedef struct _SYSTEM_THREAD_INFORMATION {
    LARGE_INTEGER KernelTime;
    LARGE_INTEGER UserTime;
    LARGE_INTEGER CreateTime;
    ULONG WaitTime;
    PVOID StartAddress;
    CLIENT_ID ClientId;
    LONG Priority;
    LONG BasePriority;
    ULONG ContextSwitches;
    ULONG ThreadState;
    ULONG WaitReason;
}SYSTEM_THREAD_INFORMATION, *PSYSTEM_THREAD_INFORMATION;

typedef struct _SYSTEM_PROCESS_INFORMATION {
    ULONG NextEntryOffset;
    ULONG NumberOfThreads;
    LARGE_INTEGER WorkingSetPrivateSize;
    ULONG HardFaultCount;
    ULONG NumberOfThreadsHighWatermark;
    ULONGLONG CycleTime;
    LARGE_INTEGER CreateTime;
    LARGE_INTEGER UserTime;
    LARGE_INTEGER KernelTime;
    UNICODE_STRING ImageName;
    LONG BasePriority;
    PVOID UniqueProcessId;
    PVOID InheritedFromUniqueProcessId;
    ULONG HandleCount;
    ULONG SessionId;
    ULONG_PTR UniqueProcessKey;
    ULONG_PTR PeakVirtualSize;
    ULONG_PTR VirtualSize;
    ULONG PageFaultCount;
    ULONG_PTR PeakWorkingSetSize;
    ULONG_PTR WorkingSetSize;
    ULONG_PTR QuotaPeakPagedPoolUsage;
    ULONG_PTR QuotaPagedPoolUsage;
    ULONG_PTR QuotaPeakNonPagedPoolUsage;
    ULONG_PTR QuotaNonPagedPoolUsage;
    ULONG_PTR PagefileUsage;
    ULONG_PTR PeakPagefileUsage;
    ULONG_PTR PrivatePageCount;
    LARGE_INTEGER ReadOperationCount;
    LARGE_INTEGER WriteOperationCount;
    LARGE_INTEGER OtherOperationCount;
    LARGE_INTEGER ReadTransferCount;
    LARGE_INTEGER WriteTransferCount;
    LARGE_INTEGER OtherTransferCount;
    SYSTEM_THREAD_INFORMATION ProcessThreads[1];
}SYSTEM_PROCESS_INFORMATION, *PSYSTEM_PROCESS_INFORMATION;

typedef enum _SYSTEM_INFORMATION_CLASS {
    SystemBasicInformation = 0,
    SystemProcessorInformation = 1,
    SystemPerformanceInformation = 2,
    SystemTimeOfDayInformation = 3,
    SystemPathInformation = 4,
    SystemProcessInformation = 5,
    SystemCallCountInformation = 6,
    SystemDeviceInformation = 7,
    SystemProcessorPerformanceInformation = 8,
    SystemFlagsInformation = 9,
    SystemCallTimeInformation = 10,
    SystemModuleInformation = 11,
    SystemLocksInformation = 12,
    SystemStackTraceInformation = 13,
    SystemPagedPoolInformation = 14,
    SystemNonPagedPoolInformation = 15,
    SystemHandleInformation = 16,
    SystemObjectInformation = 17,
    SystemPageFileInformation = 18,
    SystemVdmInstemulInformation = 19,
    SystemVdmBopInformation = 20,
    SystemFileCacheInformation = 21,
    SystemPoolTagInformation = 22,
    SystemInterruptInformation = 23,
    SystemDpcBehaviorInformation = 24,
    SystemFullMemoryInformation = 25,
    SystemLoadGdiDriverInformation = 26,
    SystemUnloadGdiDriverInformation = 27,
    SystemTimeAdjustmentInformation = 28,
    SystemSummaryMemoryInformation = 29,
    SystemMirrorMemoryInformation = 30,
    SystemPerformanceTraceInformation = 31,
    SystemObsolete0 = 32,
    SystemExceptionInformation = 33,
    SystemCrashDumpStateInformation = 34,
    SystemKernelDebuggerInformation = 35,
    SystemContextSwitchInformation = 36,
    SystemRegistryQuotaInformation = 37,
    SystemExtendedServiceTableInformation = 38,
    SystemPrioritySeparation = 39,
    SystemVerifierAddDriverInformation = 40,
    SystemVerifierRemoveDriverInformation = 41,
    SystemProcessorIdleInformation = 42,
    SystemLegacyDriverInformation = 43,
    SystemCurrentTimeZoneInformation = 44,
    SystemLookasideInformation = 45,
    SystemTimeSlipNotification = 46,
    SystemSessionCreate = 47,
    SystemSessionDetach = 48,
    SystemSessionInformation = 49,
    SystemRangeStartInformation = 50,
    SystemVerifierInformation = 51,
    SystemVerifierThunkExtend = 52,
    SystemSessionProcessInformation = 53,
    SystemLoadGdiDriverInSystemSpace = 54,
    SystemNumaProcessorMap = 55,
    SystemPrefetcherInformation = 56,
    SystemExtendedProcessInformation = 57,
    SystemRecommendedSharedDataAlignment = 58,
    SystemComPlusPackage = 59,
    SystemNumaAvailableMemory = 60,
    SystemProcessorPowerInformation = 61,
    SystemEmulationBasicInformation = 62,
    SystemEmulationProcessorInformation = 63,
    SystemExtendedHandleInformation = 64,
    SystemLostDelayedWriteInformation = 65,
    SystemBigPoolInformation = 66,
    SystemSessionPoolTagInformation = 67,
    SystemSessionMappedViewInformation = 68,
    SystemHotpatchInformation = 69,
    SystemObjectSecurityMode = 70,
    SystemWatchdogTimerHandler = 71,
    SystemWatchdogTimerInformation = 72,
    SystemLogicalProcessorInformation = 73,
    SystemWow64SharedInformationObsolete = 74,
    SystemRegisterFirmwareTableInformationHandler = 75,
    SystemFirmwareTableInformation = 76,
    SystemModuleInformationEx = 77,
    SystemVerifierTriageInformation = 78,
    SystemSuperfetchInformation = 79,
    SystemMemoryListInformation = 80,
    SystemFileCacheInformationEx = 81,
    SystemThreadPriorityClientIdInformation = 82,
    SystemProcessorIdleCycleTimeInformation = 83,
    SystemVerifierCancellationInformation = 84,
    SystemProcessorPowerInformationEx = 85,
    SystemRefTraceInformation = 86,
    SystemSpecialPoolInformation = 87,
    SystemProcessIdInformation = 88,
    SystemErrorPortInformation = 89,
    SystemBootEnvironmentInformation = 90,
    SystemHypervisorInformation = 91,
    SystemVerifierInformationEx = 92,
    SystemTimeZoneInformation = 93,
    SystemImageFileExecutionOptionsInformation = 94,
    SystemCoverageInformation = 95,
    SystemPrefetchPatchInformation = 96,
    SystemVerifierFaultsInformation = 97,
    SystemSystemPartitionInformation = 98,
    SystemSystemDiskInformation = 99,
    SystemProcessorPerformanceDistribution = 100,
    SystemNumaProximityNodeInformation = 101,
    SystemDynamicTimeZoneInformation = 102,
    SystemCodeIntegrityInformation = 103,
    SystemProcessorMicrocodeUpdateInformation = 104,
    SystemProcessorBrandString = 105,
    SystemVirtualAddressInformation = 106,
    SystemLogicalProcessorAndGroupInformation = 107,
    SystemProcessorCycleTimeInformation = 108,
    SystemStoreInformation = 109,
    SystemRegistryAppendString = 110,
    SystemAitSamplingValue = 111,
    SystemVhdBootInformation = 112,
    SystemCpuQuotaInformation = 113,
    SystemNativeBasicInformation = 114,
    SystemErrorPortTimeouts = 115,
    SystemLowPriorityIoInformation = 116,
    SystemBootEntropyInformation = 117,
    SystemVerifierCountersInformation = 118,
    SystemPagedPoolInformationEx = 119,
    SystemSystemPtesInformationEx = 120,
    SystemNodeDistanceInformation = 121,
    SystemAcpiAuditInformation = 122,
    SystemBasicPerformanceInformation = 123,
    SystemQueryPerformanceCounterInformation = 124,
    SystemSessionBigPoolInformation = 125,
    SystemBootGraphicsInformation = 126,
    SystemScrubPhysicalMemoryInformation = 127,
    SystemBadPageInformation = 128,
    SystemProcessorProfileControlArea = 129,
    SystemCombinePhysicalMemoryInformation = 130,
    SystemEntropyInterruptTimingInformation = 131,
    SystemConsoleInformation = 132,
    SystemPlatformBinaryInformation = 133,
    SystemThrottleNotificationInformation = 134,
    SystemPolicyInformation = 134,
    SystemHypervisorProcessorCountInformation = 135,
    SystemDeviceDataInformation = 136,
    SystemDeviceDataEnumerationInformation = 137,
    SystemMemoryTopologyInformation = 138,
    SystemMemoryChannelInformation = 139,
    SystemBootLogoInformation = 140,
    SystemProcessorPerformanceInformationEx = 141,
    SystemSpare0 = 142,
    SystemSecureBootPolicyInformation = 143,
    SystemPageFileInformationEx = 144,
    SystemSecureBootInformation = 145,
    SystemEntropyInterruptTimingRawInformation = 146,
    SystemPortableWorkspaceEfiLauncherInformation = 147,
    SystemFullProcessInformation = 148,
    SystemKernelDebuggerInformationEx = 149,
    SystemBootMetadataInformation = 150,
    SystemSoftRebootInformation = 151,
    SystemElamCertificateInformation = 152,
    SystemOfflineDumpConfigInformation = 153,
    SystemProcessorFeaturesInformation = 154,
    SystemRegistryReconciliationInformation = 155,
    SystemEdidInformation = 156,
    SystemManufacturingInformation = 157,
    SystemEnergyEstimationConfigInformation = 158,
    SystemHypervisorDetailInformation = 159,
    SystemProcessorCycleStatsInformation = 160,
    SystemVmGenerationCountInformation = 161,
    SystemTrustedPlatformModuleInformation = 162,
    SystemKernelDebuggerFlags = 163,
    SystemCodeIntegrityPolicyInformation = 164,
    SystemIsolatedUserModeInformation = 165,
    SystemHardwareSecurityTestInterfaceResultsInformation = 166,
    SystemSingleModuleInformation = 167,
    SystemAllowedCpuSetsInformation = 168,
    SystemDmaProtectionInformation = 169,
    SystemInterruptCpuSetsInformation = 170,
    SystemSecureBootPolicyFullInformation = 171,
    SystemCodeIntegrityPolicyFullInformation = 172,
    SystemAffinitizedInterruptProcessorInformation = 173,
    SystemRootSiloInformation = 174,
    SystemCpuSetInformation = 175,
    SystemCpuSetTagInformation = 176,
    MaxSystemInfoClass = 177
} SYSTEM_INFORMATION_CLASS;
ntexport.h
Code:
#pragma once
#include "driver.h"

typedef NTSTATUS(NTAPI *pZwQuerySystemInformation)(
    SYSTEM_INFORMATION_CLASS SystemInformationClass,
    PVOID SystemInformation,
    ULONG SystemInformationLength,
    PULONG ReturnLength OPTIONAL
    );

extern pZwQuerySystemInformation fZwQuerySystemInformation;

NTSTATUS SetupExports();
processmm.h
Code:
#pragma once
#include "driver.h"

NTSTATUS ReturnProcessStruct(
    ULONG ProcessId,
    PEPROCESS *Process
);

ULONG ReturnProcessId(
    WCHAR *ProcessName
);

NTSTATUS TerminateProcess(
    PEPROCESS Process
);

NTSTATUS WrapperTerminateProcess(
    ULONG ProcessId
);
driver.c
Code:
#include "driver.h"

DEVICE_OBJECT DeviceObject = { 0 };

UNICODE_STRING usDriverInfo[2] = { 
    RTL_CONSTANT_STRING(L"\\Device\\KmdfExample"), 
    RTL_CONSTANT_STRING(L"\\DosDevices\\KmdfExample")
};

VOID TerminateTest()
{
    NTSTATUS NtStatus = STATUS_SUCCESS;
    ULONG ProcessId = 0;
   
    NtStatus = SetupExports();

    if (!NT_SUCCESS(NtStatus))
    {
        return;
    }

    ProcessId = ReturnProcessId(L"notepad.exe");

    if (!ProcessId)
    {
        DbgPrint("The process could not be found\n");

        return;
    }

    DbgPrint("Process ID: %d", ProcessId);

    NtStatus = WrapperTerminateProcess(ProcessId);

    if (NT_SUCCESS(NtStatus))
    {
        DbgPrint("The process was terminated from kernel-mode\n");
    }
    else
    {
        DbgPrint("The process could not be terminated from kernel-mode\n");
    }
}

VOID DriverUnload(
    PDRIVER_OBJECT DriverObject
)
{
    IoDeleteSymbolicLink(&usDriverInfo[1]);
    IoDeleteDevice(DriverObject->DeviceObject);
}

NTSTATUS DriverEntry(
    PDRIVER_OBJECT DriverObject,
    PUNICODE_STRING RegistryPath
)
{
    UNREFERENCED_PARAMETER(RegistryPath);

    NTSTATUS NtStatus = STATUS_SUCCESS;

    NtStatus = IoCreateDevice(DriverObject,
        NULL,
        &usDriverInfo[0],
        FILE_DEVICE_UNKNOWN,
        FILE_DEVICE_SECURE_OPEN,
        FALSE,
        &DeviceObject);

    if (!NT_SUCCESS(NtStatus))
    {
        return NtStatus;
    }

    NtStatus = IoCreateSymbolicLink(&usDriverInfo[1],
        &usDriverInfo[0]);

    if (!NT_SUCCESS(NtStatus))
    {
        return NtStatus;
    }

    DriverObject->DriverUnload = DriverUnload;

    TerminateTest();

    return NtStatus;
}

ntexport.c

Code:
#include "driver.h"

pZwQuerySystemInformation fZwQuerySystemInformation;

NTSTATUS SetupExports()
{
    PVOID FunctionAddress = 0;
    UNICODE_STRING usSystemRoutineName = RTL_CONSTANT_STRING(L"ZwQuerySystemInformation");

    FunctionAddress = MmGetSystemRoutineAddress(&usSystemRoutineName);

    if (!FunctionAddress)
    {
        return STATUS_UNSUCCESSFUL;
    }

    fZwQuerySystemInformation = (pZwQuerySystemInformation)FunctionAddress;

    if (!fZwQuerySystemInformation)
    {
        return STATUS_UNSUCCESSFUL;
    }

    return STATUS_SUCCESS;
}

processmm.c

Code:
#include "driver.h"

NTSTATUS ReturnProcessStruct(
    ULONG ProcessId,
    PEPROCESS *Process
)
{
    NTSTATUS NtStatus = STATUS_SUCCESS;

    NtStatus = PsLookupProcessByProcessId((HANDLE)ProcessId,
        Process);
   
    return NtStatus;
}

ULONG ReturnProcessId(
    WCHAR *ProcessName
)
{
    NTSTATUS NtStatus = STATUS_SUCCESS;
    PSYSTEM_PROCESS_INFORMATION SystemProcessInfo = { 0 };
    PVOID BufferMemory = 0;
    SIZE_T RegionSize = 0;
    ULONG ReturnLength = 0, ProcessId = 0;

    NtStatus = fZwQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemProcessInformation,
        NULL,
        NULL,
        &ReturnLength);

    if (!ReturnLength)
    {
        return ProcessId;
    }

    RegionSize = (SIZE_T)ReturnLength;

    BufferMemory = ExAllocatePool(NonPagedPool,
        RegionSize);

    if (!BufferMemory)
    {
        return ProcessId;
    }

    NtStatus = fZwQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemProcessInformation,
        BufferMemory,
        ReturnLength,
        NULL);

    if (!NT_SUCCESS(NtStatus))
    {
        return ProcessId;
    }

    SystemProcessInfo = (PSYSTEM_PROCESS_INFORMATION)BufferMemory;

    while (SystemProcessInfo->NextEntryOffset)
    {
        SystemProcessInfo = (SYSTEM_PROCESS_INFORMATION*)((UCHAR*)SystemProcessInfo + SystemProcessInfo->NextEntryOffset);

        if (!wcscmp(SystemProcessInfo->ImageName.Buffer, ProcessName))
        {
            ProcessId = (ULONG)SystemProcessInfo->UniqueProcessId;
   
            break;
        }
    }

    if (BufferMemory)
    {
        ExFreePool(BufferMemory);
    }

    return ProcessId;
}

NTSTATUS TerminateProcess(
    PEPROCESS Process
)
{
    NTSTATUS NtStatus = STATUS_UNSUCCESSFUL;

    __try {
        KeAttachProcess(Process);

        NtStatus = ZwTerminateProcess(NtCurrentProcess(),
            NULL);

        KeDetachProcess();
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
    }

    return NtStatus;
}

NTSTATUS WrapperTerminateProcess(
    ULONG ProcessId
)
{
    NTSTATUS NtStatus = STATUS_SUCCESS;
    PEPROCESS Process = { 0 };

    NtStatus = ReturnProcessStruct(ProcessId,
        &Process);

    if (!NT_SUCCESS(NtStatus))
    {
        return STATUS_UNSUCCESSFUL;
    }

    NtStatus = TerminateProcess(Process);

    return NtStatus;
}


There are many different ways to do things in kernel-mode, this thread is just for demonstration purposes to provide an example of another approach to opening a process handle with ZwOpenProcess for process termination (since you can attach to it and execute within the context of the process remotely in kernel-mode, which means you are able to call ZwTerminateProcess and pass a pseudo-handle of (HANDLE)-1 to represent the current process you're in the context of, which results in the attached-to process being terminated).


Thank you for reading.
 

tim one

Level 21
Trusted
AV-Tester
Joined
Jul 31, 2014
Messages
1,072
OS
Windows 10
Antivirus
F-Secure
#2
Thanks for this really interesting input mate:)

I remember when I was studying this stuff that each routine can be translated to its service index by a formula (I don't remember what :p)
However this would suggest that every kernel service routine has its own service number stored in 4 bytes after the first byte of its opcodes.

ZwTerminateProcess has its service index and once we have the correct service number for the API we want to hook, we could replace it in the descriptor table disabling write protection with the CR0 CPU register, setting the new routine address and then restoring CR0 protection.
 

Opcode

Level 28
Content Creator
Joined
Aug 17, 2017
Messages
1,733
#3
ZwTerminateProcess has its service index and once we have the correct service number for the API we want to hook, we could replace it in the descriptor table disabling write protection with the CR0 CPU register, setting the new routine address and then restoring CR0 protection.
Code:
typedef struct _KSERVICE_DESCRIPTOR_TABLE
{
   PULONG ServiceTableBase;
   PULONG ServiceCounterTableBase;
   ULONG NumberOfServices;
   PUCHAR ParamTableBase;
}KSERVICE_DESCRIPTOR_TABLE,*PKSERVICE_DESCRIPTOR_TABLE;
ServiceTableBase is the one used for retrieving the address, however for 64-bit systems it becomes a lot more complicated since the SSDT isn't even exported by ntoskrnl.exe on x64. There is also the "shadow" table.

The System Service Dispatch Table doesn't apply for kernel-mode only functions though, therefore you cannot hook functions such as ExAllocatePool/WithTag, PsLookupProcessByProcessId, KeAttachProcess/KeDetachProcess, ObOpenObjectByPointer, etc. via altering the pointer addresses within the SSDT. You'd have to perform inline hooking (byte-patching, aka. detouring) to hook functions like those ones. Such kernel-mode only routines don't have an entry within the SSDT!

If the SSDT is patched then calling routines in kernel-mode will still trigger the patch as long as that function was targeted, but you don't have to rely on the addresses within the SSDT. You can actually scan the exports for ntoskrnl.exe to retrieve the addresses; just be cautious with calling Nt* functions in kernel-mode because Zw* stubs in kernel-mode perform operations before the Nt* call which is why Zw* is used in kernel-mode usually, and without those extra operations, the Nt* function call will fail! It is to do with the previous mode (must be changed before the Nt* call). Scanning the exports of ntoskrnl.exe is a technique which can be used for comparisons to detect manipulations to the pointer addresses in the SSDT as well.

The SSDT is essentially an array containing pointer addresses, and the address pointer for a specific NTAPI function is retrieved using the value within the EAX register internally by the OS when NTDLL exports are called in user-mode for system call stubs (e.g. Nt*/Zw* functions, whereas Ldr*/Rtl* functions don't perform system calls themselves -> redirect to Nt*/Zw* stubs). When you use Zw* in kernel-mode, the stubs will still move the index into the EAX register (although not identical to the user-mode stubs of course because you'd already be in kernel-mode, no need for a system call). I am not actually sure why there is both Zw* and Nt* in user-mode (NTDLL.DLL) because the function prologues are identical for all Nt*/Zw* functions, unlike in kernel-mode. Maybe they had different plans in the past but left it like that so people can use whichever they prefer to look at in UM...

The index value for the system service routine is stored in the EAX register. :)
 

tim one

Level 21
Trusted
AV-Tester
Joined
Jul 31, 2014
Messages
1,072
OS
Windows 10
Antivirus
F-Secure
#4
The CR0 register is used to control kernel write protection (depending on the flag you will be able to patch the System Service Dispatch Table (SSDT)). While modifying the value within the CR0 register and re-modifying it afterwards does work, it doesn't tend to be as stable as relying on MDL (Memory Descriptor List). :)

Code:
typedef struct _KSERVICE_DESCRIPTOR_TABLE
{
   PULONG ServiceTableBase;
   PULONG ServiceCounterTableBase;
   ULONG NumberOfServices;
   PUCHAR ParamTableBase;
}KSERVICE_DESCRIPTOR_TABLE,*PKSERVICE_DESCRIPTOR_TABLE;
ServiceTableBase is the one used for retrieving the address, however for 64-bit systems it becomes a lot more complicated since the SSDT isn't even exported by ntoskrnl.exe on x64. There is also the "shadow" table.

The System Service Dispatch Table doesn't apply for kernel-mode only functions though, therefore you cannot hook functions such as ExAllocatePool/WithTag, PsLookupProcessByProcessId, KeAttachProcess/KeDetachProcess, ObOpenObjectByPointer, etc. via altering the pointer addresses within the SSDT. You'd have to perform inline hooking (byte-patching, aka. detouring) to hook functions like those ones. Such kernel-mode only routines don't have an entry within the SSDT!

If the SSDT is patched then calling routines in kernel-mode will still trigger the patch as long as that function was targeted, but you don't have to rely on the addresses within the SSDT. You can actually scan the exports for ntoskrnl.exe to retrieve the addresses; just be cautious with calling Nt* functions in kernel-mode because Zw* stubs in kernel-mode perform operations before the Nt* call which is why Zw* is used in kernel-mode usually, and without those extra operations, the Nt* function call will fail! It is to do with the previous mode (must be changed before the Nt* call). Scanning the exports of ntoskrnl.exe is a technique which can be used for comparisons to detect manipulations to the pointer addresses in the SSDT as well.

The SSDT is essentially an array containing pointer addresses, and the address pointer for a specific NTAPI function is retrieved using the value within the EAX register internally by the OS when NTDLL exports are called in user-mode for system call stubs (e.g. Nt*/Zw* functions, whereas Ldr*/Rtl* functions don't perform system calls themselves -> redirect to Nt*/Zw* stubs). When you use Zw* in kernel-mode, the stubs will still move the index into the EAX register (although not identical to the user-mode stubs of course because you'd already be in kernel-mode, no need for a system call). I am not actually sure why there is both Zw* and Nt* in user-mode (NTDLL.DLL) because the function prologues are identical for all Nt*/Zw* functions, unlike in kernel-mode. Maybe they had different plans in the past but left it like that so people can use whichever they prefer to look at in UM...

The index value for the system service routine is stored in the EAX register. :)
Thanks so much for the clarification, really helpful for me, indeed I had this doubt ;)
 

Opcode

Level 28
Content Creator
Joined
Aug 17, 2017
Messages
1,733
#5
Update.

I've created a quick demonstration example on how you can open a handle to a process via ObOpenObjectByPointer (NTOSKRNL). Thankfully, the function is actually documented which did surprise me in the past; this function will be called by NtOpenProcess (NTOSKRNL - you use ZwOpenProcess which changes the Previous Mode so the call will succeed since you're already in kernel-mode, this leads to NtOpenProcess since the address is taken from the SSDT -> ObOpenObjectByPointer called by NtOpenProcess).

Below is the function prototype from MSDN.

Code:
NTSTATUS ObOpenObjectByPointer(
  _In_     PVOID           Object,
  _In_     ULONG           HandleAttributes,
  _In_opt_ PACCESS_STATE   PassedAccessState,
  _In_     ACCESS_MASK     DesiredAccess,
  _In_opt_ POBJECT_TYPE    ObjectType,
  _In_     KPROCESSOR_MODE AccessMode,
  _Out_    PHANDLE         Handle
);
Before we can call ObOpenObjectByPointer to open a handle to the target process, we must retrieve a value within an PEPROCESS structure (pointer structure for EPROCESS) for the process, which can be acquired via PsLookupProcessByProcessId which we previously saw in the source code example in the original thread post (before we attached via KeAttachProcess).

Code:
NTSTATUS ReturnProcessStruct(
    ULONG ProcessId,
    PEPROCESS *Process
)
{
    NTSTATUS NtStatus = STATUS_SUCCESS;

    NtStatus = PsLookupProcessByProcessId((HANDLE)ProcessId,
        Process);
 
    return NtStatus;
}
We use the returned PEPROCESS structure for the process in the first parameter of ObOpenObjectByPointer, since it represents the target object (the process we are targeting).

The second parameter, HandleAttributes (data-type ULONG) can be left NULL (and usually is) but since I am working in kernel-mode only for the returned handle, I will set it to OBJ_KERNEL_HANDLE. This means the opened handle will only be usable in kernel-mode, therefore sending it back down to user-mode (e.g. via LPC (Local Procedure Call), Named Pipes (-> IoCreateFile can be used to open a handle to a named pipe or create one -> FltFsControlFile and functions like ZwWriteFile/NtReadFile are useful for re-writing other user-mode Win32 API pipe APIs for KM support) will be a no-go because the handle won't be usable in user-mode.

The third parameter is for the access state and I've left it as NULL because I do not need to perform changes for this. When you pass in NULL, ObOpenObjectByPointer will create one automatically (internally).

The fourth parameter is for the desired access just like when calling Zw/NtOpenProcess (both KM/UM). You can manually define access types such as PROCESS_TERMINATE or PROCESS_SUSPEND_RESUME (although note that PROCESS_SUSPEND_RESUME will be useless unless you have access to NtSuspendThread/NtSuspendProcess -> NtSuspendProcess uses NtSuspendThread as it works by enumerating all the target processes' threads via ZwQuerySystemInformation and then calls NtSuspendThread - neither are exported by ntoskrnl.exe but the addresses can still be acquired from the SSDT). For demonstration purposes, I left this as PROCESS_ALL_ACCESS (besides, we're in kernel-mode so make the most of it!).

The fifth parameter was left as NULL because the AccessMode is KernelMode. No need to mess with AccessMode parameter in my scenario.

The sixth parameter is the access mode and as we already know, it is set to KernelMode. You can either set this as UserMode or KernelMode.

The last parameter is just for where the process handle will go too.

For more information, just read through the MSDN documentation for this function.

ObOpenObjectByPointer will internally call a function called ObpCreateHandle.


Source code example

Code:
typedef NTSTATUS(NTAPI *pObOpenObjectByPointer)(
    PVOID Object,
    ULONG HandleAttributes,
    PACCESS_STATE PassedAccessState,
    ACCESS_MASK DesiredAccess,
    POBJECT_TYPE ObjectType,
    KPROCESSOR_MODE AccessMode,
    PHANDLE Handle
    );
Code:
pZwQuerySystemInformation fZwQuerySystemInformation;
pObOpenObjectByPointer fObOpenObjectByPointer;

NTSTATUS SetupExports()
{
    UNICODE_STRING usSystemRoutineName[2] = {
        RTL_CONSTANT_STRING(L"ZwQuerySystemInformation"),
        RTL_CONSTANT_STRING(L"ObOpenObjectByPointer"),
    };

    PVOID FunctionAddresses[2] = {
        MmGetSystemRoutineAddress(&usSystemRoutineName[0]),
        MmGetSystemRoutineAddress(&usSystemRoutineName[1])
    };

    if (!FunctionAddresses[0] ||
        !FunctionAddresses[1])
    {
        return STATUS_UNSUCCESSFUL;
    }

    fZwQuerySystemInformation = (pZwQuerySystemInformation)FunctionAddresses[0];
    fObOpenObjectByPointer = (pObOpenObjectByPointer)FunctionAddresses[1];

    if (!fZwQuerySystemInformation ||
        !fObOpenObjectByPointer)
    {
        return STATUS_UNSUCCESSFUL;
    }

    return STATUS_SUCCESS;
}
Code:
HANDLE WrapperNtOpenProcess(
    ULONG ProcessId,
    ACCESS_MASK DesiredAccess
)
{
    NTSTATUS NtStatus = STATUS_SUCCESS;
    HANDLE ProcessHandle = 0;
    PEPROCESS Process = { 0 };

    NtStatus = ReturnProcessStruct(ProcessId,
        &Process);

    if (!NT_SUCCESS(NtStatus))
    {
        return 0;
    }

    NtStatus = fObOpenObjectByPointer(Process,
        OBJ_KERNEL_HANDLE,
        NULL,
        DesiredAccess,
        NULL,
        KernelMode,
        &ProcessHandle);

    return ProcessHandle;
}
Code:
VOID TerminateTest()
{
    NTSTATUS NtStatus = STATUS_SUCCESS;
    HANDLE ProcessHandle = 0;
    ULONG ProcessId = 0;
 
    NtStatus = SetupExports();

    if (!NT_SUCCESS(NtStatus))
    {
        return;
    }

    ProcessId = ReturnProcessId(L"notepad.exe");

    if (!ProcessId)
    {
        DbgPrint("The process could not be found\n");

        return;
    }

    DbgPrint("Process ID: %d", ProcessId);

    //NtStatus = WrapperTerminateProcess(ProcessId);

    ProcessHandle = WrapperNtOpenProcess(ProcessId,
        PROCESS_ALL_ACCESS);

    if (!ProcessHandle)
    {
        DbgPrint("The handle to the process could not be opened\n");

        return;
    }

    NtStatus = ZwTerminateProcess(ProcessHandle, NULL);

    if (NT_SUCCESS(NtStatus))
    {
        DbgPrint("The process was terminated from kernel-mode\n");
    }
    else
    {
        DbgPrint("The process could not be terminated from kernel-mode\n");
    }
}


The code is just modified parts from the original since I added in support for ObOpenObjectByPointer alongside attaching for termination. This is why I am using MmGetSystemRoutineAddress for both ZwQuerySystemInformation and ObOpenObjectByPointer, and why the call to WrapperTerminateProcess(...) is commented out in the test function (VOID). The modification will just open a handle via ObOpenObjectByPointer and if it is successful opening the handle the process is terminated with ZwTerminateProcess.


Thanks for reading! :)
 
Joined
Mar 13, 2017
Messages
29
OS
Windows 10
Antivirus
ESET
#6
Just saw your posts. What you are doing is the incredible job. Your posts are easy to understand, despite subject is very hard, which means you really know deeply what you are talking about, thank you.
Your posts make our lives easier and more interesting, also they motivate me to learn and practice more.
 

Opcode

Level 28
Content Creator
Joined
Aug 17, 2017
Messages
1,733
#7
Just saw your posts. What you are doing is the incredible job. Your posts are easy to understand, despite subject is very hard, which means you really know deeply what you are talking about, thank you.
Your posts make our lives easier and more interesting, also they motivate me to learn and practice more.
I would like to thank you for your kind words, they do mean a lot. The aim is to help people trying to learn and make their lives easier, you've put my mind at rest! :)
 

Opcode

Level 28
Content Creator
Joined
Aug 17, 2017
Messages
1,733
#8

Opcode

Level 28
Content Creator
Joined
Aug 17, 2017
Messages
1,733
#9
It can't hurt to update information I recently put here with more detail.

You can actually scan the exports for ntoskrnl.exe to retrieve the addresses; just be cautious with calling Nt* functions in kernel-mode because Zw* stubs in kernel-mode perform operations before the Nt* call which is why Zw* is used in kernel-mode usually, and without those extra operations, the Nt* function call will fail! It is to do with the previous mode (must be changed before the Nt* call).
Firstly, I suggest you read this article from OSR from 2003 and then keep reading this post: The NT Insider:Nt vs. Zw - Clearing Confusion On The Native API

In user-mode, NTDLL is the user-mode to kernel-mode transition gateway as we are all already aware. This is because the Native API routines exported by NTDLL will use instructions like int SYSENTER, SYSCALL (depends on the architecture/OS version) or even in the really early days when the Windows Kernel was built differently, interrupts were used (int 21h)!

NTDLL exports a Nt* and Zw* variant for each Native API system-call routine. The Ldr* prefix represents the "Loader" routines and Ldr* and Rtl* do not perform system-calls. The Nt*/Zw* routines perform the system calls, and these can also be called by the Ldr/Rtl routines sometimes; the interesting thing is that the Nt/Zw routines are not separated... If you get the address to NtTerminateProcess (NTDLL) and get the address to ZwTerminateProcess (NTDLL), the addresses will be identical. Both name versions of the routine will point to the exact same routine in-memory. I suspect that Microsoft had plans to do something with the different variants in the early days, changed their minds, but left them both there for backwards compatibility. If Microsoft revoke the Zw* name usage, a lot of software which uses NTDLL routines and where the authors prefer "Zw" to "Nt" will stop working unexpectedly.

In kernel-mode, 99% of the time you will be using Zw* instead of Nt*. You can still use the Nt* prefix routines (some are available by default such as NtOpenProcess) but it's unrecommended and a bad idea. The Native API routines reside in kernel-mode memory and they are exported by NTOSKRNL (the Windows Kernel), and there are also Nt*/Zw* stubs in kernel-mode. However, they do not point to the same address in-memory.

Zw* routines have the task of changing the PreviousMode of the current thread. The PreviousMode can either be UserMode or KernelMode (UserMode is 1, KernelMode is 0). The PreviousMode is tracked under the KTHREAD structure for the thread in discussion, and has a data-type of KPROCESSOR_MODE. Since the field for the PreviousMode is under KTHREAD, an undocumented and opaque structure, you cannot access the fields of KTHREAD directly to read/write to the PreviousMode in memory. Microsoft do provide a documented API to determine the current PreviousMode of the thread you're executing under the context of however.

When a user-mode program invokes a Native API routine from NTDLL (either directly or indirectly), execution flow lands at KiSystemCallXx eventually. KiSystemCallXx passes onto other routines, like KiSystemServiceRepeat. The purpose of all these routines is to extract the address of the Nt* routine from NTOSKRNL with the KeServiceDescriptorTable (or the shadow for win32k APIs). Execution flow then moves to the address returned from the KeServiceDescriptorTable (aka. SSDT). KiSystemCallXx = KiSystemCall32 or KiSystemCall64. The address of KiSystemCallXx can be taken from the IA32_LSTAR register, in-case you need to locate the address; no need to do a pattern scan of NTOSKRNL to find it if necessary since its non-exported.

This means that the Zw* stubs in kernel-mode are not invoked when a system call is performed. Execution flow goes to the Native API routine eventually, unlike with kernel-mode device drivers which use Zw* routines. With kernel-mode device drivers, it will go to the Zw* routine and then it will lead to the Nt* routine after KiSystemCallXx -> ... -> .... -> eventually the Nt* routine. This means that kernel-mode device drivers using the Native API will have their PreviousMode changed for the current thread, but this doesn't happen when user-mode requests are performed.

The Nt* routines will check the PreviousMode of the current thread. If the PreviousMode is not KernelMode, then more restrictions are applied. This is done for many reasons, such as separating permission for user-mode requests to use the Native API whilst passing Kernel-Mode addresses. If this isn't done like it is, it'd be a bigger security risk because then user-mode programs could calculate the address of a system service routine from user-mode by reading NTOSKRNL into memory and what-not, and then use these addresses via NTAPI (NTDLL -> system call -> kernel). But because of the PreviousMode restriction, now the Windows Kernel is aware of the trust level.

Therefore, Nt*/Zw* is the same in User-Mode. However in Kernel-Mode it is not the same, and in Kernel-Mode, you tend to use Zw* instead of Nt* because the PreviousMode is handled for you, ensuring you are not accidentally treated as a User-Mode caller.

You can change the PreviousMode of the current thread manually in kernel-mode if you wish to use Nt* APIs whilst the PreviousMode is not KernelMode, but it is undocumented, hacky and unreliable. The offsets may change at any given time, and if the offsets change and you still go ahead and do it without realising (e.g. after a patch update), you'll mess things up and cause a bug-check.

This is the difference though, and why you should stick to Zw* in kernel-mode unless you really must use Nt*. Even if you find the address of non-exported Nt* routines from the KeServiceDescriptorTable, you still have that limitation unless you handle the PreviousMode.
 

ntskrnl

New Member
Joined
Jan 20, 2018
Messages
2
#10
@Opcode

Thank you for your tutorials, they cleared some doubts I had about this type of hooking. However, I still have a doubt.

I've been trying to do some tests on a Win8.1 VM, and hooking any Zw* is working smoothly. However, the problem comes when hooking non Zw* functions. I wanted to check how to hook GetFileAttributes (GetFileAttributes function (Windows)) but it seems to be in kernel32, so my guess is that it goes into NtQueryAttributesFile but I don't see anything in there or I have problems hooking it (ntdll.dll). I tried getting the memory address of the function from ntdll.dll and index dynamically, but I don't know how to get into what GetFileAttributes is checking at kernel-level.

Any tips?
Regards!
 

Opcode

Level 28
Content Creator
Joined
Aug 17, 2017
Messages
1,733
#11
@ntskrnl

GetFileAttributes (KERNEL32) is only still around for compatibility purposes; starting from Windows 8, Microsoft have moved a lot of the Win32 API routines which originally existed in Kernel32.dll over to KernelBase.dll. The GetFileAttributes (KERNEL32) routine will call GetFileAttributes (KERNELBASE).

GetFileAttributes (KERNELBASE) will call the following Native API routines (in-order).
1. RtlDosPathNameToNtPathName_U_WithStatus (NTDLL)
2. NtQueryAttributesFile (NTDLL)

The reason RtlDosPathNameToNtPathName_U_WithStatus is called before NtQueryAttributesFile is because the GetFileAttributes routine takes in a file path which follows the DOS format. An example of a DOS file-format would be "C:\\ntskrnl.exe". It must convert the DOS File Path to an NT File Path so the NtQueryAttributesFile routine can understand it.

NtQueryAttributesFile (NTDLL) will perform a system call and then it will land at NtQueryAttributesFile (NTOSKRNL).

With all of this being said, hooking is a bad idea unless you really need to do it - you can use a File System Mini-Filter device driver to intercept file system operations system-wide with FltRegisterFilter (a documented and stable approach).
 
Last edited:

ntskrnl

New Member
Joined
Jan 20, 2018
Messages
2
#12
@ntskrnl

GetFileAttributes (KERNEL32) is only still around for compatibility purposes; starting from Windows 8, Microsoft have moved a lot of the Win32 API routines which originally existed in Kernel32.dll over to KernelBase.dll. The GetFileAttributes (KERNEL32) routine will call GetFileAttributes (KERNELBASE).

GetFileAttributes (KERNELBASE) will call the following Native API routines (in-order).
1. RtlDosPathNameToNtPathName_U_WithStatus (NTDLL)
2. NtQueryAttributesFile (NTDLL)

The reason RtlDosPathNameToNtPathName_U_WithStatus is called before NtQueryAttributesFile is because the GetFileAttributes routine takes in a file path which follows the DOS format. An example of a DOS file-format would be "C:\\ntskrnl.exe". It must convert the DOS File Path to an NT File Path so the NtQueryAttributesFile routine can understand it.

NtQueryAttributesFile (NTDLL) will perform a system call and then it will land at NtQueryAttributesFile (NTOSKRNL).

With all of this being said, hooking is a bad idea unless you really need to do it - you can use a File System Mini-Filter device driver to intercept file system operations system-wide with FltRegisterFilter (a documented and stable approach).
Thank you for your answer. I have a FileSystem driver which already gets me to where I want, but I also wanted to try the approach of checking it through hooking. I was trying to hook the NtQueryAttributesFile from NTDLL on a Win8.1 so that might have been the problem. Another option might be callbacks I guess then.
 

Opcode

Level 28
Content Creator
Joined
Aug 17, 2017
Messages
1,733
#13
@ntskrnl

On 32-bit systems you could technically patch NtQueryAttributesFile (NTOSKRNL) but the problem with this is that Microsoft don't officially support this behavior (in fact they are very much against it and go to great lengths to stop it from happening with their 64-bit kernels) and there's no telling when a problem will arise.

On a 64-bit system you can also patch the Windows Kernel, but it isn't as straight-forward as on a 32-bit environment. It's a common myth that you cannot patch the Windows Kernel on a 64-bit environment due to PatchGuard, but this really isn't the case. PatchGuard is timer-based and when the timer is triggered it will execute various routines to validate the authenticity of: various kernel-mode structures (e.g. due to Direct Kernel Object Manipulation), kernel routine function prologues (e.g. due to run-time byte patching of NTOSKRNL routines), pointer addresses held within the System Service Descriptor Table (accessible via KeServiceDescriptorTable/KeServiceDescriptorTableShadow), among other things. You can patch the Windows Kernel, however you'll end up bug-checking the system if the patch is detected.

If you wish to patch the Windows Kernel with 64-bit support you have a few options.
1. Bypass Kernel-Patch Protection (unrecommended)
2. Disable PatchGuard (unrecommended)
3. Find a target within the Windows Kernel which you can patch and is not within the region scope of PatchGuard scanning (unrecommended but it works)
4. Virtualization -> use Extended Page Table (EPT) / Rapid Virtualization Indexing (RVI) as the key to hiding any modifications from PatchGuard

The 4th option on the above list is the standard route to go if it is truly necessary to patch the Windows Kernel - a few security software vendors take this route so they can emulate KiSystemCall32/KiSystemCall64 (there's new variants now due to the recent Meltdown patch update) by patching MSRs and pointing to their emulation callback routines. With the help of EPT / RVI (former for Intel, latter for AMD as far as I am aware - both are hardware-assisted for the record) you can hide the patches from PatchGuard (and thus it will not detect them to throw an invocation to KeBugCheckEx).

Even when dealing with virtualisation though, patching the Windows Kernel will still be an unstable route to go, and it may even open up additional vulnerabilities.

I was trying to hook the NtQueryAttributesFile from NTDLL on a Win8.1 so that might have been the problem
NTDLL is a user-mode module for user-mode processes; patching NTDLL for targeted processes is fine. User-mode patching techniques are actually extremely common, and the advantage is that they only affect the targeted processes (unlike with kernel-mode patching which is system-wide and thus affects performance for every single process (and in some cases, kernel-mode software too - depends which kernel-mode patching technique has been applied)). The disadvantage however is that user-mode hooks can usually be bypassed relatively easily via a duplicate of NTDLL or a custom system call stub setup and executed (there are however solution's to this to a reasonable extent).

I think you're best bet would be:
1. Sticking to your File System Mini-Filter device driver (recommended)
2. User-mode patching (unrecommended)

If you've already got what you need to do working with the File System Mini-Filter device driver then it's probably best you stick to this considering it's all officially supported by Microsoft, officially documented, and last but not least... Stable!

I really would not recommend you dip into all these hacky rootkit-like techniques unless there is absolutely no other stable/officially supported mechanism for what you need to do, or a better idea to approach the desired feature, because such is likely to cause more hassle than good and can mess up unexpectedly across different environments. Even with user-mode hooking, you risk having a potentially buggy callback routine which may crash unexpectedly and cause data loss to an enterprise using your solution, or introducing additional vulnerabilities into targeted software (e.g. if you have a proxy routine for an NTDLL system call stub in-memory and malware locates the address with pattern scanning, now it can bypass your own hook using your own callback routine, and this also counts as a system-call).

File System Mini-Filter device driver intercepts from kernel-mode which means system calls from user-mode will not bypass it, so as long as the code-base for your driver is stable and doesn't hold up operations for too long, it should be good performance and stability wise. Make sure that you are careful with any shared communication between your kernel-mode and user-mode components though, otherwise that could be a potential target for attackers to exploit. :)

Kernel-Mode callbacks are the best approach if it is applicable. If you have already accomplished what you need to do with FltRegisterFilter, stick to it. :)