• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Ломаем игры и зарабатываем как Michael Donnelly ч. 2

albanec2023

(L3) cache
Пользователь
Регистрация
14.10.2023
Сообщения
222
Реакции
305
Доброго времени суток!

Основы работы с инструментами, такими как Cheat Engine, освоение систем счисления и их взаимосвязь с памятью и игровыми процессами, а также базовые навыки их применения: на случай, если вы еще не знакомы с первой частью. Хочу упомянуть о прекрасной статье от Unseen, продолжающей тему. Ещё одна из его публикаций.

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

Многие просят более подробного рассмотрения игр класса AAA. Однако, без понимания основных аспектов низкоуровневой работы с памятью и процессами игры, изучение высокоуровневых может оказаться бессмысленным, поскольку не укрепляется основа для понимания более сложных концепций.

Но не переживайте, мы обязательно затронем и эти темы! Всему свое время.

В ответ на запросы от lisa99 и других читателей, я подготовил приблизительный план того, что предстоит изучить и к чему придем:

Текущий этап:
  • Изучение основ архитектуры игр для более глубокого понимания: изучение структуры игровых объектов и взаимодействий между ними, что позволит лучше ориентироваться в процессе модификаций.
  • Анализ и освоение популярных инструментов геймхакинга: изучение функционала Cheat Engine для сканирования, мониторинга и изменения значений в памяти игр, разбор и анализ бинарного кода, включая исполняемые файлы и библиотеки c помощью Hex-Rays и ReClass.
  • Глубокое изучение процессов, происходящих в игре и ее памяти: анализ структур данных, таких как массивы, указатели, процессы взаимодействия между клиентом и сервером в онлайн-играх.
  • Практическое создание простых хаков для понимания базовых принципов. Например, создание небольших скриптов или трейнеров для изменения игровых параметров.
  • Изучение и анализ уязвимостей в легких анти-чит системах. Например, анализ методов обнаружения читов и попытки их обхода для понимания работы простых систем защиты.
Продвинутый этап:
  • Разбор более сложных и современных игровых движков, таких как Unity и Unreal.
  • Работа с упакованными играми, часто защищенными VMP и Themida.
  • Преодоление типичных методов анти-отладки.
  • Написание полноценных External/Internal хаков и работа с графическим движком игры.
Изучение анти-чит систем:
  • Подробный анализ принципов работы различных анти-чит систем, таких как EAC, MyAC, RAC, ESEA и FACEIT.
  • Понимание методов обнаружения читов, алгоритмов защиты и принципов, лежащих в их основе.
  • Изучение отличий и сходств различных систем защиты и их уязвимостей.
  • Изучение работы анти-чит систем на низком уровне, включая мониторинг памяти, проверку целостности файлов, сетевое взаимодействие.
  • Освоение методов анализа кода и алгоритмов защиты с помощью дизассемблеров Ida Pro и Ghidra для понимания внутреннего устройства анти-чит систем.
  • Изучение работы в режиме ядра и пользовательском режиме для понимания их различий и возможностей воздействия на анти-чит системы.
  • Освоение техник взаимодействия с операционной системой на разных уровнях доступа для более эффективного обхода защиты.
  • Практическое создание инструментов, способных обходить или отключать часть анти-чит системы для возможности вмешательства в игру без ее обнаружения.
Изучение геймхакинга может частично соприкоснуться с навыками в Malware-кодинге. Некоторые техники, такие как методы обхода защиты или крипт читов, имеют сходства с теми, что применяются при создании вредоносных программ. Например, анти-читы используют подходы, аналогичные антивирусным системам. Оба направления включают анализ, обнаружение и использование сигнатур, эвристических методов и других техник в реальном времени. Это позволяет получать знания, используемые в обоих контекстах.

Если есть ресурсы и желание погрузиться в это направление, можно обнаружить потенциально прибыльную сферу, где успех зависит от готовности вкладывать время и усилия в создание приватного софта: от небольших читов за скромную цену для игр вроде CS до разработки продуктов для Fortnite, PUBG и подобных проектов, где конкуренция и жесткая борьба с читерами требуют высокой квалификации.
Для начала мы рассмотрим шестнадцатеричное редактирование, которое используется для изменения файлов сохранений, однако эти навыки нам необходимы, так как они также применяются и в редактировании ресурсов, и в изменении памяти, и в редактировании пакетов и других областях, о которых мы узнаем немного позже.

