? Введение

Данной статьей я хочу продемонстрировать процесс погружения в неизвестную сферу, методы и средства для сбора информации. Чтобы раз и навсегда закрыть вопрос какие материалы “почитать”, ведь, действуя по гайду “я повторил, но ничего не понял”.

Что именно будем делать? Попробуем без особых знаний залезть в исходники софта для отладки и модификации кода, под названием Cheat Engine. Он создавался годами, а наша задача — апроприировать эти знания за короткий промежуток времени!

? Что такое Cheat Engine?

Cheat Engine — это мощный инструмент для реверс-инжиниринга и модификации кода. Хотя он позиционируется как “Чит Движок”, он фактически способен конкурировать с любыми дебаггерами по ряду причин:

  • Оснащён собственным драйвером, который даёт более высокий уровень прав и скрытность.

  • ?️ Обладает встроенным гипервизором, который можно загрузить прямо в рантайме ОС.

  • Бесплатный и открытый исходный код делает его удобным для изучения.

? Зачем писать свой Cheat Engine?

Мы поняли, что это классная штука, она бесплатная, открытая и она работает. Так зачем же его писать самим?!
Главное - ? Это сложная и интересная задача. Ну и я ни на что не намекаю, но …?

Не сложно догадаться, что завести такой пет-проект в портфолио будет не плохо.

? План работы

Чтобы создать минимально рабочий прототип, нам нужно реализовать ключевые функции:

  1. Подключение к процессу.

  2. Чтение и сканирование памяти.

  3. ?️ Редактирование памяти.

  4. Таблица с найденными адресами.

  5. Дизассемблирование кода.

  6. Отображение списка загруженных библиотек.

  7. ?️ Отладка (брейки, стек вызовов, регистры).

? Исследуем исходники 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, они так же представлены перегрузками для разных интейфейсов: winapidriverserver и т.д. Но в данном случае мы пока обратим внимание на 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 строк кода, и все, что они делают, так это проверяют каждый байт на константное значение.
Если совпадает, то устанавливают строковую мнемонику (ADDMOV и др.).

Рассмотрим поближе. У нас есть базовый объект дизассемблера, 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.

Спасибо за внимание :з.

Комментарии (2)


  1. DBarskiy
    12.01.2025 06:30

    А почему хаб с++ а код на паскале?


    1. osieman Автор
      12.01.2025 06:30

      Будем переписывать на с++)