Исходник Гайд Знакомимся с FFI и работаем с памятью в Lua

Доброго времени суток.
Сегодня суббота (на момент того, когда я закончил писать статью, уже воскресенье), а это значит, что самое время потерять остатки нейронных связей и пописать немножко на Lua. В этой статье мы рассмотрим, как можно использовать библиотеку FFI в ваших проектах. Также немножко поработаем с SAMP, дабы подкрепить все то, что изучили.
Советую все примеры ниже запускать самому, дабы было большее понимание о их работе.
Все примеры, связанные с SAMP будут ориентированы под R3, но вы с легко сможете это перенести на нужную Вам.

Lua FFI - библиотека, позволяющая вызывать внешние функции C и использовать структуры данных C. Для многих это просто набор слов, поэтому постараемся исправить это!
Lua:
-- Весь наш код будет нацелен под Windows, т.к. думаю у большинства именно эта OS

-- Подключаем библиотеку ffi
local ffi = require("ffi")



--[[
    Вызываем функцию cdef из таблицы ffi (т.е. из библиотеки).

    ffi.cdef позволяет нам писать некий C-код внутри Lua скриптов.
    Примеров такого кода может служить объявление функций, структур и каких-то других типов данных.
    Название функции говорит само за себя "c" обозначает то, что это C-объявления,
    "def" - в переводе "объявление".
]]
ffi.cdef([[
    /*
        Внутри строки, передаваемой в ffi.cdef действует только C-синтаксис.
        Таким образом мы не можем использовать "--" для написания комментариев.
        Поэтому как можно заметить, мы пользуемся комментариями из C.
    */

    // Объявляем функцию MessageBoxA из winuser.h, которая уже хранится в системе
    int MessageBoxA(void *hWnd, const char *lpText, const char *lpCaption, unsigned int uType);
]])



--[[
    Названия функций, что мы объявили внутри ffi.cdef хранится в таблице ffi.C
    Чтобы вызвать функцию по названию, нам необходимо использовать следующий синаксис:
]]
ffi.C.MessageBoxA(nil, "This is a guide to Lua FFI", "Hello", 0)
--[[
    Первым параметров мы передали nil, т.к. у нас нет указателя на hWnd, далее указываем текст и заголовок окна,
    последним параметром идет вид окна, в нашем случае это будет 0 (MB_OK).
    Поэкспериментируйте с этой функцией, передав в нее разные аргументы
]]
1701538244729.png
Поздравляю, ты написал свой первый код с использованием ffi, запустить его вы сможете используя LuaJIT, либо же внутри MoonLoader. Но с это все слишком просто, рассмотрим более интересный пример:
Lua:
local ffi = require("ffi")



ffi.cdef[[
    /*
        Создаем структуру Vector3D
        Она будет состоять из 3 полей: x, y, z - имеющих тип flaot

        "typedef" определяет тип данных, т.е. делаем его доступным для использования.
        "struct" указывает на то, что мы создаем структуру. 
    */
    typedef struct {
        float x;
        float y;
        float z;
    } Vector3D;


    /*
        Определять мы можем не только структуры, но и любые другие структуры.
        Например:
    */
    typedef unsigned long ULONG;
    // Создаем тип "ULONG", который эквивалентен unsigned long
]]



local function createVector(x, y, z)
    -- Создаем функцию для проверки типа данных
    local isNil = function(val) return type(val) == "nil" end

    -- Проверяем, что значения не равны
    x = isNil(x) and 0 or x
    y = isNil(y) and 0 or y
    z = isNil(z) and 0 or z

    -- Создаем новую структуру Vector3D и присваиваем ее полям значения, которые были переданы в функцию
    return ffi.new("Vector3D", x, y, z)
end



local emptyVector = createVector()
print("empty vector:", emptyVector.x, emptyVector.y, emptyVector.z)
local filledVector = createVector(31, 13, 8)
print("filled vector", filledVector.x, filledVector.y, filledVector.z)


-- Создаем тип данных ULONG со значением 4294967295
local penisLength = ffi.new("ULONG", 4294967295)
--[[
    Выводим значение penisLength.

    Для того, чтобы получить само число, необходимо использовать функцию tonumber.
    В исследовательских целях попробуйте убрать убрать преобразование в число и посмотреть результат
]]
print("Penis length:", tonumber(penisLength)) -- out: 4294967295
Это уже интересно, но все также мало где нам может пригодиться. Что на счет работы с памятью? Давайте попробуем реализовать какую-нибудь функцию для работы с SAMP.
Для начала нам необходимо определиться с тем, что мы хотим сделать. Для начала не будем делать ничего сложного, просто получим путь до chatlog.txt в нашей сборке.
Думаю, уже понятно, что придется найти адрес памяти, где хранится строка с путем до чатлога.

