Tutorial AV self-protection (process) [C/C++)

W

Wave

Guest
#1
Hello everyone.

In this thread I will be discussing self-protection mechanisms for security software for processes (I will focus on registry and files in future threads). The purpose of this thread is to explain a bit about how process protection tends to work, and how you can approach development of such a feature for security software development (therefore I have attached example source code to make the learning easier throughout the thread).

The purpose of this thread is not supposed to teach you how to develop device drivers, perform injection and API hooking, and the such… It is only meant to provide some educational theory on the self-protection topic, and the source code is just additional for an example demonstration (for any programmers who may be reading the thread).

Notes:
  • I recommend you zoom out one from the default zoom on your browser as it may make reading this thread (or any other large threads) much easier - normally I would have changed the font size but I won't do this today.
  • For the hooking example I will only provide the callback function, I will not provide the hooking functions because the last thing I want is for this thread to be an entry point for malware development… It is meant to be educational towards security software development, and rootkit developers can use hooking code for multiple purposes. I have shared the device driver code because that callback cannot be abused for concealing, but for process protection instead (and is documented and common for AV software these days). There are plenty of API hooking examples online and you can experiment with MS Detours if you are too lazy to study the how hooking really works.
  • If you do happen to take an interest in the device driver development side, I recommend you follow this guide to get started into device driver development (kernel-mode) prior to attempting to use the code I posted in this thread, since knowing the basics is essential.
  • Everything within this thread is for educational purposes, please to not take advantage and use resources within this thread for any bad purposes.
  • This thread may discuss things which you do not know about, such as the Native Windows API. You can learn more about the NTAPI from a previous thread I’ve written: https://malwaretips.com/threads/theory-c-tutorial-native-windows-api-ntapi.63573

Part 1 – Theory on how process protection works
Process protection is a topic in itself and a very large one in fact. New people are performing experiments to find new ways to protect their processes from attacks all the time, and many popular AV companies are so confident that they include process protection bypasses into their bug bounty programmes (assuming no one can bypass it from user-mode).

Process protection is very important when it comes to AV software (or any security software in general) because it will prevent malicious software from shutting down the protection (e.g. terminating the service processes running under SYSTEM, injecting code into the processes to manipulate the AV behaviour or block it from performing specific actions and so on). The last thing you want to do is be using a security product with very weak self-protection – that being said, a lot of malware won’t even attempt to shut down the primary protection anyway.

Most malicious software which will attempt to attack a security product will attempt to do so using a process handle, as opposed to targeting the process’ threads instead – this doesn’t mean you should only protect the process, but you should protect both the process and the thread. For malicious software to terminate the process, it will need to either obtain a handle to the process or the process’ threads and then the handle is used for the termination; the same logic applies for injection and suspension… A handle will be required, unless another method like DLL hijacking is being utilised. Due to how most attacks work towards the security software processes, an easy solution is to prevent the handles being created with the sufficient privileges to do anything bad in the first place. If the handle cannot be obtained, then functions like NtTerminateProcess, NtSuspendProcess, NtTerminateThread, NtAllocateVirtualMemory, NtWriteVirtualMemory cannot be used – they all require a handle to be passed in.

There are many approaches which can be taken for preventing a handle from being obtained to your AV process; the most secure method would be to use a device driver which will perform kernel-mode patching techniques to block off functions such as ObOpenObjectByPointer (kernel-only function since it is not exported by ntdll.dll) from being used to get a handle to your process, however this can only be done on x86 systems ethically due to PatchGuard/Kernel Patch Protection on x64 systems. Therefore, most security products tend to work with kernel-mode callbacks (since it is compatible with both x86 and x64 systems since Windows Vista and on-wards), and is a pretty secure method. If user-mode is your only option, then user-mode hooks which target functions like NtOpenProcess should work fine – that being said, for user-mode hooking to work you’ll need to inject your hook code into every running process you want to be affected by the hook, and it can easily be bypassed via system calls (that being said, it’s still sufficient enough, not a lot of malware is sophisticated enough to even bypass user-mode hooks these days).

