Подписывайтесь на наш Telegram и не пропускайте важные новости! Перейти

Исходник [Сурс] Античит — Обход TPM через перехват IRP и подмену IOCTL (C++)

Sloppy
Начинающий
Начинающий
Статус
Оффлайн
Регистрация
13 Фев 2026
Сообщения
209
Реакции
5
Народ, кто сейчас ковыряет обходы для жестких античитов, забирайте подгон по TPM-спуфингу. Опять пришлось дампить вручную и хукать диспетчер, потому что стандартные решения через реестр или простые макросы отлетают в детект быстрее, чем успеваешь загрузиться в катку.

Суть метода в подмене IOCTL запросов напрямую через системный драйвер TPM. Это позволяет отдавать античиту (EAC/BE/Ricochet) сгенерированные рандомные данные вместо реальных серийников железа. За идею с хуком диспетчера респект Сэмюэлю Тулаху, я лишь допилил нормальную обработку IOCTL под текущие реалии.

Технические нюансы реализации:
  1. Хук диспетчера: Перехватываем IRP_MJ_DEVICE_CONTROL и фильтруем через IOCTL_TPM_SUBMIT_COMMAND.
  2. Рандомизация: Используется seed-система для генерации RSA ключей и digest-значений, чтобы каждый запуск системы выглядел уникальным для сервера античита.
  3. Обработка данных: Полностью подменяем ответы для PCR_Read, Quote, ReadPublic и прочих критических вызовов.

Код:
Expand Collapse Copy
typedef struct _TPM_DATA_READ_PUBLIC
{
    TPM2_RESPONSE_HEADER Header;
    struct
    {
        struct
        {
            struct
            {
                TPM2B_PUBLIC_KEY_RSA rsa;
            } unique;
        } publicArea;
    } OutPublic;
} TPM_DATA_READ_PUBLIC;
 
typedef struct _TPM_DATA_GET_RANDOM
{
    TPM2_RESPONSE_HEADER Header;
    TPM2B_DIGEST RandomBytes;
} TPM_DATA_GET_RANDOM;
 
typedef struct _TPM_DATA_READ_CLOCK
{
    TPM2_RESPONSE_HEADER Header;
    TPMS_TIME_INFO CurrentTime;
} TPM_DATA_READ_CLOCK;
 
typedef struct _TPM_DATA_PCR_READ
{
    TPM2_RESPONSE_HEADER Header;
    struct
    {
        UINT32 count;
        TPM2B_DIGEST digests [ 8 ];
    } pcrValues;
} TPM_DATA_PCR_READ;
 
typedef struct _TPM_DATA_NV_READ
{
    TPM2_RESPONSE_HEADER Header;
    TPM2B_MAX_NV_BUFFER Data;
} TPM_DATA_NV_READ;
 
typedef struct _TPM_DATA_CREATE_PRIMARY
{
    TPM2_RESPONSE_HEADER Header;
    TPM_HANDLE ObjectHandle;
    struct
    {
        struct
        {
            struct
            {
                TPM2B_PUBLIC_KEY_RSA rsa;
            } unique;
        } publicArea;
    } OutPublic;
} TPM_DATA_CREATE_PRIMARY;
 
#pragma pack(pop)
 
namespace tpm
{
    static PDRIVER_DISPATCH originalDispatch = nullptr;
    static TPM2B_PUBLIC_KEY_RSA generatedKey = { 0 };
    static unsigned int tpm_seed = 0;
 
    static UINT32 SwapU32 ( UINT32 v )
    {
        return ( ( v >> 24 ) & 0x000000FFu ) |
            ( ( v >> 8 ) & 0x0000FF00u ) |
            ( ( v << 8 ) & 0x00FF0000u ) |
            ( ( v << 24 ) & 0xFF000000u );
    }
 
    static UINT16 SwapU16 ( UINT16 v )
    {
        return ( ( v >> 8 ) & 0x00FFu ) | ( ( v << 8 ) & 0xFF00u );
    }
 
    static unsigned int NextSeed ( )
    {
        tpm_seed ^= tpm_seed << 13;
        tpm_seed ^= tpm_seed >> 17;
        tpm_seed ^= tpm_seed << 5;
        return tpm_seed;
    }
 
    static void GenerateKey ( )
    {
        UINT32 s = emulated_utils::hwid_seed;
        for ( UINT16 i = 0; i < MAX_RSA_KEY_BYTES; i++ ) {
            s ^= s << 13;
            s ^= s >> 17;
            s ^= s << 5;
            generatedKey.buffer [ i ] = static_cast< UCHAR >( ( s >> 16 ) & 0xFF );
        }
        generatedKey.size = MAX_RSA_KEY_BYTES;
    }
 
    static void FillRandom ( UCHAR* buf , UINT16 len )
    {
        for ( UINT16 i = 0; i < len; i++ )
            buf [ i ] = static_cast< UCHAR > ( NextSeed ( ) >> 16 );
    }
 
    static void FillRandomBytes ( UCHAR* buf , SIZE_T len )
    {
        while ( len ) {
            UINT16 chunk = static_cast< UINT16 >( len > 65535 ? 65535 : len );
            FillRandom ( buf , chunk );
            buf += chunk;
            len -= chunk;
        }
    }
 
    static NTSTATUS HandleQuote ( PDEVICE_OBJECT device , PIRP irp , PVOID context )
    {
        if ( !context )
            return STATUS_SUCCESS;
 
        emulated_utils::IOC_REQUEST req = *static_cast< emulated_utils::PIOC_REQUEST >( context );
        ExFreePool ( context );
 
        if ( req.Buffer && MmIsAddressValid ( req.Buffer ) ) {
            TPM2_RESPONSE_HEADER* hdr = static_cast< TPM2_RESPONSE_HEADER* >( req.Buffer );
            const UINT32 rc = SwapU32 ( static_cast< UINT32 >( hdr->responseCode ) );
            if ( rc == TPM_RC_SUCCESS ) {
                const UINT32 paramSize = SwapU32 ( hdr->paramSize );
                const SIZE_T hdrSz = sizeof ( TPM2_RESPONSE_HEADER );
                if ( paramSize > hdrSz && paramSize <= 65536 ) {
                    PUCHAR base = static_cast< PUCHAR >( req.Buffer );
                    if ( MmIsAddressValid ( base + paramSize - 1 ) ) {
                        TPM2B_ATTEST* quoted = reinterpret_cast< TPM2B_ATTEST* >( base + hdrSz );
                        UINT16 asz = SwapU16 ( quoted->size );
                        if ( asz > sizeof ( quoted->attestationData ) )
                            asz = static_cast< UINT16 >( sizeof ( quoted->attestationData ) );
 
                        if ( asz )
                            FillRandom ( quoted->attestationData , asz );
 
                        PUCHAR sigPtr = base + hdrSz + 2u + static_cast< SIZE_T >( asz );
                        if ( sigPtr < base + paramSize ) {
                            const SIZE_T sigLen = static_cast< SIZE_T >( base + paramSize - sigPtr );
                            if ( sigLen )
                                FillRandomBytes ( sigPtr , sigLen );
                        }
                    }
                }
            }
        }
 
        if ( req.OldRoutine )
            return req.OldRoutine ( device , irp , req.OldContext );
        return STATUS_SUCCESS;
    }
 
    static NTSTATUS HandleReadPublic ( PDEVICE_OBJECT device , PIRP irp , PVOID context )
    {
        if ( !context )
            return STATUS_SUCCESS;
 
        emulated_utils::IOC_REQUEST req = *static_cast< emulated_utils::PIOC_REQUEST > ( context );
        ExFreePool ( context );
 
        if ( req.Buffer && MmIsAddressValid ( req.Buffer ) ) {
            TPM_DATA_READ_PUBLIC* data = static_cast< TPM_DATA_READ_PUBLIC* >( req.Buffer );
            const UINT32 respSize = SwapU32 ( data->Header.paramSize );
            const size_t keyCopy = MAX_RSA_KEY_BYTES;
            const size_t minSize = offsetof ( TPM_DATA_READ_PUBLIC , OutPublic.publicArea.unique.rsa.buffer ) + keyCopy;
 
            if ( respSize >= minSize )
                RtlCopyMemory ( data->OutPublic.publicArea.unique.rsa.buffer , generatedKey.buffer , keyCopy );
        }
 
        if ( req.OldRoutine )
            return req.OldRoutine ( device , irp , req.OldContext );
        return STATUS_SUCCESS;
    }
 
    static NTSTATUS HandleGetRandom ( PDEVICE_OBJECT device , PIRP irp , PVOID context )
    {
        if ( !context )
            return STATUS_SUCCESS;
 
        emulated_utils::IOC_REQUEST req = *static_cast< emulated_utils::PIOC_REQUEST >( context );
        ExFreePool ( context );
 
        if ( req.Buffer && MmIsAddressValid ( req.Buffer ) ) {
            TPM_DATA_GET_RANDOM* data = static_cast< TPM_DATA_GET_RANDOM* >( req.Buffer );
            UINT16 len = SwapU16 ( data->RandomBytes.size );
 
            if ( len > 0 && len <= sizeof ( data->RandomBytes.buffer ) )
                FillRandom ( data->RandomBytes.buffer , len );
        }
 
        if ( req.OldRoutine )
            return req.OldRoutine ( device , irp , req.OldContext );
        return STATUS_SUCCESS;
    }
 
    static NTSTATUS HandleReadClock ( PDEVICE_OBJECT device , PIRP irp , PVOID context )
    {
        if ( !context )
            return STATUS_SUCCESS;
 
        emulated_utils::IOC_REQUEST req = *static_cast< emulated_utils::PIOC_REQUEST >( context );
        ExFreePool ( context );
 
        if ( req.Buffer && MmIsAddressValid ( req.Buffer ) ) {
            TPM_DATA_READ_CLOCK* data = static_cast< TPM_DATA_READ_CLOCK* >( req.Buffer );
            unsigned int seed = NextSeed ( );
 
            data->CurrentTime.clockInfo.resetCount = ( seed >> 8 ) & 0xFFFF;
            data->CurrentTime.clockInfo.restartCount = ( seed >> 4 ) & 0xFFFF;
        }
 
        if ( req.OldRoutine )
            return req.OldRoutine ( device , irp , req.OldContext );
        return STATUS_SUCCESS;
    }
 
    static NTSTATUS HandleGetCapability ( PDEVICE_OBJECT device , PIRP irp , PVOID context )
    {
        if ( !context )
            return STATUS_SUCCESS;
 
        emulated_utils::IOC_REQUEST req = *static_cast< emulated_utils::PIOC_REQUEST >( context );
        ExFreePool ( context );
 
        if ( req.OldRoutine )
            return req.OldRoutine ( device , irp , req.OldContext );
        return STATUS_SUCCESS;
    }
 
    static NTSTATUS HandlePCRRead ( PDEVICE_OBJECT device , PIRP irp , PVOID context )
    {
        if ( !context )
            return STATUS_SUCCESS;
 
        emulated_utils::IOC_REQUEST req = *static_cast< emulated_utils::PIOC_REQUEST >( context );
        ExFreePool ( context );
 
        if ( req.Buffer && MmIsAddressValid ( req.Buffer ) ) {
            TPM_DATA_PCR_READ* data = static_cast< TPM_DATA_PCR_READ* >( req.Buffer );
            UINT32 count = SwapU32 ( data->pcrValues.count );
 
            if ( count > 8 ) count = 8;
 
            for ( UINT32 i = 0; i < count; i++ )
            {
                UINT16 sz = SwapU16 ( data->pcrValues.digests [ i ].size );
                if ( sz == 0 || sz > sizeof ( data->pcrValues.digests [ i ].buffer ) ) continue;
 
                FillRandom ( data->pcrValues.digests [ i ].buffer , sz );
            }
        }
 
        if ( req.OldRoutine )
            return req.OldRoutine ( device , irp , req.OldContext );
        return STATUS_SUCCESS;
    }
 
    static NTSTATUS HandleCreatePrimary ( PDEVICE_OBJECT device , PIRP irp , PVOID context )
    {
        if ( !context )
            return STATUS_SUCCESS;
 
        emulated_utils::IOC_REQUEST req = *static_cast< emulated_utils::PIOC_REQUEST >( context );
        ExFreePool ( context );
 
        if ( req.Buffer && MmIsAddressValid ( req.Buffer ) ) {
            TPM_DATA_CREATE_PRIMARY* data = static_cast< TPM_DATA_CREATE_PRIMARY* >( req.Buffer );
            RtlCopyMemory ( data->OutPublic.publicArea.unique.rsa.buffer , generatedKey.buffer , MAX_RSA_KEY_BYTES );
        }
 
        if ( req.OldRoutine )
            return req.OldRoutine ( device , irp , req.OldContext );
        return STATUS_SUCCESS;
    }
 
    static NTSTATUS HandleNVRead ( PDEVICE_OBJECT device , PIRP irp , PVOID context )
    {
        if ( !context )
            return STATUS_SUCCESS;
 
        emulated_utils::IOC_REQUEST req = *static_cast< emulated_utils::PIOC_REQUEST >( context );
        ExFreePool ( context );
 
        if ( req.Buffer && MmIsAddressValid ( req.Buffer ) ) {
            TPM_DATA_NV_READ* data = static_cast< TPM_DATA_NV_READ* >( req.Buffer );
            UINT16 sz = SwapU16 ( data->Data.size );
 
            if ( sz > 0 && sz <= sizeof ( data->Data.buffer ) )
                FillRandom ( data->Data.buffer , sz );
        }
 
        if ( req.OldRoutine )
            return req.OldRoutine ( device , irp , req.OldContext );
        return STATUS_SUCCESS;
    }
 
    static NTSTATUS Dispatch ( PDEVICE_OBJECT device , PIRP irp )
    {
        const PIO_STACK_LOCATION ioc = IoGetCurrentIrpStackLocation ( irp );
 
        if ( ioc->MajorFunction == IRP_MJ_DEVICE_CONTROL &&
            ioc->Parameters.DeviceIoControl.IoControlCode == IOCTL_TPM_SUBMIT_COMMAND )
        {
            PVOID buf = irp->AssociatedIrp.SystemBuffer;
            if ( buf && MmIsAddressValid ( buf ) &&
                ioc->Parameters.DeviceIoControl.InputBufferLength >= sizeof ( TPM2_COMMAND_HEADER ) )
            {
                const TPM2_COMMAND_HEADER* header = static_cast< const TPM2_COMMAND_HEADER* >( buf );
                const TPM_CC command = SwapU32 ( header->commandCode );
 
                switch ( command )
                {
                case TPM_CC_ReadPublic:
                    emulated_utils::change_ioc ( ioc , irp , HandleReadPublic );
                    break;
                case TPM_CC_GetRandom:
                    emulated_utils::change_ioc ( ioc , irp , HandleGetRandom );
                    break;
                case TPM_CC_ReadClock:
                    emulated_utils::change_ioc ( ioc , irp , HandleReadClock );
                    break;
                case TPM_CC_GetCapability:
                    emulated_utils::change_ioc ( ioc , irp , HandleGetCapability );
                    break;
                case TPM_CC_PCR_Read:
                    emulated_utils::change_ioc ( ioc , irp , HandlePCRRead );
                    break;
                case TPM_CC_CreatePrimary:
                    emulated_utils::change_ioc ( ioc , irp , HandleCreatePrimary );
                    break;
                case TPM_CC_NV_Read:
                    emulated_utils::change_ioc ( ioc , irp , HandleNVRead );
                    break;
                case TPM_CC_Quote:
                    emulated_utils::change_ioc ( ioc , irp , HandleQuote );
                    break;
                default:
                    break;
                }
            }
        }
 
        return originalDispatch ( device , irp );
    }
 
    NTSTATUS spoof ( )
    {
        GenerateKey ( );
 
        tpm_seed = emulated_utils::hwid_seed;
 
        UNICODE_STRING driverName;
        RtlInitUnicodeString ( &driverName , encrypt ( L"\\Driver\\TPM" ) );
 
        PDRIVER_OBJECT driverObject = nullptr;
        if ( !NT_SUCCESS ( ObReferenceObjectByName (
            &driverName , OBJ_CASE_INSENSITIVE , nullptr , 0 ,
            *IoDriverObjectType , KernelMode , nullptr ,
            reinterpret_cast< PVOID* >( &driverObject ) ) ) )
            return STATUS_UNSUCCESSFUL;
 
        originalDispatch = driverObject->MajorFunction [ IRP_MJ_DEVICE_CONTROL ];
        driverObject->MajorFunction [ IRP_MJ_DEVICE_CONTROL ] = &Dispatch;
 
        ObDereferenceObject ( driverObject );
        return STATUS_SUCCESS;
    }
}

Что по детектам?
Сам по себе этот код — только база для того, чтобы античит не видел ваши реальные ID. Если планируете использовать это на основе, не забывайте про UEFI-маппинг и чистку следов в AZURE/Registry. Без нормального спуфера по остальным параметрам (MAC, GUID, Disk) один только TPM вас не спасет от пермача по железу.

Слил вам оффсеты и логику, потому что сам юзаю похожий софт. Юзайте на свой страх и риск, желательно на виртуалках или в связке с нормальным Kernel-драйвером, который умеет скрывать свои следы в памяти. И да, нормальную генерацию серийников я тут вырезал — она сложнее, чем кажется на первый взгляд, придется немного додумать самому.

Кто уже тестил этот движок в батл-роялях или других экстракшен-шутерах? Отпишитесь, как проходит проверка attestation, нет ли проблем с "лягушкой" или внезапными киками в лобби? Кидайте свои правки, если кто допиливал обработку под специфические версии Windows.
 
О兄弟, это уже не спуфинг, это ты TPM-чипу новую карму прописываешь! 🤡💀

Чувак, ты серьезно решил, что античит будет тупо спрашивать у TPM: "Слушай, а этот парень случаем не читер?", а TPM такой: "Не, норм пацан, у меня тут ключи из рандома, таймеры из seed'а, пускай"? 🖐️😂

Твой GenerateKey() — это не генерация ключей, это «нарисуй мне подпись, чтобы античит подумал, что я новый человек, но чтобы я сам знал, что я — это я». Это как прийти в паспортный стол с фотографией, где лицо сгенерировано нейросетью, и сказать: «Ну чё, норм же, я тут в диспетчере TPM всё подменил» 🏦💀


Античит: «О, запрос PCR_Read, ща проверим целостность системы...»
Твой драйвер: «А вот тебе случайные байтики! Пусть думает, что у тебя система каждый раз новая!»
Античит: «Слушай, у тебя там PCR регистры как лотерейный билет выглядят. Ты случаем не читеришь?»
Ты: «Не, это у меня TPM такой весёлый, он всегда рандомом отвечает, это фича такая!» 🤡🔐


HandleQuote — это вообще шедевр. Ты берёшь нормальный ответ TPM, вырезаешь оттуда реальные данные и вставляешь рандомные байтики. Это как если бы ты подделал подпись в документах, просто замазав её корректором и написав сверху «ну тут типа подпись, не обращайте внимания» ✍️😂

И твой HandleReadClock с resetCount из seed'а — античит просто сравнивает твои показания с предыдущими. Когда они у тебя скачут как курсор биткоина на новостях о скаме очередной биржи, он делает вывод: «Это не TPM, это казино» 🎰💀


Самый смешной момент из твоего же спойлера:
«Без нормального спуфера по остальным параметрам один только TPM вас не спасет» — бро, да он и со спуфером не спасёт, если ты ему вместо железа подсовываешь рандомный генератор чисел в костюме TPM 🤡🔧


Кто тестил:
Тестили. Результат — «лягушка» прилетает не от античита, а от Windows, когда твой драйвер пытается подменить ответ TPM, а система такая: «Эй, а почему у меня TPM отвечает как рандомайзер, а не как железка? Ты чё, решил, что я не замечу?» 🐸🔨


Вердикт для форума:

Братва, автор молодец, конечно, запилил целый фреймворк для подмены TPM-команд. Прям чувствуется рука человека, который верит, что если подписать драйвер сертификатом из «Паблик-пиксель 2024», то античит скажет «О, подписано! Заходи, родной!» 🏆😂
ладно оперативка подорожала не зря...
 
Интересный технический разбор подхода к работе с TPM на уровне драйвера. Хорошо показана структура обработки IOCTL и подмена конкретных команд.

Однако стоит добавить важные предостережения для тех, кто захочет это использовать:

Хук MajorFunction — самый заметный метод для античита. Современные EAC/BE проверяют системные драйверы на наличие изменений в таблицах диспетчеризации.

Генерация случайных данных для PCR и Quote без учета реальной цепочки измерений платформы может быть отловлена по аномалиям — античиты собирают статистику и банают по девиациям.

Отсутствие обработки команд TPM_CC_NV_ReadPublic и TPM_CC_NV_DefineSpace может привести к тому, что некоторые игры всё равно получат реальные данные через другие пути.

Если это исследовательский проект — стоит добавить больше деталей по safe-хукингу (например, через патчинг inline вместо замены указателей) и скрытию присутствия драйвера в списке загруженных модулей. Для практического применения в играх одного этого недостаточно — нужен полноценный спуфер по всему стеку железа и скрытие самого факта наличия спуфера в системе.
 
Назад
Сверху Снизу