Этот гайд не совсем про реверс, да и я не тот человек, который будет этому учить. Однако какое-то представление о том, как это делается стоит иметь. Я буду использовать IDA Pro, вы также можете использовать любой другой дизассемблер (Cutter, Ghidra и т.д.).
Путь до чатлога это строка и будет логично просто отфильтровать все строки и найти нужную нам. Для этого в IDA можно использовать сочетание клавиш SHIFT + F12, либо же через GUI в верхней панеле выбрать "View" -> "Open subviews" -> "Strings". Далее опять же комбинацией клавиш, но уже CTRL+ F открываем фильтр и вводим туда название файла "chatlog.txt"
1701538985823.png
Двойным кликом переходим к определению строки, видим следующее:
1701539082463.png
Нажав на строку с нужной нам строкой (да, тавтология) мы выполняем сочетание "CTRL + X", которое отображает список, где используется данная строка (ссылки).
В списке находится только одна ссылка, поэтому сразу переходим к ней (ENTER).
1701540199369.png
Перед нами код на ассемблере, для большинства это непонятные буквы, которые ничего не значат. Чтобы хоть как-то упростить, нажимаем F5 и перед нами появляется псевдо-си код, который мы можем разобрать.
1701540795688.png
Немножко раскинув мозгами, мы понимаем, что значение хранится в byte_1026E558. Таким образом, адрес пути равен 0x26E558
На самом деле 10 это всего-лишь особенность дизассемблирования в IDA, это можно настроить перед тем, как вы закидываете файл на анализ.
И так, наш адрес в samp.dll нужной строки 0x26E558. Теперь напишем код, который будем его получать:
Lua:
local ffi = require("ffi")



function main()
    while not isSampAvailable() do wait(0) end

    sampRegisterChatCommand("get", function()
        local chatLogPath = getChatLogPath()
        -- Выводим значение в чат
        sampAddChatMessage(chatLogPath, -1)
    end)

    wait(-1)
end



function getChatLogPath()
    -- Получаем  адрес samp.dll в адресном пространстве gta_sa.exe
    local sampHandle = getModuleHandle("samp.dll")
    --[[
        Функция ffi.cast преобразует один тип данных в другой.
        Ее аналог в C++: reinterpret_cast.
        Таким образом мы конвертируем адрес строки в тип const char *.
        Что же за тип const char *? Пойдем справа налево:
        Звездочка - обозначает то, что это указатель.
        char - в переводе обозначает "символ". В компьютере за один символ берется 8 бит (1 байт).
        const - говорит нам о том, что тип не изменяемый, т.е. константа (как pi в математике или же плотсноть чего-либо в физике)
        Получаем следующее: мы преобразовываем значение по адресу samp.dll + 0x26E558 в указатель на неизменяемые байты
        и сохраняем их в переменную szChatLogPath.   
    ]]
    local szChatLogPath = ffi.cast("const char*", sampHandle + 0x26E558)
    --[[
        Если мы выведем вернем просто переменную szChatLogPath, то увидим примерно такую картину: cdata<const char *>: 0x198de558
        Это происходит потому, что lua не может (да ему это и не нужно) преобразовать указатель на байты в строку
        Преобразование байт в строку происходит путем сопоставления значения каждого байта с его значением в таблице ASCII,
        где каждый байт обозначает свой символ.
        Чтобы получить значение байта по указателю, нам необходимо сделать что-то вроде:
        print(szChatLogPath[0], szChatLogPath[1], szChatLogPath[2], ...) таким образом мы читаем каждый байт и переводим его в число.
        В встроенной библиотеке string у нас уже есть функция, которая сопоставляет число с его символом в таблице ASCII.
        Таким образом код будет выглядеть примерно так: print(string.char(szChatLogPath[0]), string.char(szChatLogPath[1]), string.char(szChatLogPath[2]), ...).
        В FFI есть функция, которая делает все это за Вас: ffi.string([bytes], [length])
        Мы можем явно не указывать длину, скорее всего FFI сам определит его
    ]]
    local chatLogPath = ffi.string(szChatLogPath)
    return chatLogPath
end
В книге Лоспинозо об указателях говорится следующее:
Указатели — это основной механизм, используемый для обращения к адресам памяти. Указатели кодируют обе части информации, необходимые для взаимодействия с другим объектом, то есть адрес объекта и тип объекта.
Это довольно обширная тема, поэтому стоит отдельно почитать об этом. В дальнейшем они еще пригодятся.
С получением разобрались, давайте теперь что-нибудь запишем память. Допустим, нам не нравится, что при входе пишется, что наша версия это R3. Изменим ее на R6!

Чтобы найти строку, мы повторяем первые шаги из прошлого примера, но в этот раз нам необходима сама строка, а не дальнейшее ее использования (в случае с чатлогом, там две строки объединялись и получалась одна общая, которую мы и читали).
1701543099871.png
Адрес строки: 0xE596C
При изменении чего-либо в памяти, нужно быть осторожным, дабы не записать ничего лишнего, либо наоборот недозаписать (с этим проще, т.к. не занятое пространство можно занять нулями).
Реализуем запись нового значения версии:

Lua:
local ffi = require("ffi")