Шестнадцатеричное редактирование можно разделить на следующие этапы:
1) Очевидно, сначала нужно найти сам файл сохранения
2) Найти информацию в файле, которую мы хотим изменить
3) Используя специальное ПО осуществить изменения
4) Проверить, сработали ли у нас изменения на практике и вернуться к шагу 1, если нет. (Перед этим разумеется сделав резервную копию, так как можно повредить файлы)

Давайте узнаем, как находить файлы сохранений. Это может показаться тривиальным шагом, но это важный навык.
Какой есть надежный способ для быстрого нахождения? Самый простой вариант - просто загуглить :)
Ведь нет смысла тратить время и усилия, если проблема уже решена до вас.

Это будет работать почти всегда. А что если проблема не решена? Тогда все становится сложнее.
Если вы прочитали статьи которые я упомянул, вы наверняка помните, что с помощью WinAPI можно попросить сделать систему что-то за вас.
Например, изменить содержимое памяти с помощью WriteProcessMemory.

Так вот, когда программа обращается к нашему файлу на компьютере, например, сохраняет файл сохранения или читает его, она также запрашивает это у операционной системы посредством WinAPI. Здесь нам на помощь приходит ПО, которое позволяет перехватить эти запросы и понять, куда и когда обращается наша игра, к какому конкретно файлу.

Когда вы пишите какую-то игру будь то на C, C++ или на Python, в этих языках есть функции, которые можно вызвать для чтения, записи и создания файлов. В итоге все эти функции доходят до операционной системы для выполнения вашего запроса, и именно операционная система фактически отвечает за создание файлов на жестком диске.

Эту технику часто используют Malware-аналитики и специалисты по безопасности, когда работают над сэмплами вредоносных программ. И как вы можете догадаться, что это используют и анти-читы в играх, когда условно проверяют, пытаются ли какие-то процессы "взломать" игру или например, подменить игровые библиотеки, проверить их целостность.

В нашем случае мы будем использовать для демонстрации Process Monitor, это простой инструмент для нашей задачи от Microsoft.
В качестве программы мы будем использовать собственную упрощенную программу на Go, сгенерированную ChatGPT.
Код:
package main

import (
    "fmt"
    "io/ioutil"
    "os"
    "path/filepath"
    "strconv"
)

func main() {
    // Получаем путь к исполняемому файлу
    executablePath, err := os.Executable()
    if err != nil {
        fmt.Println("Ошибка при получении пути к исполняемому файлу:", err)
        return
    }

    // Создаем путь к файлу .save рядом с исполняемым файлом
    saveFilePath := filepath.Join(filepath.Dir(executablePath), "GoldValue.save")

    // Читаем значение переменной Gold из файла .save
    savedGold, err := ioutil.ReadFile(saveFilePath)
    var Gold int
    if err != nil {
        fmt.Println("Файл .save не найден или произошла ошибка при чтении. Установлено значение по умолчанию: 100")
        Gold = 100
    } else {
        savedGoldStr := string(savedGold)
        trimmedValue := string(savedGoldStr[:len(savedGoldStr)-1]) // Убираем символ новой строки
        Gold, err = strconv.Atoi(trimmedValue)
        if err != nil {
            fmt.Println("Ошибка преобразования значения из файла .save. Установлено значение по умолчанию: 100")
            Gold = 100
        }
    }

    // Запрашиваем новое значение у пользователя
    var newGoldInput string
    fmt.Print("Введите новое значение для переменной Gold: ")
    fmt.Scanln(&newGoldInput)

    // Преобразуем введенное значение в int
    newGold, err := strconv.Atoi(newGoldInput)
    if err != nil {
        fmt.Println("Ошибка при преобразовании введенного значения. Программа будет закрыта.")
        return
    }

    // Обновляем значение переменной Gold
    Gold = newGold

    // Сохраняем новое значение переменной Gold в файл .save
    err = ioutil.WriteFile(saveFilePath, []byte(fmt.Sprintf("%d\n", Gold)), 0644)
    if err != nil {
        fmt.Println("Ошибка при записи в файл:", err)
    }

    fmt.Println("Новое значение переменной Gold сохранено:", Gold)
}

Давайте разберем программу по шагам:

