Download link: unfairgame

credits


  • Can - original poster of NtConvertBetweenAuxiliaryCounterAndPerformanceCounter .data pointer swap…
  • btbd - original creator of modmap

Overview


Unfairgame is an online game cheat provider that sells game cheats specifically for competitive play games. These games include Tom Clancy's Rainbow Six Siege, Overwatch, Rust, Escape from Tarkov, and Valorant. This cheat provider has been around for almost two years now and has easily made over one hundred thousands dollars in profits. All the while selling their users publicly available code through a poorly constructed cheat loader system.

CPL3


The majority of my reverse engineering was done inside of the kernel, only toward the end I looked into the usermode executable(s). Lets begin with the executable image that is provided to the user after purchasing. The first thing I noticed was the size of this application. It's less than a quarter of a megabyte which led me to believe that this application is one of two things. Firstly, this application was probably not protected by VMProtect or Themida. These virtualizers tend to produce executables that are much larger. Secondly, the application probably does not contain any embedded applications like drivers, dll, etc. The latter could always be false, nevertheless this application was still mutated and difficult to understand at first glance. Instead of pursuing static analysis, a combination of dynamic analysis and static analysis is preferable in these situations considering the asynchronicity of modern packers and mutators. Typically you can gain an understanding of what an application does by seeing how it interfaces with the operating system on which it's executing. After running this executable inside of a debugger it was clear that it spawns a subprocess (RuntimeBroker.exe) and injects itself into that process, finishing its execution by changing its original name on disk to something random. The newly injected module now spawns a console, and prompts the user to login. After logging in the user is prompted to select any of their subscriptions which subsequently downloads the encrypted module from their web server.

Although this url is predestined depending on your subscription one can simply change the desired file to any of their other subscriptions. In other words with a little bit of guessing I was able to stream encrypted modules that I never had a subscription for. Keep in mind we are only a few paragraphs into this write up and I already have the ability to obtain every file that every subscription offers. The lack of thought that has been put into this product disgustingly compares to the amount of profit this product has made. Furthermore, the subsequently downloaded modules get decrypted and copied into a heap allocation allowing for systematic decryption and dumping.

Moreover, not only are the decrypted modules stored in a heap allocation, but also their manually mapped driver. Now that I have all decrypted modules for Rainbow Six, Rust, and Valorant which can be found here, let's move onto the kernel part of this cheat which arguably contains much more entertaining trivial mistakes.

CPL0


[unfairgame]base address: 0xFFFFF80353B70000	
[unfairgame]driver loaded from: \??\C:\Windows\diskptex0.dat:exe	
[unfairgame]     - driver timestamp: 0x000000005EB9B094	
[unfairgame]unfair driver loaded...	

The first image loaded into the kernel by this cheat provider is a driver that is signed with a cert that they bought.

Name:          艾許工作室 (Ash Studio)
Issuer:        DigiCert SHA2 High Assurance Code Signing CA
Valid From:    2019-10-30 00:00:00
Valid To:      2021-02-10 12:00:00
Algorithm:     sha256RSA
Thumbprint:    C52DD640E34C86A824B881B1F80B27195B811B4A
Serial Number: 0E 9F 17 96 8A C5 0F 1A FA F5 3B 19 8D BF 7F FD 

It seems this company was registered in china back in 2017, more information about this company can be found here.

Although this image is signed by them, it is lacking any mutation or virtualization. This is a pretty big mistake by Unfairgame’s, considering their only line of real defense in the kernel is an open book. Moreover the code for this driver is terribly constructed, everything from imports to IOCTL is poorly thought out, barely functioning, and highly insecure.