function main()
    while not isSampAvailable() do wait(0) end

    -- Получаем указатель на строку, как и при чтении
    local szCaption = ffi.cast("const char*", getModuleHandle("samp.dll") + 0xE596C)
    --[[
        Теперь, нам необходимо преобразовать строку в указатель на const char
        Чтобы изменить szCaption, нам необходимо присвоить ей значение этих байт
    ]]
    szCaption = ffi.cast("const char*", "{FFFFFF}SA-MP {B9C9BF}0.3.7-R6 {FFFFFF}Started")
end
Если нам необходимо изменить только один знак в строке, нам не обязательно менять всю строку.
Lua:
local ffi = require("ffi")



function main()
    while not isSampAvailable() do wait(0) end

    --[[
        Как мы помним, каждый символ в ASCII это один байт, один байт.
        Таким образом, вместо того, чтобы переходить к началу строки по адерсу samp.dll + 0xE596C,
        мы можем перейти сразу на символ, который необходимо поменять.
        Это считается так: начало строки + количество символов до нужного нам символа
        0xE596C + 0x1F = 0xE598B
        0x1F - 31 в шестнадцатеричной системе счисления
    ]]
    local szCaption = ffi.cast("const char*", getModuleHandle("samp.dll") + 0xE598B)
    -- Заменяем один байт
    szCaption = ffi.cast("const char*", "6")
end

При работе с памятью какого-либо процесса нужно помнить про такую штуку, как протекция (protection - "защита"). Протекция это абстракция ОС, которая позволяет защищать какое-либо место в памяти от работы с ним из вне. В Lua FFI, мы можем воспользоваться WinAPI, которое предоставляет нам функцию VirtualProtect. Ее реализация выглядит так:
Lua:
local ffi = require("ffi")



ffi.cdef[[
    /*
        Из прошлых примеров помним, что перед использованием функций, нам необходимо их объявить,
        а также загрузить. К счастью, эта функция уже загружена автоматически
    */
    int VirtualProtect(uintptr_t lpAddress, unsigned long dwSize, unsigned long flNewProtect, unsigned long *lpflOldProtect);
]]




function main()
    while not isSampAvailable() do wait(0) end

    -- Получаем адрес относительно нашего пространства
    local address = getModuleHandle("samp.dll") + 0xE598B
    --[[
        Снимаем протекцию на запись и чтения (0x4) и получаем ее старое значение.
        0xE596C - адрес, по которому надо изменить, 1 - размер, в нашем случае мы меняем 1 байт (символ в строке),
        0x4 - константа протекции, подробнее можно почитать тут: https://learn.microsoft.com/ru-ru/windows/win32/Memory/memory-protection-constants
    ]]
    local oldProtect = virtualProtect(address, 1, 0x4)
    local szCaption = ffi.cast("const char*", address)
    szCaption = ffi.cast("const char*", "6")
    -- После работы с участком памяти, возвращаем старую протекцию
    print(oldProtect, virtualProtect(address, 1, oldProtect)) -- out: 64 (0x4 в шестнадцатеричном представлении)
end



-- Напишем обертку для C-функции, дабы было легче ее использовать
function virtualProtect(address, size, newProtect)
    -- Создаем массив на 1 элемент (другими словами, мы просто создаем указатель на пустое значение)
    local oldProtect = ffi.new("unsigned long[1]")
    -- Преобразуем адрес в unsigned int
    address = ffi.cast("uintptr_t", address)
    -- Вызываем функция из C-API
    ffi.C.VirtualProtect(address, size, newProtect, oldProtect)
    --[[
        Возвращаем первый элемент (в C все идет с 0) созданного нами массива.
        Вызов ffi.C.VirtualProtec изменил ее значение
    ]]
    return oldProtect[0]
end

С записью и чтением немножко разобрались. Теперь перейдем к вызову функций из процесса. Вызовем из samp.dll функцию, отвечающую за отображение сообщения в чате:
Lua:
local ffi = require("ffi")



function main()
    while not isSampAvailable() do wait(0) end

    sampRegisterChatCommand("msg", function(text)
        -- Передаем в функцию текст, который идет после команды /msg
        addChatMessage(text, -1)
    end)

    wait(-1)
end



