Disclosure: The Mhyprot Vulnerability - Genshin Impact

  • windows
  • mhyprot

The Mhyprot Vulnerability - Genshin Impact

Almost over a year ago, I found a vulnerability during analysis of the driver, in mhyprot2.sys, windows kernel-mode driver, who responsive for protecting game-process, the Genshin Impact by Mihoyo.

mhyprot is a part of components of the client-sided anti-cheat approach.
As the kernel-mode drivers have system-level privilege, it’s often provoke controversy about user’s privacy and its mainly called rootkit as Riot’s Vanguard, BattlEye and EasyAntiCheat does.

To clarify: I personally do not think these are ”Rootkit” since I am one of the anti-cheat developer who knows what he is doing. It is necessary to have system-level privilege to prevent from cheating. but in other hand, it is clear it also necessary to protect user’s privacy aswell.

Why

After a while, I submitted this vulnerability to the vendor, Mihoyo. And I thought that this vulnerability will be fixed very early.
Let me get straight to the point, the vendor does not respond or even acknowledge it.

PoC - Proof of Concept

Then I published it to the my github repository, as

repositories were published at Oct 2020. after a while, I decided to took down it, for personal reasons.
Also were popular and made a lot of duscussions.

Some days ago, a couple of PoC (proof of concept) code was shared on GitHub that takes advantage of the kernel-level anticheat Genshin Impact uses to be able to (edit: further, see this comment, my bad) compromise the system.

Genshin Impact’s anti-cheat is not completely secure: you can use it to read/write umode memory / read kmode memory with kernel privileges: https://github.com/ScHaTTeNLiLiE/libmhyprot Mirror repo after the original author took the repo down, but still exploitable AFAIK.

But now, the vendor company still not respond or acknowledge it, I’ve decided to publish it again (May 2021).
(BTW, those were popular from the beginning and there were many forks)

Responsible Disclosure

I, Kento Oki, am not the researcher who expect to be financially compensated.
This vulnerability is being published because the vendor does not respond or fixed it after I noticed them.

Contact: hrn832@protonmail.com

The Vulnerability

The mhyprot driver exposes a bunch of IOCTLs that must not be exposed to the user-mode.
For example, the driver could copy kernel virtual memory which could lead to information-disclosure (CWE-200), privilege-escalation (CWE-269) and denial of service since it could trigger bugcheck intentionally.

As I declared in my PoC repo (https://github.com/kkent030315/libmhyprot#features),

  • Read Arbitrary Kernel Memory
  • Read Arbitrary Process Memory
  • Write Arbitrary Process Memory
  • Get Arbitrary Process Modules
  • Get Arbitrary Process Threads
  • Get System Uptime
  • Terminate Arbitrary Process

are possible with user-privilege.
Please note that these features is not the all. I belive there are more vulnerable commands.

And the possible impacts:

  • Arbitrary Process Information Disclosure - may lead to CWE-200
  • Arbitrary Process Virtual Memory R/W - may lead to CWE-200, CWE-269, CWE-94
  • Arbitrary Kernel Memory R/W - may lead to CWE-200, CWE-269, CWE-94

Does that really makes you think product that take user privacy into consideration?
Also it scored as 8.6 by CVSS calculation.

CVSS

It feel like the biggest backdoor I’ve ever seen before.

Introduction To The Vulnerable Driver

The mhyprot is an anti-cheat kernel mode driver used in Genshin Impact.
The driver has vulnerable IOCTL commands that allows attackers to execute improperly from ring-3 (usermode),
without privileges that usually needed to be granted by OS system.

Usermode Module

Driver’s device handle is opened by the game process GenshinImpact.exe.

IMAGE

Driver Initialization

The MHYPROT_IOCTL_INITIALIZE what I defined in mhyprot.hpp can be found as follows:

PAGE:FFFFF800188CD8FD loc_FFFFF800188CD8FD:                   ; CODE XREF: sub_FFFFF800188CD6E0+213↑j
PAGE:FFFFF800188CD8FD                 cmp     ecx, 80034000h  ; MHYPROT_IOCTL_INITIALIZE
PAGE:FFFFF800188CD903                 jnz     short loc_FFFFF800188CD984
PAGE:FFFFF800188CD905                 cmp     r8d, 10h
PAGE:FFFFF800188CD909                 jnz     loc_FFFFF800188CDA4F
PAGE:FFFFF800188CD90F                 mov     rax, 0EBBAAEF4FFF89042h // <- _m_002
PAGE:FFFFF800188CD919                 xor     [rdi+8], rax
PAGE:FFFFF800188CD91D                 mov     rax, [rdi+8]
PAGE:FFFFF800188CD921                 xor     [rdi], rax
PAGE:FFFFF800188CD924                 cmp     dword ptr [rdi+4], 0BAEBAEECh // <- _m_001
PAGE:FFFFF800188CD92B                 jnz     loc_FFFFF800188CDA4F
PAGE:FFFFF800188CD931                 mov     ecx, [rdi]
PAGE:FFFFF800188CD933                 call    sub_FFFFF800188C51A8
PAGE:FFFFF800188CD938                 cmp     dword ptr cs:qword_FFFFF800188CA108, 0
PAGE:FFFFF800188CD93F                 jnz     short loc_FFFFF800188CD97D
PAGE:FFFFF800188CD941                 mov     rdx, [rdi+8]
PAGE:FFFFF800188CD945                 lea     rcx, xmmword_FFFFF800188CA0E8
PAGE:FFFFF800188CD94C                 call    sub_FFFFF800188C301C // <-
PAGE:FFFFF800188CD951                 mov     ebx, 7

and the sub_FFFFF800188C301C is look like:

.text:FFFFF800188C301C ; =============== S U B R O U T I N E =======================================
.text:FFFFF800188C301C
.text:FFFFF800188C301C
.text:FFFFF800188C301C sub_FFFFF800188C301C proc near          ; CODE XREF: sub_FFFFF800188CD6E0+26C↓p
.text:FFFFF800188C301C                                         ; DATA XREF: .upx0:FFFFF800189F2BA8↓o
.text:FFFFF800188C301C
.text:FFFFF800188C301C arg_0           = qword ptr  8
.text:FFFFF800188C301C
.text:FFFFF800188C301C                 test    rcx, rcx
.text:FFFFF800188C301F                 jz      locret_FFFFF800188C30B4
.text:FFFFF800188C3025                 mov     [rsp+arg_0], rbx
.text:FFFFF800188C302A                 push    rdi
.text:FFFFF800188C302B                 sub     rsp, 20h
.text:FFFFF800188C302F                 xor     eax, eax
.text:FFFFF800188C3031                 mov     rdi, rdx
.text:FFFFF800188C3034                 mov     [rcx], rax
.text:FFFFF800188C3037                 mov     rbx, rcx
.text:FFFFF800188C303A                 mov     [rcx+8], rax
.text:FFFFF800188C303E                 mov     edx, 9C0h       ; NumberOfBytes
.text:FFFFF800188C3043                 xor     ecx, ecx        ; PoolType
.text:FFFFF800188C3045                 call    cs:ExAllocatePool
.text:FFFFF800188C304B                 xor     edx, edx
.text:FFFFF800188C304D                 mov     r8d, 9C0h
.text:FFFFF800188C3053                 mov     rcx, rax
.text:FFFFF800188C3056                 mov     [rbx], rax
.text:FFFFF800188C3059                 call    sub_FFFFF800188C7900
.text:FFFFF800188C305E                 mov     rax, [rbx]
.text:FFFFF800188C3061                 mov     r9d, 1
.text:FFFFF800188C3067                 mov     [rbx+0Ch], r9d
.text:FFFFF800188C306B                 mov     [rax], rdi
.text:FFFFF800188C306E                 mov     [rbx+8], r9d
.text:FFFFF800188C3072
.text:FFFFF800188C3072 loc_FFFFF800188C3072:                   ; CODE XREF: sub_FFFFF800188C301C+8C↓j
.text:FFFFF800188C3072                 movsxd  r8, dword ptr [rbx+8]
.text:FFFFF800188C3076                 mov     rdx, [rbx]
.text:FFFFF800188C3079                 mov     rax, [rdx+r8*8-8]
.text:FFFFF800188C307E                 mov     rcx, rax
.text:FFFFF800188C3081                 shr     rcx, 3Eh
.text:FFFFF800188C3085                 xor     rcx, rax
.text:FFFFF800188C3088                 mov     rax, 5851F42D4C957F2Dh
.text:FFFFF800188C3092                 imul    rcx, rax
.text:FFFFF800188C3096                 add     rcx, r8
.text:FFFFF800188C3099                 mov     [rdx+r8*8], rcx
.text:FFFFF800188C309D                 add     [rbx+8], r9d
.text:FFFFF800188C30A1                 cmp     dword ptr [rbx+8], 138h
.text:FFFFF800188C30A8                 jl      short loc_FFFFF800188C3072
.text:FFFFF800188C30AA                 mov     rbx, [rsp+28h+arg_0]
.text:FFFFF800188C30AF                 add     rsp, 20h
.text:FFFFF800188C30B3                 pop     rdi
.text:FFFFF800188C30B4
.text:FFFFF800188C30B4 locret_FFFFF800188C30B4:                ; CODE XREF: sub_FFFFF800188C301C+3↑j
.text:FFFFF800188C30B4                 retn
.text:FFFFF800188C30B4 sub_FFFFF800188C301C endp

Copy Arbitrary Kernel Memory

There are so many IOCTL commands and the MHYPROT_IOCTL_READ_KERNEL_MEMORY what I defined in mhyprot.hpp can be found as follows:

PAGE:FFFFF800188CD7A9 loc_FFFFF800188CD7A9:                   ; CODE XREF: sub_FFFFF800188CD6E0+BA↑j
PAGE:FFFFF800188CD7A9                 cmp     ecx, 83064000h  ; MHYPROT_IOCTL_READ_KERNEL_MEMORY
PAGE:FFFFF800188CD7AF                 jnz     short loc_FFFFF800188CD7C8
PAGE:FFFFF800188CD7B1                 mov     rdx, [rdi]
PAGE:FFFFF800188CD7B4                 lea     rcx, [rdi+4]
PAGE:FFFFF800188CD7B8                 mov     r8d, [rdi+8]
PAGE:FFFFF800188CD7BC                 call    sub_FFFFF800188C63A8 // <-

And the sub_FFFFF800188C63A8 is like:

.text:FFFFF800188C63A8 sub_FFFFF800188C63A8 proc near          ; CODE XREF: sub_FFFFF800188CD6E0+DC↓p
.text:FFFFF800188C63A8                                         ; DATA XREF: .upx0:FFFFF800189F2EE4↓o
.text:FFFFF800188C63A8
.text:FFFFF800188C63A8 arg_0           = qword ptr  8
.text:FFFFF800188C63A8 arg_8           = qword ptr  10h
.text:FFFFF800188C63A8
.text:FFFFF800188C63A8                 mov     [rsp+arg_0], rbx
.text:FFFFF800188C63AD                 mov     [rsp+arg_8], rsi
.text:FFFFF800188C63B2                 push    rdi
.text:FFFFF800188C63B3                 sub     rsp, 20h
.text:FFFFF800188C63B7                 mov     edi, r8d
.text:FFFFF800188C63BA                 mov     rbx, rdx
.text:FFFFF800188C63BD                 mov     rsi, rcx
.text:FFFFF800188C63C0                 test    rdx, rdx
.text:FFFFF800188C63C3                 jz      short loc_FFFFF800188C63F2
.text:FFFFF800188C63C5                 test    r8d, r8d
.text:FFFFF800188C63C8                 jz      short loc_FFFFF800188C63F2
.text:FFFFF800188C63CA                 mov     rax, cs:MmHighestUserAddress
.text:FFFFF800188C63D1                 cmp     rdx, [rax]
.text:FFFFF800188C63D4                 jb      short loc_FFFFF800188C63F2
.text:FFFFF800188C63D6                 mov     r8d, edi
.text:FFFFF800188C63D9                 xor     edx, edx
.text:FFFFF800188C63DB                 call    sub_FFFFF800188C7900
.text:FFFFF800188C63E0                 mov     r8d, edi
.text:FFFFF800188C63E3                 mov     rdx, rsi
.text:FFFFF800188C63E6                 mov     rcx, rbx
.text:FFFFF800188C63E9                 call    sub_FFFFF800188C3DD8
.text:FFFFF800188C63EE                 xor     eax, eax
.text:FFFFF800188C63F0                 jmp     short loc_FFFFF800188C63F5

Here is the ioctl handlers, found the 0x83064000(MHYPROT_IOCTL_READ_KERNEL_MEMORY) as cmp ecx, 83064000h and some another ioctl codes as follows:

Call map

As I defined as DWORD result in mhyprot.hpp the first 4bytes is result.
I can guess it’s a NTSTATUS as it typedef’ed as typedef LONG NTSTATUS natively and the dispathers return types are NTSTATUS and the result will directly be got stored from it.

Copy Arbitrary Process Memory

The mhyprot calls MmCopyVirtualMemory eventually as wrapper defined as follows:

__int64 __fastcall sub_FFFFF800188C3EB8(struct _EPROCESS *a1, _DWORD *a2, __int64 a3)
{
  __int64 v3; // rbp
  _DWORD *v4; // rdi
  struct _EPROCESS *v5; // rbx
  PEPROCESS v6; // rsi
  char v8; // [rsp+28h] [rbp-20h]

  v3 = a3;
  v4 = a2;
  v5 = a1;
  if ( *a2 == 1 )
  {
    v6 = IoGetCurrentProcess();
  }
  else
  {
    v6 = a1;
    v5 = IoGetCurrentProcess();
  }
  v8 = 0;
  return MmCopyVirtualMemory(v6, *((_QWORD *)v4 + 3), v5, *((_QWORD *)v4 + 2), (unsigned int)v4[8], v8, v3);
}

Called by:

__int64 __fastcall sub_FFFFF800188C3F2C(_DWORD *a1_rw_request, __int64 a2_returnsize, __int64 a3)
{
  __int64 v3_returnsize; // rsi
  _DWORD *v4_rw_request; // rbx
  __int64 v5_processid; // rcx
  bool v6_ntstatus_lookup_success_bool; // di
  unsigned int v8_ntstatus; // ebx
  PVOID Object; // [rsp+40h] [rbp+8h]

  v3_returnsize = a2_returnsize;
  v4_rw_request = a1_rw_request;
  v5_processid = (unsigned int)a1_rw_request[2];
  Object = 0i64;
  v6_ntstatus_lookup_success_bool = (int)PsLookupProcessByProcessId(v5_processid, &Object, a3) >= 0;// NT_SUCCESS
  if ( !Object )
    return 3221225473i64;
  v8_ntstatus = sub_FFFFF800188C3EB8((struct _EPROCESS *)Object, v4_rw_request, v3_returnsize);
  if ( v6_ntstatus_lookup_success_bool )
    ObfDereferenceObject(Object);
  return v8_ntstatus;
}

Called by:

bool __fastcall sub_FFFFF800188C4214(_DWORD *a1_rw_request, _DWORD *a2_returnsize, __int64 a3)
{
  _DWORD *v3_returnsize; // rbx
  int v5_ntstatus; // [rsp+20h] [rbp-18h]
  __int64 v6_returnsize; // [rsp+50h] [rbp+18h]

  v3_returnsize = a2_returnsize;
  v6_returnsize = 0i64;
  v5_ntstatus = sub_FFFFF800188C3F2C(a1_rw_request, (__int64)&v6_returnsize, a3);
  *v3_returnsize = v6_returnsize;
  return v5_ntstatus == 0;                      // NT_SUCCESS(v5_ntstatus)
}

Finally we are at the root of the tree, this is in the packed segment and is in encryption-dedicated IOCTL handler function:

PAGE:FFFFF800188CD303 loc_FFFFF800188CD303:                   ; CODE XREF: sub_FFFFF800188CD000+2C7↑j
PAGE:FFFFF800188CD303                 and     dword ptr [rbp+1D0h+arg_20], 0
PAGE:FFFFF800188CD30A                 lea     rdx, [rbp+1D0h+arg_20]
PAGE:FFFFF800188CD311                 mov     rcx, [rsp+30h]
PAGE:FFFFF800188CD316                 call    sub_FFFFF800188C4214 // <- Here
PAGE:FFFFF800188CD31B                 jmp     loc_FFFFF800188CD21C

Call map

Proof

I have confirmed that by simply hooking mhyprot kernel module:

Enumerate Process Modules

The driver has a lots of commands that make us advantage.
In this case, we are able to enumerate modules that loaded in the target process by process id and a number which specifies we want to get.

I’ll explain herewith below how I made it managed to work it with reverse engineering.
The implementation can be found at mhyprot.cpp#L343.

First of all, As you can see there is cmp ecx, 82054000h as I defined in mhyprot.hpp as MHYPROT_IOCTL_ENUM_PROCESS_MODULES.

And it calls:

__int64 __fastcall sub_FFFFF800188C26D0(unsigned int a1, __int64 a2, __int64 a3)
{
  __int64 v3; // rsi
  unsigned int v4; // ebx
  bool v5; // di
  unsigned int v7; // ebx
  PVOID Object; // [rsp+58h] [rbp+20h]

  v3 = a2;
  Object = 0i64;
  v4 = a3;
  v5 = (int)PsLookupProcessByProcessId(a1, &Object, a3) >= 0;
  if ( !Object )
    return 0i64;
  v7 = sub_FFFFF800188C27D4(Object, v3, v4);
  if ( Object )
  {
    if ( v5 )
      ObfDereferenceObject(Object);
  }
  return v7;
}

As you can see, the function checks is process 32-bit or 64-bit by PsGetProcessWow64Process() since PEB is different between 32 and 64-bit processes.
In this case, I only talk about for 64-bit process.

After that, the function attaches from kernel using KeStackAttachProcess. the second parameter is PKAPC_STATE.
Then, call PsGetProcessPeb and get the PEB belongs to the target process.

LDR_MODULE is undocumented structure.

typedef struct _LDR_MODULE {
  LIST_ENTRY              InLoadOrderModuleList;
  LIST_ENTRY              InMemoryOrderModuleList;
  LIST_ENTRY              InInitializationOrderModuleList;
  PVOID                   BaseAddress;
  PVOID                   EntryPoint;
  ULONG                   SizeOfImage;
  UNICODE_STRING          FullDllName;
  UNICODE_STRING          BaseDllName;
  ULONG                   Flags;
  SHORT                   LoadCount;
  SHORT                   TlsIndex;
  LIST_ENTRY              HashTableEntry;
  ULONG                   TimeDateStamp;
} LDR_MODULE, *PLDR_MODULE;

And the function pseudocode for sub_FFFFF800188C27D4 is like:

__int64 __fastcall sub_FFFFF800188C27D4(
  __int64 a1,       // pEPROCESS
  __int64 a2,       // pointer to the buffer that sent from usermode
  unsigned int a3   // max count to get
)
{
  ...
  
  if ( !a1 )
    return 0i64;
    
  v9 = ((__int64 (*)(void))PsGetProcessWow64Process)() != 0;
  KeStackAttachProcess(v5, &v30);
  
  if ( !v9 ) // the process is 64-bit
  {
    v17 = PsGetProcessPeb(v5); // Lookup PEB
    v18 = v17;
    if ( v17 )
    {
      v19 = *(_QWORD *)(v17 + 24); // PEB->Ldr
      if ( v19 )
      {
        for ( j = *(__int64 **)(v19 + 16);
              j != (__int64 *)(*(_QWORD *)(v18 + 24) + 16i64); // PEB->Ldr->InMemoryOrderModuleList.Flink
              j = (__int64 *)*j )
        {
          if ( v7 < v3 ) // if the counter less than a number what we want to get
          {
            v21 = 928i64 * v7; // [IMPORTANT] we can see output structure is 0x3A0 alignment
            sub_FFFFF800188C7900(v21 + v4 + 12, 0i64, 256i64); // fill memory by 0 sizeof 0x100
            sub_FFFFF800188C7900(v21 + v4 + 268, 0i64, 520i64); // fill memory by 0 sizeof 0x208
            *(_QWORD *)(v21 + v4) = j[6];
            *(_DWORD *)(v21 + v4 + 8) = *((_DWORD *)j + 16);
            v22 = *((_WORD *)j + 44);
            v23 = 127i64;
            if ( v22 <= 0x7Fu )
              v23 = v22;
            sub_FFFFF800188C75C0(v21 + v4 + 12, j[12], v23); // copy BaseDllName to the buffer
            v24 = *((_WORD *)j + 36);
            v25 = v24;
            if ( v24 > 0x103u )
              v25 = 259i64;
            sub_FFFFF800188C75C0(v21 + v4 + 268, j[10], v25); // copy FullDllName to the buffer
            *(_QWORD *)(v21 + v4 + 792) = *((unsigned int *)j + 32);
            v3 = v32;
          }
          ++v6; // counter
          ++v7; // counter
        }
      }
    }
  } else { ... /* 32-bit PEB (Redacted) */ }
  KeUnstackDetachProcess(&v30); // detach
  return v6;
}

We got a much information from it as follows:

  • We can get BaseDllName and FullDllName using this ioctl command
  • What we need is only ProcessId and MaxCount
  • The output buffer will overrided in the request buffer
  • The output buffer also must have 0x3A0 size alignment per module

Definition of structure for the payload be like: (This is defined in mhyprot.hpp as well.)

typedef struct _MHYPROT_ENUM_PROCESS_MODULES_REQUEST
{
	uint32_t process_id;
	uint32_t max_count;
} MHYPROT_ENUM_PROCESS_MODULES_REQUEST, * PMHYPROT_ENUM_PROCESS_MODULES_REQUEST;

By:

if (uVar1_ControlCode == 0x82054000) {
	uVar6 = GetModuleListByProcessId_FUN_000126d0
                // process id
		(*(uint *)puVar3_RequestContext,

                // out buffer, the output will be stored with overriding max count...
		(longlong)(uint *)((longlong)puVar3_RequestContext + 4),

                // max count
		*(uint *)((longlong)puVar3_RequestContext + 4)
		);
	iVar3 = (int)uVar6;
}
...
// mhyprot overrides first 4byte of the payload buffer to identify success or fail
*(int *)puVar3_RequestContext = iVar3;

What we need is:

    1. Allocate memory for payload and its result, 0x3A0 * MaxCount
    1. Send the payload with the ioctl code 0x82054000
    1. Check for the first 4byte

Proof

I’ve hooked some part of mhyprot kernel module, especially PsGetProcessPEB and PsLookupProcessByProcessId and confirmed.

Call map

Enumerate Process Threads

The driver has a vulnerable ioctl that allows us to enumerate threads in specific process as ring-0 privilege.
it also make us able to read kernel structure PETHREAD because the ioctl result contains a pointer to it.
to read kernel memory, we are already able to do it through this vulnerable driver as well.

I’ll explain how I made managed to work it with reverse engineering.

First of all, the driver has a function that executes ZwQuerySystemInformation.
Here is a block found on ioctl handler subroutine (is in the encryption-dedicated IOCTL handler function):

PAGE:FFFFF800188CD77E loc_FFFFF800188CD77E:                   ; CODE XREF: sub_FFFFF800188CD6E0+8C↑j
PAGE:FFFFF800188CD77E                 cmp     ecx, 83024000h
PAGE:FFFFF800188CD784                 jnz     short loc_FFFFF800188CD794
PAGE:FFFFF800188CD786                 lea     rcx, [rdi+4]
PAGE:FFFFF800188CD78A                 mov     rdx, rdi
PAGE:FFFFF800188CD78D                 call    sub_FFFFF800188C62EC
PAGE:FFFFF800188CD792                 jmp     short loc_FFFFF800188CD7C1

sub_FFFFF800188C62EC is:

__int64 __fastcall sub_FFFFF800188C62EC(__int64 a1, _DWORD *a2)
{
  __int64 result; // rax

  if ( *a2 == 136 ) // *a2 == 0x88
    result = sub_FFFFF800188C6488(a2[2], a1, a2[1]);
  else
    result = 0xFFFFFFFFi64;
  return result;
}

We are seeing an if statement if ( *a2 == 136 ), 136 is 0x88, if the a2(given by context) is not 0x88, the driver will returns 0xFFFFFFFF.
I have no idea what is this validation is even I finished looking around it for a while…

Also sub_FFFFF800188C6488 is:

__int64 __fastcall sub_FFFFF800188C6488(int a1, __int64 a2_OutBuffer, unsigned int a3_ProcessId)
{
  v3_OutBuffer = a2_OutBuffer;
  v4 = a1;
  v5 = a3_ProcessId;
  v6 = -1;
  RtlInitUnicodeString(&SystemRoutineName, L"ZwQuerySystemInformation");
  v7_pZwQuerySystemInformation = (int (__fastcall *)(__int64, __m128 *, _QWORD, SIZE_T *))MmGetSystemRoutineAddress(&SystemRoutineName);
  v8_pZwQuerySystemInformation = v7_pZwQuerySystemInformation;
  if ( v7_pZwQuerySystemInformation )
  {
    LODWORD(NumberOfBytes) = 0;
    if ( v7_pZwQuerySystemInformation(5i64, 0i64, 0i64, &NumberOfBytes) == -1073741820 )// SystemProcessInformation
    {
      if ( (_DWORD)NumberOfBytes )
      {
        v9 = (__m128 *)ExAllocatePool(NonPagedPool, (unsigned int)NumberOfBytes);
        v10_ProcInfo = v9;
        if ( v9 )
        {
          RKM_sub_FFFFF800188C7900(v9, 0, (unsigned int)NumberOfBytes);// fill the memory by 0
          if ( v8_pZwQuerySystemInformation(5i64, v10_ProcInfo, (unsigned int)NumberOfBytes, &NumberOfBytes) >= 0 )// SystemProcessInformation
          {
            v11_ProcInfo = v10_ProcInfo;
            while ( (unsigned __int8)MmIsAddressValid(v11_ProcInfo) )
            {
              if ( v11_ProcInfo[5].m128_i32[0] == v4 )
              {
                v6 = HIDWORD(v11_ProcInfo->m128_u64[0]);
                if ( v6 <= v5 )                 // == ProcessId
                {
                  v13 = 0i64;
                  if ( v6 )
                  {
                    v14 = v3_OutBuffer + 8;     // data offset per item
                    v15 = v11_ProcInfo + 19;
                    do
                    {
                      v16 = v15->m128_u64[0];
                      Object_PETHREAD = 0i64;
                      PsLookupThreadByThreadId(v16, &Object_PETHREAD);
                      v17_PETHREAD = Object_PETHREAD;
                      v18_PETHREAD = Object_PETHREAD;
                      *(_DWORD *)(v14 - 8) = v11_ProcInfo[5].m128_i32[0];
                      *(_DWORD *)(v14 - 4) = v16;
                      *(_QWORD *)v14 = v18_PETHREAD;// set a pointer to the this PETHREAD
                      *(_QWORD *)(v14 + 8) = GetThreadStartAddress_sub_FFFFF800188C68C8((__int64)v18_PETHREAD);// set thread start address
                      *(_QWORD *)(v14 + 16) = sub_FFFFF800188C687C((__int64)v17_PETHREAD);// this actually return PETHREAD+0x400
                      *(_DWORD *)(v14 + 24) = sub_FFFFF800188C67F4((__int64)v17_PETHREAD) != 0;// unknown, bool
                      if ( v17_PETHREAD )
                        ObfDereferenceObject(v17_PETHREAD);
                      ++v13;
                      v15 += 5;
                      v14 += 168i64;            // 0xA8 alignment
                    }
                    while ( v13 < HIDWORD(v11_ProcInfo->m128_u64[0]) ); SYSTEM_PROCESS_INFORMATION->Threads
                  }
                }
                break;
              }
              v12 = LODWORD(v11_ProcInfo->m128_u64[0]);
              if ( (_DWORD)v12 )
              {
                v11_ProcInfo = (__m128 *)((char *)v11_ProcInfo + v12);
                if ( v11_ProcInfo )
                  continue;
              }
              break;
            }
            ExFreePoolWithTag(v10_ProcInfo, 0);
          }
        }
      }
    }
  }
  return v6;
}

As the pseudocode says, subroutine does:

    1. Get the pointer to the ZwQuerySystemInformation by MmGetSystemRoutineAddress
    1. Call ZwQuerySystemInformation with SystemProcessInformation to get pool size what we have to allocate. (bad implementation)
    1. Allocate memory using ExAllocatePool with the size
    1. Call ZwQuerySystemInformation again to enumerate processes
    1. Enumerate for every single processes and making sure the address is valid by MmIsAddressValid
      • If the process id is match, call PsLookupThreadByThreadId to get PETHREAD by thread id, then write information into the payload buffer, every single threads.

Also:

  • The output data structure is 0xA8 alignment
  • We can get its thread’s start address by sub_FFFFF800188C68C8
  • We can get its thread’s PETHREAD address in the kernel

So I don’t know what sub_FFFFF800188C687C and sub_FFFFF800188C67F4 does.
only one thing I know is that the first one references PETHREAD+0x400 as follows:

__int64 __fastcall sub_FFFFF800188C687C(__int64 a1_PETHREAD)
{
  __int64 v1; // rbx
  __int64 v2_PETHREAD; // rdi
  __int64 *v4; // rdi

  v1 = 0i64;
  v2_PETHREAD = a1_PETHREAD;
  if ( !qword_FFFFF800188CA728 )
    return 0i64;
  if ( (unsigned __int8)MmIsAddressValid(a1_PETHREAD) == 1 )
  {
    v4 = (__int64 *)(qword_FFFFF800188CA728 + v2_PETHREAD);// 1048i64 + v2_PETHREAD, winver depends
    if ( (unsigned __int8)MmIsAddressValid(v4) == 1 )
      v1 = *v4;
  }
  return v1;
}

qword_FFFFF800188CA728 is an static variable which has a winver-depends offset for the struct member.
Confirmed by this subroutine:
As you can see the switch-case is winver.

bool __fastcall sub_FFFFF800188C70CC(__int64 a1, __int64 a2, __int64 a3)
{
  char v4; // [rsp+20h] [rbp-128h]
  unsigned int v5; // [rsp+2Ch] [rbp-11Ch]
  __int64 v6; // [rsp+150h] [rbp+8h]

  switch ( dword_FFFFF800188CA748 )
  {
    case 61:
      qword_FFFFF800188CA700 = 384i64;
      qword_FFFFF800188CA708 = 360i64;
      qword_FFFFF800188CA710 = 496i64;
      qword_FFFFF800188CA720 = 512i64;
      qword_FFFFF800188CA718 = 616i64;
      qword_FFFFF800188CA728 = 872i64;
      qword_FFFFF800188CA730 = 1048i64; // <-
      qword_FFFFF800188CA738 = 1104i64;
      qword_FFFFF800188CA740 = 736i64;
      break;
    case 62:
      qword_FFFFF800188CA730 = 1008i64; // <-
      qword_FFFFF800188CA738 = 1068i64;
      goto LABEL_15;
    case 63:
      qword_FFFFF800188CA730 = 1656i64; // <-
      qword_FFFFF800188CA738 = 1716i64;
LABEL_15:
      qword_FFFFF800188CA718 = 936i64;
      qword_FFFFF800188CA720 = 1032i64;
      qword_FFFFF800188CA710 = 1040i64;
      qword_FFFFF800188CA700 = 736i64;
      qword_FFFFF800188CA740 = 768i64;
      break;
    case 100:
      RtlGetVersion(&v4);
      if ( v5 >= 0x4A61 )
      {
        qword_FFFFF800188CA730 = 0i64; // <-
        qword_FFFFF800188CA700 = 1088i64;
        qword_FFFFF800188CA710 = 1400i64;
        qword_FFFFF800188CA718 = 1304i64;
        qword_FFFFF800188CA720 = 1392i64;
        qword_FFFFF800188CA708 = 1128i64;
        qword_FFFFF800188CA738 = 1296i64;
LABEL_9:
        qword_FFFFF800188CA740 = 912i64;
        break;
      }
      qword_FFFFF800188CA710 = 1056i64;
      qword_FFFFF800188CA720 = 1048i64;
      if ( v5 >= 0x47BA )
      {
        qword_FFFFF800188CA700 = 744i64;
        qword_FFFFF800188CA718 = 960i64;
        qword_FFFFF800188CA708 = 784i64;
        qword_FFFFF800188CA730 = 1696i64; // <-
        qword_FFFFF800188CA738 = 1760i64;
        goto LABEL_9;
      }
      qword_FFFFF800188CA718 = 952i64;
      qword_FFFFF800188CA740 = 904i64;
      if ( v5 < 0x3AD7 )
      {
        qword_FFFFF800188CA700 = 744i64;
        qword_FFFFF800188CA730 = 1672i64; // <-
        qword_FFFFF800188CA738 = 1728i64;
      }
      else
      {
        qword_FFFFF800188CA700 = 736i64;
        qword_FFFFF800188CA708 = 776i64;
        qword_FFFFF800188CA730 = 1680i64; // <-
        qword_FFFFF800188CA738 = 1744i64;
      }
      break;
  }
  v6 = 0i64;
  PsLookupProcessByProcessId(4i64, &v6, a3);
  return sub_FFFFF800188C3D08(v6) == 4;
}

Call map

Proof

Confirmed by hooking mhyprot kernel module.
System-calls are properly called exactly same as the pseudocode:

Data is set by the driver, this is the memory view of payload buffer:

Getting System Uptime

The driver ioctl implements getting system uptime as follows:

It eventually calls KeQueryTimeIncrement which could get system uptime in nanoseconds.

PAGE:FFFFF800188CD737 loc_FFFFF800188CD737:                   ; CODE XREF: sub_FFFFF800188CD6E0+38↑j
PAGE:FFFFF800188CD737                 lea     eax, [rcx+7FEEC000h]
PAGE:FFFFF800188CD73D                 mov     edx, 80134000h
PAGE:FFFFF800188CD742                 test    eax, 0FFFCFFFFh
PAGE:FFFFF800188CD747                 jnz     short loc_FFFFF800188CD751
PAGE:FFFFF800188CD749                 cmp     ecx, edx
PAGE:FFFFF800188CD74B                 jnz     loc_FFFFF800188CDA4F
PAGE:FFFFF800188CD751
PAGE:FFFFF800188CD751 loc_FFFFF800188CD751:                   ; CODE XREF: sub_FFFFF800188CD6E0+67↑j
PAGE:FFFFF800188CD751                 cmp     ecx, edx // if (ioctl_code == 0x80134000)
PAGE:FFFFF800188CD753                 jnz     short loc_FFFFF800188CD766
PAGE:FFFFF800188CD755                 call    sub_FFFFF800188C2314 // <-
PAGE:FFFFF800188CD75A                 mov     [rdi], eax // *(unsigned int*)req_ctx = (unsigned int)result

and the sub_FFFFF800188C2314 is:

.text:FFFFF800188C2314 sub_FFFFF800188C2314 proc near          ; CODE XREF: sub_FFFFF800188C141C+C↑p
.text:FFFFF800188C2314                                         ; sub_FFFFF800188C5C0C+38↓p ...
.text:FFFFF800188C2314                 sub     rsp, 28h
.text:FFFFF800188C2318                 call    cs:KeQueryTimeIncrement // <-
.text:FFFFF800188C231E                 mov     eax, eax
.text:FFFFF800188C2320                 mov     rcx, 0FFFFF78000000320h
.text:FFFFF800188C232A                 mov     rcx, [rcx]
.text:FFFFF800188C232D                 imul    rcx, rax
.text:FFFFF800188C2331                 mov     rax, 346DC5D63886594Bh
.text:FFFFF800188C233B                 imul    rcx
.text:FFFFF800188C233E                 sar     rdx, 0Bh
.text:FFFFF800188C2342                 mov     rax, rdx
.text:FFFFF800188C2345                 shr     rax, 3Fh
.text:FFFFF800188C2349                 add     rax, rdx
.text:FFFFF800188C234C                 add     rsp, 28h
.text:FFFFF800188C2350                 retn // (unsigned integer) miliseconds
.text:FFFFF800188C2350 sub_FFFFF800188C2314 endp

Call map

Terminate Process

The driver has a vulnerable ioctl code for terminating process, with a specific process id.
It eventually calls ZwTerminateProcess in the vulnerable driver context (ring-0).

The ioctl code is 0x81034000 as you can see:

PAGE:FFFFF800188CD0F9                 cmp     ebx, 81034000h
PAGE:FFFFF800188CD0FF                 jz      short loc_FFFFF800188CD16C
PAGE:FFFFF800188CD16C loc_FFFFF800188CD16C:                   ; CODE XREF: sub_FFFFF800188CD000+FF↑j
PAGE:FFFFF800188CD16C                 mov     rax, [rsp+30h]
PAGE:FFFFF800188CD171                 mov     ecx, [rax]
PAGE:FFFFF800188CD173                 call    sub_FFFFF800188C36A8
PAGE:FFFFF800188CD178                 and     dword ptr [rbp+1D0h+arg_20], 0

and the sub_FFFFF800188C36A8 is in .text segment:

.text:FFFFF800188C36B0 sub_FFFFF800188C36B0 proc near          ; CODE XREF: sub_FFFFF800188C36A8↑j
.text:FFFFF800188C36B0                                         ; sub_FFFFF800188C4600+27↓p
.text:FFFFF800188C36B0                                         ; DATA XREF: ...
.text:FFFFF800188C36B0
.text:FFFFF800188C36B0 var_38          = qword ptr -38h
.text:FFFFF800188C36B0 var_30          = byte ptr -30h
.text:FFFFF800188C36B0 var_28          = qword ptr -28h
.text:FFFFF800188C36B0 var_18          = byte ptr -18h
.text:FFFFF800188C36B0 arg_0           = qword ptr  8
.text:FFFFF800188C36B0 Object          = qword ptr  10h
.text:FFFFF800188C36B0 Handle          = qword ptr  18h
.text:FFFFF800188C36B0 arg_18          = qword ptr  20h
.text:FFFFF800188C36B0
.text:FFFFF800188C36B0 ; __unwind { // __C_specific_handler
.text:FFFFF800188C36B0                 test    ecx, ecx // if (param1_processid != NULL)
.text:FFFFF800188C36B2                 jz      locret_FFFFF800188C3779
.text:FFFFF800188C36B8                 mov     rax, rsp
.text:FFFFF800188C36BB                 mov     [rax+8], rbx
.text:FFFFF800188C36BF                 mov     [rax+20h], rsi
.text:FFFFF800188C36C3                 push    rdi
.text:FFFFF800188C36C4                 sub     rsp, 50h
.text:FFFFF800188C36C8                 xor     ebx, ebx
.text:FFFFF800188C36CA                 mov     dil, 1
.text:FFFFF800188C36CD                 mov     [rax-18h], dil
.text:FFFFF800188C36D1                 mov     [rax+18h], rbx
.text:FFFFF800188C36D5                 mov     [rax+10h], rbx
.text:FFFFF800188C36D9                 mov     ecx, ecx
.text:FFFFF800188C36DB                 lea     rdx, [rax+10h]
.text:FFFFF800188C36DF                 call    PsLookupProcessByProcessId // <- Lookup _EPROCESS
.text:FFFFF800188C36E4                 movzx   esi, dil
.text:FFFFF800188C36E8                 test    eax, eax // if (_EPROCESS != NULL)
.text:FFFFF800188C36EA                 cmovs   esi, ebx
.text:FFFFF800188C36ED                 mov     [rsp+58h+var_18], sil
.text:FFFFF800188C36F2                 mov     rcx, [rsp+58h+Object]
.text:FFFFF800188C36F7                 test    rcx, rcx
.text:FFFFF800188C36FA                 jz      short loc_FFFFF800188C376A
.text:FFFFF800188C36FC                 lea     rax, [rsp+58h+Handle]
.text:FFFFF800188C3701                 mov     [rsp+58h+var_28], rax
.text:FFFFF800188C3706                 mov     [rsp+58h+var_30], bl
.text:FFFFF800188C370A                 mov     [rsp+58h+var_38], rbx
.text:FFFFF800188C370F                 xor     r9d, r9d
.text:FFFFF800188C3712                 xor     r8d, r8d
.text:FFFFF800188C3715                 xor     edx, edx
.text:FFFFF800188C3717                 call    cs:ObOpenObjectByPointer
.text:FFFFF800188C371D                 test    eax, eax
.text:FFFFF800188C371F                 jz      short loc_FFFFF800188C3733
.text:FFFFF800188C3721                 cmp     sil, dil
.text:FFFFF800188C3724                 jnz     short loc_FFFFF800188C3731
.text:FFFFF800188C3726                 mov     rcx, [rsp+58h+Object] ; Object
.text:FFFFF800188C372B                 call    cs:ObfDereferenceObject
.text:FFFFF800188C3731
.text:FFFFF800188C3731 loc_FFFFF800188C3731:                   ; CODE XREF: sub_FFFFF800188C36B0+74↑j
.text:FFFFF800188C3731                 jmp     short loc_FFFFF800188C376A
.text:FFFFF800188C3733 ; ---------------------------------------------------------------------------
.text:FFFFF800188C3733
.text:FFFFF800188C3733 loc_FFFFF800188C3733:                   ; CODE XREF: sub_FFFFF800188C36B0+6F↑j
.text:FFFFF800188C3733                 xor     edx, edx
.text:FFFFF800188C3735                 mov     rcx, [rsp+58h+Handle]
.text:FFFFF800188C373A                 call    cs:ZwTerminateProcess // <- terminate the process
.text:FFFFF800188C3740                 mov     rcx, [rsp+58h+Handle] ; Handle
.text:FFFFF800188C3745                 call    cs:ZwClose // <- close the handle
.text:FFFFF800188C374B                 jmp     short loc_FFFFF800188C3755
.text:FFFFF800188C374D ; ---------------------------------------------------------------------------
.text:FFFFF800188C374D                 mov     dil, 1
.text:FFFFF800188C3750                 mov     sil, [rsp+58h+var_18]
.text:FFFFF800188C3755
.text:FFFFF800188C3755 loc_FFFFF800188C3755:                   ; CODE XREF: sub_FFFFF800188C36B0+9B↑j
.text:FFFFF800188C3755                 cmp     sil, dil
.text:FFFFF800188C3758                 jnz     short loc_FFFFF800188C376A
.text:FFFFF800188C375A                 mov     rcx, [rsp+58h+Object] ; Object
.text:FFFFF800188C375F                 test    rcx, rcx
.text:FFFFF800188C3762                 jz      short loc_FFFFF800188C376A
.text:FFFFF800188C3764                 call    cs:ObfDereferenceObject
.text:FFFFF800188C376A
.text:FFFFF800188C376A loc_FFFFF800188C376A:                   ; CODE XREF: sub_FFFFF800188C36B0+4A↑j
.text:FFFFF800188C376A                                         ; sub_FFFFF800188C36B0:loc_FFFFF800188C3731↑j ...
.text:FFFFF800188C376A                 mov     rbx, [rsp+58h+arg_0]
.text:FFFFF800188C376F                 mov     rsi, [rsp+58h+arg_18]
.text:FFFFF800188C3774                 add     rsp, 50h
.text:FFFFF800188C3778                 pop     rdi
.text:FFFFF800188C3779
.text:FFFFF800188C3779 locret_FFFFF800188C3779:                ; CODE XREF: sub_FFFFF800188C36B0+2↑j
.text:FFFFF800188C3779                 retn
.text:FFFFF800188C3779 ; } // starts at FFFFF800188C36B0
.text:FFFFF800188C3779 sub_FFFFF800188C36B0 endp

Since this IOCTL handler has payload encryption measure, we have to encrypt the payload.
And the structure for the request will be like:

typedef struct _MHYPROT_TERMINATE_PROCESS_REQUEST
{
	uint64_t response;
	uint32_t process_id;
} MHYPROT_TERMINATE_PROCESS_REQUEST, * PMHYPROT_TERMINATE_PROCESS_REQUEST;

Call Map