1) Получение пути к своему исполняемому файлу с помощью функции os.Executable().
2) Используя путь к исполняемому файлу, программа создает путь к файлу .save в том же каталоге, где находится исполняемый файл.
3) Программа пытается прочитать содержимое файла .save, предполагая, что в нем хранится значение переменной Gold.
4) Если файл не найден или происходит ошибка при чтении, программа устанавливает значение переменной Gold в 100.
5) Программа выводит приглашение для ввода нового значения переменной Gold и ожидает ввода от пользователя.
6) Введенное пользователем значение преобразуется в целое число и становится новым значением переменной Gold.
7) Новое значение переменной Gold записывается в файл .save в формате текста.

Для развертывания нашей тестовой среды вы можете как взять скомпилированный мною вариант, так и скомпилировать программу самостоятельно.

Все что вам требуется это:
1) установить Go
2) создать папку в любом удобном месте
3) создать в папке файл с расширением ".go", например "game.go" (должен быть включен показ расширений для зарегистрированных типов файлов)
4) поместить в файл наш код
5) открыть папку, в строке поиска написать "cmd" и нажать Enter, у вас откроется консоль
6) Скомпилировать прописав команду "go build -o game.exe game.go"

При запуске нашей программы мы увидим следующее:

1703278461532.png


Давайте запустим Process Monitor:

1703278475176.png


У нас открыта наша "игра" и наш Process Monitor. Здесь отображаются все события, которые происходят в системе, но нас интересуют только те, что связаны с нашей игрой, мы можем как воспользоваться поиском, так и нажать на иконку захвата "окна". Также вы можете выставить фильтр, например, показывать только сетевую активность / активность с файловой системой / редактирования реестра / создание процессов и потоков. С помощью фильтра вы можете понять, например, где могут находиться лицензионные ключи, если это реестр, или сохранения, если это запись на диск. Но нужно понимать, что в больших проектах происходит работа с огромным количеством ключей реестра / файлов, например, различные ресурсы, модели, иконки, звуки и т.п., тут нас как раз выручают фильтры.

1703279566101.png


Нажатием ПКМ по столбику Process Name мы можем с помощью команды "Exclude" убрать лишние процессы из выдачи, если мы не захватили конкретное окно, или же убрать определенные системные вызовы в столбце "Operation", также через "Exclude ...".

В моем случае я оставил только "Create File" и "EXPLORER.EXE" (так как у нас консольное приложение, вся работа идет через проводник).
1703279811680.png


Таким образом при запуске нашей программы я могу увидеть, где сохранился наш ".save" файл.
А при повторном запуске можем наблюдать "WriteFile", когда происходит перезапись нашего сохранения с новым значением, которое мы присваиваем после ввода в консоль.

Давайте сделаем резервную копию нашего файла, а после этого снова запустим игру и присвоим новое значение.
Таким образом у нас будет два файла сохранений, имеющих разные значения внутри.

В контексте большой игры файл сохранения может нести огромное количество информации, такой как координаты нашего игрока, количество золота, серебра, очки здоровья, инвентарь, активные квесты и тому подобное. В нашем примере есть всего 1 переменная. Поэтому промахнуться будет сложно.

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

Как только мы нашли файл, не будем спешить его редактировать. Для начала напомню вам что же такое строки. Если вы никогда раньше с этим не были знакомы, то на очень простом языке - это текст, т.е. мы изучаем именно его и то как он хранится в компьютере.

То, с чем мы работаем - это файл, а не программа. Но между ними много сходства. Они оба представлены на диске в виде "байтов" информации. Мы узнали из прошлых уроков как хранятся целые числа в памяти. А как же хранятся строки?

Все что нам нужно знать на данном этапе:
1) Строки представляют собой текст и хранятся в виде последовательности байтов.
2) Каждая буква в строке представлена своим шестнадцатеричным значением.
3) Конец строки обычно обозначается нулевыми символами. Любые нули после текста это просто дополнительное пространство.
4) Для представления символов используются кодировки: ASCII и Unicode, ASCII - представление символов в виде байтов, Unicode - более широкий спектр символов на различных языках.
5) Unicode имеет варианты UTF-8 и UTF-16, которые представляют символы различными способами и могут занять разное кол-во байт для отдельного символа.

Нашел вот такую иллюстрацию:
1703282545933.png

Особенно важно: существует два способа хранения чисел - Little Endian и Big Endian.
В Little младший байт размещается первым, т. е. в начале, в Big - наоборот.

Little: число 0x1234 будет храниться в памяти как 34 12 (сначала младший байт, потом старший).
Big: число 0x1234 будет храниться в памяти как 12 34 (сначала старший байт, потом младший).

Способ хранения зависит от архитектуры. У нас это будет Little Endian (x86 архитектура).
Для чего это нам? Пониманием этих способов хранения помогает нам правильно интерпретировать данные при работе с HEX (шестнадцатеричным) редактором.

Установим шестнадцатеричный редактор.
Я взял HxD.

У меня имеется два файла сохранения, в одном значение "1000", в другом "5000".
Давайте откроем наши .save файлы через HxD и убедимся.

1703281788974.png

1703281807673.png


Как вы могли заметить, наш редактор по умолчанию определил Little Endian (справа внизу - Порядок байт).
Также он автоматически распознал наши числовые значения в разделе "декодированный текст".

Предлагаю вам выбрать кнопку "анализ" - "сравнение данных" - "сравнить" на верхней панели, а далее выбрать в качестве источников данных два наших сохранения и нажать "ОК".

1703281933540.png


Можем заметить такую картину:

1703281998598.png


Наш редактор сразу же нашел отличия и показал нам, какие байты отличаются.
Тем самым, если бы переменных было больше и остальные значения остались бы неизменны, мы бы нашли искомую переменную.

В каких случаях это может помочь нам?
- Если переменных с одинаковым значением слишком много, методом сравнения можно отсеять ненужные значения, подобно поиску изменяемого значения в памяти.

При этом работая с файлом сохранения вы можете выполнять поиск по строкам, например, осуществив поиск строки "Health" или "Gold" в зависимости от контекста, и, вероятно, искомая переменная будет лежать где-то рядом, а далее с помощью Data Inspector вы сможете проверить ее значение и понять, то ли вы нашли.

Рекомендую взять любую простую игру и попрактиковаться в поиске значений разными способами. Вы можете также изменить бинарные файлы одиночных игр с целью правки диалогов, имен NPC и т. п., если они не зашифрованы.

А что, если бы здесь был простой анти-чит? Как бы мы его обходили?
Для этого нужно предположить, что может быть использовано против нас в данном ключе:
1) Проверки целостности, т. е. проверка контрольных сумм, например, MD5 или CRC32 (или иные методы)
2) Перемешивание (Scrambling) содержимого файлов сохранения каждый раз.
3) Шифрование значений / строк.

Предлагаю вам немного задуматься над этим вопросом, а мы к нему вернемся в следующих статьях.

На этом уроке мы завершим знакомство с типами данных и перейдем к более продвинутым методам взлома.
В частности, узнаем как искать и взламывать числа которых мы не видим. (oleedd помню про твои таймеры)
Например, координаты XYZ или панель здоровья без отображения чисел (сканирование неизвестного значения).

Мы разобрались с ASCII и Unicode, научились представлять целые числа. Узнали, что для того чтобы хранить большие числа нужно больше байтов.
Однако, некоторые вещи мы не узнали: как хранить отрицательные числа? как хранить дробные числа? где они используются?

Рассмотрим:

- Byte (UInt8) 1 байт - от 0 до 255
- Unsigned Short (UInt16) 2 байта (от 0 до 65535)
- Unsigned Integer (UInt32) 4 байта (от 0 до 4294967295)
- Unsigned Long (UInt64) 8 байт (очень много)
- String (текст)
- Char (character) от 0 до 255
- Bool (boolean) - 1 байт - от 0 до 1.

Новые типы данных расширяют наши возможности: float используется для хранения чисел с плавающей точкой (4 байта), а double предоставляет большую точность (8 байтов). Boolean представляет логические значения true или false (1 байт). Есть и другие типы данных, но эти чаще всего используются при работе с числами и логикой в программировании.

В играх boolean представляет значения типа true или false. Например, "isAlive" для статуса жизни: true - жив, false - мертв. То же с факелом или фонариком "isOn" - true значит включен, false - выключен. Эти значения записываются как 1 (true) и 0 (false) в памяти. Boolean занимает целый байт в памяти, но на практике используется лишь 1 бит для хранения, а остальные 7 бит просто теряются. Есть также "битовые поля", где можно использовать 1 бит, но об этом позже. Игры, написанные на C / C++, обычно используют один байт, на C# - 4 байта, на Java - 8.

1703286070287.png