function addChatMessage(text, color)
    local sampHandle = getModuleHandle("samp.dll")
    --[[
         Получаем указатель на Chat.
         В моей версии SAMP он хранится по адресу samp.dll + 0x26E8C8. На R1 это 0x21A0E4
    ]]
    local pChat = ffi.cast("uintptr_t*", sampHandle + 0x26E8C8)
    -- Конвертируем строку в байты
    local szText = ffi.cast("const char*", text)
    --[[
        Функция вызова CChat::AddMessage находится по адресу samp.dll + 0x5DF0.
        Поиск адресов это немного другая уже тема, поэтому возьмем готовые. На R1 это 0x645A0
        Ее прототип выглядит следующим образом: int(__thiscall*)(uintptr_t pChat, unsigned long color, const char *szText).
        Прототип это что-то вроде предварительного описания функция, описание возвращаемого значения, соглашение о вызове, а также аргументы функции
        void - возвращаемое значение (void - ничего, т.е. функция не возвращает значение), в скобочках указано соглашение о вызове, в нашем случае __thiscall,
        звездочка после обозначает, что мы хотим получить указатель на функцию.
        Далее в еще одних скобках указан список аргументов для вызова функции,
        в нашем случае ожидается адрес pChat. Это так называемый this,
        который используется в ООП (Объектно-Ориентированном Программировании). 
        Далее идут такие аргументы, как: текст и цвет сообщения
    ]]
    local pChatAddMessage = ffi.cast("void(__thiscall*)(uintptr_t pChat, unsigned long color, const char *szText)", sampHandle + 0x679F0)
    --[[
        Вызываем функцию из samp.dll, где разыменовываем указатель pChat и передаем его в функцию вместе с цветов и байтами текста.
        Разыменование указателя означает получение значения, на которое указывает указатель.
        Когда вы разыменовываете указатель, вы обращаетесь к значению, хранящемуся по адресу, указанному указателем.
    ]]
    pChatAddMessage(pChat[0], color, szText)
end
В коде упоминается тема соглашений о вызовах. Соглашения о вызове это некий "протокол", по которому процессор будет вызывать функцию. Обычно вам будут встречаться __thiscall, __stdcall, __cdecl. Не буду сильно останавливаться на этом, единственное, что упомяну это то, что __thiscall обычно используется в методах класса и передает значение this через регистр ecx.

Вот мы и подошли к вишенке на торте. Для того, чтобы лучше понять то, что я сейчас буду рассказывать, вам стоит познакомиться с метатаблицами. Хорошие темы по ним у @attack и @date.
Для C-структур также можно использовать метаметоды. В этом нам поможет функция ffi.metatype:

Lua:
local ffi = require("ffi")



ffi.cdef[[
    // Создадим структуру вектора
    typedef struct {
        float x;
        float y;
        float z;
    } Vector3D;
]]



-- Создадим таблицу методов для структуры
local Vector3D = {}



-- Создаем метод reset, который обнуляет значение вектора
function Vector3D:reset()
    self.x = 0
    self.y = 0
    self.z = 0
end


-- Создает метод length, который вычисляет длину вектора
function Vector3D:length()
    return math.sqrt(math.pow(self.x, 2) + math.pow(self.y, 2) + math.pow(self.z, 2))
end



--[[
    Функция ffi.metatype создает метатаблицу для C-структуры Vector3D
    Добавляем для нее поле __index, которое будет
]]
ffi.metatype(ffi.typeof("Vector3D"), {
    __index = function(self, key)
        local indexes = {self.x, self.y, self.z}
        local axis = indexes[key]
        if axis then return axis end

        return rawget(Vector3D, key)
    end
})



-- Создадим новый вектор
local vector = ffi.new("Vector3D", 3, 7, 10)

-- Проверим работу метаметодов и попробуем сначала вывести поля, а потом уже длину всего вектора
print(vector[1], vector[2], vector[3], vector:length())
-- Сбрасываем вектор
vector:reset()
-- Выводим длину вектора
print(vector:length())

А теперь реальная практика. Объединим все то, что прошли до. Создадим структуру BitStream, которая будет вызывать методы из samp.dll:
Lua:
local ffi = require("ffi")



ffi.cdef[[
    // Создаем два пользовательских типа для того, чтобы несколько раз не писать длинный тип
    typedef unsigned char BYTE;
    typedef void *PVOID;

    // Создаем структуру BitStream
    typedef struct {
        int    numberOfBitsUsed;
        int    numberOfBitsAllocated;
        int    readOffset;
        BYTE *data;
        bool  copyData;
        BYTE  stackData[256];
    } BitStream;


    // Объявляем прототипы функций, который в будущем нам понадобятся
    PVOID malloc(size_t size);
    void free(PVOID ptrmem);
]]



-- Загружаем библиотеку msvcrt.dll, из которой далее будем вызывать функции malloc и free, которые мы объявили в cdef
local msvcrt = ffi.load("msvcrt.dll")


-- Создаем таблицу, в которой будудут находится все методы нашего BitStreeam
local BitStream = {}



-- Это будет являться нашим конструктором, т.е. функцией, которая будет создавать новую структуру BitStream
function BitStream:new()
    --[[
        Тут мы инициализируем память для нашего BitStream.
        Функция msvcrt.malloc (объявленная в cdef) принимает в себя одно значение
        - размер участка памяти, который нам нужен в байтах. Для того, чтобы узнать,
        сколько нам необходимо байт, мы всоспользуемся функцией ffi.sizeof
        и передадим в нее название нашей структуры.
        Нам возвращается размер в байтах (размер структуры BitStream 276 байт),
        который мы успешно передаем в msvcrt.malloc.
        malloc в свою очередь возвращает нам указатель на адрес памяти, которую мы выделили
        Далее мы вызываем функцию ffi.gc - эта функция регистрирует каллбек для сборщика мусора,
        который сработает в тот момент, когда жизненный цикл нашей структуры подойдет к концу.
        Первый аргументом мы передаем указатель на адрес, на который нам необходимо установить каллбек,
        им является то значение, которое мы получили в результате вызова msvcrt.malloc,
        второй аргумент - функция, которые вызовится при удалении объекта.     
    ]]
    print(ffi.sizeof("BitStream"))
    local pvBitStream = ffi.gc(msvcrt.malloc(ffi.sizeof("BitStream")), self.delete)
    --[[
        Так как ffi.gc возвращает нам указатель с типом void* (с void мы не можем проводить никаких операций),
        нам необходимо преобразовать его в указатель на структуру BitStream
    ]]
    local bitstream = ffi.cast("BitStream*", pvBitStream)

    --[[
        Мы также могли создать объект типа BitStream, используя функцию ffi.new("BitStream"),
        но т.к. мы любим анальный секс, мы сделали это по-умному. Особенность нашего метода является то,
        что при вызове ffi.new, все поля структуры будут равны 0, в нашем же случае, нам необходимо самим задать им эти значения
    ]]
    bitstream.numberOfBitsUsed = 0
    bitstream.numberOfBitsAllocated = 256 * 8
    bitstream.readOffset = 0
    bitstream.data = ffi.new("BYTE[?]", bitstream.numberOfBitsAllocated / 8)
    bitstream.copyData = true

    print("BitStream initialized")

    -- В результате работы функции возвращаем укзатель ан структуру BitStream
    return bitstream
end


--[[
    Метод delete, благодаря которому мы сможем освобожить память, которую мы выделили.
    После вызова данного метода, мы не должны проводить никаких операций с нашей структурой, т.к.
    память уже не пренадлежит ей
]]
function BitStream:delete()
    -- Преобразуем указатель на BitStream в указатель на void (PVOID == void*, это мы объявили в cdef)
    local pvBitStream = ffi.cast("PVOID", self)
    -- Вызовем функцию, объявленную в ffi.cdef и загруженную из msvcrt.dll, передав в нее указатель на нашу структуру
    msvcrt.free(pvBitStream)
    -- Для наглядности выведем сообщение в консоль
    print("FREE")
end


-- Метод WriteUInt32 будет вызывать функцию из samp.dll для записи значения с типом unsigned int (32 бита) в нашу структуру
function BitStream:WriteUInt32(input)
    --[[
        На вход нам дается какое-то число, нам необходимо получить указатель на него.
        Для этого, как в коед с реализацией обертки VirtualProtect создадим масив на 1 элемент
    ]]
    local data = ffi.new("unsigned int[1]", input)
    -- Посмотрев в прототип функции сампа, можем понять, что она принимает указатель на BYTE, преобразуем наш массив в указатель байт
    local pData = ffi.cast("BYTE*", data)
    -- Преобразуем наш адрес (R1 - 0x1C4F0, R3 - 0x1F950) в lua-функцию
    local pBitStreamWrite = ffi.cast("void(__thiscall*)(BitStream *bitstream, BYTE *data, int length, char rightAlignedBits)", getModuleHandle("samp.dll") + 0x1F950)
    --[[
        Вызываем ее с нужными нам параметрами, где первое - наша структура BitStream,
        второе - указатель на наши данные, третье - размер в байтах, в нашем случае 32,
        четвертый - отвечает за тип записи
    ]]
    pBitStreamWrite(self, pData, 32, 1)
end


-- Метод ReadUInt32 наоборот - вызывает функцию чтения BitStream для 32 битов (4 байта)
function BitStream:ReadUInt32()
    -- Создаем пустой массив unsigned char на 4 байта
    local output = ffi.new("BYTE[4]")
    -- Преобразуем наш адрес (R1 - 0x1C3B0, R3 - 0x1F760) в lua-функцию
    local pBitStreamRead = ffi.cast("char(__thiscall*)(BitStream *bitstream, BYTE *data, unsigned int length)", getModuleHandle("samp.dll") + 0x1F760)
    --[[
        Вызываем функцию из samp.dll, передав в нее указатель на структуру,
        указатель на массив, в который необходимо записать прочтенные данные
        и размер чтения в байтах
    ]]
    pBitStreamRead(self, output, 4)
    --[[
        Не забываем, что у нас массив из 4 байт, а нам необходимо одно целое число,
        Поэтому конвертируем наш масссив ouput в указатель на 4 байтное значение,
        после чего разыменовываем указатль и возвращаем значение функции
    ]]
    return ffi.cast("unsigned int*", output)[0]
end


-- Метод WriteUInt32 будет вызывать функцию из samp.dll для записи значения с типом unsigned int (32 бита) в нашу структуру
function BitStream:WriteUInt16(input)
   -- Создаем массив на один элемент размером 16 бит
    local data = ffi.new("unsigned short[1]", input)
    local pData = ffi.cast("BYTE*", data)
    local pBitStreamWrite = ffi.cast("void(__thiscall*)(BitStream *bitstream, BYTE *data, int length, char rightAlignedBits)", getModuleHandle("samp.dll") + 0x1F950)
    -- В этот раз в качестве размера у нас уже не 32, а 16 бит
    pBitStreamWrite(self, pData, 16, 1)
