? Введение
Данной статьей я хочу продемонстрировать процесс погружения в неизвестную сферу, методы и средства для сбора информации. Чтобы раз и навсегда закрыть вопрос какие материалы “почитать”, ведь, действуя по гайду “я повторил, но ничего не понял”.
Что именно будем делать? Попробуем без особых знаний залезть в исходники софта для отладки и модификации кода, под названием 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 Автор
Будем переписывать на с++)