0000 0000 = 0 | 1000 0000 = -128
0000 0001 = 1 | 1000 0001 = -127
0000 0010 = 2 | 1000 0010 = -126

Видите закономерность?
Первый бит - подписывает число (signed) и определяет, будет оно положительным или отрицательным. А остальные биты уже определяют само число.
В плане арифметики все должно быть понятно, взгляните на небольшую последовательность выше.

При этом обращаем внимание, что так как у нас теряется 1 бит, то меняется и диапазон допустимых значений.
Например в случае с 1 байтом это -128 до 127.

Также числа с плавающей точкой:
Float (число с плавающей точкой) - он занимает 4 байта памяти.
Double (double precision, двойная точность): То же, что и float, но использует вдвое больше памяти - обычно 8 байт. Обеспечивает большую точность при работе с дробными числами. Отличие между ними в объеме памяти и точности: float - меньше памяти, меньше точность. double - больше памяти, больше точности.

Давайте поработаем с ними на практике!

Для демонстрации можно взять любую двухмерную игру, чтобы проще было работать с координатами (XYZ).
Запустим игровой мир и попробуем сделать поиск "Unknown initial value", т.е. неизвестного значения и тип данных Float
Сделаем движением вперед и попробуем найти "Increased Value"
1703295705314.png

После нескольких движений влево/вправо пробуем искать Increased/Decreased соответственно.
(Предположим что у нас есть прямая в двухмерном пространстве и некая точка отсчета 0, т.е. движения будут либо прибавлять значение к позиции либо отнимать при перемещении влево/вправо).

Находим до разумных пределов пачку адресов:
1703295692158.png

Путем "замораживания" отдельных пачек адресов можно найти несколько тех, в которых лежит искомый нам адрес. Ненужные адреса просто удаляем (которые не привели к невозможности ходить по локации).

Таким образом у нас останется только 1 адрес.
1703295772165.png


При заморозке/разморозке которого у нас происходит фриз ходьбы.
Но что же произойдет если перезапустить игру?
1703296000150.png

Значение перестанет инициализироваться.
Важный момент: как мы уже поняли, при перезапуске игры найденные нами адреса уже неактуальны. Чтобы понять, почему же так происходит, нам нужно разобраться с тем, что же такое виртуальная память!


1703289821249.png


Здесь нам нужно вспомнить схему из 1 урока, откуда вы узнали о том, что при запуске приложения его физическая копия копируется в оперативную память - так вот все изменения которые мы делаем - мы осуществляем с копией в памяти. Рассмотрим это явление глубже: при запуске программе выделяется виртуальная память (один из ее аспектов - ее ограниченность, лимит использования ОЗУ), в зависимости от разрядности операционной системы этот лимит отличается.

Например, данная игра имеет 32 битную разрядность (x86) и позволяет потенциально использовать только 2 гигабайта ОЗУ. Таким образом при запуске мы имеем скопированный исполняемый файл и остаток от этих 2 гигабайт свободной памяти. Также часть общей памяти ОЗУ зарезервирована системой, а часть не используется до востребования.

1703290028931.png

На текущем этапе для понимания нам достаточно знать, что наш образ .EXE скопированный в память имеет следующие секции: данные (Data) и код (code)

1703290213541.png
Второй важный момент - код может порождать данные (создавать новые)

1703290287549.png


Когда исполняемый файл загружен в виртуальную память - он начинает выполняться.
В течении этого процесса он может подгружать данные - UI, игровое окружение (World, мобы) и т.п.
В реальности эта схема была бы огромной, будь это настоящий проект, а не абстракция.
1703290478985.png



Нужно понимать, что все эти объекты которые создаются также могут иметь свои свойства, такие как координаты (мобы и игрок), свои показатели здоровья, предметы у игрока в инвентаре и т.д., игровой интерфейс также имеет переменные которые синхронизируются с игроком (как было показано в уроке 1).

Когда объект создается для него выделяется память - это называется аллокацией.
Когда удаляется - деаллокацией.
Что нужно запомнить? То, что когда объекты создаются они создаются на СЛУЧАЙНЫХ позициях, т. е. с разных адресов в памяти.
А это значит что адрес который вы нашли в прошлый раз уже не будет действителен после перезапуска.
Единственная секция на которую можно полагаться - это наш образ .EXE загруженный в память (и сопутствующие ему библиотеки).
Позиция информации из этой секции никогда не меняется, но здесь я немного лукавлю, т. к. по факту она будет меняться, но мы сможем легко рассчитать где она окажется.
Так как любая информация хранящаяся в самом образе статична и неизменна относительно самого образа, нам достаточно лишь найти смещение (offset) от начала образа до нужной информации. А дальше мы работаем с самим образом и памятью - находим куда был загружен образ (по какому адресу - начало), и отталкиваясь от этого работаем с относительными смещениями.

