Reversing XignCode3 Driver – Part 4.2 – Verifying windows version

Quick post to analyze a particular function where the XC3 Driver manages the different version of Windows and bring support to each of them.

Introduction

As you may know, on windows the offsets inside of different kernel structure may change from one version to the next one.

Critical kernel structures like EPROCESS, KTHREAD, etc, have a lot of information on their attributes. Anti-Cheats usually access that information to manually check information about processes, system and memory by their own, without the need of using the windows API.

If this Driver is being used by hundreds of thousands of users, they need to properly determine the correct offsets of each attribute they want to access for each version. And this function initialize some variables, than later are used by the rest of the functions.

Previous Posts

What we will go through?

  1. How Drivers of this kind support multiple version of Window
  2. How hard-coded offsets used to access kernel structures are maintained.
  3. To identify the kernel structures being used.

j_fn_ConfigWindowsVersion (0x 140003C38)

Worth mentioning that I will just provide some examples of the offsets they are managing. Identifying each of them is really time consuming and I let you that part as homework šŸ˜‰ Those which I have identified, have been used on other functions I manually reversed before.

This function it is basically a jump to the real implementation:

.text:0000000140003C38 ; =============== S U B R O U T I N E =======================================
.text:0000000140003C38
.text:0000000140003C38 ; Attributes: thunk
.text:0000000140003C38
.text:0000000140003C38 sub_140003C38   proc near               ; CODE XREF: sub_140003550+9Aā†‘p
.text:0000000140003C38                 jmp     sub_14000646C
.text:0000000140003C38 sub_140003C38   endp
.text:0000000140003C38
.text:0000000140003C38 ; ---------------------------------------------------------------------------
.text:0000000140003C3D                 align 20h

If we take a look to sub_14000646C, we will notice that the function is basically obtaining the current version of the operative system. Then based on the Major and minor version, and build number, they initialize a group of global variables.

PsGetVersion(&MajorVersion, &MinorVersion, &BuildNumber, 0i64);

Based on the documentation, PsGetVersion returns the Major, minor and build version on the first, second and third parameter respectively. What happens later is a series of “if and else”, where they determine the exact version and build number:

dword_14000CDEC = BuildNumber;
  if ( MajorVersion == 10 )
  {
    if ( !MinorVersion )
    {
      if ( BuildNumber >= 10586 )
      {
        if ( BuildNumber > 10586 )
        {
          if ( BuildNumber > 14393 )
          {
            if ( BuildNumber > 15063 )
            {
              if ( BuildNumber <= 16299 )
              {
                dword_14000CDE8 = 13;
                qword_14000CDF0 = off_140009BA0;
                qword_14000CDF8 = off_140009BE8;
                qword_14000CE00 = off_140009C08;
                qword_14000CE08 = off_140009C10;
                qword_14000CE10 = off_140009C18;
                qword_14000CE18 = off_140009C20;
                qword_14000CE20 = off_140009C48;
                v0 = off_140009C58;

A number of 8 global variables are being initialized with function pointers that retrieve some particular offset of different structures.

Let’s focus on one case: qword_14000CDF0 is being initialize with off_140009BA0, which contains a reference to another function:

 .data:0000000140009BA0 off_140009BA0   dq offset sub_1400062D4 ; DATA XREF: sub_14000646C+23Eā†‘o

We can see how the function is taking the value of rcx (first parameter) and adding 0x418:

.text:00000001400062D4 ; =============== S U B R O U T I N E =======================================
.text:00000001400062D4
.text:00000001400062D4
.text:00000001400062D4 sub_1400062D4   proc near               ; DATA XREF: .data:off_140009520ā†“o
.text:00000001400062D4                                         ; .data:off_140009A00ā†“o ...
.text:00000001400062D4                 mov     rax, [rcx+418h]
.text:00000001400062DB                 retn
.text:00000001400062DB sub_1400062D4   endp

Here is where things get harder. In order to identify which structure is the one that is being sent via RCX, we need to find a case where qword_14000CDF0 is being used:

To sum up and avoid getting things even more complex, we are going to end with the following case (of course, after renaming and some analysis):

if ( PsLookupProcessByProcessId(ProcessId, &_pEPROCESS) >= 0 )// Retrieve reference to EPROCESS structure
  {
    v2 = _pEPROCESS;
    if ( MmIsAddressValid(_pEPROCESS) && fn_GetObjectTable(v2) )
    {

fn_GetObjectTable is our function qword_14000CDF0, and this will be the hint that tell us that the function EPROCESS is the one being sent as parameter. Note that PsLookupProcessByProcessId is being called before and the second parameter is being used to store the EPROCESS structure, as the documentation explains.

The offset 0x418 belongs to the attribute ObjectTable, a pointer to the _HANDLE_TABLE structure inside of the kernel.

Side Note

ObjectTable is used a lot to manually enumerate and analyze the HANDLEs of a process. ACs usually take this table and access to each existing handle in order to determine if other processes have a HANDLEs to the process of the game; or if the HANDLE from process like lsass.exe or csrss.exe have been manipulated.

Next Steps

  • Part 4.3 – Using ObRegisterCallbacks
  • Part 4.4 – Analyzing the Notify Routines