Удаленный вызов игровой функции

UnknownPerson

Новичок
Автор темы
2
1
Привет! Не буду тянуть кота за хвост и перейду сразу к сути

В samp.dll имеется функция, которая создает клиентские команды (адрес функции — 0x69000). Выглядит функция так:
AddCommand:
void __thiscall CInput::AddCommand(CInput *this, const char *szName, void (__cdecl *handler)(const char *))

Соответственно функция принимает название команды и хандлер для обработки этой команды, который принимает введенные игроком параметры и исполняет какой-либо код

Я вызываю эту функцию удаленно через CreateRemoteThread и прочие приблуды, функция вызывается и выполняется, команда создается, с этим никаких проблем нет, но есть проблема с хандлером, который нужно засунуть в качестве второго аргумента функции

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

В качестве хандела я пытаюсь засунуть шеллкод:

shellcode:
byte[] asmBytes = new byte[]
{
    0x55,                   // push ebp
    0x89, 0xE5,             // mov ebp, esp
    0x89, 0xEC,             // mov esp, ebp
    0x5D,                   // pop ebp
    0xC3                    // ret
};

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

Запись в процесс:
memoryAddress = VirtualAllocEx(handle, IntPtr.Zero, asmBytes.Length, 0x1000, 0x40);
WriteMemoryBytes(memoryAddress, asmBytes);

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

Мне нужно, чтобы хандлер вызывал функцию AddEntry (0x67460) в случае, если игрок не ввел ни одного параметра в команде, и функцию SendCommand (0x69190) (отправляет сразу команду в чат), если пользователь ввел какие-то параметры в команде

Функция AddEntry:
void __thiscall CInput::AddCommand(CInput *this, const char *szName, void (__cdecl *handler)(const char *))

Я пробовал вызвать AddEntry следующим образом:

C#:
byte[] asmBytes = new byte[]
{
    0x55,                               // push ebp
    0x89, 0xE5,             // mov ebp, esp
    0x89, 0xEC,             // mov esp, ebp
    0x68, 0x00, 0x00, 0x00, 0x00,       // push CChatAddress (сюда вписывается samp.dll + 0x26E8C8)
    0x6A, 0x08,                         // push 0x8
    0x68, 0x74, 0x65, 0x78, 0x74,       // push "text" 
    0x68, 0x00, 0x00, 0x00, 0x00,       // push 0x0 (empty string for szPrefix)
    0x68, 0xFF, 0xFF, 0xFF, 0xFF,       // push 0xFFFFFF (textColor)
    0x68, 0xFF, 0xFF, 0xFF, 0xFF,       // push 0xFFFFFF (prefixColor)
    0xB9, 0x00, 0x00, 0x00, 0x00,       // mov ecx, AddEntryFunctionAddress (сюда вписывается samp.dll + 0x67460)
    0xFF, 0xD1,                         // call ecx
    0x5D,                               // pop ebp
    0xC3                                // ret
};

Но игра вызывает краш после ввода команды и попытки выполнить этот код. Хочу также подметить, что в ассемблере я не силен от слова совсем

Из всего вышенаписанного вытекает вопрос: реально ли вообще реализовать вызов функции AddCommand подобным образом и если да, то мне нужен пример того, как это можно правильно сделать с краткими объяснениями (ну или если есть желание, то с подробными, было бы вообще круто)

Все вышепоказанное я делал на C#, но примеры на C++ (если таковые имеются) также приму

Заранее спасибо :)
 
Решение
Да, привет!
Ты всё понял верно, шаги у тебя правильные.
На самом деле, нужно понять на каком моменте что-то ломается.