end


-- Метод ReadUInt32 наоборот - вызывает функцию чтения BitStream для 16 битов (2 байта)
function BitStream:ReadUInt16()
    -- Создаем пустой массив unsigned char на 2 байта
    local output = ffi.new("BYTE[2]")
    local pBitStreamRead = ffi.cast("char(__thiscall*)(BitStream *bitstream, BYTE *data, unsigned int length)", getModuleHandle("samp.dll") + 0x1F760)
    -- Вместо 4 байт, передаем в этот раз 2 байта
    pBitStreamRead(self, output, 2)
    -- Преобразуем массив в 16-битное значение и также разыменовываем
    return ffi.cast("unsigned short*", output)[0]
end


-- Если Вам необходимо, то вы можете дополнить структуру остальными методами



--[[
    Устанавливаем метатаблицу для структуры BitStream,
    в поле __index мы запишем функцию, которая будет возваращть значение уже не из C-структуры BitStream,
    а из lua-таблицы BitStream при помощи функции rawget
]]
ffi.metatype(ffi.typeof("BitStream"), {
    __index = function(self, key)
        return rawget(BitStream, key)
    end
})



function main()
    -- Создадим новую структуру
    local bitstream1 = BitStream:new()
    -- Запишем в нее 4 байта, которые будут иметь значение 1337229
    bitstream1:WriteUInt32(1337229)
    -- На всякий случай выведем значения полей структуры
    print("data", bitstream1.readOffset, bitstream1.numberOfBitsUsed)
    -- Прочитаем значение размером 32 бита (4 байта)
    print("read", bitstream1:ReadUInt32())
    -- Т.к. наша работа с этой структурой окончена, удалим ее самостоятельно
    bitstream1:delete()

    -- Создадим еще одну структуру
    local bitstream2 = BitStream:new()
    -- Также записываем и читаем значение, но уже длиной 16 бит (2 байта)
    bitstream2:WriteUInt16(1337)
    print(bitstream2:ReadUInt16())
    bitstream2:delete()

    wait(-1)
end
Упоминая тему FFI, нельзя пройти мимо такой штуки, как callback. Callback-функции это такие функции, которые получаются в результате преобразования lua-функции в C-функцию. Например:
Lua:
local ffi = require("ffi")



-- Сосздадим функцию, которая будет принимать в себя число и возводить в квадрат
local function foo(arg)
    return arg^2
end



--[[
    Преобразуем нашу функцию в C-callback,
    который возвращает целочисленное значение и принимает в себя тоже целочисленное значение.
    Звездочка опять же указывает на то, что это у нас указатель.
    Если необходимо, мы также можем перед ней написать соглашение о вызове, которые мы используем (по стандарту __cdecl)
]]
local callback = ffi.cast("int(*)(int arg)", foo)
print(callback(3))
Применение каллбеков разнообразно. В пример возьмем установку хука на метод RakClient::Send, отвечающий за отправку пакетов на сервер.
Ставить мы будем хук на VMT (Таблица виртуальных методов) RakClient'а, поэтому перед тем, как перейти к коду, нам необходимо узнать, что это вообще такое?
Таблица виртуальных методов это некий "сборник" адресов функций, собранных в одном месте. В памяти это выглядит примерно так

1701614182322.png

Не обращайте внимания, что везде RakPeer, это ошибка базы данных, на самом деле там RakClient.
1701614359350.png
Приглядевшись, мы можем понять, что каждый адрес функции занимает 4 байта. Таким образом, чтобы изменить оригинальную функцию на нашу, нам нужно просто изменить эти 4 байта на адрес нашей функции.
Lua:
local ffi = require("ffi")



ffi.cdef[[ 
    int VirtualProtect(uintptr_t lpAddress, unsigned long dwSize, unsigned long flNewProtect, unsigned long *lpflOldProtect);
]]



local originalRakClientSend



function main()
    while not isSampAvailable() do wait(0) end

    -- Устанавливаем хук на 6 метод из таблицы RakClientInterface
    originalRakClientSend = installVMTHook(
        sampGetRakclientInterface(), "bool(__thiscall*)(void *pRakClient, uintptr_t bitstream,  char priority, char reliability, char orderingChannel)",
        RakClientSendHooked, 6
    )

    --[[
        Данный код не предусматривает выгрузку хука, поэтому при перезагрузке (выгрузке) скрипта - все крашнется.
        Реализацию полноценных хуков на Lua можно посмотреть тут: https://www.blast.hk/threads/55743/
    ]]



    wait(-1)
end


-- Функция, на которую мы заменяем оригинальную
function RakClientSendHooked(pRakClient, bitstream, priority, reliability, orderingChannel)
    -- Читаем первые 8 бит пакета и получает его ID
    local packetId = raknetBitStreamReadInt8(bitstream)
    -- Выводим информацию
    print(packetId, pRakClient, bitstream, priority, reliability, orderingChannel)
    --[[
        Возвращаем вызов оригинальной функции, если этого не сделать - игру крашнет.
        Наша функция возвращает true/false (указано в прототипе),
        поэтому для нопа пакета достаточно не вызывать оригинальную функцию и вернуть одно из булевых значений
    ]]
    return originalRakClientSend(pRakClient, bitstream, priority, reliability, orderingChannel)
