Prologue
Kernel Callbacks
Pre / Post operations (can be registered with ObRegisterCallbacks and talked about it in the 2nd part).
PsSet*NotifyRoutine.
CmRegisterCallbackEx.
ObRegisterCallbacks
1
2
3
4
5
6
7
typedef struct _OB_CALLBACK_REGISTRATION {
USHORT Version;
USHORT OperationRegistrationCount;
UNICODE_STRING Altitude;
PVOID RegistrationContext;
OB_OPERATION_REGISTRATION *OperationRegistration;
} OB_CALLBACK_REGISTRATION, *POB_CALLBACK_REGISTRATION;Version MUST be OB_FLT_REGISTRATION_VERSION. OperationRegistrationCount is the number of registered callbacks. Altitude is a unique identifier in form of a string with this pattern #define OB_CALLBACKS_ALTITUDE L"XXXXX.XXXX" where X is a number. Altitude isn't unique the registration will fail. RegistrationContext is the handle that will be used later on to Unregister the callbacks.OperationRegistration is an array that contains all of your registered callbacks. OperationRegistration and every callback have this structure:1
2
3
4
5
6
typedef struct _OB_OPERATION_REGISTRATION {
POBJECT_TYPE *ObjectType;
OB_OPERATION Operations;
POB_PRE_OPERATION_CALLBACK PreOperation;
POB_POST_OPERATION_CALLBACK PostOperation;
} OB_OPERATION_REGISTRATION, *POB_OPERATION_REGISTRATION;ObjectType is the type of operation that you want to register to. Some of the most common types are *PsProcessType and *PsThreadType. It is worth mentioning that although you can enable more types (like IoFileObjectType) this will trigger PatchGuard and cause your computer to BSOD, so unless PatchGuard is disabled it is highly not recommended to enable more types. If you still want to enable more types, you can do so by using this like so:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct _OBJECT_TYPE {
struct _LIST_ENTRY TypeList;
struct _UNICODE_STRING Name;
VOID* DefaultObject;
UCHAR Index;
ULONG TotalNumberOfObjects;
ULONG TotalNumberOfHandles;
ULONG HighWaterNumberOfObjects;
ULONG HighWaterNumberOfHandles;
struct _OBJECT_TYPE_INITIALIZER_TEMP TypeInfo;
struct _EX_PUSH_LOCK_TEMP TypeLock;
ULONG Key;
struct _LIST_ENTRY CallbackList;
} OBJECT_TYPE, * POBJECT_TYPE;
POBJECT_TYPE_TEMP ObjectTypeTemp = (POBJECT_TYPE_TEMP)*IoFileObjectType;
ObjectTypeTemp->TypeInfo.SupportsObjectCallbacks = 1;Operations are the kind of operations that you are interested in, it can be OB_OPERATION_HANDLE_CREATE and/or OB_OPERATION_HANDLE_DUPLICATE for a handle creation or duplication. PreOperation is an operation that will be called before the handle is opened and PostOperation will be called after it is opened. In both cases, you are getting important information through OB_PRE_OPERATION_INFORMATION or OB_POST_OPERATION_INFORMATION such as a handle to the object, the type of the object the return status, and what type of operation ( OB_OPERATION_HANDLE_CREATE or OB_OPERATION_HANDLE_DUPLICATE) occurred. Both of them mustALWAYS return OB_PREOP_SUCCESS, if you want to change the return status, you can change the ReturnStatus that you got from the operation information, but do not return anything else.PROCESS_TERMINATE permission as we did in part 2 of the series) or manipulate the object itself (if it is a process, you can change the EPROCESS structure).PsSet*NotifyRoutine
ObRegisterCallbacks PsSet notifies routines are not responsible for a handle opening or duplicating operation but for monitoring creation/killing and loading operations, while the most notorious ones are PsSetCreateProcessNotifyRoutine, PsSetCreateThreadNotifyRoutine and PsSetLoadImageNotifyRoutine all of them are heavily used by AVs/EDRs to monitor for certain process/thread creations and DLL loading. Let's break it down, and talk about each function separately and what you can do with it. PsSetCreateProcessNotifyRoutine receives a function of type PCREATE_PROCESS_NOTIFY_ROUTINE which looks like so:1
2
3
4
5
void PcreateProcessNotifyRoutine(
[in] HANDLE ParentId,
[in] HANDLE ProcessId,
[in] BOOLEAN Create
)ParentId is the PID of the process that attempts to create or kill the target process.ProcessId is the PID of the target process.Create indicates whether it is a create or kill operation.PsSetCreateThreadNotifyRoutine receives a function of type PCREATE_THREAD_NOTIFY_ROUTINE which looks like so:1
2
3
4
5
void PcreateThreadNotifyRoutine(
[in] HANDLE ProcessId,
[in] HANDLE ThreadId,
[in] BOOLEAN Create
)ProcessId is the PID of the process.
ThreadId is the TID of the target thread.
Create indicates whether it is a create or kill operation.
PsSetLoadImageNotifyRoutine receives a function of type PLOAD_IMAGE_NOTIFY_ROUTINE which looks like so:1
2
3
4
5
void PloadImageNotifyRoutine(
[in, optional] PUNICODE_STRING FullImageName,
[in] HANDLE ProcessId,
[in] PIMAGE_INFO ImageInfo
)FullImageName is the name of the loaded image (a note here: it is not only DLLs and can be also EXE for example).
ProcessId is the PID of the target process.
ImageInfo is the most interesting part and contains a struct of type IMAGE_INFO:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef struct _IMAGE_INFO {
union {
ULONG Properties;
struct {
ULONG ImageAddressingMode : 8;
ULONG SystemModeImage : 1;
ULONG ImageMappedToAllPids : 1;
ULONG ExtendedInfoPresent : 1;
ULONG MachineTypeMismatch : 1;
ULONG ImageSignatureLevel : 4;
ULONG ImageSignatureType : 3;
ULONG ImagePartialMap : 1;
ULONG Reserved : 12;
};
};
PVOID ImageBase;
ULONG ImageSelector;
SIZE_T ImageSize;
ULONG ImageSectionNumber;
} IMAGE_INFO, *PIMAGE_INFO;ImageBase and ImageSize, using these you can inspect and analyze the image pretty efficiently. A simple example is if an attacker injects a DLL into LSASS, an EDR can inspect the image and unload it if it finds it malicious. If the ExtendedInfoPresent option is available, it means that this struct is of type IMAGE_INFO_EX:1
2
3
4
5
typedef struct _IMAGE_INFO_EX {
SIZE_T Size;
IMAGE_INFO ImageInfo;
struct _FILE_OBJECT *FileObject;
} IMAGE_INFO_EX, *PIMAGE_INFO_EX;FILE_OBJECT which is a handle for the file that is backed on the disk. With that information, you can also check for reflective DLL injection (a loaded DLL without any file backed on the disk) and it opens a door for you to monitor for more injection methods that don't have a file on the disk.PsSetLoadImageNotifyRoutine to make sure that no AV/EDR agent is injected into it.CmRegisterCallbackEx
ObRegisterCallbacks functions, it receives a unique altitude and the callback function. Let's focus on the Registry callback function:1
2
3
4
5
NTSTATUS ExCallbackFunction(
[in] PVOID CallbackContext,
[in, optional] PVOID Argument1,
[in, optional] PVOID Argument2
)CallbackContext is the context that was passed on the function registration with CmRegisterCallbackEx.
Argument1 is a variable that contains the information of what operation was made (e.g. deletion, creation, setting value) and whether it is a post- operation or pre-operation.
Argument2 is the information itself that is delivered and its type matches the class that was specified in Argument1.
Registry Protector
DriverEntry: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
#define DRIVER_PREFIX "MyDriver: "
#define DRIVER_DEVICE_NAME L"\\Device\\MyDriver"
#define DRIVER_SYMBOLIC_LINK L"\\??\\MyDriver"
#define REG_CALLBACK_ALTITUDE L"31102.0003"
PVOID g_RegCookie;
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) {
UNREFERENCED_PARAMETER(RegistryPath);
NTSTATUS status = STATUS_SUCCESS;
UNICODE_STRING deviceName = RTL_CONSTANT_STRING(DRIVER_DEVICE_NAME);
UNICODE_STRING symbolicLink = RTL_CONSTANT_STRING(DRIVER_SYMBOLIC_LINK);
UNICODE_STRING regAltitude = RTL_CONSTANT_STRING(REG_CALLBACK_ALTITUDE);
// Creating device and symbolic link.
status = IoCreateDevice(DriverObject, 0, &deviceName, FILE_DEVICE_UNKNOWN, 0, FALSE, &DeviceObject);
if (!NT_SUCCESS(status)) {
KdPrint((DRIVER_PREFIX "Failed to create device: (0x%08X)\n", status));
return status;
}
status = IoCreateSymbolicLink(&symbolicLink, &deviceName);
if (!NT_SUCCESS(status)) {
KdPrint((DRIVER_PREFIX "Failed to create symbolic link: (0x%08X)\n", status));
IoDeleteDevice(DeviceObject);
return status;
}
// Registering the registry callback.
status = CmRegisterCallbackEx(RegNotify, ®Altitude, DriverObject, nullptr, &g_RegContext, nullptr);
if (!NT_SUCCESS(status)) {
KdPrint((DRIVER_PREFIX "Failed to register registry callback: (0x%08X)\n", status));
IoDeleteSymbolicLink(&symbolicLink);
IoDeleteDevice(DeviceObject);
return status;
}
DriverObject->DriverUnload = MyUnload;
return status;
}DriverEntry initializations (Creating DeviceObject and symbolic link) CmRegisterCallbackEx to register our RegNotify callback. Note that we saved the g_RegContext as a global variable, as it will be used soon in the MyUnload function to unregister the driver when the DriverUnload is called.1
2
3
4
5
6
7
8
9
10
11
12
void MyUnload(PDRIVER_OBJECT DriverObject) {
KdPrint((DRIVER_PREFIX "Unloading...\n"));
NTSTATUS status = CmUnRegisterCallback(g_RegContext);
if (!NT_SUCCESS(status)) {
KdPrint((DRIVER_PREFIX "Failed to unregister registry callbacks: (0x%08X)\n", status));
}
UNICODE_STRING symbolicLink = RTL_CONSTANT_STRING(DRIVER_SYMBOLIC_LINK);
IoDeleteSymbolicLink(&symbolicLink);
IoDeleteDevice(DriverObject->DeviceObject);
}MyUnload, we didn't just unload the driver but also made sure to unregister our callback using the g_RegContext from before.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
NTSTATUS RegNotify(PVOID context, PVOID Argument1, PVOID Argument2) {
PCUNICODE_STRING regPath;
UNREFERENCED_PARAMETER(context);
NTSTATUS status = STATUS_SUCCESS;
switch ((REG_NOTIFY_CLASS)(ULONG_PTR)Argument1) {
case RegNtPreDeleteKey: {
REG_DELETE_KEY_INFORMATION* info = static_cast<REG_DELETE_KEY_INFORMATION*>(Argument2);
// To avoid BSOD.
if (!info->Object)
break;
status = CmCallbackGetKeyObjectIDEx(&g_RegContext, info->Object, nullptr, ®Path, 0);
if (!NT_SUCCESS(status))
break;
if (!regPath->Buffer || regPath->Length < 50)
break;
if (_wcsnicmp(LR"(SYSTEM\CurrentControlSet\Services\MaliciousService)", regPath->Buffer, 50) == 0) {
KdPrint((DRIVER_PREFIX "Protected the malicious service!\n"));
status = STATUS_ACCESS_DENIED;
}
CmCallbackReleaseKeyObjectIDEx(regPath);
break;
}
}
return status;
} RegNtPreDeleteKey. When we know that Argument2 contains information of type REG_DELETE_KEY_INFORMATION we can cast to it.Object parameter to access the registry key itself to get the key's path. To do that, we can use CmCallbackGetKeyObjectIDEx:1
2
3
4
5
6
7
NTSTATUS CmCallbackGetKeyObjectIDEx(
[in] PLARGE_INTEGER Cookie,
[in] PVOID Object,
[out, optional] PULONG_PTR ObjectID,
[out, optional] PCUNICODE_STRING *ObjectName,
[in] ULONG Flags
);Cookie is our global g_RegContext variable.
Object is the registry key object.
ObjectID is a unique registry identifier for our needs it can be null.
*ObjectName is the output registry key path, make sure it is in the kernel format.Flags must be 0.
ObjectName it is just a matter of comparing it and the key that you want to protect and if it matches you can change the status to STATUS_ACCESS_DENIED to block the operation.