🌐 Введение
Данной статьей я хочу продемонстрировать процесс погружения в неизвестную сферу, методы и средства для сбора информации. Чтобы раз и навсегда закрыть вопрос какие материалы “почитать”, ведь, действуя по гайду “я повторил, но ничего не понял”.
Что именно будем делать? Попробуем без особых знаний залезть в исходники софта для отладки и модификации кода, под названием 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.
Спасибо за внимание :з.
ссылка на оригинал статьи https://habr.com/ru/articles/873028/
Добавить комментарий