Начинающий
- Статус
- Оффлайн
- Регистрация
- 13 Фев 2026
- Сообщения
- 130
- Реакции
- 4
Народ, кто сейчас ковыряет свои лоадеры или пишет кастомный спуфер под античиты, обратите внимание — наткнулся на интересный подход к WMI и EDID. В последнее время в P2C софте часто вижу одну и ту же ошибку: разрабы либо хукают только WMI, либо чистят только реестр, а топовые античиты (EAC/BattlEye/VGK) сверяют оба источника. Если данные не матчатся, сами знаете — отлет по железу гарантирован.
Слил вам рабочий пример, как это закрыть комплексно. Суть в том, что мы перехватываем \Driver\monitor и лезем в IRP_MJ_SYSTEM_CONTROL, чтобы на лету править ответы WMI, плюс патчим HKLM\SYSTEM\CurrentControlSet\Enum\DISPLAY для EDID.
Технические нюансы:
Что по детектам?
Сам драйвер монитора — это база, но не забывайте про обфускацию (Nuitka/пайплайны для C++), если юзаете это в юзер-мод лоадере. Этот код работает на уровне ядра, так что если ваш драйвер не подписан или загружается через уязвимый маппер (типа kdmapper), следите за тем, чтобы не было конфликтов с античитом.
Если кто будет интегрировать в свой проект — проверяйте оффсеты под актуальные версии билдов винды, иногда WmiMonitorID может отличаться из-за апдейтов.
Братва, делитесь опытом, кто уже тестил этот метод под BattlEye? Есть ли прилеты по мануалу из-за того, что хук на \Driver\monitor палится по сигнатуре? Кто допиливал — кидайте свои правки или методы обхода проверок целостности реестра.
Слил вам рабочий пример, как это закрыть комплексно. Суть в том, что мы перехватываем \Driver\monitor и лезем в IRP_MJ_SYSTEM_CONTROL, чтобы на лету править ответы WMI, плюс патчим HKLM\SYSTEM\CurrentControlSet\Enum\DISPLAY для EDID.
Технические нюансы:
- Хуки: Хукаем IRP_MN_QUERY_ALL_DATA для манипуляции WmiMonitorID.
- Рандомизация: Используется seed, чтобы значения были консистентными для системы, иначе при каждом запросе будет новый HWID, что сразу вызовет подозрения у аналитики.
- EDID: После патча чекаем контрольную сумму (checksum), иначе винда просто отвалится или будет ругаться на драйвер монитора.
- Кэширование: Реализовано кэширование по устройствам, чтобы не спамить в логи и не перегружать память.
Код:
#define WNODE_FLAG_FIXED_INSTANCE_SIZE 0x00000010
typedef struct _WNODE_HEADER {
ULONG BufferSize;
ULONG ProviderId;
union {
ULONG64 HistoricalContext;
struct { ULONG Version; ULONG Linkage; };
};
union {
ULONG CountLost;
HANDLE KernelHandle;
LARGE_INTEGER TimeStamp;
};
GUID Guid;
ULONG ClientContext;
ULONG Flags;
} WNODE_HEADER, *PWNODE_HEADER;
typedef struct {
ULONG OffsetInstanceData;
ULONG LengthInstanceData;
} OFFSETINSTANCEDATAANDLENGTH, *POFFSETINSTANCEDATAANDLENGTH;
typedef struct _WNODE_ALL_DATA {
WNODE_HEADER WnodeHeader;
ULONG DataBlockOffset;
ULONG InstanceCount;
ULONG OffsetInstanceNameOffsets;
union {
ULONG FixedInstanceSize;
OFFSETINSTANCEDATAANDLENGTH OffsetInstanceDataAndLength [ 1 ];
};
} WNODE_ALL_DATA, *PWNODE_ALL_DATA;
typedef struct _WmiMonitorID {
USHORT ProductCodeID [ 16 ];
USHORT SerialNumberID [ 16 ];
USHORT ManufacturerName [ 16 ];
UCHAR WeekOfManufacture;
USHORT YearOfManufacture;
USHORT UserFriendlyNameLength;
USHORT UserFriendlyName [ 1 ];
} WmiMonitorID, *PWmiMonitorID;
PDRIVER_DISPATCH g_OriginalMonitorDispatch = nullptr;
#define MAX_MONITOR_HOOKS 16
static struct {
PDRIVER_OBJECT drv;
PDRIVER_DISPATCH original;
} g_MonitorHooks [ MAX_MONITOR_HOOKS ] = {};
static ULONG g_MonitorHookCount = 0;
static char g_SpoofedSerial [ 14 ] = { 0 };
static char g_SpoofedVendor [ 4 ] = { 0 };
static char g_SpoofedName [ 14 ] = { 0 };
static bool g_MonitorStringsInit = false;
namespace monitor
{
static ULONG MonLCG ( ULONG& seed )
{
// Use xorshift32 for better distribution
seed ^= seed << 13;
seed ^= seed >> 17;
seed ^= seed << 5;
return seed;
}
static void InitMonitorStrings ( )
{
if ( g_MonitorStringsInit ) return;
static const char* vendors [ ] = {
encrypt ( "AUS" ), encrypt ( "SAM" ), encrypt ( "DEL" ), encrypt ( "LEN" ),
encrypt ( "AOC" ), encrypt ( "BNQ" ), encrypt ( "HWP" ), encrypt ( "GSM" )
};
static const char* models [ ] = {
encrypt ( "VG279QM" ), encrypt ( "VG27AQ" ), encrypt ( "VG248QG" ),
encrypt ( "S24D330" ), encrypt ( "S27F350" ), encrypt ( "C27F390" ),
encrypt ( "P2419H" ), encrypt ( "U2722D" ), encrypt ( "S2721DGF" ),
encrypt ( "27GL850" ), encrypt ( "27GP850" ), encrypt ( "24GL600F" ),
encrypt ( "Q27G2S" ), encrypt ( "CQ27G2S" ), encrypt ( "AG274QZ" ),
encrypt ( "HP24mh" ), encrypt ( "HP27h" ), encrypt ( "E24d" ),
encrypt ( "27UK850" ), encrypt ( "27GL83A" ), encrypt ( "32GN650" )
};
static const char alphanum [ ] = {
'A','B','C','D','E','F','G','H','I','J','K','L','M',
'N','O','P','Q','R','S','T','U','V','W','X','Y','Z',
'0','1','2','3','4','5','6','7','8','9', 0
};
static const char digits [ ] = { '0','1','2','3','4','5','6','7','8','9', 0 };
ULONG seed = emulated_settings::hwid_seed ^ 0xDEADBEEF;
ULONG vi = ( MonLCG ( seed ) >> 16 ) % ( sizeof ( vendors ) / sizeof ( vendors [ 0 ] ) );
ULONG mi = ( MonLCG ( seed ) >> 16 ) % ( sizeof ( models ) / sizeof ( models [ 0 ] ) );
RtlCopyMemory ( g_SpoofedVendor , vendors [ vi ] , 3 );
g_SpoofedVendor [ 3 ] = 0;
RtlCopyMemory ( g_SpoofedName , models [ mi ] , strlen ( models [ mi ] ) + 1 );
for ( int i = 0; i < 4; i++ )
g_SpoofedSerial [ i ] = alphanum [ ( MonLCG ( seed ) >> 16 ) % 26 ];
for ( int i = 4; i < 6; i++ )
g_SpoofedSerial [ i ] = digits [ ( MonLCG ( seed ) >> 16 ) % 10 ];
for ( int i = 6; i < 9; i++ )
g_SpoofedSerial [ i ] = alphanum [ ( MonLCG ( seed ) >> 16 ) % 36 ];
for ( int i = 9; i < 11; i++ )
g_SpoofedSerial [ i ] = digits [ ( MonLCG ( seed ) >> 16 ) % 10 ];
for ( int i = 11; i < 13; i++ )
g_SpoofedSerial [ i ] = alphanum [ ( MonLCG ( seed ) >> 16 ) % 26 ];
g_SpoofedSerial [ 13 ] = 0;
g_MonitorStringsInit = true;
logging::output ( encrypt ( "monitor: vendor=%s model=%s serial=%s" ) ,
g_SpoofedVendor , g_SpoofedName , g_SpoofedSerial );
}
static void WmiStringToAnsi ( PUSHORT src , ULONG maxChars , char* out , ULONG outSize )
{
WCHAR wbuf [ 33 ] = { 0 };
maxChars = min ( maxChars , 32 );
for ( ULONG i = 0; i < maxChars; i++ )
{
if ( !src [ i ] ) break;
wbuf [ i ] = ( WCHAR )src [ i ];
}
UNICODE_STRING us; ANSI_STRING as;
RtlInitUnicodeString ( &us , wbuf );
as.Buffer = out; as.MaximumLength = ( USHORT )outSize;
RtlUnicodeStringToAnsiString ( &as , &us , FALSE );
}
static void AnsiToWmiString ( const char* src , PUSHORT dst , ULONG maxChars )
{
for ( ULONG i = 0; i < maxChars; i++ )
dst [ i ] = src [ i ] ? ( USHORT )( UCHAR )src [ i ] : 0;
}
static void PatchEdidDescriptor ( PUCHAR edid , ULONG edidLen , UCHAR type , const char* newStr )
{
if ( edidLen < 128 ) return;
for ( ULONG off = 54; off <= 108; off += 18 )
{
if ( edid [ off ] != 0x00 || edid [ off + 1 ] != 0x00 ) continue;
if ( edid [ off + 2 ] != 0x00 ) continue;
if ( edid [ off + 3 ] != type ) continue;
PUCHAR data = edid + off + 5;
RtlZeroMemory ( data , 13 );
SIZE_T len = strlen ( newStr );
if ( len > 12 ) len = 12;
RtlCopyMemory ( data , newStr , len );
data [ len ] = 0x0A;
}
UCHAR sum = 0;
for ( ULONG i = 0; i < 127; i++ ) sum += edid [ i ];
edid [ 127 ] = ( UCHAR )( 0x100 - sum );
}
static void PatchRegistryEdid ( )
{
UNICODE_STRING displayPath;
OBJECT_ATTRIBUTES oa;
HANDLE hDisplay = nullptr;
RtlInitUnicodeString ( &displayPath , encrypt ( L"\\Registry\\Machine\\SYSTEM\\CurrentControlSet\\Enum\\DISPLAY" ) );
InitializeObjectAttributes ( &oa , &displayPath , OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE , nullptr , nullptr );
if ( !NT_SUCCESS ( ZwOpenKey ( &hDisplay , KEY_READ , &oa ) ) )
return;
ULONG modelIdx = 0;
while ( true )
{
ULONG modelLen = 0;
NTSTATUS s = ZwEnumerateKey ( hDisplay , modelIdx , KeyBasicInformation , nullptr , 0 , &modelLen );
if ( s == STATUS_NO_MORE_ENTRIES ) break;
if ( !modelLen ) { modelIdx++; continue; }
PKEY_BASIC_INFORMATION modelInfo = ( PKEY_BASIC_INFORMATION )ExAllocatePoolWithTag ( NonPagedPool , modelLen , 'TIDE' );
if ( !modelInfo ) { modelIdx++; continue; }
s = ZwEnumerateKey ( hDisplay , modelIdx++ , KeyBasicInformation , modelInfo , modelLen , &modelLen );
if ( s == STATUS_NO_MORE_ENTRIES ) { ExFreePoolWithTag ( modelInfo , 'TIDE' ); break; }
if ( !NT_SUCCESS ( s ) ) { ExFreePoolWithTag ( modelInfo , 'TIDE' ); continue; }
UNICODE_STRING modelName = { ( USHORT )modelInfo->NameLength , ( USHORT )modelInfo->NameLength , modelInfo->Name };
HANDLE hModel = nullptr;
InitializeObjectAttributes ( &oa , &modelName , OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE , hDisplay , nullptr );
if ( !NT_SUCCESS ( ZwOpenKey ( &hModel , KEY_READ , &oa ) ) ) { ExFreePoolWithTag ( modelInfo , 'TIDE' ); continue; }
CHAR modelAnsi [ 64 ] = { 0 };
ANSI_STRING modelAs; UNICODE_STRING modelUs = modelName;
modelAs.Buffer = modelAnsi; modelAs.MaximumLength = sizeof ( modelAnsi );
RtlUnicodeStringToAnsiString ( &modelAs , &modelUs , FALSE );
ExFreePoolWithTag ( modelInfo , 'TIDE' );
ULONG instIdx = 0;
while ( true )
{
ULONG instLen = 0;
s = ZwEnumerateKey ( hModel , instIdx , KeyBasicInformation , nullptr , 0 , &instLen );
if ( s == STATUS_NO_MORE_ENTRIES ) break;
if ( !instLen ) { instIdx++; continue; }
PKEY_BASIC_INFORMATION instInfo = ( PKEY_BASIC_INFORMATION )ExAllocatePoolWithTag ( NonPagedPool , instLen , 'TIDE' );
if ( !instInfo ) { instIdx++; continue; }
s = ZwEnumerateKey ( hModel , instIdx++ , KeyBasicInformation , instInfo , instLen , &instLen );
if ( s == STATUS_NO_MORE_ENTRIES ) { ExFreePoolWithTag ( instInfo , 'TIDE' ); break; }
if ( !NT_SUCCESS ( s ) ) { ExFreePoolWithTag ( instInfo , 'TIDE' ); continue; }
UNICODE_STRING instName = { ( USHORT )instInfo->NameLength , ( USHORT )instInfo->NameLength , instInfo->Name };
HANDLE hInst = nullptr;
InitializeObjectAttributes ( &oa , &instName , OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE , hModel , nullptr );
if ( !NT_SUCCESS ( ZwOpenKey ( &hInst , KEY_READ , &oa ) ) ) { ExFreePoolWithTag ( instInfo , 'TIDE' ); continue; }
ExFreePoolWithTag ( instInfo , 'TIDE' );
UNICODE_STRING devParams;
RtlInitUnicodeString ( &devParams , encrypt ( L"Device Parameters" ) );
HANDLE hDevParams = nullptr;
InitializeObjectAttributes ( &oa , &devParams , OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE , hInst , nullptr );
NTSTATUS openStatus = ZwOpenKey ( &hDevParams , KEY_ALL_ACCESS , &oa );
if ( !NT_SUCCESS ( openStatus ) )
{
ULONG disposition = 0;
openStatus = ZwCreateKey ( &hDevParams , KEY_ALL_ACCESS , &oa , 0 , nullptr , REG_OPTION_NON_VOLATILE , &disposition );
}
if ( !NT_SUCCESS ( openStatus ) ) continue;
UNICODE_STRING edidVal;
RtlInitUnicodeString ( &edidVal , encrypt ( L"EDID" ) );
ZwClose ( hInst );
ULONG edidInfoSize = 0;
ZwQueryValueKey ( hDevParams , &edidVal , KeyValueFullInformation , nullptr , 0 , &edidInfoSize );
if ( edidInfoSize > 0 )
{
PKEY_VALUE_FULL_INFORMATION edidInfo = ( PKEY_VALUE_FULL_INFORMATION )
ExAllocatePoolWithTag ( NonPagedPool , edidInfoSize , 'TIDE' );
if ( edidInfo )
{
ULONG needed = 0;
if ( NT_SUCCESS ( ZwQueryValueKey ( hDevParams , &edidVal , KeyValueFullInformation ,
edidInfo , edidInfoSize , &needed ) ) )
{
PUCHAR edidData = ( PUCHAR )edidInfo + edidInfo->DataOffset;
ULONG edidLen = edidInfo->DataLength;
ULONG seed = emulated_settings::hwid_seed ^ ( modelIdx * 0x1337 ) ^ ( instIdx * 0x7331 );
static const char alphanum [ ] = {
'A','B','C','D','E','F','G','H','I','J','K','L','M',
'N','O','P','Q','R','S','T','U','V','W','X','Y','Z',
'0','1','2','3','4','5','6','7','8','9', 0
};
static const char digits [ ] = { '0','1','2','3','4','5','6','7','8','9', 0 };
char devSerial [ 14 ] = { 0 };
for ( int c = 0; c < 4; c++ ) { seed = xorshift32(seed); devSerial [ c ] = alphanum [ ( seed >> 16 ) % 26 ]; }
for ( int c = 4; c < 6; c++ ) { seed = xorshift32(seed); devSerial [ c ] = digits [ ( seed >> 16 ) % 10 ]; }
for ( int c = 6; c < 9; c++ ) { seed = xorshift32(seed); devSerial [ c ] = alphanum [ ( seed >> 16 ) % 36 ]; }
for ( int c = 9; c < 11; c++ ) { seed = xorshift32(seed); devSerial [ c ] = digits [ ( seed >> 16 ) % 10 ]; }
for ( int c = 11; c < 13; c++ ) { seed = xorshift32(seed); devSerial [ c ] = alphanum [ ( seed >> 16 ) % 26 ]; }
PatchEdidDescriptor ( edidData , edidLen , 0xFF , devSerial );
PatchEdidDescriptor ( edidData , edidLen , 0xFC , g_SpoofedName );
PatchEdidDescriptor ( edidData , edidLen , 0xFE , g_SpoofedVendor );
ZwSetValueKey ( hDevParams , &edidVal , 0 , REG_BINARY , edidData , edidLen );
logging::output ( encrypt ( "monitor edid: %s -> serial=%s" ) , modelAnsi , devSerial );
}
ExFreePoolWithTag ( edidInfo , 'TIDE' );
}
}
ZwClose ( hDevParams );
}
ZwClose ( hModel );
}
ZwClose ( hDisplay );
}
NTSTATUS HandleMonitor ( PDEVICE_OBJECT device , PIRP Irp )
{
PIO_STACK_LOCATION ioc = IoGetCurrentIrpStackLocation ( Irp );
if ( ioc->MinorFunction != IRP_MN_QUERY_ALL_DATA )
return g_OriginalMonitorDispatch ( device , Irp );
PVOID wmiBuffer = ioc->Parameters.WMI.Buffer;
ULONG wmiSize = ioc->Parameters.WMI.BufferSize;
NTSTATUS status = g_OriginalMonitorDispatch ( device , Irp );
if ( NT_SUCCESS ( status ) && wmiBuffer && wmiSize >= sizeof ( WNODE_ALL_DATA ) )
{
PWNODE_ALL_DATA allData = ( PWNODE_ALL_DATA )wmiBuffer;
if ( !MmIsAddressValid ( allData ) || !allData->InstanceCount )
return status;
BOOLEAN fixed = ( allData->WnodeHeader.Flags & WNODE_FLAG_FIXED_INSTANCE_SIZE ) != 0;
PUCHAR base = ( PUCHAR )allData + allData->DataBlockOffset;
for ( ULONG i = 0; i < allData->InstanceCount; i++ )
{
PWmiMonitorID mon = nullptr;
if ( fixed )
mon = ( PWmiMonitorID )( base + i * allData->FixedInstanceSize );
else
{
OFFSETINSTANCEDATAANDLENGTH* inst = &allData->OffsetInstanceDataAndLength [ i ];
if ( !inst->OffsetInstanceData ) continue;
mon = ( PWmiMonitorID )( ( PUCHAR )allData + inst->OffsetInstanceData );
}
if ( !MmIsAddressValid ( mon ) ) continue;
static PDEVICE_OBJECT g_CachedDevice [ 16 ] = {};
static char g_CachedSerial [ 16 ] [ 12 ] = {};
static bool g_CachedInit [ 16 ] = {};
ULONG cacheIdx = 0xFF;
for ( ULONG k = 0; k < 16; k++ )
{
if ( g_CachedInit [ k ] && g_CachedDevice [ k ] == device ) { cacheIdx = k; break; }
if ( !g_CachedInit [ k ] && cacheIdx == 0xFF ) cacheIdx = k;
}
if ( cacheIdx == 0xFF ) cacheIdx = 0;
if ( !g_CachedInit [ cacheIdx ] )
{
const char alphanum [ ] = {
'A','B','C','D','E','F','G','H','I','J','K','L','M',
'N','O','P','Q','R','S','T','U','V','W','X','Y','Z',
'0','1','2','3','4','5','6','7','8','9', 0
};
const char digits [ ] = { '0','1','2','3','4','5','6','7','8','9', 0 };
ULONG seed = emulated_settings::hwid_seed ^ ( ( ULONG )( ULONG_PTR )device * 0x9E3779B9 );
char* s = g_CachedSerial [ cacheIdx ];
for ( int c = 0; c < 3; c++ ) { seed = xorshift32(seed); s [ c ] = alphanum [ ( seed >> 16 ) % 26 ]; }
for ( int c = 3; c < 5; c++ ) { seed = xorshift32(seed); s [ c ] = digits [ ( seed >> 16 ) % 10 ]; }
for ( int c = 5; c < 8; c++ ) { seed = xorshift32(seed); s [ c ] = alphanum [ ( seed >> 16 ) % 36 ]; }
for ( int c = 8; c < 10; c++ ) { seed = xorshift32(seed); s [ c ] = digits [ ( seed >> 16 ) % 10 ]; }
s [ 10 ] = 0;
g_CachedDevice [ cacheIdx ] = device;
g_CachedInit [ cacheIdx ] = true;
}
const char* spoofSerial = g_CachedSerial [ cacheIdx ];
AnsiToWmiString ( spoofSerial , mon->SerialNumberID , 16 );
AnsiToWmiString ( g_SpoofedVendor , mon->ManufacturerName , 16 );
AnsiToWmiString ( g_SpoofedName , mon->ProductCodeID , 16 );
if ( mon->UserFriendlyNameLength > 0 )
{
ULONG nameLen = min ( ( ULONG )strlen ( g_SpoofedName ) , 13UL );
AnsiToWmiString ( g_SpoofedName , mon->UserFriendlyName , nameLen );
mon->UserFriendlyNameLength = ( USHORT )nameLen;
}
logging::output ( encrypt ( "monitor %lu: after -> serial=%s name=%s" ) , i , spoofSerial , g_SpoofedName );
}
}
return status;
}
void spoof ( )
{
InitMonitorStrings ( );
PatchRegistryEdid ( );
UNICODE_STRING name;
RtlInitUnicodeString ( &name , encrypt ( L"\\Driver\\monitor" ) );
PDRIVER_OBJECT drv = nullptr;
NTSTATUS status = ObReferenceObjectByName ( &name , OBJ_CASE_INSENSITIVE , nullptr , 0 ,
*IoDriverObjectType , KernelMode , nullptr , ( void** )&drv );
if ( !NT_SUCCESS ( status ) || !drv )
{
logging::output ( encrypt ( "monitor: driver not found (0x%X)" ) , status );
return;
}
g_OriginalMonitorDispatch = drv->MajorFunction [ IRP_MJ_SYSTEM_CONTROL ];
drv->MajorFunction [ IRP_MJ_SYSTEM_CONTROL ] = HandleMonitor;
logging::output ( encrypt ( "monitor: hooked (orig: %p -> new: %p)" ) ,
g_OriginalMonitorDispatch , HandleMonitor );
ObDereferenceObject ( drv );
}
}
Что по детектам?
Сам драйвер монитора — это база, но не забывайте про обфускацию (Nuitka/пайплайны для C++), если юзаете это в юзер-мод лоадере. Этот код работает на уровне ядра, так что если ваш драйвер не подписан или загружается через уязвимый маппер (типа kdmapper), следите за тем, чтобы не было конфликтов с античитом.
Если кто будет интегрировать в свой проект — проверяйте оффсеты под актуальные версии билдов винды, иногда WmiMonitorID может отличаться из-за апдейтов.
Братва, делитесь опытом, кто уже тестил этот метод под BattlEye? Есть ли прилеты по мануалу из-за того, что хук на \Driver\monitor палится по сигнатуре? Кто допиливал — кидайте свои правки или методы обхода проверок целостности реестра.