The chances are high that the security software you are using right now has installed a device driver to work with a kernel-mode callback for process protection (method used in Part 2). It’s a very popular method and it’s even documented by Microsoft.

Part 2 – kernel-mode callback method
Kernel-mode callbacks have been around since Windows Vista and Microsoft introduced them as an alternate to kernel-mode patching since they blocked this off on x64 versions of Windows when they introduced PatchGuard/Kernel Patch Protection, which was also introduced with Windows Vista (you can read more about PatchGuard/Kernel Patch Protection at the following thread: https://malwaretips.com/threads/windows-built-in-protection-mechanisms.66165 ).

There is a kernel-mode callback called ObRegisterCallbacks, once this callback has been registered correctly you can receive post and pre operations for when a handle to any process is attempting to be created (therefore you can intercept while the handle creation request is occurring and prevent access rights from being given to the handle, resulting in the caller process attempting to open the process being denied access with Access Denied (STATUS_ACCESS_DENIED – 0xC0000022)).

To bypass this kernel-mode callback, as long as both process and threads were patched up (you can protect the threads with this callback too, but in my example I will only do it for process so anyone who wants to use the code has some work to do for themselves too (to give you a hint you’ll need to add PsThreadType to be protected in the registration as well as PsProcessType which will already be in the registration)) you’ll need to use ObOpenObjectByPointer (kernel-only function) to obtain the handle to the protected process with whatever access rights you want – the downside is that this will require kernel-mode code execution, and on x86 systems this function can be hooked, which means if the function is hooked you’ll need to unhook it first.

The below code example can be compiled as-is and is written in C (I prefer C for device driver development as opposed to C++ which I prefer for user-mode development), it is for educational purposes only:

Code:
#include <ntddk.h>

typedef struct _OB_REG_CONTEXT {
       USHORT Version;
       UNICODE_STRING Altitude;
       USHORT ulIndex;
       OB_OPERATION_REGISTRATION *OperationRegistration;
} REG_CONTEXT, *PREG_CONTEXT;

UNICODE_STRING usDriverName, usSymbolName;
PVOID ObHandle = NULL;

OB_PREOP_CALLBACK_STATUS objPreCB(PVOID RegistrationContext, POB_PRE_OPERATION_INFORMATION OperationInformation)
{
       UNREFERENCED_PARAMETER(RegistrationContext);
       if (PsGetProcessId((PEPROCESS)OperationInformation->Object) == 9012)
       {
              if (OperationInformation->Operation == OB_OPERATION_HANDLE_CREATE)
              {
                     if ((OperationInformation->Parameters->CreateHandleInformation.OriginalDesiredAccess & 0x0008) == 0x0008)
                     {
                           OperationInformation->Parameters->CreateHandleInformation.DesiredAccess &= ~0x0008;
                     }
                     if ((OperationInformation->Parameters->CreateHandleInformation.OriginalDesiredAccess & 0x0001) == 0x0001)
                     {
                           OperationInformation->Parameters->CreateHandleInformation.DesiredAccess &= ~0x0001;
                     }
                     if ((OperationInformation->Parameters->CreateHandleInformation.OriginalDesiredAccess & 0x0020) == 0x0020)
                     {
                           OperationInformation->Parameters->CreateHandleInformation.DesiredAccess &= ~0x0020;
                     }
                     if ((OperationInformation->Parameters->CreateHandleInformation.OriginalDesiredAccess & ~0x0010) == 0x0010)
                     {
                           OperationInformation->Parameters->CreateHandleInformation.DesiredAccess &= ~0x0010;
                     }
              }
       }
       return OB_PREOP_SUCCESS;
}

VOID objPostCB(PVOID RegistrationContext, POB_POST_OPERATION_INFORMATION OperationInformation)
{
       UNREFERENCED_PARAMETER(RegistrationContext);
       UNREFERENCED_PARAMETER(OperationInformation);
}

VOID mod_processprotection(BOOLEAN bEnable)
{
       if (bEnable)
       {
              NTSTATUS NtRet = STATUS_SUCCESS;
              OB_OPERATION_REGISTRATION obOpReg;
              OB_CALLBACK_REGISTRATION obCbReg;
              REG_CONTEXT regContext;
              UNICODE_STRING usAltitude;
              memset(&obOpReg, 0, sizeof(OB_OPERATION_REGISTRATION));
              memset(&obCbReg, 0, sizeof(OB_CALLBACK_REGISTRATION));
              memset(&regContext, 0, sizeof(REG_CONTEXT));
              regContext.ulIndex = 1;
              regContext.Version = 120;
              RtlInitUnicodeString(&usAltitude, L"XXXXXXX");
              if ((USHORT)ObGetFilterVersion() == OB_FLT_REGISTRATION_VERSION)
              {
                     obOpReg.ObjectType = PsProcessType;
                     obOpReg.Operations = OB_OPERATION_HANDLE_CREATE;
                     obOpReg.PostOperation = objPostCB;
                     obOpReg.PreOperation = objPreCB;
                     obCbReg.Altitude = usAltitude;
                     obCbReg.OperationRegistration = &obOpReg;
                     obCbReg.RegistrationContext = &regContext;
                     obCbReg.Version = OB_FLT_REGISTRATION_VERSION;
                     obCbReg.OperationRegistrationCount = (USHORT)1;
                     NtRet = ObRegisterCallbacks(&obCbReg, &ObHandle);
              }
       }
       else
       {
              if (ObHandle != NULL)
              {
                     ObUnRegisterCallbacks(ObHandle);
                     ObHandle = NULL;
              }
       }
}

NTSTATUS DrvDispatchRoutine(PDEVICE_OBJECT pDeviceObject, PIRP pIrp)
{
       NTSTATUS NtStatus = STATUS_SUCCESS;
       PIO_STACK_LOCATION pIo;
       pIo = IoGetCurrentIrpStackLocation(pIrp);
       pIrp->IoStatus.Information = 0;
       switch (pIo->MajorFunction)
       {
       case IRP_MJ_CREATE:
              NtStatus = STATUS_SUCCESS;
              break;
       case IRP_MJ_READ:
              NtStatus = STATUS_SUCCESS;
              break;
       case IRP_MJ_WRITE:
              break;
       case IRP_MJ_CLOSE:
              NtStatus = STATUS_SUCCESS;
              break;
       default:
              NtStatus = STATUS_INVALID_DEVICE_REQUEST;
              break;
       }
       pIrp->IoStatus.Status = STATUS_SUCCESS;
       IoCompleteRequest(pIrp, IO_NO_INCREMENT);
       return NtStatus;
}

VOID DriverUnload(PDRIVER_OBJECT pDriverObject)
{
       if (ObHandle != NULL)
       {
              mod_processprotection(FALSE);
       }
       IoDeleteSymbolicLink(&usSymbolName);
       IoDeleteDevice(pDriverObject->DeviceObject);
}

NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pUniStr)
{
       NTSTATUS NtRet = STATUS_SUCCESS;
       PDEVICE_OBJECT pDeviceObj;
       RtlInitUnicodeString(&usDriverName, L"\\Device\\WaveAVDriver");
       RtlInitUnicodeString(&usSymbolName, L"\\DosDevices\\WaveAVDriver");
       NTSTATUS NtRet2 = IoCreateDevice(pDriverObject, 0, &usDriverName, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, &pDeviceObj);
       if (NtRet2 == STATUS_SUCCESS)
       {
              ULONG i;
              for (i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++)
              {
                     pDriverObject->MajorFunction[i] = DrvDispatchRoutine;
              }
              IoCreateSymbolicLink(&usSymbolName, &usDriverName);
              pDeviceObj->Flags |= DO_DIRECT_IO;
              pDeviceObj->Flags &= (~DO_DEVICE_INITIALIZING);
       }
       pDriverObject->DriverUnload = DriverUnload;
       mod_processprotection(TRUE);
       return NtRet;
}
To use the above device driver, you will need to replace the PID in the objPreCB filtering with the target process you want to protect – the current one is set for 9012 which I used during testing.