end



function installVMTHook(pVMT, proto, func, index)
    -- Отключаем JIT-компиляцию для функции
    jit.off(func)
    -- Создаем C-каллбек
    local callback = ffi.cast(proto, func)
    -- Получаем указатель на VMT (она сама по себе указатель, поэтому 2 звездочки) и сразу же разыменовываем его
    local vmt = ffi.cast("uintptr_t**", pVMT)[0]
    -- Снимаем протекцию и сохраняем старую
    local oldProtect = virtualProtect(vmt + index, 4, 4)
    -- Получаем адрес оригинального метода и преобразуем его в C-функцию
    local originalMethod = ffi.cast(proto, vmt[index])
    -- Записываем в VMT адрес нашей функции
    vmt[index] = tonumber(ffi.cast("uintptr_t", callback))
    -- Возвращаем старую протекцию
    virtualProtect(vmt + index, 4, oldProtect)
    -- Возвращаем оригинальный метод
    return originalMethod
end



-- Функция для изменения проеткции
function virtualProtect(address, size, newProtect)
    local oldProtect = ffi.new("unsigned long[1]")
    address = ffi.cast("uintptr_t", address)
    ffi.C.VirtualProtect(address, size, newProtect, oldProtect)
    return oldProtect[0]
end




На этом, думаю, можно закончить.
Если забыл рассказать про что-то или остались какие-то вопросы, пишите в теме, постараюсь ответить
 
Последнее редактирование:

mnl

zkt-team | Между землёй и небом - война (с) Цой
Проверенный
1,553
586
гайд хуйня
 

Savok

Потрачен
178
292
Обратите внимание, пользователь заблокирован на форуме. Не рекомендуется проводить сделки.
IMG_20231119_162814_134.jpg
 
  • Нравится
  • Влюблен
Реакции: gaymerbro11, why ega и mnl

cord

contact me → cordtech.ru
Проверенный
555
409
спасибо за егу, гайд молодец
 
  • Нравится
Реакции: why ega

Gorskin

I shit on you
Проверенный
1,246
1,043
гайд говно автор говноед луаскриптер
Давай без оффтопа, свою реакцию можешь выразить с помощью кнопки "нравится". И удали своё мнение сообщение.

А гайд имеет место быть, хотя бы потому что чел наглядно показал как примерно можно что-то найти, а главное с помощью какой программы и какие кнопки надо нажимать. Хотя я думаю половине личинок этого форума все равно ничего не понятно, ибо все по своей же тупости лезут в имгуи, хуки, адреса. Не зная основ таких как типы переменных, о этом ещё долго можно писать, но я не вижу смысла. Ибо те самые личинки форума даже это сообщение читать не захотят.

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

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

movebx

Известный
48
146
Если честно, не особо вижу смысла в гайдах по ffi.
Если ты захочешь разбираться в ffi, то тебе, как минимум, надо знать основы C/C++, чтобы понимать как устроена память, размеры и названия типов данных.
Большинство людей на этом форуме не смогут банальный адрес функции найти, чтобы вызвать ее, а что уж говорить про какие то патчи с помощью ffi.
 
  • Нравится
Реакции: VanoKLR и Z3roKwq

why ega

РП игрок
Автор темы
Модератор
2,539
2,233
Тут спорно, лично я скорее наоборот, изучал основы си по ффи, до того, как я начал изучать его, мой максимум был написать простенькое говно с каким-нибудь АПИ, когда сейчас я могу написать уже более сложное говно.
В любом случае придётся либо знать, либо учить на ходу
Большинство людей на этом форуме не смогут банальный адрес функции найти
Для них уже есть всякие репозитории по типу самп апи, в которых есть большинство нужных адресов

не знаю почему, но впервые посмотрев на название темы, я прочитал его как "Гей знакомства"
Ты не ошибся
 
Последнее редактирование:
  • Нравится
Реакции: Willy4ka

tor1

Активный
158
42
code:
local oldProtect = virtualProtect(getModuleHandle("samp.dll") + 0xD3B8C + 0xA, 1, 0x4) --0D3B8C - Connected. Joining the game.
local szCaption = ffi.cast("const char*", getModuleHandle("samp.dll") + 0xD3B8C + 0xA)     --  Joining the game.
print(ffi.string(szCaption))
szCaption = ffi.cast("const char*", "A")
print(ffi.string(szCaption))
virtualProtect(getModuleHandle("samp.dll") + 0xD3B8C + 0xA, 1, oldProtect)

Привет, есть вопрос. Получаю строку по адресу корректно, но не могу перезаписать или делаю это неправильно. Я так понимаю после перезаписи при входе на сервер должна поменяться строка в чате или же нужно дополнительно что-то вызвать для этого?
 
  • Вау
Реакции: why ega

why ega

РП игрок
Автор темы
Модератор
2,539
2,233
code:
local oldProtect = virtualProtect(getModuleHandle("samp.dll") + 0xD3B8C + 0xA, 1, 0x4) --0D3B8C - Connected. Joining the game.
local szCaption = ffi.cast("const char*", getModuleHandle("samp.dll") + 0xD3B8C + 0xA)     --  Joining the game.
print(ffi.string(szCaption))
szCaption = ffi.cast("const char*", "A")
print(ffi.string(szCaption))
virtualProtect(getModuleHandle("samp.dll") + 0xD3B8C + 0xA, 1, oldProtect)

Привет, есть вопрос. Получаю строку по адресу корректно, но не могу перезаписать или делаю это неправильно. Я так понимаю после перезаписи при входе на сервер должна поменяться строка в чате или же нужно дополнительно что-то вызвать для этого?
Пока не дома, днём постараюсь посмотреть
 
  • Нравится
Реакции: tor1

kin4stat

mq-team
Всефорумный модератор
2,730
4,710
code:
local oldProtect = virtualProtect(getModuleHandle("samp.dll") + 0xD3B8C + 0xA, 1, 0x4) --0D3B8C - Connected. Joining the game.
local szCaption = ffi.cast("const char*", getModuleHandle("samp.dll") + 0xD3B8C + 0xA)     --  Joining the game.
print(ffi.string(szCaption))
szCaption = ffi.cast("const char*", "A")
print(ffi.string(szCaption))
virtualProtect(getModuleHandle("samp.dll") + 0xD3B8C + 0xA, 1, oldProtect)

Привет, есть вопрос. Получаю строку по адресу корректно, но не могу перезаписать или делаю это неправильно. Я так понимаю после перезаписи при входе на сервер должна поменяться строка в чате или же нужно дополнительно что-то вызвать для этого?
В текущем коде ты меняешь указатель, а не то, на что он указывается. Указатель это фактически число, просто с другим типом. Можешь представить свой код примерно так:

Lua:
local oldProtect = virtualProtect(getModuleHandle("samp.dll") + 0xD3B8C + 0xA, 1, 0x4) --0D3B8C - Connected. Joining the game.
local szCaption = getModuleHandle("samp.dll") + 0xD3B8C + 0xA     --  Joining the game.
szCaption = addrof("A")
virtualProtect(getModuleHandle("samp.dll") + 0xD3B8C + 0xA, 1, oldProtect)
Очевидно в таком случае что твой код меняет просто циферку внутри своего скрипта. Чтобы поменять текст, тебе надо либо записать другой текст внутрь указателя, либо заменить указатель на этот текст, там где он используется. В первом случае у тебя максимальная длина текста будет ограничена длиной буфера на который указывает указатель.

Собсна для первого случая код примерно такой:
Lua:
local oldProtect = virtualProtect(getModuleHandle("samp.dll") + 0xD3B8C + 0xA, 1, 0x4) --0D3B8C - Connected. Joining the game.
local szCaption = ffi.cast("const char*", getModuleHandle("samp.dll") + 0xD3B8C + 0xA)     --  Joining the game.
print(ffi.string(szCaption))
ffi.copy(szCaption, "A")
print(ffi.string(szCaption))
virtualProtect(getModuleHandle("samp.dll") + 0xD3B8C + 0xA, 1, oldProtect)
 
  • Нравится
Реакции: tor1 и why ega

tor1

Активный
158
42
В текущем коде ты меняешь указатель, а не то, на что он указывается. Указатель это фактически число, просто с другим типом. Можешь представить свой код примерно так:

Lua:
local oldProtect = virtualProtect(getModuleHandle("samp.dll") + 0xD3B8C + 0xA, 1, 0x4) --0D3B8C - Connected. Joining the game.
local szCaption = getModuleHandle("samp.dll") + 0xD3B8C + 0xA     --  Joining the game.
szCaption = addrof("A")
virtualProtect(getModuleHandle("samp.dll") + 0xD3B8C + 0xA, 1, oldProtect)
Очевидно в таком случае что твой код меняет просто циферку внутри своего скрипта. Чтобы поменять текст, тебе надо либо записать другой текст внутрь указателя, либо заменить указатель на этот текст, там где он используется. В первом случае у тебя максимальная длина текста будет ограничена длиной буфера на который указывает указатель.

Собсна для первого случая код примерно такой:
Lua:
local oldProtect = virtualProtect(getModuleHandle("samp.dll") + 0xD3B8C + 0xA, 1, 0x4) --0D3B8C - Connected. Joining the game.
local szCaption = ffi.cast("const char*", getModuleHandle("samp.dll") + 0xD3B8C + 0xA)     --  Joining the game.
print(ffi.string(szCaption))
ffi.copy(szCaption, "A")
print(ffi.string(szCaption))
virtualProtect(getModuleHandle("samp.dll") + 0xD3B8C + 0xA, 1, oldProtect)
Спасибо за помощь, получилось.