Доброго времени суток.
Сегодня суббота (на момент того, когда я закончил писать статью, уже воскресенье), а это значит, что самое время потерять остатки нейронных связей и пописать немножко на Lua. В этой статье мы рассмотрим, как можно использовать библиотеку FFI в ваших проектах. Также немножко поработаем с SAMP, дабы подкрепить все то, что изучили.
Советую все примеры ниже запускать самому, дабы было большее понимание о их работе.
Все примеры, связанные с SAMP будут ориентированы под R3, но вы с легко сможете это перенести на нужную Вам.
Lua FFI - библиотека, позволяющая вызывать внешние функции C и использовать структуры данных C. Для многих это просто набор слов, поэтому постараемся исправить это! Советую все примеры ниже запускать самому, дабы было большее понимание о их работе.
Все примеры, связанные с SAMP будут ориентированы под R3, но вы с легко сможете это перенести на нужную Вам.
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).
Поэкспериментируйте с этой функцией, передав в нее разные аргументы
]]
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
Для начала нам необходимо определиться с тем, что мы хотим сделать. Для начала не будем делать ничего сложного, просто получим путь до
chatlog.txt
в нашей сборке.Думаю, уже понятно, что придется найти адрес памяти, где хранится строка с путем до чатлога.
Этот гайд не совсем про реверс, да и я не тот человек, который будет этому учить. Однако какое-то представление о том, как это делается стоит иметь. Я буду использовать IDA Pro, вы также можете использовать любой другой дизассемблер (Cutter, Ghidra и т.д.).
Путь до чатлога это строка и будет логично просто отфильтровать все строки и найти нужную нам. Для этого в IDA можно использовать сочетание клавиш SHIFT + F12, либо же через GUI в верхней панеле выбрать "View" -> "Open subviews" -> "Strings". Далее опять же комбинацией клавиш, но уже CTRL+ F открываем фильтр и вводим туда название файла "chatlog.txt"
Двойным кликом переходим к определению строки, видим следующее:
Нажав на строку с нужной нам строкой (да, тавтология) мы выполняем сочетание "CTRL + X", которое отображает список, где используется данная строка (ссылки).
В списке находится только одна ссылка, поэтому сразу переходим к ней (ENTER).
Перед нами код на ассемблере, для большинства это непонятные буквы, которые ничего не значат. Чтобы хоть как-то упростить, нажимаем F5 и перед нами появляется псевдо-си код, который мы можем разобрать.
Немножко раскинув мозгами, мы понимаем, что значение хранится в
Путь до чатлога это строка и будет логично просто отфильтровать все строки и найти нужную нам. Для этого в IDA можно использовать сочетание клавиш SHIFT + F12, либо же через GUI в верхней панеле выбрать "View" -> "Open subviews" -> "Strings". Далее опять же комбинацией клавиш, но уже CTRL+ F открываем фильтр и вводим туда название файла "chatlog.txt"
В списке находится только одна ссылка, поэтому сразу переходим к ней (ENTER).
byte_1026E558
. Таким образом, адрес пути равен 0x26E558
На самом деле 10 это всего-лишь особенность дизассемблирования в IDA, это можно настроить перед тем, как вы закидываете файл на анализ.
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!
Чтобы найти строку, мы повторяем первые шаги из прошлого примера, но в этот раз нам необходима сама строка, а не дальнейшее ее использования (в случае с чатлогом, там две строки объединялись и получалась одна общая, которую мы и читали).
Адрес строки:
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
Вот мы и подошли к вишенке на торте. Для того, чтобы лучше понять то, что я сейчас буду рассказывать, вам стоит познакомиться с метатаблицами. Хорошие темы по ним у @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
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))
Ставить мы будем хук на VMT (Таблица виртуальных методов) RakClient'а, поэтому перед тем, как перейти к коду, нам необходимо узнать, что это вообще такое?
Таблица виртуальных методов это некий "сборник" адресов функций, собранных в одном месте. В памяти это выглядит примерно так
Не обращайте внимания, что везде RakPeer, это ошибка базы данных, на самом деле там RakClient.
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
На этом, думаю, можно закончить.
Если забыл рассказать про что-то или остались какие-то вопросы, пишите в теме, постараюсь ответить
Если забыл рассказать про что-то или остались какие-то вопросы, пишите в теме, постараюсь ответить
Последнее редактирование: