? Введение
Данной статьей я хочу продемонстрировать процесс погружения в неизвестную сферу, методы и средства для сбора информации. Чтобы раз и навсегда закрыть вопрос какие материалы “почитать”, ведь, действуя по гайду “я повторил, но ничего не понял”.
Что именно будем делать? Попробуем без особых знаний залезть в исходники софта для отладки и модификации кода, под названием Cheat Engine. Он создавался годами, а наша задача — апроприировать эти знания за короткий промежуток времени!
? Что такое Cheat Engine?
Cheat Engine — это мощный инструмент для реверс-инжиниринга и модификации кода. Хотя он позиционируется как “Чит Движок”, он фактически способен конкурировать с любыми дебаггерами по ряду причин:
? Оснащён собственным драйвером, который даёт более высокий уровень прав и скрытность.
?️ Обладает встроенным гипервизором, который можно загрузить прямо в рантайме ОС.
? Бесплатный и открытый исходный код делает его удобным для изучения.
? Зачем писать свой Cheat Engine?
Мы поняли, что это классная штука, она бесплатная, открытая и она работает. Так зачем же его писать самим?!
Главное - ? Это сложная и интересная задача. Ну и я ни на что не намекаю, но …?

Не сложно догадаться, что завести такой пет-проект в портфолио будет не плохо.
? План работы
Чтобы создать минимально рабочий прототип, нам нужно реализовать ключевые функции:
? Подключение к процессу.
? Чтение и сканирование памяти.
?️ Редактирование памяти.
? Таблица с найденными адресами.
? Дизассемблирование кода.
? Отображение списка загруженных библиотек.
?️ Отладка (брейки, стек вызовов, регистры).
? Исследуем исходники Cheat Engine
Открываем репозиторий Cheat Engine и видим несколько важных директорий:

Cheat Engine — основной код.
DBKKernel — проект драйвера.
DBVM — гипервизор.
lua — движок для скриптов.
Так как драйвер и гипервизор мы пока трогать не будем, откроем основной проект.
? Тут нас ждёт сюрприз: Cheat Engine написан на Паскале!
.lfm — GUI формы.
.pas — исходники с функциями.️️ ? (они-то нам и нужны)
? Разбираемся с кодом
Заглянув в основной проект, можно заметить, что там ку-у-у-у-ча файлов, но пугаться не стоит, так как главных всего-то ничего! А именно…
? ProcessList.pas
Этот файл нужен для сбора и хранения информации о процессах.
procedure GetProcessList(ProcessList: TStrings; NoPID: boolean=false; noProcessInfo: boolean=false);
Перегрузка процедуры для управления выводом информации о процессах.
procedure GetProcessList(ProcessList: TStrings; NoPID: boolean=false; noProcessInfo: boolean=false);
var SNAPHandle: THandle;
ProcessEntry: PROCESSENTRY32;
Check: Boolean;
{$IFDEF WINDOWS}
lwindir: string;
HI: HICON;
ProcessListInfo: PProcessListInfo;
{$ENDIF}
i,j: integer;
s,s2: string;
begin
// ? Полная очистка списка перед добавлением новых элементов
cleanProcessList(ProcessList);
// ?️ Если текущий отладчик - TGDBServerDebuggerInterface, получаем список процессов через него
if CurrentDebuggerInterface is TGDBServerDebuggerInterface then
begin
// ? Получаем список процессов через getProcessList
TGDBServerDebuggerInterface(CurrentDebuggerInterface).getProcessList(processlist);
// ✅ Если список получен, выходим
if processlist.count<>0 then exit;
end;
// ? Для MacOSX получаем список процессов через macport, если нет соединения с сервером
{$ifdef darwin}
if getconnection=nil then
begin
macport.GetProcessList(processlist);
exit; // ⏭️ Выходим, если список уже сформирован
end;
{$endif}
// ?️ Windows: подготовка переменных
{$ifdef windows}
lwindir:=lowercase(windowsdir); // ? Директория Windows в нижнем регистре
ProcessListInfo:=nil; // ❌ Инициализация указателя
HI:=0; // ? Дескриптор иконки (0 - нет иконки)
j:=0;
// ? Создаём снимок процессов
SNAPHandle:=CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,0);
If SnapHandle<>0 then // ✅ Успешно создан
begin
ZeroMemory(@ProcessEntry, sizeof(ProcessEntry)); // ? Очистка структуры перед использованием
if not assigned(Process32First) then // ❗ Проверяем доступ к функции Process32First
begin
exit; // ? Выход при ошибке
end;
ProcessEntry.dwSize:=SizeOf(ProcessEntry); // ? Устанавливаем размер структуры
// ?️ Для удалённого отладчика не запрашиваем доп. информацию
{$ifdef windows}
if getconnection<>nil then
noProcessInfo:=true;
{$else}
noProcessInfo:=true;
{$endif}
// ? Начинаем перебор процессов
Check:=Process32First(SnapHandle,ProcessEntry);
while check do // ? Пока есть процессы
begin
{$ifdef windows}
// ? Фильтрация по пользователю, если включен флаг ProcessesCurrentUserOnly
if (not ProcessesCurrentUserOnly) or (GetUserNameFromPID(processentry.th32ProcessID)=username) then
{$endif}
begin
// ? Проверка, что PID не равен 0
if processentry.th32ProcessID<>0 then
begin
{$ifdef windows}
// ? Выделяем память для информации о процессе, если требуется
if noprocessinfo=false then
begin
getmem(ProcessListInfo,sizeof(TProcessListInfo));
// ? Заполняем структуру
ProcessListInfo.processID:=processentry.th32ProcessID;
ProcessListInfo.processIcon:=0;
ProcessListInfo.winhandle:=0;
end;
{$endif}
// ? Формируем строку с PID, если NoPID=false
if noPID then
s:=''
else
s:=IntTohex(processentry.th32ProcessID,8)+'-';
// ? Добавляем имя исполняемого файла
s:=s+ExtractFilename(WinCPToUTF8(processentry.szExeFile));
{$ifdef windows}
// ? Добавляем в список с объектом ProcessListInfo, если требуется доп. информация
if noprocessinfo then
ProcessList.Add(s)
else
ProcessList.AddObject(s, TObject(ProcessListInfo));
{$else}
// ➕ Для других платформ просто добавляем имя процесса
ProcessList.Add(s)
{$endif}
end;
end;
// ? Переход к следующему процессу
check:=Process32Next(SnapHandle,ProcessEntry);
end;
// ? Закрываем снимок
closehandle(snaphandle);
end
else // ❌ Ошибка создания снимка
begin
{$ifdef windows}
// ⚠️ Выводим исключение, если список процессов получить невозможно
raise exception.Create(rsICanTGetTheProcessListYouArePropablyUsingWindowsNT);
{$endif}
end;
end; // ? Конец процедуры GetProcessList
Чтобы продвинуться дальше, просто забиваем в поиск по файлам название функции 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.
Спасибо за внимание :з.
DBarskiy
А почему хаб с++ а код на паскале?
osieman Автор
Будем переписывать на с++)
Playa
Чтобы что?
Nemoumbra
Наверное, чтобы увеличить расширяемость/взламываемость (hacking) приложения. Вот кто сходу из присутствующих сможет набросать в форке CE новую фичу? Я на Паскале не прогал с начала 10 класса, наверное. Сейчас на 4 курсе вуза уже, мой мозг принял плюсы, Питон и ±шарпы. Оно и неприятно уже немного - спускаться в эти дебри нестандартного синтаксиса...