Подпишитесь на наш Telegram-канал, чтобы всегда быть в курсе важных обновлений! Перейти

Гайд Подробное описание встроенной античит-системы CS2

Арбитр
Арбитр
Арбитр
Статус
Оффлайн
Регистрация
13 Июл 2018
Сообщения
1,534
Реакции
1,633
🎁 Предновогодний подарок: Глубокий анализ встроенной античит-системы CS2 🎁


Как игра собирает информацию о вас для будущих банов, а вы об этом даже не подозреваете

Всем привет! Решил сделать предновогодний подарок сообществу. Давно не было нормальных глубоких технических статей, так что устраивайтесь поудобнее — будет что почитать в новогодние праздники :)

В этой статье я полностью разберу ingame античит-проверки Counter-Strike 2 — встроенные в клиент защиты (как и классический AntiTamper). Valve внедрила многоуровневую защиту прямо в client.dll, которая работает параллельно с VAC.

В моей статье про AntiTamper (https://www.unknowncheats.me/forum/counter-strike-2-a/695907-analyzing-antitamper-system.html) я дал ясно понять, что игра собирает достаточно информации, чтобы в будущем заблокировать вас. Многие думают, что с ними ничего не произойдет, если они сделают функцию Glow через систему Netvar. Я ясно объяснил, почему это плохо, и привел примеры работы системы сбора данных (https://www.unknowncheats.me/forum/4365681-post6.html).

Введение

Эта статья продолжает анализ античит-систем CS2, фокусируясь на инфраструктуре диагностического сбора данных. Эта система собирает обширную телеметрию о состоянии игрового клиента и отправляет её на серверы Valve для анализа.

Ключевой момент: Это система сбора данных, а не механизм мгновенного детекта. Собранные данные анализируются на стороне сервера для построения профилей игроков с течением времени и корреляции с другими античит-сигналами.

Поиск функций

Диагностическая система обнаруживается через анализ протокола пользовательских сообщений. Ищем CSVCMsg_UserMessage_Setup в IDA и изучаем кросс-референсы для поиска обработчиков сообщений. Сообщения 158-386 (второй параметр с типом EBaseUserMessages) используются для коммуникации с сервером.

Код:
Expand Collapse Copy
enum class EBaseUserMessages : std::uint32_t
{
    UM_AchievementEvent = 0x65,
    UM_CloseCaption = 0x66,
    // ... (сокращено для краткости)
    UM_RequestUtilAction = 0x9d,        // 157
    UM_UtilActionResponse = 0x9e,       // 158
    UM_DllStatusResponse = 0x9f,        // 159
    UM_RequestInventory = 0xa0,         // 160
    UM_InventoryResponse = 0xa1,        // 161
    UM_RequestDiagnostic = 0xa2,        // 162
    UM_DiagnosticResponse = 0xa3,       // 163
    // ...
};

Глобальное диагностическое состояние: CDllVerificationMonitor

Система поддерживает состояние в глобальной структуре по адресу 0x181CFB790, которая является экземпляром класса CDllVerificationMonitor. Этот класс зарегистрирован как IGameSystem и управляет всем диагностическим сбором данных.

Код:
Expand Collapse Copy
struct CDllVerificationMonitor {
    void* vtable;                                    // +0x00
    RTL_CRITICAL_SECTION critical_section;           // +0x08
 
    // Коллбэки, разрешенные из экспортов cs2.exe
    int (*GetTotalFilesLoaded)();                   // +0x18
    int (*CountFilesNeedTrustCheck)();              // +0x20
    int (*CountFilesCompletedTrustCheck)();         // +0x28
    bool (*BSecureAllowed)(char*, int, int);        // +0x30
    int (*CountItemsToReport)();                    // +0x38
 
    // Отслеживание состояния отправки
    int last_send_state;                            // +0x40
    bool suppress_flag;                             // +0x41
    DWORD last_send_time;                           // +0x48
    int total_files_cached;                         // +0x50
    int prev_files_loaded;                          // +0x54
    int send_counter;                               // +0x58
 
    // Хендлы системных DLL для верификации PE timestamp
    HMODULE kernel32_dll;                           // +0x60
    HMODULE kernelbase_dll;                         // +0x68
    HMODULE ntdll_dll;                              // +0x70
    HMODULE gameoverlay_dll;                        // +0x78
 
    // Указатели на функции NT API
    void* NtQueryInformationThread;                 // +0x80
    void* callback_1;                               // +0x88
    void* cpuid_callback;                           // +0x90
    void* NtOpenFile;                               // +0x98
    void* LoadLibraryExW;                           // +0xA0
 
    // Отслеживаемые адреса памяти
    void* monitored_addr_1;                         // +0xA8 - Поле 31
    void* monitored_addr_2;                         // +0xB0 - Поле 32
    void* monitored_addr_3;                         // +0xB8 - Поле 1 (хендл модуля)
    void* monitored_addr_4;                         // +0xC0 - Поле 2 (старт потока)
    void* monitored_addr_5;                         // +0xC8 - Поле 5 (адрес возврата)
    void* monitored_addr_6;                         // +0xD0 - Поле 16 (база аллокации)
 
    // Отслеживаемые значения
    DWORD monitored_value_1;                        // +0xD8 - Поле 3 (ID потока)
    DWORD monitored_value_2;                        // +0xDC - Поле 4 (защита)
 
    // Буфер подписи
    char signature_buffer[4096];                    // +0xE4 - Поле 17 (дамп памяти)
    DWORD signature_size;                           // +0x10E4 - Поле 18
};

Процесс инициализации

Инициализация системы происходит в виртуальной таблице функций по индексу 27 класса CDllVerificationMonitor:

Код:
Expand Collapse Copy
// vtable index 27
bool __fastcall CheckDllIntegrity(CDllVerificationMonitor* state)
{
    // Вызываем ValidateDiagnosticState для проверки нарушений
    bool state_changed = ValidateDiagnosticState(state);
 
    if (state_changed)
    {
        state->integrity_checked = true;
    }
 
    return state_changed;
}

Эта функция вызывает ValidateDiagnosticState, которая инициализирует необходимые данные для телеметрии:

Код:
Expand Collapse Copy
bool __fastcall ValidateDiagnosticState(CDllVerificationMonitor* state)
{
    if (!g_pTime)
        InitGlobalTimePID();
 
    LoadSystemDLLHandles(state);
    InitializeDiagnosticCallbacks(state);
 
    if (!state->GetTotalFilesLoaded || !state->CountFilesNeedTrustCheck ||
        !state->CountFilesCompletedTrustCheck || !state->BSecureAllowed ||
        !state->CountItemsToReport)
    {
        return false;
    }
 
    int file_count = state->CountItemsToReport();
    state->total_files_cached = file_count;
 
    if (!state->suppress_flag)
    {
        uint8_t has_violations = state->BSecureAllowed(NULL, 0, 0);
        state->suppress_flag = (has_violations == 0);
     
        if (state->suppress_flag)
        {
            g_bClientIsntAllowedToPlayOnSecureServers = 1;
            Source2EngineToClient001->NotifySecurityStateChanged();
        }
    }
 
    return (state->total_files_cached != state->prev_files_loaded);
}

Глобальная переменная по адресу 0x181CFB788 (g_bClientIsntAllowedToPlayOnSecureServers) — булевый флаг, который контролирует, может ли клиент подключаться к серверам, защищенным VAC. Устанавливается в 1 при обнаружении нарушений или при использовании параметра запуска -insecure.

LoadSystemDLLHandles

Собирает хендлы системных DLL, используемых для верификации PE timestamp:

Код:
Expand Collapse Copy
void __fastcall LoadSystemDLLHandles(CDllVerificationMonitor* state)
{
    if (state->ntdll_dll != NULL)
        return;
     
    state->ntdll_dll = GetModuleHandleA("ntdll.dll");
    if (state->ntdll_dll)
    {
        state->NtOpenFile = GetProcAddress(state->ntdll_dll, "NtOpenFile");
        state->NtQueryInformationThread = GetProcAddress(state->ntdll_dll, "NtQueryInformationThread");
    }
 
    state->kernelbase_dll = GetModuleHandleA("kernelbase.dll");
    if (state->kernelbase_dll)
    {
        state->LoadLibraryExW = GetProcAddress(state->kernelbase_dll, "LoadLibraryExW");
    }
 
    state->kernel32_dll = GetModuleHandleA("kernel32.dll");
    state->gameoverlay_dll = GetModuleHandleA("gameoverlayrenderer64.dll");
}

InitializeDiagnosticCallbacks

Разрешает экспортируемые функции из cs2.exe:

Код:
Expand Collapse Copy
void __fastcall InitializeDiagnosticCallbacks(CDllVerificationMonitor* state)
{
    if (state->GetTotalFilesLoaded && state->CountFilesNeedTrustCheck &&
        state->CountFilesCompletedTrustCheck && state->BSecureAllowed)
    {
        return;
    }
 
    HMODULE cs2_exe = GetModuleHandleA(NULL);
    if (!cs2_exe)
        return;
 
    state->GetTotalFilesLoaded = (void*)GetProcAddress(cs2_exe, "GetTotalFilesLoaded");
    state->CountFilesNeedTrustCheck = (void*)GetProcAddress(cs2_exe, "CountFilesNeedTrustCheck");
    state->CountFilesCompletedTrustCheck = (void*)GetProcAddress(cs2_exe, "CountFilesCompletedTrustCheck");
    state->BSecureAllowed = (void*)GetProcAddress(cs2_exe, "BSecureAllowed");
    state->CountItemsToReport = (void*)GetProcAddress(cs2_exe, "CountItemsToReport");
}

MonitorThreadContext (DllMain)

Вызывается при DLL_THREAD_ATTACH в точке входа:

Код:
Expand Collapse Copy
__int64 __fastcall EntryPoint(__int64 unused_hinstDLL, int reason_for_call)
{
    if (reason_for_call == DLL_THREAD_ATTACH)
        MonitorThreadContext(&g_DiagnosticState);
    return 1LL;
}

HMODULE __fastcall MonitorThreadContext(CDllVerificationMonitor* monitor)
{
    LPCVOID thread_start_address = NULL;
    HMODULE thread_module = NULL;
    MEMORY_BASIC_INFORMATION mem_info;
 
    DWORD current_thread_id = GetCurrentThreadId();
    LoadSystemDLLHandles(monitor);
 
    if (monitor->NtQueryInformationThread)
    {
        HANDLE current_thread = GetCurrentThread();
     
        NTSTATUS status = monitor->NtQueryInformationThread(
            current_thread,
            ThreadQuerySetWin32StartAddress,
            &thread_start_address,
            sizeof(thread_start_address),
            NULL);
     
        if (NT_SUCCESS(status))
        {
            VirtualQuery(thread_start_address, &mem_info, sizeof(mem_info));
         
            GetModuleHandleExA(
                GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
                GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
                (LPCSTR)thread_start_address,
                &thread_module);
         
            if (mem_info.Protect == PAGE_EXECUTE_READWRITE ||
                thread_module == monitor->kernel32_dll ||
                thread_module == monitor->kernelbase_dll)
            {
                monitor->monitored_addr_3 = thread_module;
                monitor->monitored_addr_4 = thread_start_address;
                monitor->monitored_value_1 = current_thread_id;
                monitor->monitored_value_2 = mem_info.Protect;
                monitor->monitored_addr_5 = _ReturnAddress();
                monitor->monitored_addr_6 = mem_info.AllocationBase;
             
                uint64_t aligned_address = (uint64_t)thread_start_address & 0xFFFFFFFFFFFFF000ULL;
                memcpy(monitor->signature_buffer, (void*)aligned_address, 4096);
             
                monitor->send_counter++;
            }
        }
    }
 
    return thread_module;
}

Регистрация обработчиков

В функции инициализации CSource2Client (vtable index 3) регистрируются обработчики через GameEventSystemServerV001->RegisterCallback:

Код:
Expand Collapse Copy
void __fastcall InitializeClientSystems(CSource2Client* client)
{
    // ...

    // Регистрация обработчика сообщения 157 (RequestUtilAction)
    GameEventSystemServerV001->RegisterCallback(
        0, &ReceiveRequestUtilAction, &g_RequestUtilActionCallbackHandle, 0);
 
    // Регистрация обработчика сообщения 160 (RequestInventory)
    GameEventSystemServerV001->RegisterCallback(
        0, &SendInventoryRequest, &g_RequestInventoryCallbackHandle, 0);
 
    // Регистрация обработчика сообщения 162 (RequestDiagnostic)
    GameEventSystemServerV001->RegisterCallback(
        0, &ReceiveRequestDiagnostic, &g_RequestDiagnosticCallbackHandle, 0);

    // ...
}

SerializeSystemDiagnostics - Основная функция сбора данных

Эта функция является сердцем диагностической системы. Она собирает более 40 различных диагностических полей и отправляет их как сообщение 159. Функция работает в несколько фаз:

Код:
Expand Collapse Copy
bool __fastcall SerializeSystemDiagnostics(
    CDllVerificationMonitor* monitor,
    CUserMessage_DllStatus* response,
    CUtlBuffer* detail_buffer,
    uint8_t flags)
{
    // ФАЗА 1: Снимок модулей
    InitializeDiagnosticCallbacks(monitor);
 
    CModuleListSnapshot module_snapshot;
    uint8_t module_buffer[8200];

    CModuleListSnapshot::Constructor(&module_snapshot, module_buffer, 0x2000);

    DWORD current_pid = GetCurrentProcessId();

    // КРИТИЧНО: Захват ВСЕХ загруженных DLL
    if (CModuleListSnapshot::Capture(&module_snapshot, current_pid))
    {
        const void* module_iter = NULL;
        const char* module_name;
        void* module_base;
        uint64_t module_size;
     
        while (CModuleListSnapshot::GetNextModule(
            &module_snapshot,
            &module_iter,
            &module_name,
            &module_base,
            &module_size))
        {
            CModule* module_entry = AllocateModuleEntry(response);
         
            SetModuleBase(module_entry, module_base);
            SetModuleName(module_entry, module_name);
            SetModuleSize(module_entry, module_size);
        }
    }

    // ФАЗА 2: Подготовка буфера нарушений
    bool has_violations = false;

    // Резервируем 100 КБ для детального отчета
    CUtlBuffer::EnsureCapacity(detail_buffer, 102400);
    char* violation_report = CUtlBuffer::GetWritePointer(detail_buffer, 102400);

    if (violation_report)
        *violation_report = '\0';

    // ФАЗА 3: Проверка целостности файлов
    if (monitor->GetTotalFilesLoaded &&
        monitor->CountFilesNeedTrustCheck &&
        monitor->CountFilesCompletedTrustCheck &&
        monitor->BSecureAllowed &&
        monitor->CountItemsToReport)
    {
        int total_files = monitor->GetTotalFilesLoaded();
        int files_need_check = monitor->CountFilesNeedTrustCheck();
        int files_completed = monitor->CountFilesCompletedTrustCheck();
     
        // КЛЮЧЕВАЯ ПРОВЕРКА: BSecureAllowed
        if (violation_report)
        {
            has_violations = monitor->BSecureAllowed(
                violation_report,
                102400,
                flags
            );
         
            // Игнорируем нарушения для режима -insecure
            if (monitor->suppress_flag)
                has_violations = false;
        }
     
        // Добавляем отчет о нарушениях в ответ
        if (violation_report && *violation_report)
        {
            response->flags |= 1;
            CBufferString report_str;
            CBufferString::SetString(&report_str, violation_report);
            SetViolationReport(response, &report_str);
        }
     
        // Добавляем командную строку
        ICommandLine* cmdline = CommandLine();
        const char* cmdline_str = cmdline->GetCmdLine();
     
        response->flags |= 2;
        CBufferString cmdline_buf;
        CBufferString::SetString(&cmdline_buf, cmdline_str);
        SetCommandLine(response, &cmdline_buf);
     
        // ФАЗА 4: Базовая статистика
        SetDiagnosticField(response, 21, total_files);
        SetDiagnosticField(response, 22, files_need_check);
        SetDiagnosticField(response, 23, files_completed);
        SetDiagnosticField(response, 24, GetCurrentProcessId());
        SetDiagnosticField(response, 25, GetCurrentThreadId());
        SetDiagnosticField(response, 29, has_violations ? 1 : 0);
     
        // ФАЗА 5: PE Timestamps системных DLL
        HMODULE client_dll = NULL;
        GetModuleHandleExA(
            GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
            GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
            (LPCSTR)&GetPETimestamp,
            &client_dll
        );
     
        HMODULE cs2_exe = GetModuleHandleA(NULL);
     
        // Поля 8-15, 38-39: Хендлы модулей и timestamp'ы
        SetDiagnosticField(response, 8, (uint64_t)client_dll);
        SetDiagnosticField(response, 9, (uint64_t)monitor->kernel32_dll);
        SetDiagnosticField(response, 10, (uint64_t)cs2_exe);
        SetDiagnosticField(response, 14, (uint64_t)monitor->ntdll_dll);
        SetDiagnosticField(response, 38, (uint64_t)monitor->gameoverlay_dll);
     
        SetDiagnosticField(response, 11, GetPETimestamp(client_dll));
        SetDiagnosticField(response, 12, GetPETimestamp(monitor->kernel32_dll));
        SetDiagnosticField(response, 13, GetPETimestamp(cs2_exe));
        SetDiagnosticField(response, 15, GetPETimestamp(monitor->ntdll_dll));
        SetDiagnosticField(response, 39, GetPETimestamp(monitor->gameoverlay_dll));
     
        // ФАЗА 6: Данные MonitorThreadContext
        SetDiagnosticField(response, 1, (uint64_t)monitor->monitored_addr_3);
        SetDiagnosticField(response, 2, (uint64_t)monitor->monitored_addr_4);
        SetDiagnosticField(response, 3, monitor->monitored_value_1);
        SetDiagnosticField(response, 4, monitor->monitored_value_2);
        SetDiagnosticField(response, 5, (uint64_t)monitor->monitored_addr_5);
        SetDiagnosticField(response, 16, (uint64_t)monitor->monitored_addr_6);
     
        // Поле 17: 4KB дамп памяти по адресу потока
        SetDiagnosticFieldBuffer(response, 17,
            monitor->signature_buffer, 4096);
        SetDiagnosticField(response, 18, monitor->signature_size);
     
        SetDiagnosticField(response, 31, (uint64_t)monitor->monitored_addr_1);
        SetDiagnosticField(response, 32, (uint64_t)monitor->monitored_addr_2);
     
        // ФАЗА 7: Данные коллбэков
        uint64_t callback_val1 = 0;
        uint64_t callback_val2 = 0;
     
        if (monitor->callback_1)
        {
            monitor->callback_1(&callback_val1, &callback_val2);
        }
     
        SetDiagnosticField(response, 35, callback_val1);
        SetDiagnosticField(response, 36, callback_val2);
        SetDiagnosticField(response, 34, GetCurrentThreadId());
     
        // ФАЗА 8: Состояние приложения
        uint32_t app_state = 0;
     
        if (VApplication001)
        {
            if (VApplication001->IsActive())
                app_state |= 1;
         
            if (VApplication001->HasFocus())
                app_state |= 2;
        }
     
        SetDiagnosticField(response, 33, app_state);
     
        // ФАЗА 9: Системная информация
        SetDiagnosticField(response, 27, Plat_GetOSType());
     
        FILETIME system_time;
        GetSystemTimeAsFileTime(&system_time);
        SetDiagnosticField(response, 28, *(uint64_t*)&system_time);
     
        SetDiagnosticField(response, 30, (uint64_t)monitor->callback_1);
     
        // ФАЗА 10: Обнаружение отладчика
        BOOL debugger_present = IsDebuggerPresent();
        SetDiagnosticField(response, 37, debugger_present);
     
        // ФАЗА 11: CPUID (обнаружение VM)
        // Примечание: Не полностью уверен, но это возможно
        __m128i cpuid_data;
        memset(&cpuid_data, 0, sizeof(cpuid_data));
        cpuid_data.m128i_i32[0] = 148;
     
        if (monitor->cpuid_callback)
        {
            monitor->cpuid_callback(&cpuid_data);
        }
     
        // Упаковываем результат CPUID
        uint32_t cpuid_packed =
            cpuid_data.m128i_i32[3] |
            (cpuid_data.m128i_i32[2] << 16) |
            (cpuid_data.m128i_i32[1] << 24);
     
        SetDiagnosticField(response, 40, cpuid_packed);
    }

    CModuleListSnapshot::Purge(&module_snapshot);

    return has_violations;
}

Разбивка по фазам:

Фаза 1: Снимок модулей

Захватывает ВСЕ загруженные DLL с помощью CModuleListSnapshot. Мгновенно обнаруживает любые инжектированные чит-модули.

Фаза 2: Подготовка буфера нарушений
Резервирует 100KB для детальных отчетов о нарушениях от BSecureAllowed.

Фаза 3: Проверка целостности файлов
Вызывает BSecureAllowed для проверки целостности файлов и генерации отчетов о нарушениях.

Фазы 4-11: Диагностические поля
Собирает и отправляет:
- Поля 21-25: Статистика файлов, PID, TID, флаги нарушений
- Поля 8-15, 38-39: PE timestamps критических DLL
- Поля 1-5, 16-18, 31-32: Данные MonitorThreadContext включая 4KB дамп памяти
- Поля 34-36: Данные коллбэков
- Поле 33: Состояние приложения (активно/в фокусе)
- Поля 27-28, 30: Системная информация
- Поле 37: IsDebuggerPresent()
- Поле 40: Результат CPUID для обнаружения VM

Все данные отправляются через сообщение 159 (DiagnosticResponse).

Сообщение 157: Проверка целостности ConVar

Этот обработчик выполняет массовую проверку всех консольных переменных и отправляет детальный отчет. Работает в двух режимах:

Режим 1: Все ConVar
- Обрабатывает имя и флаги (12 байт)
- Быстрая проверка целостности всей конфигурации

Режим 2: FCVAR_REPLICATED ConVar
- Фильтр: 0x4000
- Исключение: 0x10000
- Обрабатывает имя, значение и флаги (16 байт)
- Детальная проверка критических для геймплея переменных

Код:
Expand Collapse Copy
void __fastcall ReceiveRequestUtilAction(CUserMessageRequestUtilAction_t* request)
{
    INetworkClient* net_client = Source2EngineToClient001->GetNetworkClient(0);
    if (!net_client)
        return;
 
    netadr_t server_addr;
    netadr_t::Initialize(&server_addr);
 
    const char* addr_str = net_client->GetServerAddress();
    SetupAddressForConnection(&server_addr, addr_str, false);
 
    if (server_addr.type != 2)
    {
        netadr_t::Clear(&server_addr);
        return;
    }
 
    netadr_t::Clear(&server_addr);
 
    int max_items_mode1 = request->param1;
    int max_items_mode2 = request->param2;
    int detail_count_mode1 = request->param3;
    int detail_count_mode2_1 = request->param4;
    int detail_count_mode2_2 = request->param5;
 
    CUserMessage_UtilMsg_Response response;
    CUserMessage_UtilMsg_Response::Constructor(&response);
 
    // РЕЖИМ 1
    int mode1_count = 0;
    int mode1_total = 0;
 
    uint32_t crc1 = ComputeConVarChecksum(
        &response, 0LL, 0LL, 0,
        max_items_mode1, &mode1_total, &mode1_count);
 
    response.flags |= 0x83;
    response.crc_mode1 = crc1;
    response.count_mode1 = mode1_total;
    response.tag_mode1 = mode1_count;
 
    AddConVarDetails(&response, 0LL, 0LL,
        detail_count_mode1, detail_count_mode2_1, 1);
 
    // РЕЖИМ 2
    int mode2_count = 0;
    int mode2_total = 0;
 
    uint32_t crc2 = ComputeConVarChecksum(
        &response, 0x4000LL, 0x10000LL, 1,
        max_items_mode2, &mode2_total, &mode2_count);
 
    response.flags |= 0x10C;
    response.crc_mode2 = crc2;
    response.count_mode2 = mode2_total;
    response.tag_mode2 = mode2_count;
 
    AddConVarDetails(&response, 0x4000LL, 0x10000LL,
        detail_count_mode1, detail_count_mode2_2, 2);
 
    response.flags |= 0x10;
    response.pe_timestamp = dword_181CFB774;
 
    int os_type = Plat_GetOSType();
    response.flags |= 0x20;
    response.os_type = os_type;
 
    SendUserMessage(158, &response);
    CleanupUserMessage(&response);
}

ComputeConVarChecksum — вычисление хэша CRC32
C++:
Expand Collapse Copy
    uint32_t __fastcall ComputeConVarChecksum(
        CUserMessage_UtilMsg_Response* response,
        uint64_t filter_flags,      // ConVar must have these flags
        uint64_t exclude_flags,     // ConVar must NOT have these flags
        int mode,                   // 0 or 1
        int max_items,              // Maximum number of items to process
        int* out_total,             // Output: total found
        int* out_count)             // Output: processed
    {
        if (!VEngineCvar007)
            return 0;
        
        // Create a temporary CUtlRBTree for sorting ConVars
        CUtlRBTree<ConVar*> tree;
        tree.alloc_pool = g_pMemAlloc->Alloc(160000);
        tree.capacity = CalculateCapacity(g_pMemAlloc->GetSize(tree.alloc_pool));
        tree.root = -1;
        tree.count = 0;
        tree.free_list = -1;
        
        // Phase 1: Collect all ConVars into the tree
        ICvar::Iterator iter;
        ConVarHandle first = VEngineCvar007->GetFirstConVar(&iter);
        
        if (first != INVALID_CONVAR_HANDLE)
        {
            do
            {
                // Retrieve ConVar
                ConVar* cvar = VEngineCvar007->GetConVar(first);
                
                // Insert into sorted tree (sorted by name)
                tree.Insert(cvar);
                
                // Move to next ConVar
                first = VEngineCvar007->GetNextConVar(&iter, first);
            }
            while (first != INVALID_CONVAR_HANDLE);
        }
        
        // Phase 2: Traverse tree and compute CRC
        uint32_t crc32 = 0xFFFFFFFF;  // Initial CRC32 value
        int processed = 0;
        int skip_count = 0;
        
        // Traverse the tree (in-order = alphabetical order)
        for (TreeNode* node = tree.GetLeftmost();
             node != nullptr;
             node = tree.GetNext(node))
        {
            ConVar* cvar = node->data;
            uint64_t cvar_flags = cvar->flags;
            
            // Apply filters
            if (filter_flags && (cvar_flags & filter_flags) != filter_flags)
                continue;
                
            if (exclude_flags && (cvar_flags & exclude_flags) != 0)
                continue;
            
            // Skip the first skip_count items
            if (skip_count < max_items)
            {
                skip_count++;
                continue;
            }
            
            if (processed >= max_items)
                break;
            
            // Compute CRC32 for this ConVar
            struct ConVarData {
                uint32_t name_hash;        // +0x00 - Name hash
                uint32_t value_hash;       // +0x04 - Value hash (if mode=1)
                uint64_t flags;            // +0x08 - ConVar flags
            };
            
            ConVarData data;
            
            // Hash ConVar name
            CBufferString name_buffer;
            cvar->GetName(&name_buffer);
            
            const char* name_str = name_buffer.GetString();
            uint32_t name_hash = 1171724434;  // Initial value
            
            for (const char* p = name_str; *p; p++)
                name_hash = *p + 33 * name_hash;
            
            data.name_hash = name_hash;
            data.flags = cvar_flags;
            
            // Hash value (only in mode 1)
            if (mode == 1)
            {
                CBufferString value_buffer;
                cvar->GetStringValue(-1, &value_buffer);
                
                const char* value_str = value_buffer.GetString();
                uint32_t value_hash = 1171724434;
                
                for (const char* p = value_str; *p; p++)
                    value_hash = *p + 33 * value_hash;
                
                data.value_hash = value_hash;
                
                CBufferString::Purge(&value_buffer);
                
                // CRC32 processes 16 bytes
                CRC32_ProcessBuffer(&crc32, &data, 16);
            }
            else
            {
                // CRC32 processes 12 bytes (without value_hash)
                CRC32_ProcessBuffer(&crc32, &data, 12);
            }
            
            processed++;
        }
        
        *out_total = processed;
        *out_count = tree.count;  // Total ConVars in the tree
        
        // Cleanup
        tree.Destroy();
        
        return ~crc32;  // Finalize CRC32
    }

Добавление подробной информации — AddConVarDetails
Эта функция добавляет в ответ подробные записи о конкретных ConVar.


C++:
Expand Collapse Copy
    void __fastcall AddConVarDetails(
        CUserMessage_UtilMsg_Response* response,
        uint64_t filter_flags,
        uint64_t exclude_flags,
        int start_index,        // Start from this index
        int max_details,        // Maximum details to add
        int detail_type)        // 1 or 2
    {
        if (!VEngineCvar007)
            return;
        
        // Create the same tree
        CUtlRBTree<ConVar*> tree;
        // ... (similar to ComputeConVarChecksum)
        
        int current_index = 0;
        int details_added = 0;
        
        for (TreeNode* node = tree.GetLeftmost();
             node != nullptr && details_added < max_details;
             node = tree.GetNext(node))
        {
            ConVar* cvar = node->data;
            uint64_t cvar_flags = cvar->flags;
            
            // Apply filters
            if (filter_flags && (cvar_flags & filter_flags) != filter_flags)
                continue;
                
            if (exclude_flags && (cvar_flags & exclude_flags) != 0)
                continue;
            
            // Start from start_index
            if (current_index < start_index)
            {
                current_index++;
                continue;
            }
            
            // Create Detail Entry
            CUserMessage_UtilMsg_Response_ItemDetail* detail;
            
            if (response->details_capacity)
            {
                // There is free space in the pre-allocated array
                detail = response->details_buffer[response->details_count];
                response->details_count++;
            }
            else
            {
                // Allocate a new detail
                detail = AllocateDetail(response->details_allocator);
                AddDetailToRepeatedField(&response->details, detail);
            }
            
            // Fill in detail
            detail->flags = 0x0A;
            detail->index = current_index;
            detail->crc32 = ComputeDetailCRC32(cvar);
            
            // Copy ConVar name
            CBufferString name_buffer;
            cvar->GetName(&name_buffer);
            
            const char* name_str = name_buffer.GetString();
            size_t name_len = strlen(name_str);
            
            // Allocate memory for the name
            std::string* name_field = &detail->name;
            name_field->reserve(name_len);
            name_field->assign(name_str, name_len);
            
            detail->flags |= 0x01;  // name field is set
            
            CBufferString::Purge(&name_buffer);
            
            details_added++;
            current_index++;
        }
        
        // Set flag in response
        response->flags |= 0x40;
        response->detail_type = detail_type;
        
        // Cleanup
        tree.Destroy();
    }

Подробности расчета CRC32
C++:
Expand Collapse Copy
    uint32_t ComputeDetailCRC32(ConVar* cvar)
    {
        uint32_t crc32 = 0xFFFFFFFF;
        
        struct DetailData {
            uint32_t name_hash;
            uint32_t value_hash;
            uint64_t flags;
        };
        
        DetailData data;
        
        // Name hash
        CBufferString name_buffer;
        cvar->GetName(&name_buffer);
        
        const char* name_str = name_buffer.GetString();
        data.name_hash = HashString(name_str);
        
        CBufferString::Purge(&name_buffer);
        
        // Flags
        data.flags = cvar->flags;
        
        // Value hash
        CBufferString value_buffer;
        cvar->GetStringValue(-1, &value_buffer);
        
        const char* value_str = value_buffer.GetString();
        data.value_hash = HashString(value_str);
        
        CBufferString::Purge(&value_buffer);
        
        // Compute CRC32
        CRC32_ProcessBuffer(&crc32, &data, 16);
        
        return ~crc32;
    }

Строковая хеш-функция (вариант DJB2)
C++:
Expand Collapse Copy
    uint32_t HashString(const char* str)
    {
        uint32_t hash = 1171724434;
        
        for (; *str; str++)
            hash = *str + 33 * hash;
        
        return hash;
    }

Структура ответа
C++:
Expand Collapse Copy
    struct CUserMessage_UtilMsg_Response
    {
        void* vtable;                                   // +0x00
        void* allocator;                                // +0x08
        uint32_t flags;                                 // +0x10
        
        // Mode 1 data
        uint32_t crc_mode1;                            // +0x14 - CRC32 of all ConVars
        uint32_t count_mode1;                          // +0x18 - Number processed
        uint32_t tag_mode1;                            // +0x1C - Total found
        
        // Mode 2 data
        uint32_t crc_mode2;                            // +0x20 - CRC32 with filters
        uint32_t count_mode2;                          // +0x24 - Number processed
        uint32_t tag_mode2;                            // +0x28 - Total found
        
        // Detailed information
        RepeatedPtrField<ItemDetail> details;          // +0x30 - Array of details
        uint32_t detail_type;                          // +0x48 - Detail type (1 or 2)
        
        // Additional data
        uint32_t pe_timestamp;                         // +0x50 - client.dll PE timestamp
        uint32_t os_type;                              // +0x54 - OS type
        
        // Pre-allocated detail buffer
        ItemDetail** details_buffer;                   // +0x58
        uint32_t details_capacity;                     // +0x60
        uint32_t details_count;                        // +0x64
    };

Структура ItemDetail
C++:
Expand Collapse Copy
    struct CUserMessage_UtilMsg_Response_ItemDetail
    {
        void* vtable;                                  // +0x00
        void* allocator;                               // +0x08
        uint32_t flags;                                // +0x10
        
        std::string name;                              // +0x18 - ConVar name
        uint32_t index;                                // +0x28 - Index in the list
        uint32_t crc32;                                // +0x2C - CRC32 hash
    };

Система создает отсортированное RB-дерево всех ConVars и вычисляет хэши CRC32. Для конкретных ConVars отправляется подробная информация, включая их полное имя, индекс и индивидуальный хэш CRC32.
 
Последнее редактирование:
Сообщение 160: VMT указатели и целостность DLL

Этот обработчик собирает критическую информацию для античита: VMT указатели и данные о загруженных модулях.

Фаза 1: Сбор VMT интерфейсов

Собирает VMT указатели всех 112 зарегистрированных глобальных интерфейсов. Для каждого интерфейса:
- VEngineCvar007, Source2EngineToClient001, ResourceSystem013, SchemaSystem_001, и 108 других
- Вычисляет VMT offset от базы модуля
- Создает CRC32 хэш оффсетов
- Любая модификация VMT мгновенно обнаруживается

Фаза 2: Сбор VMT энтитей

Обходит все активные игровые сущности:
- Подсчитывает уникальные VMT указатели
- Отслеживает счетчик использования для каждого VMT
- Помечает аномальные VMT (встречающиеся только один раз)

Фаза 3: Анализ модулей (асинхронно)

При запросе с флагом 0x02 запускается в отдельном потоке для анализа всех загруженных модулей.
C++:
Expand Collapse Copy
    void __fastcall SendInventoryRequest(CUserMessageRequestInventory_t* request)

    {

        // Check connection to a secure server
        INetworkClient* net_client = Source2EngineToClient001->GetNetworkClient(0);

        if (!net_client)
            return;

        netadr_t server_addr;
        netadr_t::Initialize(&server_addr);
        const char* addr_str = net_client->GetServerAddress();
        SetupAddressForConnection(&server_addr, addr_str, false);

        // CRITICAL: Must be of type NA_TYPE_SERVER (2)
        if (server_addr.type != 2)
        {
            netadr_t::Clear(&server_addr);
            return;
        }

        netadr_t::Clear(&server_addr);

        // Create response
        CUserMessage_Inventory_Response response;
        CUserMessage_Inventory_Response::Constructor(&response);

        // Phase 1: Collect VMT pointers of interfaces
        CollectInterfacesData(&response);
     
        // Phase 2: Collect VMT pointers of entities
        CollectEntitiesVMTData(&response);
       
        // Phase 3: Collect module (DLL) information
        bool should_include_modules = (request->flags & 2) != 0;  // +0x50 (80)       
        if (should_include_modules)
        {
            bool calculate_hash = (request->flags & 4) != 0;         

            // CRITICAL: Run in a separate thread
            auto lambda = [calculate_hash]() {
                CollectAndSendModuleInventory(calculate_hash);
            };           
            g_pThreadPool->QueueCall(lambda, 2);

        }
      
        // Send response (Message ID 161)
        SendUserMessage(161, &response);
    
        // Cleanup
        CUserMessage_Inventory_Response::Destructor(&response);
    }
CollectInterfacesData — сбор указателей VMT интерфейсов
Эта функция собирает указатели VMT всех глобальных игровых интерфейсов:

C++:
Expand Collapse Copy
    void __fastcall CollectInterfacesData(CUserMessage_Inventory_Response* response)
    {

        uint64_t start_ticks = __rdtsc();
       
        // Get the list of all registered interfaces
        int interface_count = 0;
        InterfaceReg** interfaces = GetGlobalInterfaceList(&interface_count);   
        response->flags |= 0x02;
        response->interface_count = interface_count;  // Total: 112 interfaces
     
        uint32_t crc32 = 0xFFFFFFFF;
    
        // Process each interface
        for (int i = 0; i < interface_count; i++)
        {
            InterfaceReg* reg = interfaces[i];
            const char* interface_name = reg->name;
            void* instance = reg->create_fn();  // Call the factory function

            if (!instance)
                continue;

            void** vtable = *(void***)instance;
            if (!vtable)
                continue;
           
            void* first_vfunc = vtable[0];
      
            // CRITICAL: Compute VMT offset from module base
            uint64_t vmt_offset = (uint64_t)vtable - (uint64_t)first_vfunc;
            CRC32_ProcessBuffer(&crc32, &vmt_offset, 8);
          
            // Create detail entry
            CUserMessage_Inventory_Response_InventoryDetail* detail;

            if (response->details_capacity)
            {
                detail = response->details_buffer[response->details_count];
                response->details_count++;
            }
            else
            {
                detail = AllocateInventoryDetail(response->details_allocator);
                AddDetailToRepeatedField(&response->details, detail);
            }
     
            detail->flags = 0x3C;
            detail->index = i;                     // +0x40 (64) - Interface index
            detail->vmt_address = (uint64_t)vtable;    // +0x28 (40) - VMT address
            detail->first_vfunc = (uint64_t)first_vfunc; // +0x30 (48) - First vfunc
            detail->vmt_offset = vmt_offset;       // +0x38 (56) - Offset from base
       
            // Hash interface name
            uint32_t name_hash = 1171724434;

            for (const char* p = interface_name; *p; p++)
                name_hash = *p + 33 * name_hash;

            detail->flags |= 0x200;
            detail->name_hash = name_hash;         // +0x54 (84)
        }

        // Finalize CRC32
        response->flags |= 0x01;
        response->crc_interfaces = ~crc32;         // +0x60 (96)

        // Additional data
        if (!g_pTime)
            InitGlobalTimePID();

        response->flags |= 0x10;
        response->pe_timestamp = dword_181CFB774;  // +0x70 (112) - PE timestamp
        int engine_build = Source2EngineToClient001->GetEngineBuildNumber();
        response->flags |= 0x80;
        response->engine_build = engine_build;     // +0x7C (124)
        response->flags |= 0x200;
        response->process_id = g_pThreadGetCurrentProcessId;  // +0x88 (136)
        response->flags |= 0x100;
        response->time = g_pTime;                  // +0x80 (128)
        response->flags |= 0x20;
        response->os_type1 = Plat_GetOSType();     // +0x74 (116)
        response->flags |= 0x04;
        response->os_type2 = Plat_GetOSType();     // +0x68 (104)

        // Execution time in microseconds
        uint64_t end_ticks = __rdtsc();
        uint64_t freq = Plat_CPUTickFrequency();
        uint64_t elapsed_us = 1000000 * ((end_ticks - start_ticks) / freq) +
                              1000000 * ((end_ticks - start_ticks) % freq) / freq;
  
        response->flags |= 0x48;
        response->elapsed_time_us = elapsed_us;    // +0x6C (108)
        response->collection_phase = 0;            // +0x78 (120) - Phase 0

    }
GetGlobalInterfaceList
C++:
Expand Collapse Copy
    InterfaceReg** __fastcall GetGlobalInterfaceList(int* out_count)
    {
        if (out_count)
            *out_count = 112;  // Total 112 registered interfaces
    
        return &g_pInterfaceList;
    }
CollectEntitiesVMTData — сбор данных VMT объектов
Собирает указатели VMT всех активных игровых объектов:
C++:
Expand Collapse Copy
    void __fastcall CollectEntitiesVMTData(CUserMessage_Inventory_Response* response)

    {

        // Create an RB-Tree to count unique VMTs

        CUtlRBTree<void*, int> vmt_tree;

        vmt_tree.alloc_pool = g_pMemAlloc->Alloc(6400);

        vmt_tree.capacity = CalculateCapacity(g_pMemAlloc->GetSize(vmt_tree.alloc_pool));

        vmt_tree.root = -1;

        vmt_tree.count = 0;

        vmt_tree.free_list = -1;

       

        // Iterate all entities in the world

        for (CEntityInstance* entity = g_pEntitySystem->first_entity;

             entity != nullptr;

             entity = entity->next)

        {

            void** vtable = *(void***)entity;

            if (!vtable)

                continue;

           

            void* vmt_ptr = vtable;

           

            // Find or insert in the tree

            TreeNode* node = vmt_tree.Find(vmt_ptr);

           

            if (!node)

            {

                // New VMT - insert into tree

                vmt_tree.Insert(vmt_ptr, 0);  // count = 0

                node = vmt_tree.Find(vmt_ptr);

            }

           

            // Increment counter for this VMT

            node->count++;

        }

       

        // Add local player controller VMT

        CCSPlayerController* controller = GetPlayerController(0);

        if (controller)

        {

            void* vmt_ptr = *(void**)controller;

            TreeNode* node = vmt_tree.Find(vmt_ptr);

           

            if (!node)

            {

                vmt_tree.Insert(vmt_ptr, 0);

                node = vmt_tree.Find(vmt_ptr);

            }

           

            node->count++;

        }

       

        // Add local pawn VMT

        C_CSPlayerPawn* pawn = GetLocalPawnFromController(0);

        if (pawn)

        {

            void* vmt_ptr = *(void**)pawn;

            TreeNode* node = vmt_tree.Find(vmt_ptr);

           

            if (!node)

            {

                vmt_tree.Insert(vmt_ptr, 0);

                node = vmt_tree.Find(vmt_ptr);

            }

           

            node->count++;

        }

       

        // Send VMT data

        int vmt_count = 0;

       

        for (TreeNode* node = vmt_tree.GetLeftmost();

             node != nullptr && vmt_count < 300;  // Maximum 300 VMTs

             node = vmt_tree.GetNext(node))

        {

            void* vmt_ptr = node->key;

            int usage_count = node->count;

           

            CUserMessage_Inventory_Response_InventoryDetail* detail;

           

            if (response->details_capacity)

            {

                detail = response->details_buffer[response->details_count];

                response->details_count++;

            }

            else

            {

                detail = AllocateInventoryDetail(response->details_allocator);

                AddDetailToRepeatedField(&response->details, detail);

            }

           

            detail->flags = 0x20;

            detail->index = vmt_count;             // +0x40 (64)

           

            detail->flags |= 0x04;

            detail->vmt_address = (uint64_t)vmt_ptr;  // +0x28 (40)

           

            detail->flags |= 0x48;

            detail->usage_count = usage_count;     // +0x44 (68) - How many occurrences

            detail->image_base = 0x180000000;      // +0x30 (48) - client.dll base

           

            // CRITICAL: Compute PE timestamp of the module

            uint64_t pe_timestamp = 0;

           

            if (*(uint16_t*)0x180000000 == 0x5A4D)  // 'MZ' signature

            {

                IMAGE_DOS_HEADER* dos = (IMAGE_DOS_HEADER*)0x180000000;

                IMAGE_NT_HEADERS* nt = (IMAGE_NT_HEADERS*)(0x180000000 + dos->e_lfanew);

               

                pe_timestamp = 0x180000000 + nt->OptionalHeader.AddressOfEntryPoint;

            }

           

            detail->flags |= 0x10;

            detail->pe_entry_point = pe_timestamp;  // +0x38 (56)

           

            vmt_count++;

        }

       

        // Cleanup

        vmt_tree.Destroy();

    }


CollectAndSendModuleInventory — подробный анализ DLL

Наиболее важная функция — анализирует все загруженные модули:

C++:
Expand Collapse Copy
    char __fastcall CollectAndSendModuleInventory(bool calculate_hash)

    {

        uint64_t start_ticks = __rdtsc();

       

        // Create response

        CUserMessage_Inventory_Response response;

        CUserMessage_Inventory_Response::Constructor(&response);

       

        // Create RB-Tree for modules

        CUtlRBTree<ModuleInfo, int> module_tree;

        InitializeModuleRBTree(&module_tree, 100, 1);

       

        // Get all registered modules

        int module_count = 0;

        const wchar_t** module_list = Plat_GetRegisteredModules();

       

        for (const wchar_t** module_ptr = module_list;

             *module_ptr != nullptr;

             module_ptr++)

        {

            module_count++;

            const wchar_t* module_path = *module_ptr;

           

            // Analyze the module

            ModuleInfo info;

            memset(&info, 0, sizeof(info));

           

            info.index = module_count;

            info.path = module_path;

           

            bool success = AnalyzePEModuleAndCalculateHash(

                module_path,

                &info,

                calculate_hash

            );

           

            if (success)

            {

                // Hash the filename

                const char* filename = V_UnqualifiedFileName(info.filename);

               

                uint32_t name_hash = 1171724434;

                for (const char* p = filename; *p; p++)

                    name_hash = *p + 33 * name_hash;

               

                info.name_hash = name_hash;

            }

           

            // Insert into the tree

            module_tree.Insert(info);

        }

       

        // Send module data

        uint32_t crc32 = 0xFFFFFFFF;

       

        for (TreeNode* node = module_tree.GetLeftmost();

             node != nullptr;

             node = module_tree.GetNext(node))

        {

            ModuleInfo* info = &node->data;

           

            CUserMessage_Inventory_Response_InventoryDetail* detail;

           

            if (response.details_capacity)

            {

                detail = response.details_buffer[response.details_count];

                response.details_count++;

            }

            else

            {

                detail = AllocateInventoryDetail(response.details_allocator);

                AddDetailToRepeatedField(&response->details, detail);

            }

           

            detail->flags = 0x20;

            detail->index = info->index;           // +0x40 (64)

           

            detail->flags |= 0x80;

            detail->name_hash = info->name_hash;   // +0x48 (72)

           

            detail->flags |= 0x40;

            detail->section_count = info->section_count;  // +0x44 (68)

           

            detail->flags |= 0x08;

            detail->image_size = info->image_size;  // +0x30 (48)

           

            detail->flags |= 0x10;

            detail->timestamp = info->timestamp;    // +0x38 (56)

           

            detail->flags |= 0x100;

            detail->checksum = info->checksum;      // +0x50 (80)

           

            detail->flags |= 0x200;

            detail->machine_type = info->machine_type;  // +0x54 (84)

           

            detail->flags |= 0x04;

            detail->sha1_hash = info->sha1_hash;    // +0x28 (40) - 20 bytes SHA1

           

            // CRC32 for machine_type

            CRC32_ProcessBuffer(&crc32, &info->machine_type, 4);

        }

       

        // Finalize response

        response.flags |= 0x03;

        response.module_crc = ~crc32;              // +0x4E (78)

        response.module_count = module_tree.count; // +0x4F (79)

       

        if (!g_pTime)

            InitGlobalTimePID();

       

        response.flags |= 0x10;

        response.pe_timestamp = dword_181CFB774;

       

        response.flags |= 0x380;

        response.engine_build = Source2EngineToClient001->GetEngineBuildNumber();  // +0x55 (85)

        response.process_id = g_pThreadGetCurrentProcessId;  // +0x57 (87)

        response.time = g_pTime;  // +0x56 (86)

       

        response.flags |= 0x20;

        response.os_type1 = Plat_GetOSType();

       

        response.flags |= 0x04;

        response.os_type2 = Plat_GetOSType();

       

        // Execution time

        uint64_t end_ticks = __rdtsc();

        uint64_t freq = Plat_CPUTickFrequency();

        uint64_t elapsed_us = 1000000 * ((end_ticks - start_ticks) / freq) +

                              1000000 * ((end_ticks - start_ticks) % freq) / freq;

       

        response.flags |= 0x48;

        response.elapsed_time_us = elapsed_us;

        response.collection_phase = 1;  // Phase 1 - modules

       

        // Send (Message ID 161)

        SendUserMessage(161, &response);

       

        // Cleanup

        module_tree.Destroy();

        CUserMessage_Inventory_Response::Destructor(&response);

       

        return 1;

    }
AnalyzePEModuleAndCalculateHash - PE анализ

Самая критическая функция — выполняет детальный анализ PE структуры модуля. Процесс включает:

C++:
Expand Collapse Copy
    bool __fastcall AnalyzePEModuleAndCalculateHash(

        const wchar_t* module_path,

        ModuleInfo* out_info,

        bool calculate_hash)

    {

        IMAGE_DOS_HEADER* dos_header = (IMAGE_DOS_HEADER*)module_path;

       

        if (dos_header->e_magic != 0x5A4D)

            return false;

       

        IMAGE_NT_HEADERS64* nt_headers =

            (IMAGE_NT_HEADERS64*)((uint8_t*)dos_header + dos_header->e_lfanew);

       

        if (nt_headers->Signature != 0x4550)

            return false;

       

        if (nt_headers->FileHeader.Machine != 0x8664)

            return false;

       

        if (nt_headers->OptionalHeader.DataDirectory[14].Size != 0)

            return false;

       

        out_info->image_size = nt_headers->OptionalHeader.SizeOfImage;

        out_info->timestamp = nt_headers->FileHeader.TimeDateStamp;

        out_info->machine_type = nt_headers->FileHeader.Machine;

        out_info->section_count = nt_headers->FileHeader.NumberOfSections;

       

        if (calculate_hash && module_path && nt_headers)

        {

            // 1. Allocate virtual copy of module

            size_t image_size = nt_headers->OptionalHeader.SizeOfImage;

            void* image_copy = VirtualAlloc(nullptr, image_size,

                                            MEM_COMMIT | MEM_RESERVE,

                                            PAGE_READWRITE);

           

            // 2. Copy headers

            size_t headers_size = nt_headers->OptionalHeader.SizeOfHeaders;

            memcpy(image_copy, dos_header, headers_size);

           

            // 3. Copy all sections (excluding uninitialized data)

            IMAGE_SECTION_HEADER* sections = IMAGE_FIRST_SECTION(nt_headers);

           

            for (int i = 0; i < nt_headers->FileHeader.NumberOfSections; i++)

            {

                IMAGE_SECTION_HEADER* section = &sections[i];

               

                if ((section->Characteristics & IMAGE_SCN_CNT_UNINITIALIZED_DATA) == 0)

                {

                    memcpy(

                        (uint8_t*)image_copy + section->VirtualAddress,

                        (uint8_t*)dos_header + section->VirtualAddress,

                        section->SizeOfRawData);

                }

            }

           

            // 4. Normalize relocations

            IMAGE_DATA_DIRECTORY* reloc_dir =

                &nt_headers->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];

           

            if (reloc_dir->VirtualAddress != 0)

            {

                IMAGE_BASE_RELOCATION* reloc =

                    (IMAGE_BASE_RELOCATION*)((uint8_t*)dos_header + reloc_dir->VirtualAddress);

               

                IMAGE_BASE_RELOCATION* reloc_end =

                    (IMAGE_BASE_RELOCATION*)((uint8_t*)reloc + reloc_dir->Size);

               

                uint64_t image_delta = 0x180000000 - (uint64_t)dos_header;

               

                while (reloc < reloc_end && reloc->VirtualAddress != 0)

                {

                    int entry_count = (reloc->SizeOfBlock - 8) / 2;

                    uint16_t* entries = (uint16_t*)(reloc + 1);

                   

                    for (int i = 0; i < entry_count; i++)

                    {

                        uint16_t type = entries[i] >> 12;

                        uint16_t offset = entries[i] & 0xFFF;

                       

                        if (type == 10)  // IMAGE_REL_BASED_DIR64

                        {

                            uint64_t* target = (uint64_t*)(

                                (uint8_t*)image_copy + reloc->VirtualAddress + offset);

                           

                            *target -= image_delta;

                        }

                    }

                   

                    reloc = (IMAGE_BASE_RELOCATION*)((uint8_t*)reloc + reloc->SizeOfBlock);

                }

            }

           

            // 5. Zero Security Directory and Checksum

            IMAGE_NT_HEADERS64* copy_nt =

                (IMAGE_NT_HEADERS64*)((uint8_t*)image_copy + dos_header->e_lfanew);

           

            IMAGE_DATA_DIRECTORY* cert_dir =

                &copy_nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY];

           

            memset((uint8_t*)image_copy + cert_dir->VirtualAddress, 0, cert_dir->Size);

            copy_nt->OptionalHeader.CheckSum = 0;

           

            // 6. Compute CRC32 across all sections

            uint32_t crc32 = 0xFFFFFFFF;

           

            for (int i = 0; i < nt_headers->FileHeader.NumberOfSections; i++)

            {

                IMAGE_SECTION_HEADER* section = &sections[i];

               

                if ((section->Characteristics & IMAGE_SCN_CNT_UNINITIALIZED_DATA) == 0)

                {

                    if (section->SizeOfRawData > 0)

                    {

                        CRC32_ProcessBuffer(&crc32,

                            (uint8_t*)image_copy + section->VirtualAddress,

                            section->SizeOfRawData);

                    }

                }

            }

           

            out_info->crc32 = ~crc32;

           

            // 7. Compute SHA1

            CSHA1::Final(&sha1);

           

            VirtualFree(image_copy, 0, MEM_RELEASE);

        }

       

        // Extract PDB path from debug directory

        IMAGE_DATA_DIRECTORY* debug_dir =

            &nt_headers->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG];

       

        if (debug_dir->Size >= 28)

        {

            IMAGE_DEBUG_DIRECTORY* debug =

                (IMAGE_DEBUG_DIRECTORY*)((uint8_t*)dos_header + debug_dir->VirtualAddress);

           

            if (debug->Type == IMAGE_DEBUG_TYPE_CODEVIEW)

            {

                RSDSI* rsds = (RSDSI*)((uint8_t*)dos_header + debug->AddressOfRawData);

               

                if (rsds->signature == 0x53445352)  // 'RSDS'

                {

                    V_strncpy(out_info->pdb_path, rsds->path, 260);

                }

            }

        }

       

        return true;

    }

