Reversing XignCode3 Driver – Part 1 – Identifying the Driver Entry Point

This will be a series of posts related to XignCode3 (XC3) Driver. I started this at the beginning of 2019 while I was doing my research for Unveiling the underground world of Anti-Cheats. Due to that, you will find that the version of XC3 is not the lastest one available.

Reversing a Driver of this kind is not only fun but also let you learn a lot about Windows Internals. That said, I will be as short and precise as possible, and I will try to write down some highlights of what I think are some interesting code snippets or features of this driver.

What we will go through?

  1. Learn about the DriverEntry
  2. Learn to identify this function on the binary
  3. Identify how the Driver Object is created
  4. Analyze the Major functions implemented, especially the dispatcher one
  5. Detect some additional code, which we don’t know what it does. Yet.

You can also find here the final result of reversing the function DriverEntry.

Where do I begin?

Getting the .sys file, of course. If you want to reverse this by your own, these are the hashes of the file I used:

  • md5: C82DE0BC9A3A08E351B9BFA960F8757A
  • sha1: F3AE1406B4CD7D1394DAC529C62BB01F3EB46D4A
  • sha256: E7994B56152955835B5025049ABEE651E9C52A6CCAD7E04F2C458C8568967245

When I want to reverse a Driver, I usually like to start with the DriverEntry. In this function, we can find some interesting details that will help us identify most of the features that a driver has implemented.

IDA Pro and Ghidra are usually able to detect this function automatically. But sometimes you may have to do this manually. As the Microsoft site says:

“DriverEntry is the first driver-supplied routine that is called after a driver is loaded. It is responsible for initializing the driver.”

Basically we need to look for a function that calls to IoCreateDevice, this is usually done inside of the DriverEntry, and it is used to create a new device object. This is just one way to do it, but since IDA did it automatically for us, let’s jump straight to the reversing phase.

DriverEntry (0x1400047B8)

To keep this post clean, you can see here how DriverEntry was decompiled by IDA Pro at the very beginning, and here the assembly. I encourage you to take a few minutes to read the code and try to identify the main goal of the function before continuing with your reading. Let’s see how we can clean this up. Sometimes renaming and changing the types of the variables can be 50% of the work and of course, will help us to understand the function better.

We know that DriverEntry receives two parameters: a _DRIVER_OBJECT pointer and a PUNICODE_STRING with the registry path. So, we will change the name and type of those two variables.

Something to take into account is that many of the structure types we need when reversing are not available by default on IDA Pro. Therefore, we can import them by doing: “File->Load File->Parse C Header file…”.

Going back to the code, we have some validations and concatenation of the SymbolicLinkName and the DeviceName, nothing weird. They are just setting up the required strings:

	_DriverObject = DriverObject;
	DeviceObject = 0i64;
	if (!RegistryPath->Length || fn_strcat(Dest, RegistryPath) < 0)
		return 0xC0000001i64;
	if (sub_140003A50(&SymbolicLinkName, Dest) < 0)
	{
		Real_Driver_Entry(Dest);
		return 0xC0000001i64;
	}

DriverEntry needs to implement some particular “required responsibilities or routines” as explained here. Let’s see how they did it before calling IoCreateDevice:

_DriverObject->DriverUnload = fn_DriverUnloadDispatcher;
_DriverObject->MajorFunction[0] = fn_DispatchCreate;// IRP_MJ_CREATE
_DriverObject->MajorFunction[2] = fn_DispatchClose;// IRP_MJ_CLOSE                        
_DriverObject->MajorFunction[4] = fn_DriverIOCTLDispatcher;// IRP_MJ_WRITE                        
ntStatus = IoCreateDevice(_DriverObject, 0, &DeviceName, 0x22u, 0, 0, &DeviceObject);

I renamed a few variables and functions to describe their main purpose. However, let me explain how you can easily identify them:

Here we can see all the IRP Major Function Codes available on Windows. In the previous snippet of code, only the numbers 0, 2 and 4 have been implemented:

  • IRP_MJ_CREATE 0x00
  • IRP_MJ_CLOSE 0x02
  • IRP_MJ_WRITE 0x04

A full list can be found on my github.

IRP_MJ_CREATE and IRP_MJ_CLOSE are just generic functions. The interesting one is IRP_MJ_WRITE, which will handle the requests coming from user-mode and it is implemented on fn_DriverIOCTLDispatcher. I will explain in another post how this function works and how it dispatches every IRP request. For now, we will focus on interesting functions that DriverEntry calls.

After some analysis of the inner behavior of the functions that are being called, we can get to the following code:

		fn_InitDispatchMethodArray();
		ntStatus = fn_InitRegistrationNotifyAndCallbackRoutines();
		if (ntStatus >= 0)
		{
			ntStatus = fn_ObtainKernelFunctions();
			if (ntStatus >= 0)
			{
				ntStatus = IoCreateSymbolicLink(&SymbolicLinkName, &DeviceName);
				if (ntStatus >= 0)
				{
					ntStatus = 0;
					goto LABEL_18;
				}
				nullsub_1();
			}
			fn_RegisterCreateProcessNotifyRoutine();
			nullsub_1();
		}

Some interesting names have appeared:

  • fn_InitDispatchMethodArray
  • fn_InitRegistrationNotifyAndCallbackRoutines
  • fn_ObtainKernelFunctions
  • and fn_RegisterCreateProcessNotifyRoutine

As you may have noticed I tend to write very descriptive names haha. Those names might be wrong or not, depending on what we discover during the reversing process later. As I mentioned, most of this work has been done months ago. Sometimes reversing has a lot of guessing, so we need to start to imagine what each function could potentially do and give them names so we can picture the goal of every single one of them.

The name is based on what I could infer about those functions. We are going to see them in depth in the following posts.

We can basically separate them into two groups based on their goals. The functions that register some kind of callback; fn_InitRegistrationNotifyAndCallbackRoutines and fn_RegisterCreateProcessNotifyRoutine. And the function that inits basic information that the Driver requires in order to work; fn_InitDispatchMethodArray and fn_ObtainKernelFunctions.

Next Steps