You also need to make sure that you have added /integritycheck to the linker settings, otherwise the callback registration will fail with access denied.

The code works by registering the kernel-mode callback and from the Pre operation callback (objPreCB function) it will filter out the process which is target for the handle creation and if the PID of the process is the same to the one you wish to protect it will remove the access rights from being allowed to be returned in the handle, resulting in the caller process attempting to open the handle being greeted with access denied.

If you register the callback but do not unregister it then it will cause a bugcheck BSOD once the driver is unloaded, since the callback must always be unregistered. Therefore, I've sorted this out in the DriverUnload routine. Make sure to compile for both x86 and x64 and for testing on x64 you'll need to enable Test Mode if you do not have a production digital signature.

You can find more information about how ObRegisterCallbacks works at the official documentation over at MSDN: ObRegisterCallbacks routine (Windows Drivers)

Part 3 – API hooking method
I did mention in the notes that I could not provide the actual hook code and the reasons behind this is that I do not want a malware developer to end up on this thread and then copy paste it to help them in developing a rootkit – since the hook code could be abused to be used for other functions to conceal process/file/registry detection, etc. However, the device driver development source code would be trickier for a malware author to use on x64 systems at least, and would only protect the process as opposed to be abused for other purposes also. There are plenty of hooking examples written in C/C++ or other languages online and you can easily find some good articles through a Google search anyway.

There are many different types of API hooking: SSDT, IAT, EAT hooking, etc. SSDT hooking is for kernel-mode (and it stands for the System Service Dispatch Table hooking – for x86 systems only), IAT (Import Address Table) hooking is for user-mode and so is EAT (Export Address Table) hooking.

If you want to perform user-mode hooking techniques, then you will require to inject into all running processes you want to be affected by the hook and then have your hook code executed… Bear in mind that user-mode hooking is not as secure as kernel-mode hooking and can easily be circumvented through un-hooking the hooked functions or via a direct NTAPI system call.

If you are performing user-mode hooking, then you only need to hook NtOpenProcess and NtOpenThread. If a handle to your process/threads cannot be obtained then functions for injection, termination and suspension cannot be used from user-mode unless via an NTAPI system call. However, if you are performing SSDT hooking (the below code is for a user-mode hook callback) then you should hook kernel-mode functions like ObOpenObjectByPointer and maybe PsLookupProcessByProcessId instead for enhanced security.

Below is an example source code to the callback of an NtOpenProces hook:
Code:
NTSTATUS NTAPI NtTerminateProcess_CallB(HANDLE ProcessHandle, NTSTATUS ExitStatus)
{
       if (GetProcessId(ProcessHandle) == protectPid)
       {
              return 0xC0000022;
       }
       return NtTerminateProcessTramp(ProcessHandle, ExitStatus);
}

NTSTATUS NTAPI NtOpenProcess_CallB(PHANDLE ProcessHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, PCLIENT_ID  ClientId)
{
       if (ClientId->UniqueProcess == (HANDLE)protectPid) { return 0xC0000022; }
       return NtOpenProcessTramp(ProcessHandle, DesiredAccess, ObjectAttributes, ClientId);

NTSTATUS NTAPI NtSuspendProcess_CallB(HANDLE ProcessHandle)
{
       if (GetProcessId(ProcessHandle) == protectPid) { return 0xC0000022; }    
       return NtSuspendProcessTramp(ProcessHandle);
}
I decided to add the basic callback code for NtTerminateProcess, NtOpenProcess and NtSuspendProcess hooks. You can use the above callbacks as long as you replace the protectPid variable with the PID of the process you want to protect, I recommend setting up a DWORD variable called protectPid and assigning the PID to be protected as the value.

All you need to do to use the above callback is find a hook function or study the different hooking techniques and write your own hook function. For x86 hooking all you need to do for a basic hook is manipulate the function prologue of the target Win32/NTAPI function at the start with JMP <address of the callback> so when the function is called it will JMP to the address of your callback, therefore executing the code within your callback function. However, a trampoline hook is much better (the above callbacks are for a trampoline hook since you may have noticed the return at the end of each function), and therefore you can do this by allocating memory at the trampoline and copying the original bytes from the target function you are hooking to it, allowing you to call the original function through the trampoline without it triggering your hook and causing a process crash (due to deadlock).

For a basic x64 hook you can use: MOV RAX, <addr> and then JMP RAX. You should not just attempt to place a JMP instruction to your callback address because of the sizes of the address (I believe).
----------------------------------------------------------------------------

I apologise for this being a short thread but hopefully it still helped someone out there. Maybe the device driver code will be helpful to a security developer on this forum?

Stay safe and good luck,
Wave. ;)
 