NTSTATUS __stdcall DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
  //...
  if ( (int)ZwQuerySystemInformation(0xBi64, v4, (unsigned int)NumberOfBytes, 0i64) < 0 )
  {
    ExFreePoolWithTag(v5, 0x47536353u);
    return 0xC0000365;
  }
  ntoskrnlBaseAddress = v5[3];                // first entry is always ntoskrnl.exe
  ExFreePoolWithTag(v5, 0x47536353u);
  if ( !ntoskrnlBaseAddress )
    return 0xC0000365;
  *(_QWORD *)MmMapIoSpace = GetProcAddress(ntoskrnlBaseAddress, "MmMapIoSpace");
  *(_QWORD *)MmUnmapIoSpace = GetProcAddress(ntoskrnlBaseAddress, "MmUnmapIoSpace");
  *(_QWORD *)IofCompleteRequest = GetProcAddress(ntoskrnlBaseAddress, "IofCompleteRequest");
  IoCreateDevice = GetProcAddress(ntoskrnlBaseAddress, "IoCreateDevice");
  IoCreateSymbolicLink = GetProcAddress(ntoskrnlBaseAddress, "IoCreateSymbolicLink");
  IoDeleteDevice = GetProcAddress(ntoskrnlBaseAddress, "IoDeleteDevice");
  *(_QWORD *)IoDeleteSymbolicLink = GetProcAddress(ntoskrnlBaseAddress, "IoDeleteSymbolicLink");
  MmGetPhysicalAddress = GetProcAddress(ntoskrnlBaseAddress, "MmGetPhysicalAddress");
  //...
}

As shown above, this driver obtains the base address of ntoskrnl.exe by making a call to NtQuerySystemInformation with SystemModuleInformation assuming the first RTL_PROCESS_MODULE_INFORMATION is the kernels information. The inexperience of the developer for this cheat is really starting to show itself. Getting the base address of the kernel can be done without making a single function call. One can simply import PsInitialSystemProcess which is a pointer to an EPROCESS that contains various things about a process including its base address. Adding 0x3C0 to the pointer and then dereferencing it as a qword will provide you the base address of the kernel.

.text:000000014006CE70 PsGetProcessSectionBaseAddress proc near
.text:000000014006CE70                 mov     rax, [rcx+3C0h]
.text:000000014006CE77                 retn
.text:000000014006CE77 PsGetProcessSectionBaseAddress endp

After the driver obtains the base address of the kernel it then dynamically resolves the imports that it uses later. Hooking their implementation of GetProcAddress easily enabled me to hook the rest of their imports such as MmMapIoSpace and MmGetPhysicalAddress.

namespace hooks
{
	void* get_addr_hook(const void* base_addr, const char* func_name)
	{
		DBG_PRINT("");
		DBG_PRINT("=============== %s ==============", __FUNCTION__);
		DBG_PRINT("func_name: %s", func_name);
		if (!strcmp(func_name, "MmMapIoSpace"))
			return &map_io_space;
		else if (!strcmp(func_name, "MmUnmapIoSpace"))
			return &unmap_io_space;
		else if (!strcmp(func_name, "MmGetPhysicalAddress"))
			return &get_phys_addr;
		else if (!strcmp(func_name, "IoCreateSymbolicLink"))
			return &create_sym_link;
		else if (!strcmp(func_name, "IoCreateDevice"))
			return &create_device;
		return driver_util::get_kmode_export("ntoskrnl.exe", func_name);
	}
}

Now that I have full control over all external dependencies via a varied assortment of hooks it is time to document every function call and their subsequent parameters. This heavy documentation of calls and parameters allows for swift and complete reverse engineering to commence. MmMapIoSpace/MmGetPhysicalAddress are probably the most interesting functions considering when used together they can map the physical memory of a virtual address effectively making a second mapping of a specific address range. Adding to this interest is the fact that both functions are exposed to usermode via IOCTL.

[unfairgame]=============== hooks::get_phys_addr ==============	
[unfairgame]getting physical address of: 0xFFFFF588CB335660 // vtable ptr of DxgkReclaimAllocations2 (inside of win32kbase.sys)
[unfairgame]base_addr value: 0xFFFFF80B2C504420	// the actual pointer to DxgkReclaimAllocations2
[unfairgame]physical address: 0x00000001D7317660
[unfairgame]=============== hooks::map_io_space ==============	
[unfairgame]mapping physical memory 0x00000001D7317660 of size 0x8
[unfairgame]mapped io space 0xFFFFA3818A146660, value: 0xFFFFF80B2C504420 // the actual pointer to DxgkReclaimAllocations2