Структура ответа

C++:
Expand Collapse Copy
    struct CUserMessage_Inventory_Response

    {

        void* vtable;                              // +0x00

        void* allocator;                           // +0x08

        uint32_t flags;                            // +0x10

   

        // Interfaces

        uint32_t crc_interfaces;                   // +0x60 (96)

        uint32_t interface_count;                  // +0x64 (100)

        uint32_t os_type2;                         // +0x68 (104)

        uint32_t elapsed_time_us;                  // +0x6C (108)

        uint32_t pe_timestamp;                     // +0x70 (112)

        uint32_t os_type1;                         // +0x74 (116)

        uint32_t collection_phase;                 // +0x78 (120)

        uint32_t engine_build;                     // +0x7C (124)

        uint64_t time;                             // +0x80 (128)

        uint32_t process_id;                       // +0x88 (136)

   

        // Details

        RepeatedPtrField<InventoryDetail> details; // +0x18

        InventoryDetail** details_buffer;          // +0x28

        uint32_t details_count;                    // +0x20

        uint32_t details_capacity;                 // +0x28

    };

    struct CUserMessage_Inventory_Response_InventoryDetail

    {

        void* vtable;                              // +0x00

        void* allocator;                           // +0x08

        uint32_t flags;                            // +0x10

   

        std::string name;                          // +0x18 - Name (unused)

        uint64_t sha1_hash;                        // +0x28 (40) - SHA1 (20 bytes)

        uint64_t image_size;                       // +0x30 (48)

        uint64_t timestamp;                        // +0x38 (56)

        uint32_t index;                            // +0x40 (64)

        uint32_t section_count;                    // +0x44 (68) or usage_count

        uint64_t checksum;                         // +0x48 (72) or name_hash

        uint32_t machine_type;                     // +0x54 (84)

   

        // For interfaces and VMT

        uint64_t vmt_address;                      // +0x28 (40)

        uint64_t first_vfunc;                      // +0x30 (48)

        uint64_t vmt_offset;                       // +0x38 (56)

    };

   

    struct ModuleInfo

    {

        uint32_t name_hash;                        // +0x00

        uint32_t crc32;                            // +0x04

        int index;                                 // +0x08

        uint32_t image_size;                       // +0x0C

        uint32_t timestamp;                        // +0x10

        uint32_t machine_type;                     // +0x14

        uint32_t checksum;                         // +0x18

        uint32_t section_count;                    // +0x1C

        const wchar_t* path;                       // +0x20

        char pdb_path[260];                        // +0x28

        uint8_t sha1_hash[20];                     // +0x12C

    };



1. Выделение виртуальной копии модуля
2. Копирование заголовков и всех секций
3. Нормализацию релокаций (вычитание image delta)
4. Обнуление Security Directory и Checksum для детерминированного хеширования
5. Вычисление CRC32 по всем секциям
6. Извлечение Pути PDB из debug directory

Эта нормализация обеспечивает детерминированные хэши независимо от ASLR, делая невозможным скрытие модифицированных модулей.

Сообщение 162: Расширенная диагностика в реальном времени

После базового сбора данных сервер может запросить дополнительную диагностику через сообщение 162. Эта система позволяет серверу выполнять произвольные проверки на клиенте в реальном времени.

Обработчик запроса диагностики

C++:
Expand Collapse Copy
    void __fastcall RequestDiagnostic(CUserMessageRequestDiagnostic_t* request)

    {

        INetworkClient* net_client = Source2EngineToClient001->GetNetworkClient(0);

        if (!net_client)

            return;

       

        // Checking connection to a secured server

        netadr_t server_addr;

        netadr_t::Initialize(&server_addr);

       

        const char* addr_str = net_client->GetServerAddress();

        SetupAddressForConnection(&server_addr, addr_str, false);

       

        // Must be of type NA_TYPE_SERVER (2)

        if (server_addr.type != 2)

        {

            netadr_t::Clear(&server_addr);

            return;

        }

       

        netadr_t::Clear(&server_addr);

       

        // CRITICAL: Processing is done in a separate thread via the thread pool

        DWORD current_thread_id = GetCurrentThreadId();

       

        CUserMessageRequestDiagnostic_t request_copy;

        CopyDiagnosticRequest(&request_copy, request);

        request_copy.thread_id = current_thread_id;

       

        // Start processing in the thread pool

        g_pThreadPool->QueueCall(

            [request_copy]() {

                BuildAndSendDiagnosticResponse(&request_copy, request_copy.thread_id);

            },

            2

        );

    }


BuildAndSendDiagnosticResponse — основная функция обработки
Эта функция анализирует запрос и выполняет различные диагностические проверки:

C++:
Expand Collapse Copy
    bool __fastcall BuildAndSendDiagnosticResponse(

        CUserMessageRequestDiagnostic_t* request,

        DWORD thread_id)

    {

        // Initialize response protobuf

        CUserMessage_Diagnostic_Response response;

        CUserMessage_Diagnostic_Response::Constructor(&response);

       

        int diagnostic_count = request->diagnostics.count;  // +0x48 (72)

       

        // Process each diagnostic request

        for (int i = 0; i < diagnostic_count; i++)

        {

            CUserMessageRequestDiagnostic_Diagnostic* diag = request->diagnostics[i];

           

            // Diagnostic type is determined by field 'type' (+0x44)

            switch (diag->type)

            {

                case 23:  // Thread/callback check

                    ProcessThreadDiagnostic(&response, diag);

                    break;

                   

                case 26:  // Async diagnostic in a separate thread

                    ProcessAsyncDiagnostic(&response, diag);

                    break;

                   

                case 27:  // Reset exception counter

                    _InterlockedExchange(&dword_181CFB700, 0);

                    break;

                   

                case 28:  // Memory dump

                    ProcessMemoryDump(&response, diag);

                    break;

                   

                case 29:  // Module function check

                    ProcessModuleFunctionCheck(&response, diag);

                    break;

                   

                case 30:  // Set exception tracking pointers

                    qword_181CFB718 = diag->param1;

                    qword_181CFB720 = diag->param2;

                    break;

            }

        }

       

        // Add data from DebugMonitor

        ProcessDiagnosticResponse(&response);

       

        // Add thread context data

        FinalizeDiagnosticResponse(&response, thread_id);

       

        // Fill system information

        response.field1 = Source2EngineToClient001->GetSomeField();

        response.os_type1 = Plat_GetOSType();

        response.os_type2 = Plat_GetOSType();

        response.flags |= 0x19;

       

        if (!g_pTime)

            InitGlobalTimePID();

       

        response.process_id = g_pThreadGetCurrentProcessId;

        response.time = g_pTime;

        response.flags |= 6;

       

        // Send response (Message ID 163)

        SendDiagnosticResponseToServer(&response);

       

        return true;

    }


Поддерживаемые типы диагностики:
Тип 23: Проверка потока/коллбэка
- Режим 1: Простой вызов обратного вызова
- Режим 2: Вызов с параметрами
- Режим 3: Проверка с буфером данных
- Режим 4: Проверка с возвращаемым значением

C++:
Expand Collapse Copy
    void ProcessThreadDiagnostic(

        CUserMessage_Diagnostic_Response* response,

        CUserMessageRequestDiagnostic_Diagnostic* diag)

    {

        int mode = diag->mode;  // +0x38 (56) - execution mode for callback

       

        if (mode == 1)

        {

            // Simple callback invocation without parameters

            CallbackFunc callback = (CallbackFunc)diag->callback;

            callback(mode - 1, i, /* additional parameters */);

        }

        else if (mode == 2)

        {

            // Callback with parameters

            CallbackFunc callback = (CallbackFunc)diag->callback;

            callback(diag->param1, diag->param2, diag->param3);

        }

        else if (mode == 3)

        {

            // Diagnostic with a data buffer

            CallbackFunc callback = (CallbackFunc)diag->callback;

            void* data_ptr = diag->param1;      // pointer to input data

            uint32_t data_size = diag->param2;  // size of data

           

            // Copy request data into a temporary buffer

            CUtlBuffer temp_buffer;

            CUtlBuffer::Constructor(&temp_buffer, 0, 0, 0);

           

            ProtobufString* data_str = GetProtobufString(&diag->data);

            const char* str_data = GetStringData(data_str);

            uint32_t str_len = GetStringLength(data_str);

           

            CUtlBuffer::Put(&temp_buffer, str_data, str_len);

           

            void* buffer_ptr = CUtlBuffer::GetBasePointer(&temp_buffer);

            memset(buffer_ptr, 0, data_size);  // Clear buffer before callback

           

            // Allocate a response entry

            CVDiagnostic* result = AllocateDiagnosticField(&response);

            result->type = 17;

            result->flags |= 0x200;

           

            // KEY: invoke the callback with the buffer

            bool success = callback(data_ptr, buffer_ptr, data_size);

           

            // Fill response with callback results

            result->value1 = data_ptr;

            result->value2 = data_ptr;

            result->result = success;

            result->mode = diag->mode;

            result->flags |= 0xD0;

           

            // Copy buffer content back to response

            CopyBufferToResponse(result, buffer_ptr, str_len);

           

            result->correlation_id = diag->correlation_id;

            result->flags |= 0x4000;

           

            CUtlBuffer::Destructor(&temp_buffer);  // Cleanup temporary buffer

        }

        else if (mode == 4)

        {

            // Callback that returns a value via pointer

            CallbackFunc callback = (CallbackFunc)diag->callback;

            uint32_t param = diag->param2;

            void* data_ptr = diag->param1;

           

            CVDiagnostic* result = AllocateDiagnosticField(&response);

            result->type = 17;

            result->flags |= 0x200;

           

            uint64_t return_value = 0;

            // Call the callback, storing the return value in return_value

            bool success = callback(param, data_ptr, &return_value);

           

            // Fill response fields

            result->value1 = data_ptr;

            result->value2 = data_ptr;

            result->result = success;

            result->mode = diag->mode;

            result->return_value = return_value;

            result->correlation_id = diag->correlation_id;

            result->flags |= 0x4D0;

        }

    }
Тип 26: Асинхронная диагностика
Запускает диагностику в отдельном рабочем потоке thread pool.

C++:
Expand Collapse Copy
    void ProcessAsyncDiagnostic(

        CUserMessage_Diagnostic_Response* response,

        CUserMessageRequestDiagnostic_Diagnostic* diag)

    {

        CallbackFunc callback = (CallbackFunc)diag->callback;

        uint32_t param1 = diag->param1;

        void* param2 = diag->param2;

        void* param3 = diag->param3;

        void* param4 = diag->param4;

       

        // Create a lambda to execute the callback in the thread pool

        auto lambda = [callback, param1, param2, param3, param4]() {

            callback(param1, param2, param3, param4);

        };

       

        // Queue the lambda in the thread pool

        g_pThreadPool->QueueCall(lambda, 2);

    }



Тип 27: Сброс счетчика исключений
Код:
Expand Collapse Copy
_InterlockedExchange(&dword_181CFB700, 0)
 
Последнее редактирование:
Тип 28: Дамп памяти (САМЫЙ ОПАСНЫЙ)

Позволяет серверу читать произвольную память клиента. Поддерживает:
- Прямое копирование памяти для размеров <= 1024 байт
- Режим 1: Декодирование закодированных указателей
- Режим 2: Обработка массивов указателей, чтение памяти по каждому декодированному адресу
C++:
Expand Collapse Copy
    void ProcessMemoryDump(

        CUserMessage_Diagnostic_Response* response,

        CUserMessageRequestDiagnostic_Diagnostic* diag)

    {

        const void* address = diag->address;      // Memory address to read

        uint64_t size = diag->size;               // Size of the memory block

       

        uint8_t dump_buffer[1024];

        memset(dump_buffer, 0, sizeof(dump_buffer));

       

        CVDiagnostic* result = AllocateDiagnosticField(&response);

       

        result->type = 14;

        result->address = address;

        result->size = size;

        result->correlation_id = diag->correlation_id;

        result->flags |= 0x310;

       

        // CRITICAL: Read memory from the specified address

        if (size <= 1024)

        {

            // Direct copy for small sizes

            memcpy(dump_buffer, address, size);

            CopyBufferToResponse(result, dump_buffer, size);

        }

        else

        {

            // Direct copy for large sizes

            CopyBufferToResponse(result, address, size);

        }

       

        // Special handling based on mode

        if (diag->mode == 1)

        {

            // Decode encoded pointer

            void* decoded = DecodePointer(diag->callback);

            result->decoded_pointer = decoded;

            result->flags |= 0x80;

        }

        else if (diag->mode == 2)

        {

            // Handle array of pointers

            uint64_t* ptr_array = (uint64_t*)address;

            uint32_t count = size / 8;

           

            for (uint32_t i = 0; i < count; i++)

            {

                if (ptr_array[i] == 0)

                    continue;

                   

                CVDiagnostic* array_entry = AllocateDiagnosticField(&response);

               

                void* target_ptr = (void*)ptr_array[i];

                void* decoded_ptr = DecodePointer(target_ptr);

               

                array_entry->type = 14;

                array_entry->index = i;

                array_entry->source_address = address;

                array_entry->target_address = target_ptr;

                array_entry->decoded_address = decoded_ptr;

                array_entry->correlation_id = diag->correlation_id;

                array_entry->flags |= 0x6B0;

               

                uint32_t read_size = diag->param1;  // Number of bytes to read

                if (read_size > 0)

                {

                    // CRITICAL: Read memory from decoded address

                    uint8_t target_buffer[1024];

                    memset(target_buffer, 0, sizeof(target_buffer));

                    memcpy(target_buffer, decoded_ptr, read_size);

                   

                    CopyBufferToResponse(array_entry, target_buffer, read_size);

                   

                    // Also copy the pointer itself

                    uint8_t ptr_buffer[1024];

                    memset(ptr_buffer, 0, sizeof(ptr_buffer));

                    memcpy(ptr_buffer, decoded_ptr, read_size);

                   

                    CopyBufferToResponse(&array_entry->alt_data, ptr_buffer, read_size);

                }

            }

        }

    }
Тип 29: Проверка функции модуля
Проверяет наличие функции и читает её код.
C++:
Expand Collapse Copy
    void ProcessModuleFunctionCheck(

        CUserMessage_Diagnostic_Response* response,

        CUserMessageRequestDiagnostic_Diagnostic* diag)

    {

        // Get strings from protobuf

        ProtobufString* module_name_str = GetProtobufString(&diag->string1);

        ProtobufString* func_name_str = GetProtobufString(&diag->string2);

       

        const char* module_name = GetStringData(module_name_str);

        const char* func_name = GetStringData(func_name_str);

       

        CVDiagnostic* result = AllocateDiagnosticField(&response);

       

        result->type = 15;

        result->flags |= 0x200;

        result->correlation_id = diag->correlation_id;

        result->flags |= 0x4000;

       

        // Load the module

        HMODULE module = GetModuleHandleA(module_name);

        result->module_handle = (uint64_t)module;

        result->flags |= 0x82;

       

        // Copy module and function names to the response

        CopyStringToResponse(&result->module_name, module_name);

        CopyStringToResponse(&result->function_name, func_name);

       

        if (module)

        {

            // Get the address of the function

            void* func_addr = GetProcAddress(module, func_name);

            result->function_address = (uint64_t)func_addr;

            result->flags |= 0x10;

           

            uint64_t read_size = diag->size;  // Number of bytes to read from function

           

            if (read_size > 0 && func_addr)

            {

                // CRITICAL: Read function code

                result->code_size = read_size;

                result->flags |= 0x101;

               

                CopyBufferToResponse(result, func_addr, read_size);

            }

        }

       

        // Record GetLastError for diagnostics

        DWORD last_error = GetLastError();

        result->last_error = last_error;

        result->flags |= 0x40;

    }
Тип 30: Установка указателей отслеживания исключений
Код:
Expand Collapse Copy
qword_181CFB718 = diag->param1;

qword_181CFB720 = diag->param2;

ProcessDiagnosticResponse

Добавляет данные из Plat_GetDebugMonitor():
- Тип 2: Аппаратные точки останова (Dr0-Dr3)
- Тип 3: Программные точки останова (INT3/0xCC)
- Тип 13: Регионы памяти

C++:
Expand Collapse Copy
    void __fastcall ProcessDiagnosticResponse(CUserMessage_Diagnostic_Response* response)

    {

        CThreadMutex::Lock(&g_DiagnosticMutex);

       

        DebugMonitor* monitor = Plat_GetDebugMonitor();

       

        // Type 2: Hardware breakpoint data

        int hw_bp_count = monitor->hardware_bp_count;

       

        for (int i = 0; i < hw_bp_count; i++)

        {

            HardwareBreakpoint* bp = &monitor->hardware_breakpoints[i];

           

            // Skip disabled breakpoints

            if (bp->enabled < 0)

                continue;

               

            CVDiagnostic* entry = AllocateDiagnosticField(&response);

           

            entry->type = 2;

            entry->bp_index = bp->index;           // Dr0/Dr1/Dr2/Dr3

            entry->bp_type = bp->type;             // Read/Write/Execute

            entry->bp_size = bp->size;             // 1/2/4/8 bytes

            entry->bp_address = bp->address;

            entry->bp_value1 = bp->value1;

            entry->bp_value2 = bp->value2;

            entry->flags |= 0x2E0;

        }

       

        // Type 3: Software breakpoint data

        int sw_bp_count = monitor->software_bp_count;

       

        for (int i = 0; i < sw_bp_count; i++)

        {

            SoftwareBreakpoint* bp = &monitor->software_breakpoints[i];

           

            if (bp->address == 0)

                break;

               

            CVDiagnostic* entry = AllocateDiagnosticField(&response);

           

            entry->type = 3;

            entry->bp_type = bp->type;

            entry->bp_original = bp->original_byte;  // Original byte (before 0xCC)

            entry->bp_size = bp->size;

            entry->bp_address = bp->address;

            entry->bp_value1 = bp->value1;

            entry->bp_value2 = bp->value2;

            entry->flags |= 0x2E0;

        }

       

        // Type 13: Memory region data

        int region_count = monitor->memory_region_count;

       

        for (int i = 0; i < region_count; i++)

        {

            MemoryRegion* region = &monitor->memory_regions[i];

           

            if (region->enabled < 0)

                continue;

               

            CVDiagnostic* entry = AllocateDiagnosticField(&response);

           

            entry->type = 13;

            entry->region_address = region->address;

            entry->region_base = region->base;

            entry->region_size = region->size;

            entry->flags |= 0x290;

        }

       

        CThreadMutex::Unlock(&g_DiagnosticMutex);

    }


FinalizeDiagnosticResponse
Открывает указанный поток, приостанавливает его и получает контекст:
- Тип 1: Базовая информация о потоке с Dr0
- Типы 1-3: Dr1-Dr3
- Тип 4: Dr6 (Debug Status Register)
- Тип 5: RSP и RIP
- Тип 16: Данные отслеживания исключений через связанные списки
C++:
Expand Collapse Copy
    void __fastcall FinalizeDiagnosticResponse(

        CUserMessage_Diagnostic_Response* response,

        DWORD thread_id)

    {

        // Open the thread for analysis

        HANDLE thread = OpenThread(THREAD_ALL_ACCESS, FALSE, thread_id);

       

        CONTEXT context;

        memset(&context, 0, sizeof(context));

        context.ContextFlags = CONTEXT_ALL;

       

        // Suspend thread and get context

        bool success = false;

        if (SuspendThread(thread) != -1)

        {

            success = GetThreadContext(thread, &context);

            ResumeThread(thread);

        }

       

        CloseHandle(thread);

       

        // Type 1: Base thread info

        CVDiagnostic* base_entry = AllocateDiagnosticField(&response);

        base_entry->type = 1;

        base_entry->thread_index = 0;

        base_entry->thread_id = thread_id;

        base_entry->flags |= 0x260;

       

        if (success)

        {

            // Dr0: Debug register 0

            base_entry->dr0 = context.Dr0;

            base_entry->flags |= 0x10;

           

            // Dr1-Dr3: Additional debug registers

            AddDrEntry(response, 1, thread_id, context.Dr1);

            AddDrEntry(response, 2, thread_id, context.Dr2);

            AddDrEntry(response, 3, thread_id, context.Dr3);

           

            // Type 4: Dr6 (Debug Status Register)

            CVDiagnostic* dr6_entry = AllocateDiagnosticField(&response);

            dr6_entry->type = 4;

            dr6_entry->thread_index = 4;

            dr6_entry->thread_id = thread_id;

            dr6_entry->dr6 = context.Dr6;

            dr6_entry->flags |= 0x270;

           

            // Type 5: Stack and Instruction Pointers

            // RSP: Stack pointer

            CVDiagnostic* rsp_entry = AllocateDiagnosticField(&response);

            rsp_entry->type = 5;

            rsp_entry->thread_index = 5;

            rsp_entry->thread_id = thread_id;

            rsp_entry->rsp = context.Rsp;

            rsp_entry->flags |= 0x270;

           

            // RIP: Instruction pointer

            CVDiagnostic* rip_entry = AllocateDiagnosticField(&response);

            rip_entry->type = 5;

            rip_entry->thread_index = 6;

            rip_entry->thread_id = thread_id;

            rip_entry->rip = context.Rip;

            rip_entry->flags |= 0x270;

        }

        else

        {

            // Failed to get thread context

            base_entry->error_code = 0;

            base_entry->flags |= 0x140;

        }

       

        // Type 16: Exception tracking data

        CVDiagnostic* tracking_entry = AllocateDiagnosticField(&response);

       

        tracking_entry->type = 16;

        tracking_entry->index = 0;

        tracking_entry->value1 = qword_181CFB710;  // VEH handler pointer

        tracking_entry->decoded_pointer = DecodePointer(NULL);

        tracking_entry->flags |= 0x2010;

       

        // Process qword_181CFB718 (linked list)

        if (qword_181CFB718)

        {

            AddLinkedListData(response, qword_181CFB718, 1);

            AddLinkedListData(response, qword_181CFB718 + 24, 2);

        }

       

        // Process qword_181CFB720

        if (qword_181CFB720)

        {

            PVOID decoded = DecodePointer(NULL);

           

            CVDiagnostic* entry = AllocateDiagnosticField(&response);

           

            entry->type = 16;

            entry->index = 0;

            entry->mode = 3;

            entry->value1 = *qword_181CFB720;

            entry->value2 = qword_181CFB720;

            entry->decoded_pointer = decoded;

            entry->flags |= 0x2490;

        }

       

        // Send exception data

        SendExceptionData(response);

    }
AddLinkedListData — обход связанного списка
C++:
Expand Collapse Copy
    void __fastcall AddLinkedListData(

        CUserMessage_Diagnostic_Response* response,

        void* list_head,

        int list_type)

    {

        if (!list_head)

            return;

       

        ListNode* current = *(ListNode**)(list_head + 8);

        ListNode* head_node = (ListNode*)list_head;

       

        // Check for empty list

        if (current == head_node || current->next == current)

            return;

       

        int count = 0;

       

        // Traverse linked list (max 10 elements)

        while (current && count < 10)

        {

            void* ptr1 = current->data1;

            void* ptr2 = current->data2;

            void* ptr3 = current->data3;

           

            // Decode pointers

            PVOID decoded1 = DecodePointer(ptr1);

            PVOID decoded2 = DecodePointer(ptr2);

            PVOID decoded3 = DecodePointer(NULL);

           

            CVDiagnostic* entry = AllocateDiagnosticField(&response);

           

            entry->type = 16;

            entry->mode = list_type;

            entry->index = count;

            entry->ptr_original1 = ptr1;

            entry->ptr_original2 = ptr2;

            entry->ptr_decoded1 = decoded1;

            entry->ptr_decoded2 = decoded2;

            entry->ptr_decoded3 = decoded3;

            entry->list_address = list_head;

            entry->flags |= 0x3EF1;

           

            // Copy raw node data (48 bytes)

            uint8_t node_data[48];

            memcpy(&node_data[0], current, 16);

            memcpy(&node_data[16], (uint8_t*)current + 16, 16);

            memcpy(&node_data[32], (uint8_t*)current + 32, 8);

            node_data[40] = 0;

           

            CopyBufferToResponse(&entry->raw_data, node_data, 40);

           

            current = current->next;

            count++;

           

            // Check for loop

            if (current == head_node)

                break;

        }

    }
SendExceptionData — отправка истории исключений
Заключительная функция отправляет все сохраненные исключения:
C++:
Expand Collapse Copy
    void __fastcall SendExceptionData(CUserMessage_Diagnostic_Response* response)

    {

        int exception_count = dword_181CFB700;

       

        if (exception_count <= 0)

            return;

       

        // Limit to 64

        if (exception_count > 64)

            exception_count = 64;

       

        for (int i = 0; i < exception_count; i++)

        {

            ExceptionRecord* exc = &g_ExceptionBuffer[i];

           

            CVDiagnostic* entry = AllocateDiagnosticField(&response);

           

            // Type 6: Exception data

            entry->type = 6;

            entry->exc_flags = exc->flags & 0xF;

            entry->exc_address = exc->exception_address;

            entry->exc_code = exc->exception_code;

            entry->exc_access_type = exc->access_type;

            entry->exc_fault_addr = exc->fault_address;

            entry->exc_rsp = exc->rsp;

            entry->exc_dr6 = exc->dr6;

            entry->flags |= 0x2F0;

           

            // Add stack memory dump if available

            if (exc->memory_dump && exc->memory_dump_size > 0)

            {

                CopyBufferToResponse(

                    &entry->memory_dump,

                    exc->memory_dump,

                    exc->memory_dump_size

                );

            }

        }

    }
Отправляет все 64 сохраненных исключения с дампами памяти стека (до 2944 байт каждый).
Векторный обработчик исключений

Система регистрирует векторный обработчик исключений с приоритетом 1:

Код:
Expand Collapse Copy
qword_181CFB710 = AddVectoredExceptionHandler(1, Handler);


DWORD dword_181CFB700;  // Счетчик исключений (макс 64)

DWORD dword_181CFB704;  // Смещение дампа памяти (макс 16KB)


struct ExceptionRecord {

    void* exception_address;

    DWORD exception_code;

    ULONG_PTR access_type;

    ULONG_PTR fault_address;

    void* rsp;

    DWORD64 dr6;

    void* memory_dump;

    DWORD memory_dump_size;

    DWORD flags;

};


ExceptionRecord g_ExceptionBuffer[64];

uint8_t g_MemoryDumpBuffer[0x4000];
Handler — основной обработчик исключений
C++:
Expand Collapse Copy
    LONG __fastcall Handler(EXCEPTION_POINTERS* ExceptionInfo)

    {

        if (!ExceptionInfo)

            return EXCEPTION_CONTINUE_SEARCH;

       

        EXCEPTION_RECORD* exc_record = ExceptionInfo->ExceptionRecord;

        DWORD exc_code = exc_record->ExceptionCode;

       

        // PHASE 1: Only handle two types of exceptions:

        // 1. EXCEPTION_ACCESS_VIOLATION (0xC0000005 = -1073741819)

        // 2. EXCEPTION_SINGLE_STEP (0x80000004 = -2147483644) with hardware breakpoints set

       

        if (exc_code != EXCEPTION_ACCESS_VIOLATION)

        {

            if (exc_code != EXCEPTION_SINGLE_STEP)

                return EXCEPTION_CONTINUE_SEARCH;

               

            // For single-step, check that a hardware breakpoint is set

            if ((ExceptionInfo->ContextRecord->Dr6 & 0xF) == 0)

                return EXCEPTION_CONTINUE_SEARCH;

        }

       

        // Extract context data

        CONTEXT* context = ExceptionInfo->ContextRecord;

        void* rsp = (void*)context->Rsp;

        DWORD64 dr6 = context->Dr6;

       

        // Extract exception information

        ULONG_PTR access_type = exc_record->ExceptionInformation[0];  // 0=read, 1=write, 8=DEP

        ULONG_PTR fault_addr = exc_record->ExceptionInformation[1];   // Address causing AV

       

        int current_count = dword_181CFB700;

       

        // PHASE 2: Check for duplicates

        if (current_count > 0)

        {

            // Check if this exception is already recorded

            ExceptionRecord* buffer = g_ExceptionBuffer;

           

            for (int i = 0; i < current_count; i++)

            {

                ExceptionRecord* entry = &buffer[i];

               

                // Compare all parameters

                if (entry->exception_address == exc_record->ExceptionAddress &&

                    entry->exception_code == exc_code &&

                    entry->access_type == access_type &&

                    entry->fault_address == fault_addr &&

                    entry->rsp == rsp &&

                    entry->dr6 == dr6)

                {

                    // Duplicate found – skip recording

                    goto SET_TRAP_FLAG;

                }

            }

        }

       

        // PHASE 3: Record new exception

        if (current_count < 64)  // Max 64 records

        {

            int index = current_count;

            uint64_t offset = index * 64;  // Each record is 64 bytes

           

            ExceptionRecord* new_entry = (ExceptionRecord*)((uint8_t*)g_ExceptionBuffer + offset);

           

            // Record main data

            new_entry->exception_address = exc_record->ExceptionAddress;

            new_entry->exception_code = exc_code;

            new_entry->access_type = access_type;

            new_entry->fault_address = fault_addr;

            new_entry->rsp = rsp;

            new_entry->dr6 = dr6;

           

            // PHASE 4: Stack memory dump

            DWORD memory_offset = dword_181CFB704;

           

            if (memory_offset < 0x4000)  // Max 16KB for dumps

            {

                void* dump_ptr = &g_MemoryDumpBuffer[memory_offset];

               

                // Calculate dump size

                // Align RSP down to page boundary

                uintptr_t rsp_aligned = (uintptr_t)rsp & ~0xFFF;

                int available = 4096 - ((uintptr_t)rsp & 0xFFF);

               

                // Limit to maximum 2944 bytes

                if (available > 2944)

                    available = 2944;

               

                // Align to 8 bytes

                int dump_size = available & ~7;

               

                // CRITICAL: Copy stack memory

                memcpy(dump_ptr, rsp, dump_size);

               

                // Record pointer and size

                new_entry->memory_dump = dump_ptr;

                new_entry->memory_dump_size = dump_size;

               

                // Update offset atomically

                _InterlockedAdd(&dword_181CFB704, 0x1000);

            }

           

            // Increment exception counter atomically

            _InterlockedExchange(&dword_181CFB700, current_count + 1);

        }

       

        // PHASE 5: Auto-fix for Access Violation

    SET_TRAP_FLAG:

       

        if (exc_code == EXCEPTION_ACCESS_VIOLATION)

        {

            // Check if fault address is inside client.dll image

            if (fault_addr >= qword_180001000)  // Start of sections

            {

                // Check PE header

                if (*(WORD*)0x180000000 == IMAGE_DOS_SIGNATURE)  // "MZ"

                {

                    DWORD pe_offset = *(DWORD*)0x18000003C;

                    uintptr_t image_end = 0x180000000 +

                        *(DWORD*)(0x180000000 + pe_offset + 0x1C);  // SizeOfImage

                   

                    if (fault_addr <= image_end)

                    {

                        // IMPORTANT: Restore memory protection

                        // Protection against game self-patches

                        Plat_VirtualProtect(

                            (void*)fault_addr,

                            4096,

                            PAGE_EXECUTE_READ  // 5

                        );

                    }

                }

            }

        }

       

        // PHASE 6: Set Trap Flag for single-step after exception

        context->EFlags |= 0x10000;  // TF bit

       

        return EXCEPTION_CONTINUE_EXECUTION;  // -1

    }

Фазы обработчика:
Фаза 1: Фильтрация
- EXCEPTION_ACCESS_VIOLATION (0xC0000005)
- EXCEPTION_SINGLE_STEP (0x80000004) только если (Dr6 & 0xF) != 0

Фаза 2: Дедупликация
Проверяет все 6 параметров перед записью.

Фаза 3: Запись
Сохраняет до 64 исключений в g_ExceptionBuffer.

Фаза 4: Дамп стека
- Выравнивает RSP вниз до границы страницы
- Вычисляет доступный размер: 4096 - (RSP & 0xFFF)
- Ограничивает до 2944 байт
- Копирует в g_MemoryDumpBuffer

Фаза 5: Авто-исправление
Для EXCEPTION_ACCESS_VIOLATION проверяет, находится ли fault_address между qword_180001000 (начало .text) и концом образа. Если да — восстанавливает PAGE_EXECUTE_READ через Plat_VirtualProtect(). Это означает, что игра патчит сама себя и использует AV как механизм активации!

Фаза 6: Trap Flag
Устанавливает context->EFlags |= 0x10000 для single-step после исключения.
Мониторинг Counter-Strafe

Valve отслеживает технику контр-стрейфа для обнаружения макросов и автоматизации. Эта телеметрия отправляется через сообщение 385.
C++:
Expand Collapse Copy
    void __fastcall SendCounterStrafeData(

        CounterStrafeState* state,      // +0xC20 from CCSGOInput

        int event_type,                 // 0 = key events

        InputValue* input_value,        // Input event structure

        __int64 frame_data)             // Current frame data

    {

        // Ignore all events except key press/release

        if (event_type != 0)

            return;

       

        if (!input_value)

            return;

       

        // Ignore if input is not digital (button)

        if (input_value->type != 0)  // +0x08 type (0 = digital)

            return;

       

        int direction_change = 0;

       

        if (input_value == &g_InputValue_MoveLeft)  // "left"

        {

            bool pressed = input_value->current_state;  // +0x2A

           

            // Check for state change

            if (pressed != state->left_pressed)

            {

                // 2 = left press, 1 = left release

                direction_change = 2 - (pressed ? 0 : 1);

            }

           

            state->left_pressed = pressed;

        }

        else if (input_value == &g_InputValue_MoveRight)  // "right"

        {

            bool pressed = input_value->current_state;  // +0x2A

           

            if (pressed != state->right_pressed)

            {

                // 4 = right press, 3 = right release

                direction_change = 4 - (pressed ? 0 : 1);

            }

           

            state->right_pressed = pressed;

        }

        else

        {

            return;  // Not an input we care about

        }

       

        // No change in direction

        if (!direction_change)

            return;

       

        // Skip if other checks are enabled

        if (g_CheckEnabled1 || g_CheckEnabled2)

            return;

       

        // Map direction_change to action

        int action;

        switch (direction_change)

        {

            case 1:  action = 4; break;  // left release -> action 4

            case 2:  action = 3; break;  // left press   -> action 3

            case 3:  action = 2; break;  // right release -> action 2

            case 4:  action = 1; break;  // right press  -> action 1

            default: return;

        }

       

        uint64_t current_tick = *(frame_data + 0x660);  // Current game tick

       

        // Check for counter-strafe

        if (state->last_action == action)

        {

            uint64_t last_tick = state->last_tick;

           

            if (last_tick && current_tick)

            {

                int tick_delta = (int)(current_tick - last_tick);

               

                // Overflow check

                if (tick_delta <= 0x7FFFFFFF)

                {

                    // CRITICAL: Invert delta for left press/release

                    if (((direction_change - 1) & 0xFFFFFFFD) == 0)

                    {

                        tick_delta = ~tick_delta;  // Bitwise inversion

                    }

                   

                    // Count pressed keys

                    int keys_pressed = 0;

                   

                    if (InputSystemVersion001)

                    {

                        // Check all 317 possible keys

                        for (int key_code = 0; key_code <= 316; key_code++)

                        {

                            if (InputSystemVersion001->IsKeyDown(key_code))

                                keys_pressed++;

                        }

                    }

                   

                    // Send data to server

                    CSVCMsg_UserMessage user_msg;

                    InitializeSVCMessage(&user_msg);

                   

                    CCSUsrMsg_CounterStrafe counter_strafe_msg;

                    CCSUsrMsg_CounterStrafe::Constructor(&counter_strafe_msg);

                   

                    counter_strafe_msg.flags |= 0x03;

                    counter_strafe_msg.tick_delta = tick_delta;

                    counter_strafe_msg.keys_pressed = keys_pressed;

                   

                    if (NetworkClientService_001->IsConnected())

                    {

                        if (CSVCMsg_UserMessage_Setup(

                                &user_msg,

                                385,  // Message ID

                                &counter_strafe_msg))

                        {

                            NetworkClientService_001->SendNetMessage(

                                0,

                                &user_msg,

                                true  // reliable

                            );

                        }

                    }

                   

                    // Cleanup

                    CCSUsrMsg_CounterStrafe::Destructor(&counter_strafe_msg);

                    CleanupSVCMessage(&user_msg);

                }

            }

        }

       

        // Save state for next event

        state->last_tick = current_tick;

        state->last_action = direction_change;

    }
Интеграция с CCSGOInput
Эта функция вызывается через vtable CCSGOInput:

Система отслеживает состояния клавиш «влево» и «вправо»:
C++:
Expand Collapse Copy
    // vtable offset +0x878 (index 29)

    void __fastcall CCSGOInput::OnInputEvent(

        CCSGOInput* this,

        int event_type,

        InputValue* input_value,

        __int64 frame_data)

    {

        // this + 0xC20 points to CounterStrafeState

        SendCounterStrafeData(this + 0xC20, event_type, input_value, frame_data);

    }
Структуры данных:
C++:
Expand Collapse Copy
    struct CounterStrafeState

    {

        int last_action;           // +0x00 - Last action (1-4)

        uint64_t last_tick;        // +0x08 - Tick of the last action

        bool left_pressed;         // +0x10 - State of the left key (A)

        bool right_pressed;        // +0x11 - State of the right key (D)

    };

   

    struct CCSUsrMsg_CounterStrafe

    {

        void* vtable;              // +0x00

        uint64_t allocator;        // +0x08

        uint32_t flags;            // +0x10

        int tick_delta;            // +0x14 - Difference in ticks

        int keys_pressed;          // +0x18 - Number of keys pressed

    };

   

    struct InputValue

    {

        const char* name;          // +0x00 - "left" or "right"

        int type;                  // +0x08 - 0 = digital, 1 = analog

        int flags;                 // +0x0C

        // ... (many other fields)

        bool current_state;        // +0x2A (42) - Current button state

    };
Система отслеживает состояния клавиш "влево" (A) и "вправо" (D):
Код:
Expand Collapse Copy
Отображение изменения направления → Действие:
1: отпускание влево  → действие 4
2: нажатие влево     → действие 3
3: отпускание вправо → действие 2
4: нажатие вправо    → действие 1

Когда одно и то же действие происходит дважды подряд:
1. Вычисляет tick_delta = current_tick - last_tick
2. Применяет побитовую инверсию для нажатия/отпускания влево
3. Подсчитывает все нажатые клавиши (сканирует все 317 кодов клавиш)
4. Отправляет на сервер: tick_delta + keys_pressed

Возможности обнаружения:
Статистический анализ:
Сервер собирает со временем:
- Средний tick_delta и стандартное отклонение
- Частоту идеальных контр-страфов (delta=1)
- Корреляцию с точностью

Что отслеживается
1. Инжектированные DLL - CModuleListSnapshot перечисляет все модули
2. PE timestamps - проверяет подлинность системных библиотек
3. Подозрительные потоки - MonitorThreadContext обнаруживает необычные атрибуты потоков
4. Произвольное чтение памяти - Тип 28 позволяет серверу читать любой адрес
5. Проверка функций - Тип 29 читает код функций в модулях
6. Аппаратные точки останова - Dr0-Dr6 через контекст потока
7. Программные точки останова - через Plat_GetDebugMonitor()
8. История исключений - все AV и single-step исключения с дампами стека
9. Обнаружение отладчика - IsDebuggerPresent()
10. Целостность ConVar - CRC32 хэши всех консольных переменных
11. VMT указатели - все виртуальные таблицы интерфейсов и сущностей
12. Хэши модулей - CRC32 и SHA1 всех загруженных DLL
13. Паттерны контр-страфа - анализ времени

Примечание: Игра также собирает данные о PlayerDecalDigitalSignature ("spray tint id") и некоторые данные из FX_FireBullets. Но я не считаю это достаточно важным для описания в этой статье.

Берегите себя и счастливых праздников! :)
 
Последнее редактирование:
Супер. Спасибо! Сейчас как никогда актуально когда вак модули выключены и нужно обходить онли клиент<->сервер сайд чеки!
 
Назад
Сверху Снизу