Welcome back to Dissecting the Windows Defender Driver, in the previous part we saw how WdFilter gets initialized and how it handles the process creation throughout a process-creation callback. We also saw the ProcessCtx structure which will be used all over the driver to keep track of the different process running on the system. And now for this part, we will focus on the following things:

Image loading callback

Thread creation callback

Sending Sync/Async notifications

Disclaimer: The callbacks I’ll explain in this post rely mainly on ProcessCtx.ProcessRules and as much as I’ve tried with different type of process (Even malware) I haven’t been able to determine to what type of process corresponds each rule (Maybe it has to do with the Windows Defender configuration) I deeply apologize for not having this info, hopefully once I get further into the driver I will discover more about the different rules. Just for demonstration purpose, I’ve forced the code to follow different paths.

And after that disclaimer, let’s get into it!

MpCreateThreadNotifyRoutineEx - MpCreateThreadNotifyRoutine

The first two callbacks we will look are MpCreateThreadNotifyRoutine and MpCreateThreadNotifyRoutineEx , both of them are notified whenever a new thread is created or a thread is deleted. There’s two different callbacks because the first one is registered using PsSetCreateThreadNotifyRoutine while the second one is registered using PsSetCreateThreadNotifyRoutineEx, this function is available starting from Windows 10, and a pointer to it is saved in MpData, of course if the pointer is NULL this second callback won’t be registered.

As explained on the remark section from PsSetCreateThreadNotifyRoutineEx documentation this two functions differ in the context in which the callback is executed quoting MS documentation: “With PsSetCreateThreadNotifyRoutine, the callback is executed on the creator thread. With PsSetCreateThreadNotifyRoutineEx, the callback is executed on the newly created thread.”

MpCreateThreadNotifyRoutine

The code of the callbacks differ more than what you may expect, so we will study both. Starting with MpCreateThreadNotifyRoutine – Keep in mind this callback is executed in the context of the creator thread – this callback will check the following three things in order to execute:

Create parameter is set to TRUE

ProcessId is different than 0x4 (System)

(System) Curren thread is not a system thread – !PsIsSystemThread

In case this three conditions are met, the code will proceed to set a flag that indicates if the current process is the same as the one from the parameter ProcessId.

A process could be creating a thread in another process, and since this callback executes in the context of the creator thread the current process would be the creator while the parameter ProcessId would be the one where the thread is going to execute.

If they are the same then the current process ProcessCtx.ProcessRules will be tested against rule NotifyNewThreadSameProcess ( 0x10000000 ) and a flag will be set accordingly. In case the current process is not the same then the ProcessRules will be tested against rule NotifyNewThreadDifferentProcess ( 0x400000 ) and other flag will be set accordingly. If none of these flags is set then the callback will return – The following pseudocode shows this behavior in case my explanation is not clear enough