Код успешно регистрирует клиентскую команду и при использовании ее в игре краша нет, но и хандлер ничего не выполняет, а служит обычной "заглушкой"
Шаг первый. Нужно проверить, работает ли твоя заглушка. В консоль твоей программы выпиши адрес области, которая выделяется VirtualAllocEx (эта же функция и возвращает этот адрес). После инъекции, уже при работе программы, открываешь Cheat Engine (CE), дальше Memory Viewer, дальше Ctrl + G и вставляй этот адрес. У тебя должна открыться твоя же функция (те байты, которые ты написал). Выбираешь любой из них (любую строку) и нажимаешь F5 (либо ПКМ и «Break and trace...

RedHolms

Известный
Проверенный
619
366
на счёт C# не знаю вообще, но на C++ ты просто буквально передаешь функцию:
C++:
void handler(const char* arg) {
    /// ....
}

void reg() {
    CInput::AddCommand("mycmf", handler);
}

Я бы советовал поискать в интернете, возможно ли передать C#-пную функцию как аргумент C-шной (100% функция должна быть статической)

И еще, не балуйся с потоками, так как самп не thread-safe
 
  • Эм
Реакции: Vintik

whyega52

Гений, миллионер, плейбой, долбаеб
Модератор
2,783
2,609
на счёт C# не знаю вообще, но на C++ ты просто буквально передаешь функцию:
если я правильно понимаю, автор темы делает экстернал чит, в таком случае, если я я верно думаю, необходимо загружать байты (в данном случае функцию) внутрь виртуальной памяти процесса
 
  • Нравится
Реакции: Vintik

Vintik

Через тернии к звёздам
Проверенный
1,526
1,011
Да, привет!
Ты всё понял верно, шаги у тебя правильные.
На самом деле, нужно понять на каком моменте что-то ломается.

Код успешно регистрирует клиентскую команду и при использовании ее в игре краша нет, но и хандлер ничего не выполняет, а служит обычной "заглушкой"
Шаг первый. Нужно проверить, работает ли твоя заглушка. В консоль твоей программы выпиши адрес области, которая выделяется VirtualAllocEx (эта же функция и возвращает этот адрес). После инъекции, уже при работе программы, открываешь Cheat Engine (CE), дальше Memory Viewer, дальше Ctrl + G и вставляй этот адрес. У тебя должна открыться твоя же функция (те байты, которые ты написал). Выбираешь любой из них (любую строку) и нажимаешь F5 (либо ПКМ и «Break and trace instructions»). А теперь попробуй ввести в чат команду, которую ты зарегистрировал. CE должен будет остановить твою игру, а в меню Memory Viewer появится активная кнопка «Продолжить» (как при паузе). Если так, то это оно! Значит игра проходит через твою заглушку.

Но игра вызывает краш после ввода команды и попытки выполнить этот код. Хочу также подметить, что в ассемблере я не силен от слова совсем
Шаг второй. Я думаю, что в ASM коде мало кто силён. Если шаг первый успешно выполнен, то единственная задача — правильно написать функцию, не сломав при этом значения регистров и программный стек. Прототип твоей функции следующий:
C++:
void __cdecl handler(const char*);
И никто не мешает тебе в отдельной программе (не основной!) написать на C++ ту функцию, которая тебе нужна:
C++:
// создаёшь прототипы своих функций (я не знаю какие аргументы там)
typedef void(__thiscall* AddEntry)(int, int, const char*, int, int, int);

int SampDll; // получаешь адрес модуля samp.dll

void __cdecl handler(const char* arg)
{
    if (strlen(arg) == 0)
        reinterpret_cast<AddEntry>(SampDll + 0x67460)(SampDll + 0x26E8C8, 8, "text", 0x0, 0xFFFFFF, 0xFFFFFF);
    ...
}
А дальше компилируешь это всё дело в x32. Можешь в коде где-то в main() написать std::cout << (void*)handler;. Дальше опять заходишь в CE, Memory Viewer, Ctrl + G и вставляй этот адрес. Правой кнопкой мыши на выделенную строку — и выбирай «Выделить текущую функцию». Дальше можно скопировать байты («Copy —> Only Bytes») и вставить в массив. Всю неприятную работу по созданию ASM кода выполнит компилятор. Если изначальные прототипы функций (не только твоей, но и тех, что ты вызываешь внутри) верны, то краша быть не должно.
Даже при краше пишет адрес краша («Exception at address ...»), так ты можешь посмотреть, где и что конкретно вызывает краш.

upd.
Чтобы получить адрес модуля samp.dll воспользуйся этой функцией (это можно делать как внутри твоего вызова каждый раз, так и один раз при запуске):
C++:
DWORD GetProcId(const char* procname)
{
    PROCESSENTRY32 pe;
    HANDLE hSnap;

    pe.dwSize = sizeof(PROCESSENTRY32);
    hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
    if (Process32First(hSnap, &pe)) {
        do {
            if (strcmp(pe.szExeFile, procname) == 0)
                break;
        } while (Process32Next(hSnap, &pe));
    }
    return pe.th32ProcessID;
}

HMODULE GetModuleHandleExtern(const char *szModuleName, DWORD dwProcessId)
{
    if (!szModuleName || !dwProcessId) { return NULL; }
    HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwProcessId);
    if (hSnap == INVALID_HANDLE_VALUE) { return NULL; }
    MODULEENTRY32 me;
    me.dwSize = sizeof(MODULEENTRY32);
    if (Module32First(hSnap, &me))
    {
        while (Module32Next(hSnap, &me))
        {
            if (!strcmp(me.szModule, szModuleName))
            {
                CloseHandle(hSnap);
                return me.hModule;
            }
        }
    }
    CloseHandle(hSnap);
    return NULL;
}
Использование:
HMODULE SampDll = GetModuleHandleExtern("samp.dll", GetProcId("gta_sa.exe"));
// HMODULE — это обычный указатель, который занимает 4 байта в x32. Поэтому можешь использовать C-cast: (unsigned int)SampDll

Будут еще вопросы — пиши.
 
Последнее редактирование:
  • Вау
  • Нравится
Реакции: UnknownPerson и whyega52

UnknownPerson

Новичок
Автор темы
2
1
Огромное спасибо за помощь, подсказка с плюсами и CE мне очень помогла, правда, я сделал немого иначе — написал хандлер с asm кодом для вызова определенных функций, скомпилировал все в dll, инжектнул в игру и получил тело всей функции уже в виде шеллкода, ну и соответственно на шарпе я выполнил все те действия, которые описывал выше, в конечном итоге это сработало

Еще раз огромное спасибо
 
  • Нравится
Реакции: Vintik