Данной статьей я хочу продемонстрировать процесс погружения в неизвестную сферу, методы и средства для сбора информации. Чтобы раз и навсегда закрыть вопрос какие материалы “почитать”, ведь, действуя по гайду “я повторил, но ничего не понял”.
Что именно будем делать? Попробуем без особых знаний залезть в исходники софта для отладки и модификации кода, под названием Cheat Engine. Он создавался годами, а наша задача — апроприировать эти знания за короткий промежуток времени!
Cheat Engine — это мощный инструмент для реверс-инжиниринга и модификации кода. Хотя он позиционируется как “Чит Движок”, он фактически способен конкурировать с любыми дебаггерами по ряду причин:
Мы поняли, что это классная штука, она бесплатная, открытая и она работает. Так зачем же его писать самим?!
Главное - 🎓 Это сложная и интересная задача. Ну и я ни на что не намекаю, но …😉
Не сложно догадаться, что завести такой пет-проект в портфолио будет не плохо.
🔄 План работы
Чтобы создать минимально рабочий прототип, нам нужно реализовать ключевые функции:
🔐 Подключение к процессу.
🤖 Чтение и сканирование памяти.
🛠️ Редактирование памяти.
🔄 Таблица с найденными адресами.
📝 Дизассемблирование кода.
🏢 Отображение списка загруженных библиотек.
🌧️ Отладка (брейки, стек вызовов, регистры).
🔍 Исследуем исходники Cheat Engine
Открываем репозиторий Cheat Engine и видим несколько важных директорий:
Cheat Engine — основной код.
DBKKernel — проект драйвера.
DBVM — гипервизор.
lua — движок для скриптов.
Так как драйвер и гипервизор мы пока трогать не будем, откроем основной проект.
😅 Тут нас ждёт сюрприз: Cheat Engine написан на Паскале!
🌟 Разбираемся с кодом
Заглянув в основной проект, можно заметить, что там ку-у-у-у-ча файлов, но пугаться не стоит, так как главных всего-то ничего! А именно…
🔄 ProcessList.pas
Этот файл нужен для сбора и хранения информации о процессах.
procedure GetProcessList(ProcessList: TStrings; NoPID: boolean=false; noProcessInfo: boolean=false);
Перегрузка процедуры для управления выводом информации о процессах.
Чтобы продвинуться дальше, просто забиваем в поиск по файлам название функции GetProcessList, это приведет нас к следующему файлу и еще одной интересной функции…
🔄 MainUnit.pas (Это главный файл с логикой для GUI форм)
Здесь вызывается Open_Process, который используется во всех интерфейсах дебаггера (Kernel, DBVM).
🔄 CEFuncProc.pas
Тут находится реализация Open_Process.
🔄 NewKernelHandler.pas
Содержит ключевые функции, такие как:
function ReadProcessMemory(...); function WriteProcessMemory(...);
🔄 Disassembler.pas
Файл на 16 000 строк, отвечающий за дизассемблирование кода.
🚀 Итоги
Теперь мы знаем как:
✅ Получать список процессов.
✅ Открывать хендл к процессу.
✅ Читать и записывать память.
Следующий шаг — разобраться с отладчиком и дизассемблером! 💪
Функции отладчика находятся все в том же NewKernelHandler.pas, они так же представлены перегрузками для разных интейфейсов: winapi, driver, server и т.д. Но в данном случае мы пока обратим внимание на winapi и уже после будет шаг за шагом разбирать другие методы работы.
Ключевыми будут 👇
// Получение адреса функции GetThreadContext из библиотеки WindowsKernel и присваивание его переменной GetThreadContext GetThreadContext:=GetProcAddress(WindowsKernel,'GetThreadContext');
// Получение адреса функции SetThreadContext из библиотеки WindowsKernel и присваивание его переменной SetThreadContext SetThreadContext:=GetProcAddress(WindowsKernel,'SetThreadContext');
🤏 Именно через SetThreadContext мы и будем устанавливать аппаратные точки останова, изменяя регистр DRx (Debug Registers).
🔹 Как установить хардверный бряк через SetThreadContext?
1️⃣ Использовать GetThreadContext, чтобы получить текущий контекст потока.
2️⃣ Изменить один из DR0–DR3 (адрес бряка).
3️⃣ Настроить DR7 для активации бряка.
4️⃣ Применить изменения через SetThreadContext.
🔹 Пример кода
CONTEXT ctx; ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS; GetThreadContext(hThread, &ctx);
ctx.Dr0 = targetAddress; // Адрес для бряка
ctx.Dr7 |= 1; // Включаем бряк
SetThreadContext(hThread, &ctx);
📌 Важно: бряк привязан к конкретному потоку, а не всему процессу.
👁️👁️👁️ Там же видим
// 🔍 Получение адреса функции Wow64GetThreadContext из библиотеки WindowsKernel и присваивание его переменной Wow64GetThreadContext
Wow64GetThreadContext:=GetProcAddress(WindowsKernel,'Wow64GetThreadContext');
// 🔍 Получение адреса функции Wow64SetThreadContext из библиотеки WindowsKernel и присваивание его переменной Wow64SetThreadContext
Wow64SetThreadContext:=GetProcAddress(WindowsKernel,'Wow64SetThreadContext');
SuspendThread:=GetProcAddress(WindowsKernel,'SuspendThread'); // ⏸️ Получение адреса функции SuspendThread из библиотеки WindowsKernel, которая используется для приостановки выполнения потока
ResumeThread:=GetProcAddress(WindowsKernel,'ResumeThread'); // ▶️ Получение адреса функции ResumeThread из библиотеки WindowsKernel, которая используется для возобновления выполнения ранее приостановленного потока
WaitForDebugEvent:=GetProcAddress(WindowsKernel,'WaitForDebugEvent'); // ⏳ Получение адреса функции WaitForDebugEvent из библиотеки WindowsKernel, которая используется для ожидания события отладки в процессе или потоке
ContinueDebugEvent:=GetProcAddress(WindowsKernel,'ContinueDebugEvent'); // 🔄 Получение адреса функции ContinueDebugEvent из библиотеки WindowsKernel, которая используется для продолжения выполнения после обработки события отладки
DebugActiveProcess:=GetProcAddress(WindowsKernel,'DebugActiveProcess'); // 🛠️ Получение адреса функции DebugActiveProcess из библиотеки WindowsKernel, которая используется для начала отладки процесса по его идентификатору
Здесь же видим непонятный WindowsKernel, пробуем поискать в файле и находим:
WindowsKernel: Thandle; // 🏗️ Переменная, хранящая дескриптор ядра операционной системы Windows
Но это только объявление, пробуем прощелкать далее и находим определение:
WindowsKernel:=LoadLibrary('Kernel32.dll'); // 📦 Попытка загрузить библиотеку ядра Windows (Kernel32.dll) и сохранение дескриптора в переменную WindowsKernel. // ❌ Если библиотека не найдена, то WindowsKernel будет равен 0.
Этого достаточно для базовой работы с процессами. Остается поглядеть, что там в Disassembler’е.
Как мы помним, там 16 000 строк кода, и все, что они делают, так это проверяют каждый байт на константное значение.
Если совпадает, то устанавливают строковую мнемонику (ADD, MOV и др.).
Рассмотрим поближе. У нас есть базовый объект дизассемблера, defaultDisassebler:
defaultDisassembler:=TDisassembler.create; // 🛠️ Создаем объект по умолчанию и присваиваем его глобальной переменной.
Который инициализируется методом create в классе TDisassembler,
далее на участок памяти вызывается функция disassemble, которая возвращает строку.
Начинается функция на 1624 строке, а заканчивается на 15710 строке.
Как уже выше упомянуто, почти все эти строки занимает switch,
который проверяет байты и отдает назад название инструкции.
case memory[0] of //opcode
$00 : begin
//🏹🏹
if (aggressivealignment and (((offset) and $f)=0) and (memory[1]<>0) ) or ((memory[1]=$55) and (memory[2]=$89) and (memory[3]=$e5)) then
begin
description:='Filler'; lastdisassembledata.opcode:='db'; LastDisassembleData.parameters:=inttohex(memory[0],2);
end
else
begin
description:='Add';
//🏹🏹
lastdisassembledata.opcode:='add';
lastdisassembledata.parameters:=modrm(memory,prefix2,1,2,last)+r8(memory[1]); inc(offset,last-1);
end;
end;
$01 : begin....
Думаю, на этом можно закончить введение, более подробно рассмотрим каждый из методов уже на практике, когда начнем писать свой mega-omega чит-движок с плюшками.
На этом откланяюсь, а все претензии и пожелания можно писать сюда 👉 https://t.me/osiechan/52, здесь же можно скачать исходные файлы с комментариями на каждой строке (да-да, даже на 16 000 строк) и pdf статьи.
А прокачать свой навыки чито-строителя можно на бесплатном, открытом курсе по созданию бота для мморпг 👉 https://t.me/osiechan/41.