BOOLEAN SameProcess = 1 ; BOOLEAN NotifyNewThreadSameProcFlag = 0 ; BOOLEAN NotifyNewThreadDiffProcFlag = 0 ; if ( Create && ProcessId != 4 && ! PsIsSystemThread ( KeGetCurrentThread ()) ) { SameProcess = ProcessId == PsGetCurrentProcessId (); // Retrieve the ProcessCtx by the ProcessId MpGetProcessContextById ( PsGetCurrentProcessId (), & CurrentProcessCtx ); if ( SameProcess && CurrentProcessCtx -> ProcessRules & NotifyNewThreadSameProcess ) NotifyNewThreadSameProcFlag = 1 ; if ( ! SameProcess && CurrentProcessCtx -> ProcessRules & NotifyNewThreadDifferentProcess ) NotifyNewThreadDiffProcFlag = 1 ; if ( ! NotifyNewThreadSameProcFlag && ! NotifyNewThreadDiffProcFlag ) goto Cleanup ; }

In case one of the flags is set the code will proceed to obtain the structure I called AuxPidCreationTime – We saw in part 1, but as a remainder it contains de PID and the CreationTime of the process – after it has this structure for both process (Is obtained two times even if is the same process) the code will proceed to call MpGetPriorityInfo , this function will mainly call FltRetrieveIoPriorityInfo to get the IO_PRIORITY_INFO of the current thread and use this data to fill a structure I coined MP_IO_PRIORITY :

typedef struct _MP_IO_PRIORITY { IO_PRIORITY_HINT IoPriority ULONG ThreadPriority ULONG PagePriority } MP_IO_PRIORITY , * PMP_IO_PRIORITY ;

Different messages will be send to MsMpEng depending on the flag that was set. In the case of NotifyNewThreadDifferentProcess, MpSendSyncMonitorNotification will be called with OperationType equal to NewThreadDifferentProcess ( 0x3 ) and the Data will be the AuxPidCreationTime from the process that will execute the thread.

In the case the thread is being created in the same process, before calling MpSendSyncMonitorNotification the data that to send will be initialized, function MpCreatePsThreadSyncMonitorData is in charge of doing this. This function will basically fill the following structure:

typedef struct _ThreadNotifySyncMessage { AuxTidCreationTime CreatedThread ; AuxTidCreationTime CurrentThread ; AuxPidCreationTime Process ; INT64 Unk ; PVOID ThreadStartAddress ; } ThreadNotifySyncMessage , * PThreadNotifySyncMessage ;

to get the value of the ThreadStartAddress it will open obtain a handle to the thread (PsLookupThreadByThreadId) and then using this handle it will call ZwQueryInformationThread with class ThreadQuerySetWin32StartAddress. Once ThreadNotifySyncMessage is filled, function MpSendSyncMonitorNotification will be called with this structure as the Data and OperationType equal to NewThreadSameProcess ( 0x6 )

Lastly if NotifyNewThreadDifferentProcess is set the callback will execute one last step. This step will consist on sending an async notification with the following data

typedef struct _ThreadNotifyMessage { AuxPidCreationTime CurrentProcess ; INT CurrentThreadId ; AuxPidCreationTime CreatedThreadProcess ; AuxTidCreationTime CreatedThread ; WCHAR * ImageFileName ; } ThreadNotifyMessage , * PThreadNotifyMessage ;

Fields are pretty self-explanatory, in the case of the ImageFileName it will be retrieved from the ProcessCtx – In this case the ProcessCtx corresponds to the one from the thread creator process, which may not be the same as the one where the thread is going to run

MpCreateThreadNotifyRoutineEx

This routine is much simple than the previous one, in this case the function executes on the new thread, this basically means that the current process will always match the one indicated by the parameter ProcessId. First, in order to actually send the notification a lot of conditions must be met:

MpProcessTable->CreateThreadNotifyLock set to a value different than 0 (I know, lock is not the best name for this field, is locked when is zero 😆)

set to a value different than 0 (I know, lock is not the best name for this field, is locked when is zero 😆) Create param set to TRUE

Current process other than PsInitialSystemProcess

Flag ThreadNotifyRoutineExSet ( 0x400 ) set in ProcessCtx.ProcessFlags

( ) set in Rule NotifyProcessCmdLine ( 0x20000000 ) set in ProcessCtx.ProcessRules

As before, the following pseudocode explains this a bit better:

if ( _InterlockedCompareExchange ( & MpProcessTable -> CreateThreadNotifyLock , 0 , 0 ) && IoGetCurrentProcess () != PsInitialSystemProcess && Create ) { // Retrieve the ProcessCtx using the Process Object, in the end it will use // the CreationTime (PsGetProcessCreateTimeQuadPart) and the ProcessId (PsGetProcessId) MpGetProcessContextByObject ( IoGetCurrentProcess (), & ProcessCtx ) // Same as ((ProcessCtx->ProcessFlags >> 10) & 1 && (ProcessCtx->ProcessRules >> 0x1D) & 1) if (( ProcessCtx -> ProcessFlags & ThreadNotifyRoutineExSet ) && ( ProcessCtx -> ProcessRules & NotifyProcessCmdLine )) { ..... } }

A couple of clarifications here, the flag ThreadNotifyRoutineExSet is set in every ProcessCtx if the pointer to PsSetCreateThreadNotifyRoutineEx is not NULL in the MP_DATA :

in the case of the rule NotifyProcessCmdLine, comes from MsMpEng when setting the process info – Again, I haven’t managed to trigger this rule with any process, so I don’t really know to what kind of process does this rule apply, I apologize for this – So in the end of the process creation if this rule is set then the MpProcessTable->CreateThreadNotifyLock value will be incremented:

Getting back into the actual function, if all the conditions are met, then first thing is to decrement CreateThreadNotifyLock and remove the ThreadNotifyRoutineExSet from the ProcessCtx, once this is done, a handle to the Process Object will be obtained ( ObOpenObjectByPointer with ObjectType as PsProcessType) this handle will be used in order to retrieve the Process CommandLine, inside MpGetProcessCommandLineByHandle , this function pretty much uses ZwQueryInformationProcess with ProcessInformationClass set to ProcessCommandLineInformation. This command line is going to be compared against the one inside ProcessCtx->ProcessCmdLine , in case they don’t match then the function will get the MP_IO_PRIORITY , the AuxPidCreationTime and it will call MpSendSyncMonitorNotification with both Command Lines as the Data.

As seen on the image, if someone modifies the command line from the PEB this callback would notify MsMpEng of the tampered command line (Of course if the rules and flags for that ProcessCtx are set)

MpLoadImageNotifyRoutine

MpLoadImageNotifyRoutine is the callback routine that gets triggered whenever an image is loaded or mapped into memory. In order to register this callback the driver uses the function PsSetLoadImageNotifyRoutine.

Getting into the actual callback code, first thing is to check if the image to be load is going to be mapped into user space or kernel space, checking the bit Properties.SystemModeImage inside the IMAGE_INFO . In case it is a kernel-mode component the information of the image will be added to a DRIVER_INFO structure and then chained into the Loaded drivers list entry – Similar to the process creation adding boot process to the boot process list – this is done inside MpAddDriverInfo .

After this check, the ProcessCtx will be obtained and ProcessCtx->ProcessRules will be checked to see if NotifyWow64cpuLoad ( 0x800 ) is set. In case the rule is set, the function will proceed to compare the FullImageName byte by byte against the string \Windows\System32\Wow64cpu.dll. If they match the ProcessCtx->ProcessFlags will be OR’ed with ImageWow64cpuLoaded ( 0x200 ) and the ImageBase will be written to ProcessCtx->Wow64CpuImageBase – If you remember first part of the series this field was named as ImageBase, I’ve double-checked and this field is only set here in the whole code, that’s why I renamed it.

From here on the main functionality of the routine starts – Just to make it clear, this point of the code is reached even if the ImageName don’t match or the NotifyWow64cpuLoad is not set – this piece of code will first check if the IMAGE_INFO has the ExtendedInfoPresent bit set, if ExtendedInfoPresent is set then IMAGE_INFO is contained inside IMAGE_INFO_EX which keeps a pointer to the FileObject, this pointer will be used to retrieve a StreamContext ( MpGetStreamContextFromFileObject ) – Basically a structure defined by the minifilter that’s associated to a Stream objects, we’ll discuss this much more when we get to how the filtering works – with the StreamCtx and the ProcessCtx the following checks are done:

If StreamCtx->StreamCtxRules has NotifyImageLoadRule ( 0x8000 ) active, then NotifyImageLoadPerStreamFlag is set.

has ( ) active, then is set. If ProcessCtx->ProcessRules has NotifyImageLoadRule ( 0x8000000 ) active, then NotifyImageLoadPerProcessFlag is set.

has ( ) active, then is set. If ProcessCtx->ProcessRules has 0x200 active (Haven’t figured out this value yet), in case is not set then AsyncNotificationFlag is activated.

If AsyncNotificationFlag is set the function will create an AsyncMessageData structure where the union TypeOfMessage will take the structure ImageLoadAndProcessNotify , we already saw this structure in the previous post, the main difference is that AsyncMessageData->TypeOfOperation will be set to LoadImage ( 0x3 ). Lastly the notification will be send by calling MpAsyncSendNotification.

For the other two cases, the notification will be send synchronously, and the sent data will be the same for both cases. Only thing that differ will be the OperationType and the Rule – We’ll discuss this params shortly when we look into how synchronous messages are sent.

NotifyImageLoadPerProcessFlag -> OperationType = NewImageLoadPerProcess ( 0x5 ) and Rule = ProcessCtx->ProcessRules

-> OperationType = ( ) and Rule = NotifyImageLoadPerStreamFlag -> OperationType = NewImageLoadPerStream ( 0x1 ) and Rule = StreamCtx->StreamCtxRules

Finally function MpSendSyncMonitorNotification is called with the parameter Data as a UNICODE_STRING with the normalized name FullImageName of the loaded image.

One edge case where the code flow is a bit different is when ImageWow64cpuLoaded is set on the ProcessCtx->ProcessFlags . If this happens, then an AsyncMessageData structure of size 0x30 is allocated and the TypeOfMessage will contain the following structure:

typedef struct _Wow64CpuLoadMessage { INT ProcessId ; INT ThreadId ; PVOID Wow64CpuImageBase ; } Wow64CpuLoadMessage , * PWow64CpuLoadMessage ;

finally with AsyncMessageData populated the routine will call FltSendMessage – Fun fact, the AsyncMessageData->SizeOfData is set to 0x70 when actually the size of the structure is 0x30 , even parameter SenderBufferLength of FltSendMessage is set to 0x30 , this could lead to some potential error if AsyncMessageData->SizeOfData is used by MsMpEng.

So to explain a bit the flow, once Wow64cpu.dll is loaded this callback will set ImageWow64cpuLoaded in the ProcessCtx->ProcessFlags and will continue the execution through the main path. Next time this process loads an image, since ImageWow64cpuLoaded was previously set, the code will follow this path before taking the main path.

Synchronous notifications

NTSTATUS MpSendSyncMonitorNotification ( MP_SYNC_NOTIFICATION OperationType , PAuxPidCreationTime ProcessIdAndCreationTime , PVOID Data , PMP_IO_PRIORITY MpIoPriority , PULONG Rule ); typedef enum _MP_SYNC_NOTIFICATION_OPERATION { NewImageLoadPerStream = 0x1 , RegistryEventSync = 0x2 , NewThreadDifferentProcess = 0x3 , NewImageLoadPerProcess = 0x5 , NewThreadSameProcess = 0x6 , NewThreadProcessCmdLine = 0x7 , } MP_SYNC_NOTIFICATION_OPERATION ;

MpSendSyncMonitorNotification is the one in charge of sending the synchronous messages through out the MicrosoftMalwareProtectionPort, in order for this function to execute the flag SyncMonitorNotificationFlag from the MP_DATA must be set. After this check has been done, the code will check if the OperationType is within the range of the MP_SYNC_NOTIFICATION enum, also it will check that none of the other parameters is NULL .

If every check is fulfilled, the code will proceed to obtain the size of the parameter Data – As we saw during the post, the data provided in this parameter differs on each type of operation – to do this the code uses the function MpConstructSyncMonitorVariableData .

ULONG MpConstructSyncMonitorVariableData ( INT OperationType , PVOID Data , PVOID * __shifted ( SyncMessageData , 0x30 ) DataToSend , ULONG SizeOfData )

this function can be used in two ways:

To obtain the size of the data to send (DataToSend == NULL )

) To fill te buffer that’s going to be send using the data from the parameter Data

In the first case the pseudocode would look something like this:

if ( ! DataToSend ) { switch ( OperationType ) { case NewImageLoadPerStream : case NewImageLoadPerProcess : case NewThreadAndCmdLine : return ( UNICODE_STRING * ) Data -> Length + 0xA ; case RegistryEventSync : return ( RegistryNotifySyncMessage ) Data -> RegDataLength ; case NewThreadDifferentProcess : return sizeof ( AuxPidCreationTime ); case NewThreadSameProcess : return sizeof ( ThreadNotifySyncMessage ); } }

getting back to the main function, after calling MpConstructSyncMonitorVariableData for the first time the code will obtain the size of the data to send, this size will be added to the size of the message header ( 0x30 ) and with the whole size a pool will be allocated and filled accordingly. The message header has the following definition

typedef struct _SyncMessageData { SHORT Magic ; // Set to 0x5D SHORT SizeHeader ; // Sizeof 0x30 ULONG TotalSize ; MP_IO_PRIORITY MpIoPriority ; INT TypeOfOperation ; AuxPidCreationTime CurrentProcess ; INT SizeOfData ; union SyncVariableData { WCHAR * NewThreadAndCmdLine ; WCHAR * NewImageLoadPerStream ; WCHAR * NewImageLoadPerProcess ; RegistryNotifySyncMessage RegistryEventSync ; AuxPidCreationTime NewThreadDifferentProcess ; ThreadNotifySyncMessage NewThreadSameProcess ; }; } SyncMessageData , * PSyncMessageData ;

lastly before sending the message the variable data has to be copied into the SyncMessageData structure, to do this MpConstructSyncMonitorVariableData is called again but this time the parameter DataToSend is pointing to the structure SyncMessageData shifted by 0x30 (Pointing to the variable data), in this case the function will just copy the data from the buffer Data to the buffer DataToSend – In case the buffer Data is a UNICODE_STRING the UNICODE_STRING.Buffer will be copied using memcpy_s .

At this point everything is ready to send the data to MsMpEng, just one more check needs to be done, inside MpAcquireSendingSyncMonitorNotification which will basically check that MpData->SendSyncNotificationFlag is active and after this the function will wait on MpData->SendingSyncSemaphore using FltCancellableWaitForSingleObject – The timeout used for this wait comes from the variable MpData->SyncMonitorNotificationTimeout – in case the wait returns anything other than STATUS_SUCCESS the main function won’t send any message and will increment and set accordingly the following two variables:

MpData->ErrorSyncNotificationsCount[OperationType]

MpData->ErrorSyncNotificationsStatus[OperationType]

In case the wait succeeds, FltSendMessage will be called and based on the returned status different variables will be filled. First variable is a structure that keeps a counter of the notifications and the total timestamp of them (For each OperationType). The structure array can be found in the variable MpData->SyncNotifications[OperationType] and the definition of it looks like this:

typedef struct _MP_SYNC_NOTIFICATIONS { INT64 Timestamp ; INT NotificationsCount ; } MP_SYNC_NOTIFICATIONS , * PMP_SYNC_NOTIFICATIONS ;

In case FltSendMessage returns an error the following variables will be updated:

MpData->ErrorSyncNotificationsCount[OperationType]

MpData->ErrorSyncNotificationsStatus[OperationType]

MpData->SyncNotificationsIoTimeoutCount[OperationType] -> Incremented just in case the returned status from FltSendMessage is STATUS_TIMEOUT

if FltSendMessage returned STATUS_SUCCESS then the function will proceed to check the reply buffer. This buffer should contain the same OperationType in offset 0x8 if this is the case then it will proceed to reset the ProcessCtx->ProcessRules or StreamCtx->StreamCtxRule that triggered this specific notification – It uses the parameter Rule – This can be seen in the following image:

There are two more variables in this last step MpData->SyncNotificationRecvCount[OperationType] and MpData->SyncNotificationsRecvErrorCount[OperationType] . The latter is incremend in case the ReplyBuffer check doesn’t match, the former in the other case.

Asynchronous Notifications

In this section I will explain how does the driver handles sending the asynchronous notifications, there are two functions in charge of doing this. MpAsyncSendNotification which is in charge of adding the message to the async messages queue and MpAsyncpWorkerThread which is a worker thread checking the async messages queue and sending the messages if there are any.

MpAsyncpWorkerThread

In the first part of the series we already mention this worker thread. We saw that it is initialized, along with the async structure, inside MpAsyncInitialize . This function uses PsCreateSystemThread to create the worker thread, setting up MpAsyncpWorkerThread as the StartRoutine – No StartContext is passed into this new thread.

This thread will work mainly with the structure MP_ASYNC , this structure has the following definition (However much I tried to cross-reference this structure, I couldn’t manage to get more fields for now. That’s the main reason why I’m missing many fields):

typedef struct _MP_ASYNC { SHORT Magic ; // Set to 0xDA07 SHORT StructSize ; // Sizeof 0x180 LIST_ENTRY HighPriorityNotificationsList ; LIST_ENTRY NotificationsList ; PETHREAD WorkerThread ; KEVENT AsyncNotificationEvent ; KSEMAPHORE AsyncSemaphore ; FAST_MUTEX AsyncFastMutex ; INT NotificationsCount ; INT64 field_A8 ; INT64 field_B0 ; INT64 field_B8 ; PAGED_LOOKASIDE_LIST NotificationsLookaside ; INT64 TotalSizeNotificationsSent ; INT64 TotalSizeRemainingNotifications ; INT FailedNotifications ; INT64 field_158 ; INT64 field_160 ; INT64 field_168 ; INT64 field_170 ; INT64 field_178 ; } MP_ASYNC , * PMP_ASYNC ;

Once the worker thread starts executing it will enter an infinite loop waiting for two synchronization objects, MpAsync->AsyncSemaphore and MpAsync->AsyncNotificationEvent . In order to do this it uses KeWaitForMultipleObjects

I want to stop in this call and how is used for a second because I think is pretty cool, as we can see WaitType is set to WaitAny which means it will wait until any of the objects attains a signaled state. Also using WaitAny means that if the function returns STATUS_SUCCESS , it will actually return the zero-based index of the object as the NTSTATUS . Taking this into account, since the Event is set as the first element of the Objects array whenever the Event is signaled the returned value will be STATUS_WAIT_0 , which corresponds to 0x0 . Which, again as seen in the image, would make the condition of the for-loop FALSE which would make the loop stop and the thread would terminate by calling PsTerminateSystemThread .

In the case the semaphore is the signaled object, the thread will proceed to obtain the data that must be sent to MsMpEng. To do this, first the value MpConfig.AsyncStarvationLimit will be compared against the global variable AsyncStarvationLimit – If they are the same the global AsyncStarvationLimit will be set to 0x0 – In case they don’t match, data will be searched on the MpAsync->HighPriorityNotificationsList , if any entry is found in the LIST_ENTRY then AsyncStarvationLimit will be incremented by one. If no entries are found then MpAsync->NotificationsList will be checked and if an entry is found AsyncStarvationLimit is clear. In case the starvation limit is reached the list entries will be checked in reverse order, first the normal priority then the higher. The following pseudo-horrible-code shows this:

if ( MpConfig . AsyncStarvationLimit == _InterlockedCompareExchange ( & AsyncStarvationLimit , 0 , MpConfig . AsyncStarvationLimit )) { if ( & MpAsync -> NotificationsList != MpAsync -> NotificationsList . Flink ) goto SendMessage ; if ( & MpAsync -> HighPriorityNotificationsList != MpAsync -> HighPriorityNotificationsList . Flink ) { IncrementLimit : _InterlockedAdd ( & AsyncStarvationLimit , 1 ); goto SendMessage } } if ( & MpAsync -> HighPriorityNotificationsList != MpAsync -> HighPriorityNotificationsList . Flink ) goto IncrementLimit if ( & MpAsync -> NotificationsList != MpAsync -> NotificationsList . Flink ) { _InterlockedCompareExchange ( & AsyncStarvationLimit , 0 , AsyncStarvationLimit ); // Atomic set to 0 goto SendMessage ; }

As you can see, messages from MpAsync->HighPriorityNotificationsList have a higher priority because this LIST_ENTRY will be checked first unless the starvation limit is reached

The next part is pretty straightforward, if an entry is found in any of the two list entries then the following steps will take part:

Decrement MpAsync->NotificationsCount

Subtract the data size from MpAsync->TotalSizeRemainingNotifications

Set the Magic and Size of MP_ASYNC_NOTIFICATION (We will see this structure shortly) to 0xBABAFAFA

(We will see this structure shortly) to Push or free, in case the max depth has been reached, the MP_ASYNC_NOTIFICATION entry to the lookaside MpAsync->AsyncNotificationsLookaside

entry to the lookaside Send the actual message using FltSendMessage

In case of error increment MpAsync->FailedNotifications

Add the data size to MpAsync->TotalSizeNotificationsSent

And after that the thread would iterate again over the for-loop waiting for any of the two objects to be signaled.

Just to complete the full circle on this worker thread, function MpAsyncpShutdownWorkerThreads is the one that calls KeSetEvent with MpAsync->AsyncNotificationEvent as the event to signal, which as we saw before would end the loop and terminate the thread. This function is called from within MpAsyncShutdown which is in charge of cleaning all related to async notifications.

MpAsyncSendNotification

NTSTATUS MpAsyncSendNotification ( PVOID * __shifted ( AsyncMessageData , 8 ) AsyncMessageBuffer , ULONG SizeOfBuffer , INT PriorityFlag , PProcessCtx ProcessCtx );

We’ve already seen a couple of cases where the code will create a AsyncMessageData structure and populate it with the data that’s going to be send afterwards to MsMpEng. We just saw how this data is sent, now we are going to see how this data is added to the previously seen list entries.

The function in charge of this is MpAsyncSendNotification , which will first do a sanity check on AsyncMessageBuffer and SizeOfBuffer. If checks are fulfilled function will test if the SenderBuffer->TypeOfOperation is less than 0xA , if it’s the case, the value MpData->AsyncNotificationCount will be incremented and assigned to SenderBuffer->NotificationNumber – The Possible values of TypeOfOperation are the following:

typedef enum _MP_ASYNC_NOTIFICATION_OPERATION { CreateProcess = 0x0 , RegistryEvent = 0x1 , SendFile = 0x2 , LoadImage = 0x3 , OpenProcess = 0x4 , RawVolumeWrite = 0x5 , // High-Priority CreateThread = 0x6 , DocOpen = 0x7 , PostMount = 0x8 , // High-Priority OpenDesktop = 0x9 , PanicMode = 0xB , CheckJournal = 0xC , // High-Priority TrustedOrUntrustedProcess = 0xD , // High-Priority LogPrint = 0xE , Wow64cpuLoad = 0xF , OpenWithoutRead = 0x10 , FolderGuardEvents = 0x11 , DlpOnFileObjectClose = 0x13 , } MP_ASYNC_NOTIFICATION_OPERATION ;

next step is to increment the ProcessCtx->NotificationsSent – If there is a ProcessCtx – once this is done an entry from MpAsync->AsyncNotificationsLookaside will be popped or allocated and the following structure will be initialized in that buffer:

typedef struct _MP_ASYNC_NOTIFICATION { SHORT Magic ; // Set to 0xDA08 SHORT StructSize ; // Sizeof 0x18 - Header Size LIST_ENTRY AsyncNotificationsList ; PVOID * __shifted ( AsyncMessageData , 8 ) pMessageBuffer ; INT MessageBufferSize ; } MP_ASYNC_NOTIFICATION , * PMP_ASYNC_NOTIFICATION ;

Once this structure is initialized there’s tow possible paths, first path in case MpAsync->NotificationsCount is less than MpConfig.MaxAsyncNotificationCount . In this case, the initialized structure will be inserted at the end of MpAsync->HighPriorityNotificationsList or MpAsync->NotificationsList based on the PriorityFlag – If is set then is chained to the former, in the other case to the latter – then MpAsync->NotificationsCount is incremented, MessageBufferSize is added to MpAsync->TotalSizeRemainingNotifications and lastly the semaphore is signaled – KeReleaseSemaphore

The second path is taken when MpAsync->NotificationsCount is greater or equal to MpConfig.MaxAsyncNotificationCount , if this happens then the first entry of MpAsync->HighPriorityNotificationsList or MpAsync->NotificationsList (Again based on the PriorityFlag) will be unchained from the LIST_ENTRY and the newly created entry will be inserted at the end of it. Before finishing, as we saw in the worker thread, the function will increment MpAsync->AsyncMessagesFailed and push/free the unchained entry to/from the lookaside list (After setting the first bytes to 0xBABAFAFA )

This is basically to make sure there will be enough resources to allocate a pool from the lookaside list in case a new notification must be created, also this makes sure the newer notifications are the ones kept on the LIST_ENTRY in case the worker thread is not getting enough execution time to free the notifications list

Conclusion

And that’s all for this part folks! Sorry again for the long post but I’m trying to explain and clarify as much as possible – And even with theses long posts I’m leaving some stuff out – This part was a bit messy, not being able to know which process or options trigger some paths makes things a bit harder. Anyway, I hope you guys liked and still want to keep reading the series! This is just the tip of the iceberg!! On the next post we’ll look into the registered callback for objects (PsProcessType and ExDesktopObjectType) and also we will look into how drivers information is saved and how their verification is done.

If there’s any mistake or something not clear, please don’t hesitate to reach out to me on twitter @n4r1b