Last edited by a moderator:
Joined
Mar 13, 2017
Messages
29
OS
Windows 10
Antivirus
ESET
#2
As I see Kernel-mode callbacks are very useful, is there any post or paper or something like this about Kernel-mode callbacks with descriptions and examples?
 
D

Deleted member 65228

Guest
#4
P.S. I found a post about kernel-mode callbacks, but it's in Russian, using Google Translate: Google Translate
The author seems to be MIA so here is my chance to step in!

The post you linked us to is about PsSetLoadImageNotifyRoutine. It is another documented kernel-mode callback, however the purpose of this one is to receive notifications for when images are mapped/loaded into memory. This is also very common in security software for a wide variety of reasons: real-time scanning of DLLs as they are loaded by processes; self-defence since you can identify DLLs being loaded into your own process which should not be loaded. A user-mode alternative would be setting a detour on a routine like LdrLoadDll (exported by NTDLL - responsible for loading DLLs into memory and called by LoadLibraryW, will call internal functions like LdrpLoadDll) or NtCreateSection (sections need to be created to store the memory - you can change the parameter flags to detect if it is for SEC_IMAGE and if the memory is executable (e.g. PAGE_EXECUTE_READWRITE, or PAGE_EXECUTE, etc.)).

There are many different kernel-mode callbacks, some of which do happen to be undocumented. I'll note down the most common and popular ones for you.
- FltRegisterFilter
- ObRegisterCallbacks
- CmRegisterCallbackEx
- PsSetCreateProcessNotifyRoutine/Ex/Ex2
- PsSetLoadImageNotifyRoutine/Ex
- PsSetCreateThreadNotifyRoutine

It is important to remember that there are different altitudes and depending on the altitude will depend on who will receive the notification and can take action first. This is why some security software may intercept and take action before another first, even if both are using the exact same technique, without conflict.

I linked the Ex prefixes to the appropriate documentation for the Ex prefix versions of the function. The Ex prefix is applied for versions of the function which are updated. Microsoft cannot go back and change a kernel-mode callback if and when they want depending on the change-log because it will cause existent device drivers relying on the routine to potentially start improperly working, and this can lead to system-wide crashes (BSODs). Therefore, they create a new version of the function with the Ex suffix and apply it as usable only for the versions of Windows which receive support for it (e.g. via an OS update -> any OS previous to the current does not have it).

You can view the documentation for all of the ones listed above. There are still many more, but I suspect those ones are the most common. Bear in mind that to use kernel-mode callbacks such as FltRegisterFilter, you will require to be using a file-system mini-filter device driver (however this is the callback used by traditional security software for intercepting file-system activities for real-time protection).

Reverse device drivers belonging to Windows Defender on modern versions of Windows to potentially find undocumented kernel-mode callbacks, they'll be using them for sure... At least one of them. I know they use one for boot-time scanning of new image loads (for device driver scanning).
 
D

Deleted member 65228

Guest
#6
Np :)

ObRegisterCallbacks is good but it simply is not enough - it needs to be implemented properly to be used to its full potential as well (block handle creation and duplication to processes or threads within processes which are marked to be protected).