Taking a closer look at the pointer passed to MmGetPhysicalAddress shows that it resides inside of win32base.sys. I was able to conclude which module this address lands in by looking at Process Hacker’s list of kernel modules and their base addresses.

After double checking that win32kbase.sys was indeed the correct module I then opened the win32kbase.sys located at C:\Windows\System32\drivers\win32kbase.sys and navigated to the correct offset inside of the driver. I landed inside of the data section of the module specifically on an entry into a vtable. This pointer happens to be a pointer to an export of dxgkrnl.sys. After further analysis it was clear that win32kbase.sys acts as a huge vtable for dxgkrnl.sys exports and can be easily manipulated by changing the pointer to anything you want thus allowing you to call any function in the kernel with any amount of parameters you want.

.data:00000001C01A5618 qword_1C01A5618 dq ?                    ; DATA XREF: NtGdiDdDDIRemoveSurfaceFromSwapChain+4
.data:00000001C01A5620 qword_1C01A5620 dq ?                    ; DATA XREF: NtGdiDdDDIUnOrderedPresentSwapChain+4
.data:00000001C01A5628 qword_1C01A5628 dq ?                    ; DATA XREF: NtGdiDdDDIAcquireSwapChain+4
.data:00000001C01A5630 qword_1C01A5630 dq ?                    ; DATA XREF: NtGdiDdDDIReleaseSwapChain+4
.data:00000001C01A5638 qword_1C01A5638 dq ?                    ; DATA XREF: NtGdiDdDDIGetSetSwapChainMetadata+4
.data:00000001C01A5640 qword_1C01A5640 dq ?                    ; DATA XREF: NtGdiDdDDIAbandonSwapChain+4
.data:00000001C01A5648 qword_1C01A5648 dq ?                    ; DATA XREF: NtGdiDdDDISetDodIndirectSwapchain+4
.data:00000001C01A5650 qword_1C01A5650 dq ?                    ; DATA XREF: NtGdiDdDDICheckMultiPlaneOverlaySupport2+4
.data:00000001C01A5658 qword_1C01A5658 dq ?                    ; DATA XREF: NtGdiDdDDIPresentMultiPlaneOverlay2+4
.data:00000001C01A5660 ; __int64 (*NtGdiDdDDIReclaimAllocations2_0)(void) notice how this ends with 0x660!
.data:00000001C01A5660 NtGdiDdDDIReclaimAllocations2_0 dq ?    ; DATA XREF: NtGdiDdDDIReclaimAllocations2+4
.data:00000001C01A5668 qword_1C01A5668 dq ?                    ; DATA XREF: NtGdiDdDDIGetResourcePresentPrivateDriverData+4
.data:00000001C01A5670 qword_1C01A5670 dq ?                    ; DATA XREF: NtGdiDdDDISetStablePowerState+4
.data:00000001C01A5678 qword_1C01A5678 dq ?                    ; DATA XREF: NtGdiDdDDIQueryClockCalibration+4

Checking the ex references to this pointer revealed that it is used inside of a function exported from win32kbase called DxgkReclaimAllocations2. Win32kbase effectively exports all the same functions as dxgkrnl.sys. However this is not the case for newer versions of windows 10. On my virtual machine running windows 10 2004, win32kbase lacks a lot of the exports that are in older versions of windows 10. Potentially limiting the software to only supporting specific versions of windows.

.text:00000001C0068330 NtGdiDdDDIReclaimAllocations2 proc near
.text:00000001C0068330                 sub     rsp, 28h
.text:00000001C0068334                 mov     rax, cs:NtGdiDdDDIReclaimAllocations2_0
.text:00000001C006833B                 call    cs:__guard_dispatch_icall_fptr
.text:00000001C0068341                 add     rsp, 28h
.text:00000001C0068345                 retn
.text:00000001C0068345 NtGdiDdDDIReclaimAllocations2 endp

Driver Mapping


Now that we understand the basics of this hooking/communication method, all other calls to MmGetPhysicalAddress become much more clear in their intent. The next time MmGetPhysicalAddress is called a pointer that resides inside of ntoskrnl is passed. This address turns out to be the address for ExAllocatePool. Typically this use of ExAllocatePool is used to allocate a space for a driver that is not signed.