1703291508130.png

1703291631020.png

А как же нам узнать адрес нашего образа .EXE? Спросить у системы, да.
Вы можете проделать этот трюк, запустив любую программу, открыть ее в HxD, а также подключиться к ней с помощью Cheat Engine.
Например, откроем Notepad.exe
1703292106136.png


Подключимся и откроем Memory View
1703292230703.png


Find - Go To Addres - notepad++.exe

1703292488297.png

Как видите - тот же самый образ без изменений. Но как же найти объекты, которые были выделены? Нам на помощь приходят указатели.
Что это и как их найти?

После того как мы нашли адрес нужных нам координат - выполняем сканирование для поиска указателей.
1703297603882.png

"Pointer Scan For This Addres"
После этого перезаходим в игру и снова находим нужный нам адрес.
1703297662906.png

Программа найдет огромное количество указателей - но не все они (далеко не все) могут быть валидны.
Указатель — это особый тип переменной, который хранит адрес памяти, где находится другая переменная или объект.
Как же понять какие из них валидны, а какие нет?
Для этого нам нужно перезапустить игру и сделать сканирование по новому адресу.
1703297770619.png

Таким образом можно будет отсеять количество неверных указателей, т.к. будут отобраны только те из списка, которые "остались жить" после рестарта.
1703297997503.png

Простыми словами, указатели - это односторонние связи между объектами. Исполняемый файл загружается в виртуальную память и создает мир, а мир загружает игроков и так далее.
1703300201420.png

Связи на этой схеме - это указатели, позволяющие объектам обмениваться информацией друг с другом. Представим такой путь, например, если мы хотим найти золото.
У нас есть исполняемый файл, который указывает на мир, мир указывает на игрока, игрок указывает на инвентарь.
Таким образом можно понять, что указатели имеют глубину.
В данном случае "золото лежит глубже".
Еще одна вещь, которую стоит отметить - эти адреса относительные.

То есть вне зависимости от того, куда будет загружен образ, относительно образа каждый раз все эти объекты будут на своих местах неизменно.
Например, золото всегда будет лежать по пути [[[[[GAME.EXE + C] + 8]] + 4] + 0]] (Последний 0 можно даже не считать, например).
А вот стрелы будут лежать уже по адресу [[[[[GAME.EXE + C] + 8]] + 4] + 4]]

Можно настроить параметры, такие как "максимальная глубина", определяющая, насколько глубоко будет искать указатель. Чем меньше это число, тем быстрее сканирование, но есть риск пропуска результатов. Запускаем сканирование и ждем завершения. После этого получим список указателей и их путей к переменной которую ищем. При этом стоит помнить что не все из них будут надежными. Лучший способ проверить их надежность - перезапустить игру.

После перезапуска игры начинаем процесс поиска переменной снова, а когда нашли начинаем сканирование заново. Для упрощения этого процесса можно использовать сохраненные данные предыдущего поиска. После обновления значений нужно пересканить память и обновить указатели, исключив те, что перестали работать после перезапуска.

Получив список более коротких и, вероятно, более надежных путей указателей, добавляем их в наш список. Затем проводим тесты, чтобы убедиться, что эти пути указателей надежно ведут к переменной которую мы ищем. Примерно так и происходит поиск. Порой это рутинная и монотонная работа./

Надеюсь найдется инфа полезной :smile10:
Специально для xss.pro от Албанца!
Всех с Наступающим Новым Годом!
 

Вложения

  • 1703283138006.png
    1703283138006.png
    37.4 КБ · Просмотры: 19
  • 1703293097367.png
    1703293097367.png
    45.8 КБ · Просмотры: 12
  • 1703293309513.png
    1703293309513.png
    19.7 КБ · Просмотры: 12
  • 1703294201195.png
    1703294201195.png
    15.4 КБ · Просмотры: 12
  • 1703294314577.png
    1703294314577.png
    28.4 КБ · Просмотры: 12


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх