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

Использование объекта памяти

Внутренний объект Memory предоставляет методы для доступа и управления данными или кодом в текущем процессе. Он имеет следующий интерфейс:

interface Memory {
    ReadFloat(address: int, vp: boolean, ib: boolean): float;
    WriteFloat(address: int, value: float, vp: boolean, ib: boolean): void;
    ReadI8(address: int, vp: boolean, ib: boolean): int;
    ReadI16(address: int, vp: boolean, ib: boolean): int;
    ReadI32(address: int, vp: boolean, ib: boolean): int;
    ReadU8(address: int, vp: boolean, ib: boolean): int;
    ReadU16(address: int, vp: boolean, ib: boolean): int;
    ReadU32(address: int, vp: boolean, ib: boolean): int;
    WriteI8(address: int, value: int, vp: boolean, ib: boolean): void;
    WriteI16(address: int, value: int, vp: boolean, ib: boolean): void;
    WriteI32(address: int, value: int, vp: boolean, ib: boolean): void;
    WriteU8(address: int, value: int, vp: boolean, ib: boolean): void;
    WriteU16(address: int, value: int, vp: boolean, ib: boolean): void;
    WriteU32(address: int, value: int, vp: boolean, ib: boolean): void;
    Read(address: int, size: int, vp: boolean, ib: boolean): int;
    Write(address: int, size: int, value: int, vp: boolean, ib: boolean): void;

    ToFloat(value: int): float;
    FromFloat(value: float): int;
    ToU8(value: int): int;
    ToU16(value: int): int;
    ToU32(value: int): int;
    ToI8(value: int): int;
    ToI16(value: int): int;
    ToI32(value: int): int;

    CallFunction(address: int, ib: boolean, numParams: int, ...funcParams: int[]): void;
    CallFunctionReturn(address: int, ib: boolean, numParams: int, ...funcParams: int[]): int;
    CallFunctionReturnFloat(address: int, ib: boolean, numParams: int, ...funcParams: int[]): float;

    Fn: {
        X64(address: int, ib: boolean): (...funcParams: int[]) => int;
        X64Float(address: int, ib: boolean): (...funcParams: int[]) => float;
        X64I8(address: int, ib: boolean): (...funcParams: int[]) => int;
        X64I16(address: int, ib: boolean): (...funcParams: int[]) => int;
        X64I32(address: int, ib: boolean): (...funcParams: int[]) => int;
        X64U8(address: int, ib: boolean): (...funcParams: int[]) => int;
        X64U16(address: int, ib: boolean): (...funcParams: int[]) => int;
        X64U32(address: int, ib: boolean): (...funcParams: int[]) => int;
    }
}

Чтение и запись значений

Группа методов доступа к памяти (ReadXXX/WriteXXX) может использоваться для чтения или изменения значений, хранящихся в памяти. Каждый метод предназначен для определенного типа данных. Чтобы изменить значение с плавающей запятой (которое в исходной игре занимает 4 байта), используйте Memory.WriteFloat, например:

    Memory.WriteFloat(address, 1.0, false, false)

Где address — это переменная, хранящая адрес памяти, 1.0 — это значение для записи, первое «false» означает, что нет необходимости изменять защиту памяти с помощью VirtualProtect (адрес уже доступен для записи). Второй false — это значение флага ib, который предписывает CLEO рассматривать address либо как абсолютный адрес (ib = false), либо как относительное смещение к текущему базовому адресу образа (ib = true). Поскольку в окончательных версиях используется функция ASLR, их абсолютные адреса памяти меняются при запуске игры из-за изменения начального адреса. Рассмотрим следующий пример:

0x1400000000 ImageBase
...
...
0x1400000020 SomeValue

Вы хотите изменить SomeValue, которое в настоящее время находится по адресу 0x1400000020. Вы можете сделать это с помощью Memory.Write(0x1400000020, 1, 1, false, false). Однако при следующем запуске игры расположение памяти может выглядеть так:

0x1500000000 ImageBase
...
...
0x1500000020 SomeValue

Эффективно ломая сценарий. В этом случае рассчитайте относительное смещение от базы изображения (0x1500000020 - 0x1500000000 = 0x20), которое будет постоянным для конкретной версии игры. Используйте Memory.Write следующим образом: Memory.Write(0x20, 1, 1, false, true). CLEO суммирует смещение (0x20) с текущим значением базы изображения (0x1400000000, 0x1500000000 и т. д.) и записывает по правильному абсолютному адресу.

Для вашего удобства вы можете узнать текущее значение базы образа в cleo_redux.log, например:

09:27:35 [INFO] Image base address 0x7ff7d1f50000

Точно так же, чтобы прочитать значение из памяти, используйте один из методов ReadXXX, в зависимости от того, какой тип данных содержит адрес памяти. Например, чтобы прочитать 8-битное целое число со знаком (также известное как char или uint8), используйте Memory.ReadI8, например:

    var x = Memory.ReadI8(offset, true, true)

Переменная x теперь содержит 8-битное целое число в диапазоне (0..255). Чтобы показать возможные варианты, в этом примере в качестве последнего аргумента используется true, что означает, что атрибут защиты по умолчанию для этого адреса будет изменен на PAGE_EXECUTE_READWRITE перед чтением.

    var gravity = Memory.ReadFloat(gravityOffset, false, true);
    gravity += 0.05;
    Memory.WriteFloat(gravityOffset, gravity, false, true);

Наконец, последние два метода Read и Write — это то, что другие методы используют под капотом. Они имеют прямую привязку к коду Rust, который читает и записывает память. В коде JavaScript вы можете использовать входные аргументы размером до 53-битных чисел.

Параметр size в методе Read может быть только 1, 2, 4 или 8. CLEO обрабатывает value` как целое число со знаком, хранящееся в формате с прямым порядком байтов.

В методе Write допускается любой size больше 0. Размеры «3, 5, 6, 7 и 9 и далее могут использоваться только вместе с одним байтом value. CLEO использует их для заполнения непрерывного блока памяти, начиная с address, заданным value (подумайте об этом как о memset в C++).

    Memory.Write(offset, 0x90, 10, true, true) // "noping" 10 байт кода, начиная с базы смещения+изображения

Обратите внимание, что для использования любого из методов чтения/записи требуется mem разрешение.

Метод приведения типов

По умолчанию методы Read и Write обрабатывают данные как целочисленные значения со знаком. Это может быть неудобно, если память содержит значение с плавающей запятой в формате IEEE 754 или большое 32-битное целое число со знаком (например, указатель). В этом случае используйте методы приведения ToXXX/FromXXX. Они действуют аналогично оператору reinterpret_cast в C++.

Чтобы получить представление о том, чего ожидать от этих методов, см. следующие примеры:

    Memory.FromFloat(1.0) => 1065353216
    Memory.ToFloat(1065353216) => 1.0
    Memory.ToU8(-1) => 255
    Memory.ToU16(-1) => 65535
    Memory.ToU32(-1) => 4294967295
    Memory.ToI8(255) => -1
    Memory.ToI16(65535) => -1
    Memory.ToI32(4294967295) => -1

В качестве альтернативы используйте соответствующие методы для чтения/записи значения в виде числа с плавающей запятой (ReadFloat/WriteFloat) или целого числа без знака (ReadUCX/WriteUXXX).

Вызов внешних функций

Объект Memory позволяет вызвать чужую (собственную) функцию по ее адресу одним из следующих способов:

  • Memory.CallFunction
  • Memory.CallFunctionReturn
  • Memory.CallFunctionReturnFloat
    Memory.CallFunction(0xEFFB30, true, 1, 13)

Где 0xEFFB30 — это смещение функции относительно IMAGE BASE (представьте себе, что это случайный начальный адрес игровой памяти), true — это флаг ib (см. ниже), 1 – количество входных аргументов, и 13 — единственный аргумент, передаваемый в функцию.

Параметр ib в Memory.CallFunction имеет то же значение, что и в командах чтения/записи памяти. При значении true CLEO добавляет текущий известный адрес базы образа к значению, указанному в качестве первого аргумента, для вычисления абсолютного адреса памяти функции. Если установлено значение false, первый аргумент не изменяется.

Чтобы передать функции значения с плавающей запятой, преобразуйте значение в целое число, используя Memory.FromFloat:

    Memory.CallFunction(0x1234567, true, 1, Memory.FromFloat(123.456));

Возвращаемое значение функции, вызванной с помощью Memory.CallFunction, игнорируется. Чтобы прочитать результат, используйте Memory.CallFunctionReturn с теми же параметрами. Используйте Memory.CallFunctionReturnFloat для вызова функции, которая возвращает значение с плавающей запятой.

CLEO Redux поддерживает вызов сторонних функций с параметрами до 16.

Обратите внимание, что для использования любого из методов вызова требуется mem разрешение.

Удобные методы с объектом Fn

Memory.Fn предоставляет удобные методы для вызова различных типов внешних функций.

    Fn: {
        X64(address: int, ib: boolean): (...funcParams: int[]) => int;
        X64Float(address: int, ib: boolean): (...funcParams: int[]) => float;
        X64I8(address: int, ib: boolean): (...funcParams: int[]) => int;
        X64I16(address: int, ib: boolean): (...funcParams: int[]) => int;
        X64I32(address: int, ib: boolean): (...funcParams: int[]) => int;
        X64U8(address: int, ib: boolean): (...funcParams: int[]) => int;
        X64U16(address: int, ib: boolean): (...funcParams: int[]) => int;
        X64U32(address: int, ib: boolean): (...funcParams: int[]) => int;
    }

Эти методы предназначены для охвата всех поддерживаемых типов возврата. Например, этот код:

    Memory.CallFunction(0xEFFB30, true, 1, 13)

Также можно записать как:

    Memory.Fn.X64(0xEFFB30, true)(13)

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

    var f = Memory.Fn.X64(0xEFFB30, true);
    f(13) // вызывает функцию 0xEFFB30 с аргументом 13
    f(11) // вызывает метод 0xEFFB30 с аргументом 11

Второе отличие состоит в том, что здесь нет параметра numParams. Каждый метод Fn вычисляет это автоматически.

По умолчанию возвращаемый результат считается 64-битным целым числом со знаком. Если функция возвращает другой тип (например, логическое значение), используйте один из методов, соответствующих сигнатуре функции:

    var flag = Memory.Fn.X64U8(0x1234567, true)()

Этот код вызывает функцию по адресу 0x1234567 + IMAGE_BASE без аргументов и сохраняет результат как 8-битное целое число без знака.

    var float = Memory.Fn.X64Float(0x456789, true)()

Этот код вызывает функцию по адресу 0x456789 + IMAGE_BASE без аргументов и сохраняет результат как значение с плавающей запятой.