Reversing XignCode3 Driver – Part 4.1 – Registering Notify and Callback Routines

First of all, if you are just jumping into this topic I recommend you reading this post from GuidedHacking that will give you a lot of information about this Anti-Cheat.

I thought I could cover all about Notify and Callback routines on one post, my bad. To keep it short, I’m dividing this part of the analysis on multiple posts.

As a short summary I leave you here the link to the previous posts:

When we were analyzing the DriverEntry, we identified two functions that were responsible for different kinds of callbacks registrations (fn_InitRegistrationNotifyAndCallbackRoutines and fn_RegisterCreateProcessNotifyRoutine). I mentioned them but we didn’t go more deeply into what they do.

What we will go through?

  • Identify mutex and spinlocks
  • Learn how notify routines are registered using the win API
  • Learn how PCREATE_PROCESS_NOTIFY_ROUTINE is created
  • Learn how the driver manages the different error states that can appear during the execution.

Introduction

Since the goal is to analyze the Driver and give you the information you need to learn the rest on your own. I’m not going to go very deep on what Notify and Callback routines are. Here you have some interesting links:

The most important thing you need to understand is the fact that Anti-cheats need to control what is happening in your system. Therefore, they are going to register this so-called notify and callback routines. This will allow them to execute pre- and post-operation when an event is triggered, for example, a new process has been created, a new DLL has been loaded, etc.

fn_InitRegistrationNotifyAndCallbackRoutines (0x140003550)

There are some functions that initialize variables, spinlocks, and arrays that we haven’t seen yet. All those variables are used across multiple functions of the driver, and what usually happens is that we will discover the meaning of those (if we ever do it) while we reverse other functions we haven’t seen yet.

Since functions are getting bigger and more complex, I will avoid explaining some basic things about driver development like mutex initializations, allocations, etc.

Let’s start with this function: You can take a look at the original function here (asm and c code). And here the final result of the reversing (try not to spoil yourself). As always, take your time to try to understand it by yourself.

At the beginning of the function we have some buffer and mutex initializations. We don’t know yet what are those variables for, but we can recognize them because of how those variables are being used; for example, as parameter to mutex related functions such as KeInitializeMutex, or assembly opcodes such as lock xadd:

After that, we will find the first interesting function sub_140003C38, which I decided to call j_fn_ConfigWindowsVersion. You will see this function reversed on the following post. In a nutshell, the function identifies the current version of windows and initializes a series of offset variables with information about some particular kernel structures. If this function does not fail, the execution goes on:

 result = j_fn_ConfigWindowsVersion();
  if ( result < 0 )
    return result;
  fn_InitWeirdVariables_();
  fn_InitWeirdVariables2_();
  status_PsSetCreateProcessNotifyRoutine = 0;
  status_PsSetCreateProcessNotifyRoutineEx = 0;

We are going to ignore fn_InitWeirdVariables_ and fn_InitWeirdVariables2_, given that those functions just initialize some spinlocks and weird variables that the Driver uses later. We are not interested on that for now. Actually if you know what they are doing on these lines let me know, because I don’t know what kind of sorcery they are casting (I believe it is a way to generate an GUID for each process, but who knows):

  ProcessId >>= 2;
  v3 = (ProcessId >> 5) & 0x1FF;
  v4 = ~(1 << (0x1F - (ProcessId & 0x1F)));

After that, two NTSTATUS variables are initialized: status_PsSetCreateProcessNotifyRoutine and status_PsSetCreateProcessNotifyRoutineEx. We are going to see that if the first attempt to register the NotifyRoutine using PsSetCreateProcessNotifyRoutineEx fails, they are going to use these variables to control the execution flow and make a second attempt using PsSetCreateProcessNotifyRoutineEx.

  RtlInitUnicodeString(&DestinationString, L"PsSetCreateProcessNotifyRoutineEx");
  PsSetCreateProcessNotifyRoutineEx = MmGetSystemRoutineAddress(&DestinationString);
  fn_pPsSetCreateProcessNotifyRoutineEx = PsSetCreateProcessNotifyRoutineEx;

The address of PsSetCreateProcessNotifyRoutineEx is retrieved and stored into a variable. If this variable value is not NULL, the first attempt is done:

if ( PsSetCreateProcessNotifyRoutineEx )
  {
    ntStatus = PsSetCreateProcessNotifyRoutineEx(fn_CreateProcessNotifyRoutineExImp, 0i64);
    v6 = status_PsSetCreateProcessNotifyRoutineEx;
    if ( ntStatus >= 0 )
      v6 = 1;
    status_PsSetCreateProcessNotifyRoutineEx = v6;
  }
  else
  {
    v6 = status_PsSetCreateProcessNotifyRoutineEx;
  }

We can see that PsSetCreateProcessNotifyRoutineEx is called. In the first parameter, it sends the routine (fn_CreateProcessNotifyRoutineExImp) to be executed whenever a new process is created or exited; and in the second parameter, it establishes that the Notify Routine needs to be registered instead of removed. As you can see the same function is used to create and remove a NotifyRoutine.

Moving a little bit further on the function, if the first attempt fails, we are going to see that PsSetCreateProcessNotifyRoutine is called as a second attempt:

  if ( !v6 )
  {
    ntStatus_1 = PsSetCreateProcessNotifyRoutine(fn_CreateProcessNotifyRoutine, 0i64);
    if ( ntStatus_1 < 0 )
    {
      status_PsSetCreateProcessNotifyRoutine = 0;
      goto LABEL_13;
    }
    status_PsSetCreateProcessNotifyRoutine = 1;
  }

Again, we have identify another of the callback routines: fn_CreateProcessNotifyRoutine.

fn_CreateProcessNotifyRoutine and fn_CreateProcessNotifyRoutineExImp

If we check fn_CreateProcessNotifyRoutineExImp and fn_CreateProcessNotifyRoutine, we will notice that both of them are wrappers to the real routines:

void __fastcall fn_CreateProcessNotifyRoutineExImp(PEPROCESS Process, __int64 ProcessId, PVOID CreateInfo)
{
  if ( CreateInfo )                             //  If CreateInfo parameter is NULL, the specified process is exiting.
    fn_Analyze_CreateProcessNotifyRoutine(ProcessId);
  else
    fn_Analyze_ExitProcessNotifyRoutine(ProcessId);
}

The parameters received by the PCREATE_PROCESS_NOTIFY_ROUTINE  are explained on the documentation. Based on that, we can identify how this function decides if the callback is being invoked due to a process being created or deleted.

fn_Analyze_CreateProcessNotifyRoutine and fn_Analyze_ExitProcessNotifyRoutine are big functions that we are going to analyze later

Back to fn_InitRegistrationNotifyAndCallbackRoutines

Finally, the last Callback registration is attempted:

  ntStatus_1 = fn_RegisterCallbackFunction();

  if ( ntStatus_1 < 0 )                         // In case the registerCallbackFunction failed, we need to remove teh Notify routines previously registered.
  {

    if ( status_PsSetCreateProcessNotifyRoutineEx && fn_pPsSetCreateProcessNotifyRoutineEx )
      fn_pPsSetCreateProcessNotifyRoutineEx(fn_CreateProcessNotifyRoutineExImp, 1u);// 2nd Parameter equal to 1 == remove
    if ( status_PsSetCreateProcessNotifyRoutine )
    {
      LOBYTE(_RemoveRoutine) = 1;
      PsSetCreateProcessNotifyRoutine(fn_CreateProcessNotifyRoutine, _RemoveRoutine);
    }
    goto label_exit;

  }

However, as you can see, if registerCallbackFunction failed, the previous created NotifyRoutine needs to be removed before leaving. In that case, they set _RemoveRoutine to 1 and then they call again PsSetCreateProcessNotifyRoutine to remove it.

fn_RegisterCallbackFunction is going to be reversed in the following posts as well as the other two functions.

Next Steps

  • Part 4.2 – Managing Window Versions
  • Part 4.3 – Using ObRegisterCallbacks
  • Part 4.4 – Analyzing the Notify Routines