[unfairgame]=============== hooks::get_phys_addr ==============	
[unfairgame]getting physical address of: 0xFFFFF588CB335660	// address of vtable in win32kbase.sys
[unfairgame]base_addr value: 0xFFFFF80046968650	// address of ExAllocatePool
[unfairgame]physical address: 0x00000001D7317660
.text:00000001400D1650 ; =============== S U B R O U T I N E =======================================
.text:00000001400D1650 ; NOTICE HOW THIS ENDS WITH 0x650! :)
.text:00000001400D1650
.text:00000001400D1650 ; PVOID __stdcall ExAllocatePool(POOL_TYPE PoolType, SIZE_T NumberOfBytes)
.text:00000001400D1650                 public ExAllocatePool
.text:00000001400D1650 ExAllocatePool  proc near
.text:00000001400D1650                 sub     rsp, 28h
.text:00000001400D1654                 mov     r8d, 656E6F4Eh  ; Tag
.text:00000001400D165A                 call    ExAllocatePoolWithTag
.text:00000001400D165F                 add     rsp, 28h
.text:00000001400D1663                 retn
.text:00000001400D1663 ExAllocatePool  endp

To say nothing of this terrible code design would be a dishonor to the people who pay for this product. Using such a technique as to swap a pointer inside of the win32kbase's vtable to an address of another function in the kernel makes no sense in the current context considering that exposing such a function to a user mode process can be done simply by adding the ability to invoke such routines to new IOCTL switch cases. Instead of swapping pointers and complicating the code for no reason one could simply do the following. Keep in mind that the only reason I say this is because their leaked cert signed driver is still loaded in the kernel! None of the stuff they are doing makes any sense, nor does it make anything more secure!

switch (IOCTL_CODE)
{
case ALLOCATE_MEMORY:
	// allocate memory 
	break;
case CALL_DRIVER_ENTRY:
    // ...
    break;
default:
	//...
}

Finally the last call to MmGetPhysicalAddress passes a pointer that is not inside of any legitimate modules, but instead it's inside of the allocated pool that was created by the previous pointer swap and function invocation (ExAllocatePool). This pointer is 0x2004 deep into the allocation which makes it easy for one to assume that this could be a pointer to the entry point of the manually mapped driver. As previously stated in the CPL3 section of this write up, the manually mapped driver was easily obtained since it was located inside of a heap allocation. Comparing the entry point of the driver inside of the heap allocation and the offset into the kernel pool allocation reveals that such offsets are equal.

[unfairgame]base_addr value: 0xFFFF800506202004	// probably driver entry
[unfairgame]physical address: 0x0000000199516660	
[unfairgame]mapping physical memory 0x0000000199516660 of size 0x8	
[unfairgame]mapped io space 0xFFFF9D008C958660, value: 0xFFFF800506202004 // probably driver entry
.text:0000000140002004 ; As you can see this entry point is 0x2004 into this module. Same as the IOCTL log data.
.text:0000000140002004
.text:0000000140002004 ; NTSTATUS __stdcall DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
.text:0000000140002004                 public DriverEntry
.text:0000000140002004 DriverEntry     proc near
.text:0000000140002004
.text:0000000140002004 arg_0           = qword ptr  8
.text:0000000140002004 arg_8           = qword ptr  10h
.text:0000000140002004 arg_10          = qword ptr  18h
.text:0000000140002004 arg_18          = qword ptr  20h
.text:0000000140002004
.text:0000000140002004                 call    get_proc_addr
.text:0000000140002009                 push    rax
.text:000000014000200A                 rcr     ecx, 1
.text:000000014000200C                 fdiv    dword ptr [rbx+38EE64BAh]
.text:0000000140002012                 db      36h
.text:0000000140002012                 mov     edx, 555B13A0h
.text:0000000140002018                 mov     rbp, rsp
.text:000000014000201B                 sub     rsp, 50h
.text:000000014000201F                 mov     rax, cs:qword_140004000

Communication


Now that we have a deep understanding of how the unsigned driver is mapped into the kernel, let's take a look at what exactly this driver does, and equally as important, how does this driver communicate to and from user mode applications. Firstly this manually mapped driver is nothing more than a clear copy of modmap, a popular module extender made by btbd. This can be conclusively proven by looking at the communication method, data communicated, and modules that have been loaded into the game(s) and extended.

To begin, the communication method of this manually mapped driver is the same as the one used in modmap, simply by changing xKdEnumerateDebuggingDevices, a pointer located inside of ntoskrnl.exe data section, one can invoke what would seem to be a harmless function and harbor it as a means for communication. This method of communication was originally discussed by Can at this game hacking forum.

__int64 __fastcall NtConvertBetweenAuxiliaryCounterAndPerformanceCounter(char a1, unsigned __int64 a2, _QWORD *a3, _QWORD *a4)
{
  _QWORD *v4; // rbx
  _QWORD *v5; // rdi
  char v6; // si
  __int64 v7; // r14
  __int64 (__fastcall *v8)(); // rax
  unsigned int v9; // ecx
  __int64 (__fastcall *v10)(); // rax
  __int64 v12; // [rsp+20h] [rbp-28h]
  __int64 v13; // [rsp+28h] [rbp-20h]
  __int64 v14; // [rsp+30h] [rbp-18h]

  v4 = a4;
  v5 = a3;
  v6 = a1;
  if ( KeGetCurrentThread()->PreviousMode )
  {
    if ( a2 & 3 )
      ExRaiseDatatypeMisalignment();
    if ( a2 + 8 > 0x7FFFFFFF0000i64 || a2 + 8 < a2 )
      MEMORY[0x7FFFFFFF0000] = 0;
    v7 = *(_QWORD *)a2;
    v14 = *(_QWORD *)a2;
    ProbeForWrite(a3, 8ui64, 4u);
    if ( v4 )
      ProbeForWrite(v4, 8ui64, 4u);
    v8 = off_140398A08[0];
    if ( !v6 )
      v8 = off_140398A00[0]; // this pointer gets swapped to the address of the manually mapped function hook handler.
    v9 = ((__int64 (__fastcall *)(__int64, __int64 *, __int64 *))v8)(v7, &v12, &v13);
    if ( (v9 & 0x80000000) == 0 )
    {
      *v5 = v12;
      if ( v4 )
        *v4 = v13;
    }
  }
  else
  {
    v10 = off_140398A08[0];
    if ( !a1 )
      v10 = off_140398A00[0];
    v9 = ((__int64 (__fastcall *)(_QWORD, _QWORD *, _QWORD *))v10)(*(_QWORD *)a2, a3, a4);
  }
  return v9;
}

As you can see, this function allows attackers to easily swap a function pointer and then invoke this function whilst not only allowing an attacker to pass data but also validating it for the attacker. As shown above, the second parameter passed to this function should be treated as a pointer to a pointer. Attaching a debugger and setting a break point on NtConvertBetweenAuxiliaryCounterAndPerformanceCounter, located inside of ntdll.dll, should reveal the necessary information to conclusively prove that not only is this entire cheat a copy and paste, but a poorly constructed one.

Sure enough this function does indeed get invoked. At this point it doesn't take a crystal ball to foresee that RDX is going to be a pointer to a pointer that contains the same structure’s as modmap's.

Indeed the data does match the structures seen in modmap. Moreover, the dll seen in the screenshot above is NOT native to windows. It is signed by MSI, and does not come with standard windows 10. Although there are modules with the same name lacking the “64” at the end, these modules are not signed. In addition, if I were to delete this module, it will be recreated by the second stage loader presumably. Additionally, this module is not normally loaded into Rust or Rainbow Six, and when unloaded using process hacker, the process in which it is executing will crash. This is due to the fact that it is extended with whatever module your subscription provides.

Conclusion


To conclude this cheat is nothing more than public code and offsets found on game hacking forums. The developers and resellers of this cheat do not even understand how their own product works, nor do they understand how disgustingly insecure it is. The ignorance of these people coupled with their staggering six figure profits only make me wonder how Easy Anti Cheat or BattlEye could let this slide, let alone the companies who pay these anti cheat providers. On that note I shall leave you with this….