Войти или зарегистрироваться
  • Вы находитесь в сообществе Rubukkit. Мы - администраторы серверов Minecraft, разрабатываем собственные плагины и переводим на различные языки плагины наших коллег из других стран.
  • Данный раздел создан исключительно для релизов! Вопросы по лаунчеру или обвязке задавайте ТОЛЬКО в соответсвующей теме автора. Любые другие темы будут удалены, а авторы понесут наказание.

Лаунчер [C++17|Avn2]: Пишем защиту вместе! #7 Виртуализация на службе у читера

HoShiMin

Старожил
Пользователь
Баллы
173
Avanguard 2
The Win32 Anti-Intrusion Library

​

Итак, погнали!

Коротко, в чём суть: пишем библиотеку для защиты от инжектов.
Писать будем с нуля, основываясь на опыте прошлой версии. Постараемся сделать проект максимально простым, легковесным и быстрым, а главное - надёжным.

В рамках проекта, кроме базового функционала, постараемся решить инжект валидных библиотек (стримилки, драйвера) и сделать удобную систему информирования об инжектах, чтобы решение о блокировке принимала не библиотека, а человек, что позволит минимизировать последствия ложных срабатываний.

Делать будем эталонную реализацию, пользуясь идеями которой, каждый сможет написать свой собственный вариант защиты от распространённых видов инжектов для своих собственных защит и лаунчеров. В комментариях также буду описывать принципы работы основных компонентов (защита от создания потоков, защита от модификации памяти, защита от макросов и т.д.), подводные камни и возможные проблемы, а также, возможные пути их решения.


Список возможностей (будет пополняться по мере разработки):
- [✔️] Защита от создания сторонних потоков
- [✔️] Защита от инжекта через оконные хуки
- [✔️] Фильтр загружаемых модулей
- [✔️] Защита от инжекта через AppInit_DLLs
- [✔️] Защита от мануалмаппинга библиотек
- [✔️] Защита от угона контекста потоков
- [✔️] Фильтр APC
- [❌] Проверка исполняемых секций
- [❌] Поддержка пользовательских хуков
- [✔️] Проверка памяти на наличие неизвестных регионов
- [✔️] Проверка стактрейсов
- [❌] Система репортов
- [❌] Привязки к Java, C# и Delphi
- [❌] Связка с драйвером для запрета на получение хэндлов со стороны

Для проекта нам понадобятся:
- Библиотека для перехвата функций - берём свою (HookLib), чтобы иметь полный контроль над установкой перехватов и не тащить лишних зависимостей.
- Дизассемблер (необходим для хуков и проверок) - возьмём Zydis, как самый быстрый и легковесный.
- Некий хэш для проверки секций - возьмём t1ha, как самый быстрый из всех существующих.
- Библиотека для шифрования строк, чтобы в бинарнике нельзя было найти нужную строку и по ней выйти на место её использования - берём xorstr.

Основной репозиторий проекта: https://github.com/HoShiMin/Avanguard
WinXP не поддерживается. Возможно, в будущем поддержка появится, но не в приоритете.

Заметки о принципах работы защиты:
#1: Защищаемся от создания потоков со стороны
#2: Защищаемся от AppInit_DLLs
#3: Защищаемся от инжектов через оконные хуки
#4: Защищаемся от инжектов через APC
#5: Защита от угона контекста
#6: Защита от скана памяти
#7: Виртуализация на службе у читера

Все идеи по предотвращению ложных срабатываний, по защите, предложения по интерфейсу API, по нужным функциям или по оптимизации алгоритмов пишем в комментариях.
 
Последнее редактирование:
Очень годно. Но написано, что пишем защиту вместе. Неплохо бы вставлять сюда какие-либо комментарии, а не просто выкладывать исходник на гит =)
 
Очень годно. Но написано, что пишем защиту вместе. Неплохо бы вставлять сюда какие-либо комментарии, а не просто выкладывать исходник на гит =)
Буду писать мини-статьи в комментах по мере времени, чтобы было понятно, как работает внутри. Потом, возможно, скомпоную их и выложу на хабр
 
Это будет на сокетах или в виде api чтобы можно было отправлять на php обработчик например?
В виде API. Разработчик подписывается на рассылку уведомлений, и ему прилетает полная информация об инжекте: тип и сопутствующая информация (имена дллок, адреса в памяти, некие дампы (возможно, даже полный дамп всего процесса), списки перехваченных функций) - всё, что поможет понять, чит это или нет. И в рантайме уже на стороне процесса или проанализировать на месте, или отправить куда-то на сервер всё необходимое. В первой версии хотел сделать подобное, но остановился на простых уведомлялках о самом факте инжекта, без подробностей - в таких репортах было мало смысла.
 
Последнее редактирование:
В виде API. Разработчик подписывается на рассылку уведомлений, и ему прилетает полная информация об инжекте: тип и сопутствующая информация (имена дллок, адреса в памяти, некие дампы (возможно, даже полный дамп всего процесса), списки перехваченных функций) - всё, что поможет понять, чит это или нет. И в рантайме уже на стороне процесса или проанализировать на месте, или отправить куда-то на сервер всё необходимое. В первой версии хотел сделать подобное, но остановился на простых уведомлялках о самом факте инжекта, без подробностей.
Ахахах, а ведь всё так хорошо началось :(
 
А это точно C++17?
Из C++ толком ничего не используется, а код как был в голых функциях, так и остался.

Это вообще читаемо? Почему используется LONG вместо long long int, VOID вместо void, почему все имена функций начинаются с большой буквы, почему нет мало мальских классов? Да даже глобальные супер объекты выглядят и то лучше этой простыни из нечитаемого текста.
Если используем стандартную библиотеку, то почему не пользуемся всеми возможностями? Почему в логгере нативные вызовы WriteFile/ReadFile, когда есть fstream? Почему мы используем printf для вывода самых обычных строк из стандартной библиотеки?
Если мы берем для работы последний стандарт C++17, то почему 95% кода написано как 20 лет назад?
 
Почему используется LONG вместо long long int, VOID вместо void, почему все имена функций начинаются с большой буквы
Стиль, принятый в WinAPI: CamelCase для названий функций и венгерская нотация для переменных. Аналогично для названий типов, принятых в разработке под Win32 и использующихся во всех Win-заголовочниках (ULONG/VOID и т.д.).
Использовать стиль, принятый в Linux-сообществе, для разработки под Win - моветон. Будем придерживаться стандартов платформы.
Если используем стандартную библиотеку, то почему не пользуемся всеми возможностями?
В обработчиках хуков нельзя использовать то, во внутреннем устройстве чего ты не уверен. Так как практически любой обработчик может быть вызван в контексте занятого лоадер-лока или лока кучи, есть вероятность получить сложноотлаживаемые взаимные блокировки, как было в прошлой версии. Кроме того, все операции внутри обработчиков должны быть максимально быстрыми, поэтому есть смысл в отказе от fstream/iostream в пользу WriteFile (или сразу NtWriteFile) и printf (но даже его использовать небезопасно).
Поэтому, коль скоро мы работаем в специфичном окружении, следует минимизировать количество сущностей, которые могут выделять память, захватывать свои блокировки или вызывать функции из библиотек уровнем выше, чем текущий контекст. Логгирование само по себе - небезопасная операция в данном случае.
почему нет мало мальских классов
Хороший вопрос: если фильтры предполагают существование исключительно в одном экземпляре, какой практический смысл выносить их в отдельный класс и усложнять архитектуру при том, что у такого класса не будет специфичных для него методов, и нужен он будет лишь как структура для хранения служебной информации, а вся работа будет происходить внутри внеклассовых обработчиков хуков (как вариант - внутри статических методов)?
 
Последнее редактирование:
Стиль, принятый в WinAPI: CamelCase для названия функций и или CamelCase, или венгерская нотация для переменных. Аналогично для названий типов, принятых в разработке под Win32 и использующихся во всех Win-заголовочниках.
То, что такой стиль принят в заголовочных файлах Windows еще не делает его хорошим. Эти файлы были написаны ОЧЕНЬ много лет назад. Ничто не мешает использовать стиль, принятый в стандартной библиотеке C++ как минимум для повышения читабельности и поддерживаемости кода.
В обработчиках хуков нельзя использовать то, во внутреннем устройстве чего ты не уверен. Так как практически любой обработчик может быть вызван в контексте занятого лоадер-лока или лока кучи, есть вероятность получить сложноотлаживаемые взаимные блокировки. Кроме того, все операции внутри обработчиков должны быть максимально быстрыми, поэтому есть смысл в отказе от fstream/iostream в пользу WriteFile (или сразу NtWriteFile) и printf.
Следуя этой логике даже в printf нельзя быть уверенным. Используя WriteFile/ReadFile мы максимально усложняем чтение и развитие этой части кода. А это значит меньше людей станут развивать этот проект. Меньше людей развивают проект -> повторяем судьбу Avanguard 1.
P.S. Никаких проблем с fstream и iostream в самых разных локов не возниает. Проверено на собственном опыте и опыте многих людей, что пользуются моей защитой.
Хороший вопрос: если фильтры предполагают существование исключительно в одном экземпляре, какой практический смысл выносить их в отдельных класс и усложнять архитектуру при том, что у такого класса не будет специфичных для него методов, и нужен он будет лишь как структура для хранения служебной информации, а вся работа будет происходить внутри внеклассовых обработчиков хуков (как вариант - статические методы)?
То, что обработчики хуков внеклассовые - исключительно ограничение вашей хук либы. По хорошему они так же должны находиться внутри класса. С этим проблем нет, подтверждено опытом моей защиты.
Разбиение кода на классы УПРОЩАЕТ архитектуру как для написания кода, так и для чего разработки.

Проблемы, заложенные в Avanguard 1 перенесены сюда без каких либо изменений. Жаль, очень жаль
 
То, что такой стиль принят в заголовочных файлах Windows еще не делает его хорошим.
Это холивар, т.к. субъективное мнение и вкусовщина. Ты можешь сказать: "Мне не нравится стиль Win32" - это нормально. Но чтобы утверждать "стиль Win32 плох, и вам всем следует использовать %стиль_нейм%", требуется компетенция и авторитет. Мне одинаково просто читать и линуксовый стиль, и стиль Win32, поэтому, аналогично, могу сказать, что оба стиля одинаково хороши, что тоже будет субъективным мнением. Но так как, де-факто, при разработке под Win именно это - стандарт, то и придерживаться следует его, т.к., по большей части, проект состоит из WinAPI/NativeAPI, а не из кода стандартной библиотеки STL. И этот код никак не портируется на другие платформы.
Используя WriteFile/ReadFile мы максимально усложняем чтение и развитие этой части кода
Странное заявление, учитывая, что низкоуровневые библиотеки под Win практически всегда пишутся с широким использованием WinAPI, т.к. весь код находится на одном уровне абстракций и не нужно прыгать между высоким (STL) и низким (WinAPI) уровнями - STL служит ВСПОМОГАТЕЛЬНОЙ библиотекой, а не основной. Что конкретно в данном случае усложняет поддержку кода?
P.S. Никаких проблем с fstream и iostream в самых разных локов не возниает. Проверено на собственном опыте и опыте многих людей, что пользуются моей защитой.
С этим проблем нет, подтверждено опытом моей защиты.
Это здорово, но, чтобы судить и сравнивать объективно, хотелось бы увидеть исходники твоей защиты, потому что сейчас это голословные заявления без конкретики и больше похоже на попытку протолкнуть свои предпочтения в коде в другие проекты. Возможно, в функционале твоей защиты просто нет накладывающих свои ограничения частей, а возможно, найдутся места, до которых уже я смогу докопаться, поэтому говорить о том, что это "подтверждено" твоей защитой - некорректно.
 
Последнее редактирование:
Это холивар, т.к. субъективное мнение и вкусовщина. Ты можешь сказать: "Мне не нравится стиль Win32" - это нормально. Но чтобы утверждать "стиль Win32 плох, и вам всем следует использовать %стиль_нейм%", требуется компетенция и авторитет. Мне одинаково просто читать и линуксовый стиль, и стиль Win32, поэтому, аналогично, могу сказать, что оба стиля одинаково хороши, что тоже будет субъективным мнением. Но так как, де-факто, при разработке под Win именно это - стандарт, то и придерживаться следует его, т.к., по большей части, проект состоит из WinAPI/NativeAPI, а не из кода стандартной библиотеки STL. И этот код никак не портируется на другие платформы.
Это не столько холивар, сколько использование стиля 20 летней давности. Никаких вопросов бы не возникало, если бы не это:
Делать будем эталонную реализацию
В комментариях также буду описывать принципы работы основных компонентов (защита от создания потоков, защита от модификации памяти, защита от макросов и т.д.), подводные камни и возможные проблемы, а также, возможные пути их решения.
Если вы спускаетесь до уровня объяснений "как это работает", раз уж вы заявляете эталонную реализацию, где КАЖДЫЙ, повторюсь, КАЖДЫЙ, не программист времен 2000-x, не профессор престарелых лет, а "мамкины кодеры", которые и знать не знали "как было раньше" смогут написать собственную защиту.
Вы заявляете эталонную реализацию, но делаете её максимально нечитабельной для этой аудитории. Более того, я уверен, что аудитория Хабра, куда более подкованная в техническом плане чем аудитория RuBukkit, не "схавает" ваш код. Сейчас разобраться в этом коде смогут единицы, а развивать его - только Вы сами. Это дорога в никуда.

Смотрим на Ваш код. Смотрим на "C++17" в названии. Еще раз смотрим на Ваш код.
Так где обещанный C++17? Честно - я ждал эту защиту что бы присоединится к разработке. Но видя этот дикий ужас пришел к выводу - поддержка такого кода силами сторонних разработчиков почти невозможна.

Подход к защите ровно такой же как и в Avanguard 1. Ничего не было изменено. Сейчас это тот же Avanguard 1, с теми же векторами атаки что и раньше, с теми же проблемами что и раньше.
Мир изменился, HoShiMin
 
@Gravit, и всё же, где конкретика, что то, что ты предлагаешь, лучше? Пока это просто странный хейт и два непонятных реверанса в сторону твоей защиты, где всё идеально. Пусть так! Мне непонятно само обоснование нечитабельности. Давай откроем, например, фильтр памяти (MemoryFilter.cpp). Внутри у нас класс-хранилище и глобальная структурка со служебной инфой (в том числе, с единственным инстансом хранилища).
Скроллим дальше: 3 обработчика и 3 интерфейсных функции для включения, выключения и проверки. Всё это обёрнуто в соответствующий неймспейс, так?
Наружу у нас смотрит только это:
C++:
namespace MemoryFilter {
    BOOL EnableMemoryFilter();
    VOID DisableMemoryFilter();
    BOOL IsMemoryKnown(LPCVOID Address);
}
Пока всё понятно.

Давай то же самое перепишем в виде класса. Получится нечто такое:
C++:
class MemoryFilter {
private:
    // ... Здесь содержимое той глобальной структурки ...
    static NTSTATUS NtAllocHandler(...);
    static NTSTATUS NtProtectHandler(...);
    static NTSTATUS NtFreeHandler(...);
public:
    bool enable();
    void disable();
    bool is_memory_known(const void* address) const;
}

А теперь сравним использование:
C++:
MemoryFilter::IsMemoryKnown(0x1234);
vs
mem_filter_instance.is_memory_known(0x1234);

Мне кажется, или получилось абсолютно одно и то же? В чём конкретно заявленное тобой упрощение?

А теперь что нас ждёт, если будем использовать твой вариант, и какие возникают вопросы:
1. Если класс объявлен в хедере, везде, где мы будем его подключать, придётся тащить хедеры <winternl> и <ntstatus> (и, скорей всего, Windows.h, что не так страшно, т.к. он и так используется почти везде).
2. Как обеспечить единственность экземпляра фильтра? Синглтон?
3. Кто будет ответственен за создание и уничтожение объекта?
4. Если создать глобальный объект в *.cpp, и extern'ить при необходимости (или даже в самом хедере), вместо лаконичного интерфейса наружу будет торчать немалых размеров класс.

Не кажется ли тебе, что всё это капельку усложнит понимание связей в коде и твой бугурт совсем неуместен?
Возможно, в твоей защите ты сделал невероятно удобную архитектуру, но мы не видим исходников, чтобы это оценить. И не знаем, какие задачи ты решал, чтобы сравнивать, где уместен твой подход, а где - нет.

И наконец, про возможности C++17. В первую очередь, код должен оставаться валидным на самых новых стандартах (поэтому, формально, судя по /std:c++latest, у меня стоит поддержка черновика C++20). Все возможности стандартов используются там, где они уместны и применимы. Из мелочей - пару раз понадобилось поставить std::data. Но это не значит, что я должен искусственно изобретать ситуации, чтобы хоть где-то поставить какой-нибудь if constexpr, чтобы выпендриться умением в C++17. То, что ты напишешь простыню из шаблонов в идеально выверенном стиле, не сделает твой код понятнее, если то же самое можно заменить парой простых функций. Так что, про нечитабельность и неподдерживаемость - очень спорное утверждение.

с теми же векторами атаки что и раньше, с теми же проблемами что и раньше.
Так с какими "теми же"? Как замена WriteFile на fstream и кусочков процедурности на совершенно чистый ООП их исправит?
Ничего не было изменено
В 5 раз меньше кода для тех же задач, а "всё тот же". Ну блин!
 
Последнее редактирование:
Аффтар жжот пеши ишо.

Жду использование авн на практике.
 
#1: Защищаемся от создания потоков со стороны

Для начала, разберёмся, что это такое и зачем от него защищаться.

Создание потоков с точкой входа на нужном коде - самый распространённый и самый простой способ заинжектить библиотеку или выполнить нужный код в контексте нужного процесса. Этот метод реализован во всех инжекторах, про этот метод пишут во всех статьях о внедрении кода (например, очень хорошие статьи Ms-Rem'a: Часть 1 и Часть 2) - и от него надо защищаться в первую очередь.

Как происходит внедрение кода:
C:
#include <Windows.h>

int main() {
    CHAR LibPath[] = "C:\\Folder\\Library.dll"; // Путь к дллке, которую хотим подгрузить
    DWORD ProcessId = 1234; // ID целевого процесса

    // Открываем целевой процесс:
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ProcessId);

    // Получаем адрес функции LoadLibrary, которую выполним в чужом процессе:
    PVOID pLoadLibrary = GetProcAddress(GetModuleHandle(L"kernel32"), "LoadLibraryA");

    // Выделяем в целевом процессе память под путь к дллке:
    PVOID pRemoteLibPath = VirtualAllocEx(
        hProcess,
        NULL,
        sizeof(LibPath),
        MEM_RESERVE | MEM_COMMIT,
        PAGE_READWRITE
    );

    // Записываем путь в память процесса:
    DWORD Written = 0;
    WriteProcessMemory(hProcess, pRemoteLibPath, LibPath, sizeof(LibPath), &Written);

    // Создаём в процессе поток, который подгрузит нашу DLL:
    HANDLE hThread = CreateRemoteThread(
        hProcess,
        NULL,
        0,
        (LPTHREAD_START_ROUTINE)pLoadLibrary,
        pRemoteLibPath,
        NULL
    );

    // Ждём завершения потока:
    WaitForSingleObject(hThread, INFINITE);
    CloseHandle(hThread);

    // Освобождаем выделенную под путь память:
    VirtualFreeEx(hProcess, pRemoteLibPath, 0, MEM_RELEASE);

    CloseHandle(hProcess);
}

Что здесь происходит: мы выделяем в процессе память, записываем туда путь к дллке и создаём поток с точкой входа на LoadLibrary, передав ему, как параметр, выделенную память с путём.
Этот трюк работает, потому что основные системные библиотеки (в том числе, нужная нам kernel32.dll) загружаются во все процессы по одинаковым адресам. Соответственно, адреса функций в них совпадают во всех процессах. Поэтому, получив адрес LoadLibrary в нашем процессе, мы можем создать поток по этому же адресу в другом процессе, и тоже попадём на LoadLibrary. А так как сигнатуры LPTHREAD_START_ROUTINE и LoadLibrary совпадают, мы получаем успешный инжект.

Чтобы понять, как от этого защищаться, подумаем, как происходит создание потоков и что происходит с потоком с момента создания его ядром.

Поток начинает исполнение в юзермоде с точки входа LdrInitializeThunk, расположенной в ntdll.dll, и, после базовой инициализации, прыгает на RtlUserThreadStart с помощью функции NtContinue, откуда потом прыгает на заданную, уже пользовательскую, точку входа. Любые функции создания потоков сводятся к двум системным - NtCreateThread и NtCreateThreadEx. Таким образом, перехватив их две и LdrInitializeThunk, мы узнаем, запущен ли поток нашим процессом, или создан со стороны: осталось лишь в хуках NtCreateThread[Ex] как-то запомнить новый поток, а в хуке LdrInitializeThunk проверять, есть ли текущий поток в списке ранее созданных.

Подводные камни: в Windows 7 появился системный пул потоков и API для работы с ним. Система создаёт потоки в пуле в обход NtCreateThread[Ex]. При инициализации процесса система создаёт дефолтный пул с неэкспортирующейся точкой входа TppWorkerThread в ntdll.dll.

upload_2019-3-17_2-14-18.png​

Создание этого пула происходит на ранних этапах инициализации процесса функцией NtCreateWorkerFactory, куда передаётся адрес TppWorkerThread. Чтобы предотвратить ложные срабатывания, необходимо перехватить NtCreateWorkerFactory и запоминать все передаваемые ей точки входа, чтобы в проверке в обработчике хука LdrInitializeThunk считать их безусловно валидными, а также, безусловно добавить TppWorkerThread в список валидных точек входа.

upload_2019-3-17_2-16-19.png​

Т.к. TppWorkerThread не экспортируется, сделаем небольшой трюк. Мы видим, что NtCreateWorkerThread вызывается из функции TpAllocPool -> TpAllocPoolInternal и в качестве параметра ему всегда передаётся адрес TppWorkerThread. TpAllocPool экспортируется из ntdll. Итак, всё, что нам нужно - самим вызвать TpAllocPool, в обработчике NtCreateWorkerThread запомнить адрес TppWorkerThread и вернуть любой статус ошибки (например, STATUS_UNSUCCESSFUL), не вызывая оригинальную функцию. В этом случае TpAllocPool не станет создавать новый пул и корректно закроет все открытые хэндлы.
 
Последнее редактирование:
1. Если класс объявлен в хедере, везде, где мы будем его подключать, придётся тащить хедеры <winternl> и <ntstatus> (и, скорей всего, Windows.h, что не так страшно, т.к. он и так используется почти везде).
В этом нет ничего страшного, ничего того, что стоит избегать.
2. Как обеспечить единственность экземпляра фильтра? Синглтон?
3. Кто будет ответственен за создание и уничтожение объекта?
4. Если создать глобальный объект в *.cpp, и extern'ить при необходимости (или даже в самом хедере), вместо лаконичного интерфейса наружу будет торчать немалых размеров класс.
Именно это и нужно. Наружу должен торчать не "лаконичный интерфейс" как Вы его назвали, а полноценный класс. Класс, решающий возложенные на него обязанности, а не горстка функций, пусть и объеденная пространством имен.
Использование extern необходима не для всех "модулей" этой библиотеки. Т.к. например для Logger extern нужен, а для MemoryFilter - нет.
Все классы защит, вроде MemoryFilter, ThreadsFilter, HWID и всех всех новых функций защиты должны быть доступны из главного класса вашей библиотеки - Avanguard. А вот уже класс Avanguard инициализировать extern'ом. Так получается понятная, выверенная система, где всегда видно какие виды защит вообще существуют, сразу видны все их методы, а так же все общие методы библиотеки. Класс Avanguard - единственное место, где должны собраться все модули одновременно. Это не иделаьная архитектура библиотеки, и быть может, есть более корректная. Но даже она куда логичнее вашей.
В Вашем подходе каждый элемент существует сам по себе, нет никакой структурированности.

И наконец, про возможности C++17. В первую очередь, код должен оставаться валидным на самых новых стандартах (поэтому, формально, судя по /std:c++latest, у меня стоит поддержка черновика C++20). Все возможности стандартов используются там, где они уместны и применимы. Из мелочей - пару раз понадобилось поставить std::data. Но это не значит, что я должен искусственно изобретать ситуации, чтобы хоть где-то поставить какой-нибудь if constexpr, чтобы выпендриться умением в C++17. То, что ты напишешь простыню из шаблонов в идеально выверенном стиле, не сделает твой код понятнее, если то же самое можно заменить парой простых функций. Так что, про нечитабельность и неподдерживаемость - очень спорное утверждение.
Если вы не используете в должной мере возможности C++17 - не стоит писать, что проект на C++17. Этим вы вводите в заблуждение, и, прочитав вашу рекламную брошурку, люди поверят, что этот код - вершина современного C++. Что конечно же не является правдой. Хуже того, по вашим гайдам(которые вы планируете писать) начнут учиться, и, дальше лучше не продолжать. Ничего хорошего не выйдет.
Назвались эталонной реализацией - используйте возможности C++17 по максимуму. Назвались писать гайды и объяснять как что устроено для хотя бы средних обитателей этого форума - лучше использовать более привычные средства, такие как iostream/fstream. Иначе не поймут.
Назвались OpenSource проектом - позаботьтесь о других разработчиках: сделайте код более понятным и легким для чтения.
Не хотите использовать iostream/fstream по какой то причине - ну сделайте понятную обертку. Ничего из этого сейчас нет.

Архитектура проекта не поменялась. Тот же прием с CFFExplorer и APC, тот же самый AvnAPI, тот же самый ужас из голых функций, то же самое отсутствие связанности между компонентами. Поменялись лишь детали реализации.

Не верите мне - опубликуйте гайды на таком ресурсе как Хабр. Толковых статей нет - конкуренции с вашей никакой. С простынями вашего кода, разумеется, и "C++17" в заголовке. Подробно объясняя почему вы юзаете printf/WriteFile в C++17 в 2019 году. И тогда Вы может быть поймете, что голые функции и C++17 это несовместимые вещи.
 
#2: Защищаемся от AppInit_DLLs

Второй по популярности способ инжекта - не требует написания кода и осуществляется штатными средствами Windows.

Смотрим официальную документацию и видим, что система подгружает в каждый процесс список библиотек, указанных в реестре в ключе HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows\AppInit_DLLs:
upload_2019-3-17_16-8-13.png​

Судя по документации, подгрузка происходит в момент загрузки библиотеки user32.dll, в обработчике DLL_PROCESS_ATTACH. Откроем user32.dll в IDA Pro и попытаемся узнать, как же происходит подгрузка библиотек.

Нас встречает функция инициализации UserClientDllInitialize. Она большая и сложная, поэтому попробуем наудачу найти в библиотеке что-то со словами "AppInit". В списке функций такого сочетания нет, в экспортах тоже, а вот в импортах...
upload_2019-3-17_16-30-51.png
​
Похоже, именно то, что нам нужно. Жмём на X и убеждаемся, что она вызывается из функции ClientThreadSetup, на которую есть ссылка в InitializeCsrUserPfn, а она, в свою очередь, вызывается из UserClientDllInitialize - из той самой инициализации user32.dll, описанной в документации.

Осталось её найти и перехватить. Однако, библиотеки "api-ms-win-core-appinit-***" нет на диске. Это виртуальная библиотека (а точнее, ApiSet), которая ресолвится в одну или несколько физических библиотек. Можно найти готовые ресолверы этих имён в сети, но, скорей всего, в данном случае, этот аписет ссылается на kernel32 или kernelbase.

Откроем kernel32.dll в IDA Pro, сразу же идём в экспорты, вбиваем "LoadAppInitDlls", и находим:
upload_2019-3-17_16-39-53.png​

Осталось лишь её перехватить и отменить выполнение. Сделать это на основе HookLib чрезвычайно просто:
upload_2019-3-17_16-44-28.png​

Небольшое замечание: так как LoadAppInitDlls выполняется при загрузке user32, перехватывать нужно до этого момента. Чтобы наша библиотека загрузилась раньше, чем user32, она должна стоять в таблице импортов выше, чем user32, и этот перехват должен выполняться в DllMain. Данный макрос обеспечивает выполнение в DllMain, а убедиться в том, что ваша библиотека загружается раньше, чем user32, вы должны сами. Например, с помощью CFF Explorer. И, разумеется, ваша библиотека не должна сама зависеть от user32.
 
BTW не слушай гравита, он не программист, скорее среднего уровня джуниор-говнокодер. Открывать исходники его лаунчера тоже не рекомендую)
 

Поделиться этой страницей

Назад
Верх