[c++] SEH + dbghelp vs Твой говно чит

Я лучше тебя
Участник
Статус
Оффлайн
Регистрация
31 Июл 2017
Сообщения
383
Реакции[?]
448
Поинты[?]
1K
Салями народ.
Давно не постился на югейме, как настоящий православный грешник.
Сегодня мы посмотрим, что может обычный, самый тупой SEH обработчик в связке с возможностями библиотеки dbghelp против твоего говно чита.
Общая схема на которой мы рассмотрим, как недальновидность кодера чита, может дать разработчику игры нехуевую телегу данных о своем чите.

  1. Есть игра.
  2. Есть твой говно чит.
  3. В игре есть функция.
  4. У функции в игре свой SEH обработчик ошибок.
  5. Твой говно чит хукает эту функцию в игре для своей обработки и извлечения данных для своих анальных моневров.
  6. Игру обновляют / ты лажаешь в коде. (не важно что именно)
  7. В твоем коде, который оборачивается в хуке вокруг кода игры возникает ошибка. (неверный оффсет, или еще какая хуйня)
  8. Обработчик ошибок функции которую ты хукаешь отлавливает эту ошибку.
  9. Начинается веселье :)
В чем тут хиросима и где ногавсраке?
Начинается все с незамысловатого, обычная ошибка в коде, только вот по традиции всех паблик сорсов, у нехилого большинства читов нету собственных обработчиков для ошибок в функциях, соответственно, ошибка в твоем коде,обработается обработчиком ошибок функции которую ты хукаешь.
То есть - если на пример после обновления игры, в твоем коде какая то ошибка вылетела и котото это заинжектил в игру - игра может это отловить :)
Далее следует незамысловатый код:
Код:
#define SEH seh_filter(GetExceptionInformation())

//некий игровой класс
class SomeClassInGame
{
public:
    //некая игровая функция которую ты нежно хукаешь, думая что никто ж не узнает
    int funcForHook( int al, int lpParam)
    {
        int hReturn = 0;
        //обработка, код который ты заменяешь на свой. (чисто для примера)
        __try
        {
            if (al > lpParam)
            {
                hReturn = al + lpParam;
            }
            else
            {
                hReturn = al * lpParam;
            }
            //условно ошибка которая возникает в твоем коде в случае резкого обновления игры или твоей ошибки
            throw new exception;
        }
        __except (SEH)
        {
            //to do error
        }
        return hReturn;
    }
};
А теперь сам SEH обработчик:
Код:
int seh_filter(_EXCEPTION_POINTERS* ex)
{
    //если флаг контекста потока в котором произошла ошибочка ДЕБУГ РЕГИСТЕР
    if (ex->ContextRecord->ContextFlags == CONTEXT_DEBUG_REGISTERS)
    {
        //ты дебагер тебе бан, лох.
        return 0;
    }
    //для получения модуля в котором произошла ошибка
    HMODULE hModule = NULL;
    size_t moduleSize = 0;
    //берем адресс ошибки в коде, и получаем базовый адресс модуля который выделил
    //память под код в котором возникла ошибка
    void* lpAddress = (void*)ex->ExceptionRecord->ExceptionAddress;
    MEMORY_BASIC_INFORMATION mbi = { 0 };
    //Получаем инфо о странице памяти по которой ошибка.
    if (VirtualQuery(lpAddress, &mbi, sizeof(mbi)))
    {
        hModule = (HMODULE)mbi.AllocationBase;
        //делаем магическое дрючево))
        //если по адресу в памяти возникла ошибка, значит это КОД. по тому что не может быть ошибки по адресу в котором ничего нет
        //за вычетом ошибки доступа, когда программа пытается получить доступ к участку памяти который уже освобожден.

        //проверяем пе хидеры
        PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)hModule;
        if (dosHeader->e_magic == IMAGE_DOS_SIGNATURE)
        {
            //ЭТО ВИДИМЫЙ МОДУЛЬ БЕЗ ЗАТЕРТЫХ ЗАГОЛОВКОВ
            PIMAGE_NT_HEADERS ntHeader = (PIMAGE_NT_HEADERS)((DWORD)(dosHeader)+(dosHeader->e_lfanew));
            if (ntHeader)
            {
                //тапаем размер твоей длл чита.
                moduleSize = ntHeader->OptionalHeader.SizeOfImage;
            }
        }
        else
        {
            //ЭТО СКРЫТЫЙ КОД, ЗАИНЖЕКЧЕННЫЙ МАППИНГОМ, ЛИБО ШЕЛЛКОД.
            //ЛИБО ДЛЛ С ЗАТЕРТЫМИ ЗАГОЛОВКАМИ, ЧТО ТАК ЧТО СЯК ОДИН ХУЙ ЭТО ЧИТ.
            //ТЕБЕ БАН
        }
    }
    //Берем контекст потока для трасировки фрейма потока.
    PCONTEXT ctx = ex->ContextRecord;

    BOOL    result;
    HANDLE  process = GetCurrentProcess();
    HANDLE  thread = GetCurrentThread();
   
    STACKFRAME        stack;
    DWORD64             displacement = 0;
    DWORD disp;
    char buffer[sizeof(SYMBOL_INFO) + 2000 * sizeof(TCHAR)];
    char name[MaxNameLen];
    char module[MaxNameLen];
    PSYMBOL_INFO pSymbol = (PSYMBOL_INFO)buffer;
    memset(&stack, 0, sizeof(STACKFRAME));

#if !defined(_M_AMD64)
    stack.AddrPC.Offset = (*ctx).Eip;
    stack.AddrPC.Mode = AddrModeFlat;
    stack.AddrStack.Offset = (*ctx).Esp;
    stack.AddrStack.Mode = AddrModeFlat;
    stack.AddrFrame.Offset = (*ctx).Ebp;
    stack.AddrFrame.Mode = AddrModeFlat;
#endif
    //извлекаем
    SymInitialize(process, NULL, TRUE);
    for (ULONG frame = 0; ; frame++)
    {
#if defined(_M_AMD64)
        result = StackWalk64(IMAGE_FILE_MACHINE_AMD64, process, thread, &stack, ctx, NULL, SymFunctionTableAccess64, SymGetModuleBase64, NULL);
#else
        result = StackWalk(IMAGE_FILE_MACHINE_I386, process, thread, &stack, ctx, NULL, SymFunctionTableAccess, SymGetModuleBase, NULL);
#endif
        if (!result)
            break;

        pSymbol->SizeOfStruct = sizeof(SYMBOL_INFO);
        pSymbol->MaxNameLen = 2000;
        //фишка dbghelp в том что можно попытатся прочитать
        //имя функции в которой возникла ошибка :)))
        BOOL SymStatus = SymFromAddr(process, (ULONG)stack.AddrPC.Offset, &displacement, pSymbol);
        //Если модуль есть :)
        if (hModule != NULL)
        {
            //берем его имя :)
            K32GetModuleBaseNameA(GetCurrentProcess(), hModule, module, MaxNameLen);
           
            if (SymStatus)
            {
                //если имя функции есть, мы его выведем
                DWORD offset = (DWORD)pSymbol->Address;
                printf("%s -> %s, %s!0x%0X.\n", module, pSymbol->Name, offset);
            }
            else
            {
                //если имя функции нема значит скорей всего твой ссаный чит под пакером, по тому что нечего больше бля там такого быть не может.
            }
           
            //теперь твой бан, если размер длл определен вместе с базовым адрессом, мы можем сдампить длл
            //даже заинжекченую маппингом прямо из памяти.

            if (hModule && moduleSize)
            {
                BYTE* const buffer = new BYTE[moduleSize];
                DWORD dwModuleBase = (DWORD)hModule;

                if (!ReadProcessMemory(process, (void*)dwModuleBase, buffer, 0x400, NULL))
                {
                    std::cout << "Error: " << GetLastError() << std::endl;
                    std::cout << "Cannot read module base" << std::endl;
                }
                const IMAGE_NT_HEADERS32* pNTHeader = (IMAGE_NT_HEADERS32*)(buffer + ((IMAGE_DOS_HEADER*)buffer)->e_lfanew);
                const IMAGE_OPTIONAL_HEADER32* pOptionalHeader = (IMAGE_OPTIONAL_HEADER32*)&pNTHeader->OptionalHeader;

                unsigned int i = 0;
                IMAGE_SECTION_HEADER* pSecHeader;
                unsigned int bufPtr = pOptionalHeader->SizeOfHeaders;

                for (pSecHeader = IMAGE_FIRST_SECTION(pNTHeader); i < pNTHeader->FileHeader.NumberOfSections; ++i, ++pSecHeader)
                {
                    pSecHeader->Misc.VirtualSize = pSecHeader->SizeOfRawData;

                    if (!memcpy(buffer + bufPtr, pSecHeader, sizeof(IMAGE_SECTION_HEADER)))
                    {
                        std::cout << "Error: " << GetLastError() << std::endl;
                        std::cout << "Cannot copy memory" << std::endl;
                    }
                    bufPtr += sizeof(IMAGE_SECTION_HEADER);

                    if (!ReadProcessMemory(process, (void*)(pOptionalHeader->ImageBase + pSecHeader->VirtualAddress), buffer + pSecHeader->PointerToRawData, pSecHeader->SizeOfRawData, NULL))
                    {
                        std::cout << "Error: " << GetLastError() << std::endl;
                        std::cout << "Cannot read headers" << std::endl;
                    }
                }


                //дампим тупо в файл чтоб даже в иде открыть можно было
                std::ofstream file;
                file.open("C:/dump.dll", std::ios_base::binary);
                file.write((const char*)buffer, moduleSize);
                file.close();
            }
        }
        else
        {
            //если модуля нэма. ну штош, он наверно скрыт и тебе уже бан.
            //потому что ошибка из скрытого модуля явно не к добру, не думаешь?
        }
    }
    SymCleanup(process);

    return EXCEPTION_EXECUTE_HANDLER;
}
Зависимости:
Код:
#include <windows.h>
#include <iostream>

#include "dbghelp.h"
#pragma comment(lib,"Dbghelp.lib")
#include <psapi.h>
#pragma comment(lib,"psapi")

А теперь, вот тебе список фактов:

  1. Когда возникнет ошибка обработчик запишет весь стек действующего потока, в котором эта ошибка возникла​
  2. обработчик попытается записать линию и нити ошибки чтобы сделать снимок и получить: имя файла в котором произошла ошибка, номер строчки кода в котором произошла ошибка, имя функции в которой произошла ошибка, пример: ошибка: { КОД ОШИБКИ } { main.cpp+0x45 АДРЕСС ОШИБКИ } { НОМЕР СТРОЧКИ КОДА }​
  3. если имя файла и номер строки кода не получаются - значит длл не подключена к твоему обработчику кода и она внешняя - считай ЧУЖАЯ.​
  4. если не получается имя функции в которой произошла ошибка длл под пакером.​
  5. если взять адресс ошибки и получить AllocationBase по этому адрессу и он не будет являтся BaseAddress ни одного видимого модуля в публично доступном списке модулей (Ldr) - модуль скрыт.​
  6. если по адресу AllocationBase полученного выше нет ПЕ заголовков - они затерты, либо это шеллкод, либо чит. что в обоих случаях палево :)​
Это просто обычный обработчик ошибок, а уже похоже на античит.
Представьте какой вам пиздец, если юзать подобные штуки в полную силу.


И не надо мне телегу загонять типо я ПЕ хидеры потру и все меня не видно, очень даже видно в памяти. Даже драйверный инжектор палится таким тупым методом. Действительно, без хидеров гораздо сложнее сдампить, токо вот никому кроме чита хидеры затирать и не надо, это гарантия того что тебя можно банить, нету хидеров = чит, для игры так точно. А если хидеры на месте и длл скрыта? Если ее в Ldr списке нету, а в памяти есть? тоже палево.

Выводы о том как с этим жить сделаете для себя сами.

Дополнительные материалы для самообразования по теме:

Пожалуйста, авторизуйтесь для просмотра ссылки.

Пожалуйста, авторизуйтесь для просмотра ссылки.

Пожалуйста, авторизуйтесь для просмотра ссылки.

Пожалуйста, авторизуйтесь для просмотра ссылки.

 
Начинающий
Статус
Оффлайн
Регистрация
30 Дек 2018
Сообщения
61
Реакции[?]
25
Поинты[?]
0
Откуда символы возьмутся то? Разве что ты сам .pdb античиту подаришь. Кто вообще статик SEH использует, когда есть VEH? Кто хедеры и прочие CRT стабы в памяти оставляет? С таким же успехом античиту выгодней в тупую засканить память на их наличие, а не засирать себе хэндлер стектрейсом, ибо сильно бьет по производительности. Все хорошо в теории, но на практике чтобы работало, нужно много чего допиливать. Да и вообще такой себе вектор детекта.
 
midnight.im
Администратор
Статус
Оффлайн
Регистрация
1 Июл 2015
Сообщения
1,649
Реакции[?]
2,171
Поинты[?]
161K
На словах ты лев толстой, а на деле ...
Берётся ебаная длл подписанная с протектом, она инжектится, и переписывается содержимое секции на твой код. После чего если триггерится это говно - чекается твоя проверка, видим легитимный модуль с подписью, скипаем.
 
Я лучше тебя
Участник
Статус
Оффлайн
Регистрация
31 Июл 2017
Сообщения
383
Реакции[?]
448
Поинты[?]
1K
Откуда символы возьмутся то? Разве что ты сам .pdb античиту подаришь.
если символов нету - сборка левая. простой пример - Point Blank, там BugTrap как то выкупает символы со всех дллок игры при ошибках, хотя никакими .pdb не пахнет.
Кто вообще статик SEH использует, когда есть VEH?
ты не поверишь но поверх VEH можно тупо свой хандлер приебенить, а на статик SEH удачи.
Кто хедеры и прочие CRT стабы в памяти оставляет?
возьми дллки читов из паблика, увидишь
С таким же успехом античиту выгодней в тупую засканить память на их наличие, а не засирать себе хэндлер стектрейсом, ибо сильно бьет по производительности. Все хорошо в теории, но на практике чтобы работало, нужно много чего допиливать. Да и вообще такой себе вектор детекта.
сканить 4 гб вместо того чтобы подождать, когда из твоего кода дропнется ошибка? и что тут может бить по производительности, если каждая игра в мире которая умнее симулятора камня при краше программы делает похожий снимок...Речь об ошибках которые не отловит твой код в обычных условиях, но отловит сама игра для рапорта о краше/ошибке/утечке памяти/etc.
На словах ты лев толстой, а на деле ...
Берётся ебаная длл подписанная с протектом, она инжектится, и переписывается содержимое секции на твой код. После чего если триггерится это говно - чекается твоя проверка, видим легитимный модуль с подписью, скипаем.
найди мне подписанную длл с протектом в кс. ах да, спойлер, такой там нету. все подписанные и не одной с протектом. Прокси хорошая мысль, только реализация проигрышная в половине случаев. Попробуй подменить тот же HID.dll или d3dx9_38/d3dx9_43.dll.....

P/S - даже так, при ошибке, объекты указывающие на память твоей длл останутся в регистрах. Cloak dll давно научились палить
 
Последнее редактирование:
Начинающий
Статус
Оффлайн
Регистрация
30 Дек 2018
Сообщения
61
Реакции[?]
25
Поинты[?]
0
Анализ стека действительно работает и многие античиты пользуются этим, но есть гораздо более надежные векторы применения данной фичи. Проблема в том, что атакующий я, это дает гарантию того, что мой обработчик будет вызван первым практически в 100% случаев, следовательно могу зафейкать всю проверку.

iDReeM написал(а):
сканить 4 гб вместо того чтобы подождать, когда из твоего кода дропнется ошибка?
Это все равно придется делать и маркировать регионы памяти. Иначе как ты будешь уверен в том, что эта память выделена не самой игрой/античитом/дискордом/антивирусом/etc ? Одно дело теория или даже концепт на коленке, а другое дело прод. И у тебя нет права на ошибку, банить честных юзеров не очень хорошо.

А про то, что у многих палево в крэшдампах ты прав, но я не думаю, что их кто-то анализирует именно с целью выявления читерства.
 
midnight.im
Администратор
Статус
Оффлайн
Регистрация
1 Июл 2015
Сообщения
1,649
Реакции[?]
2,171
Поинты[?]
161K
найди мне подписанную длл с протектом в кс. ах да, спойлер, такой там нету. все подписанные и не одной с протектом. Прокси хорошая мысль, только реализация проигрышная в половине случаев. Попробуй подменить тот же HID.dll или d3dx9_38/d3dx9_43.dll.....
Action screen recorder
 
Я лучше тебя
Участник
Статус
Оффлайн
Регистрация
31 Июл 2017
Сообщения
383
Реакции[?]
448
Поинты[?]
1K
Анализ стека действительно работает и многие античиты пользуются этим, но есть гораздо более надежные векторы применения данной фичи. Проблема в том, что атакующий я, это дает гарантию того, что мой обработчик будет вызван первым практически в 100% случаев, следовательно могу зафейкать всю проверку.
я не говорю о том что атакующий ты не управишься с обработкой ексепшена под своим кодом, я говорю что почти овер дохуя читов на этом может отловится хоть прямо сейчас, через семпл я указываю на одну из глобальных уязвимостей читов, которой подвержены многие сегодняшние и предшествующие сборки :)
Это все равно придется делать и маркировать регионы памяти. Иначе как ты будешь уверен в том, что эта память выделена не самой игрой/античитом/дискордом/антивирусом/etc ?
Для этого есть VirtualQuery и прочие функции для получения инфы о памяти, как раз они могут показать какой именно модуль выделил эту память, так как AllocationBase в структуре MBI (мемери босик инфраматион) как раз указывает на базовый адресс модуля который выделил страницу памяти под свои нужды :) а если этого AllocationBase - нету в списке Ldr в качестве BaseAddress некого легитимно загруженного модуля или если этот модуль неизвестный - это уже аномалия пригодная для передачи багрепорта под юриздикцию античита и как раз этот же критерий закономерно показывает, кто там в памяти копается, будь то дискорд, игра, античит, etc.
Одно дело теория или даже концепт на коленке, а другое дело прод. И у тебя нет права на ошибку, банить честных юзеров не очень хорошо.
Речи не идет о моментальной карме при аномалии, это сборка данных для аналитики, считай, 1+ случай = случайность, 50+ = закономерность, от 200 до 1000+ четкая схема взаимодействия требующая детального осмотра специалиста для заключения на тему банабельный случай или нет.
А про то, что у многих палево в крэшдампах ты прав, но я не думаю, что их кто-то анализирует именно с целью выявления читерства.
со вторым ты ошибаешься, если данные за пределом целевой сборки (игры), на пример - левое приложение догружающее свой компонент в сборку игры (Bandicam тот же) и аномалия при этом системная, их всегда принято разбирать.
опять же, внешняя программа :)
 
midnight.im
Администратор
Статус
Оффлайн
Регистрация
1 Июл 2015
Сообщения
1,649
Реакции[?]
2,171
Поинты[?]
161K
Начинающий
Статус
Оффлайн
Регистрация
30 Дек 2018
Сообщения
61
Реакции[?]
25
Поинты[?]
0
Для этого есть VirtualQuery
Это понятно. Проблема в том, что нужно выявить вредоносную активность и связать ее с конкретным регионом памяти, что проблематично. Просто банить за исполняемый регион памяти никто не будет, даже если он не принадлежит какому либо модулю. Нужны более веские доказательства. Большинство решений просто сканируют подозрительный регион на наличие сигнатур, дампят и все. А по-хорошему нужно устанавливать наблюдение за регионом и искать связи. Понятное дело, что это должна быть полная автоматика и общие случаи. Никто не будет писать отдельный модуль под детект твоего софта, если ты решишь все заропить или что-нибудь хитровыебанное придумаешь.
 
Я лучше тебя
Участник
Статус
Оффлайн
Регистрация
31 Июл 2017
Сообщения
383
Реакции[?]
448
Поинты[?]
1K
Это понятно. Проблема в том, что нужно выявить вредоносную активность и связать ее с конкретным регионом памяти, что проблематично. Просто банить за исполняемый регион памяти никто не будет, даже если он не принадлежит какому либо модулю. Нужны более веские доказательства. Большинство решений просто сканируют подозрительный регион на наличие сигнатур, дампят и все. А по-хорошему нужно устанавливать наблюдение за регионом и искать связи. Понятное дело, что это должна быть полная автоматика и общие случаи. Никто не будет писать отдельный модуль под детект твоего софта, если ты решишь все заропить или что-нибудь хитровыебанное придумаешь.
что за ахинею ты несешь? причем тут банить за регион? я показал как сдампить твою сраную длл целиком если из нее возникла левандоская ошибка, а за длл без ПЕ заголовков, априори можно банить по железу, считай нет заголовов = вирус или чит в 100% случев абсолютно всегда неопровержимое и окончательное доково вредноносного По. А по дампу отдельных накопившися случаев "легитимных модулей с багами" уже решай, пропускать ли ту или инную тулзу или нет.
подписанная. попытайся доебаться до легитимной тулы, забань тех кто с этой тулой играл)
зачем доебыватся и банить?
можно просто сделать свое ПО несовместимым с конфликтными "легитимными тулзами* вроде твоей, как делает тот же EAC со всеми программами которые ему не нравятся, вроде драйверов PCHunter, cheat Engine. тоесть тупо не запускать свою программу с программой которая тебе чет не нравится. и все.
 
Начинающий
Статус
Оффлайн
Регистрация
30 Дек 2018
Сообщения
61
Реакции[?]
25
Поинты[?]
0
У меня антивирус RE регион памяти выделяет в каждом процессе. Почему меня не банят?
 
Я лучше тебя
Участник
Статус
Оффлайн
Регистрация
31 Июл 2017
Сообщения
383
Реакции[?]
448
Поинты[?]
1K
У меня антивирус RE регион памяти выделяет в каждом процессе. Почему меня не банят?
запусти какую нибудь игру с фростом или фейситом, или чем нибудь еще серьезным - получишь бан.
 
Сверху Снизу