On 32-bit systems where PatchGuard is not present, you could try patching functions like PsLookupProcessByProcessId and ObOpenObjectByPointer. If you can find the address to non-exported NTOSKRNL functions such as PspOpenProcess (e.g. scan memory for ntoskrnl.exe with a byte pattern) then patching such functions are much deeper and powerful.

Generally speaking though, many techniques need to be applied. You need to protect the window of the user-mode process if it has one to present WM_ events in the message queue system to force it to terminate. You also need to ensure any handles opened by system processes (csrss.exe pre-Windows 8, lsass.exe post-Windows 8, and even svchost.exe) are either closed or have reduced access rights.

If you call NtQuerySystemInformation with the class for SystemHandleInformation (and then duplicate the handle with NtDuplicateObject and call NtQueryObject to get more information) you will find that system processes like lsass.exe on modern versions of Windows has opened handles to running processes with sufficient access rights for termination, suspension, virtual memory operations and thread creation. (Check with the Properties -> Handles tab on Process Hacker).

You could fake shutdown -> prevent it from actually happening. This may cause services which were originally protected to shutdown for the shutdown request.

You can even leverage explorer.exe, but I must hush on this topic otherwise I might get sued for discussing a submission I recently put in to a vendor (has been patched since though).

People find new methods all the time. Look at attacks like DoubleAgent. If a product does not protect the file-system then there is potential for DLL hijacking as well, and registry key values have been hijacked before for RCE using DLLs.

There are so many ways of doing things in Windows. Self-Defence is a very huge topic, you would have never guessed how big the topic really is to those who are serious about it. Even game developers take great lengths with Anti-Cheat sometimes (some turn into full rootkits haha).

I don't even do gaming but thought to mention that anyway. Writing from a phone right now hopefully this is all readable
 
Joined
Mar 23, 2018
Messages
22
#7
Speaking from years of experience, you cannot “protect” your process on windows, period.

Yes there are many techniques, but all of them can be compromised. In this example callbacks; you can easily create another callback with the same altitude to cause a collision or with a higher altitude and job done.

I’m not going to list go into every detail, because it would take forever, but long story short is that windows is a screwed up OS.

All it takes is determination.

It is sad, it makes me sad ( because it affects my work too ), but there is nothing you can do.

Anyone who says otherwise either has not enough knowledge about security, either he’s dreaming.

Once your in the kernel... the world is yours.
 
D

Deleted member 65228

Guest
#8
Speaking from years of experience, you cannot “protect” your process on windows, period.

Yes there are many techniques, but all of them can be compromised. In this example callbacks; you can easily create another callback with the same altitude to cause a collision or with a higher altitude and job done.

I’m not going to list go into every detail, because it would take forever, but long story short is that windows is a screwed up OS.

All it takes is determination.

It is sad, it makes me sad ( because it affects my work too ), but there is nothing you can do.

Anyone who says otherwise either has not enough knowledge about security, either he’s dreaming.

Once your in the kernel... the world is yours.
Self-defence techniques in Anti-Virus software are usually not developed to cover kernel-mode attacks.

You're point regarding defeating ObRegisterCallbacks however is flawed because you are forgetting the part where you must gain kernel-mode code execution which will require SeLoadDriverPrivilege and if on Windows 10, a digitally signed driver. You can defeat it from user-mode, I have done this, but if the kernel is compromised then it is already game over.

There are many things you can do for self-defence:
- PsSetLoadImageNotifyRoutine/Ex
- PsSetCreateThreadNotifyRoutine/Ex
- ObRegisterCallbacks
- Locally patching LdrInitializeThunk
- Locally patching KiUserApcDispatcher
- Closing open handles within system processes like lsass.exe

You can also use the hyper-visor and then patch routines such as ObpCreateHandle and other non-exported routines.

Eventually, someone determined enough will find a way to bypass it regardless of all the techniques being applied if they have the skill-set to do so.
 
Last edited by a moderator: