Как работает встройка в .dll (и лимит денег игрока)

Кратко:

Используется утилка https://github.com/jewalky/DLLInject. Эта утилка добавляет пользовательский загрузчик в конец исполняемого файла, а также создает кастомную таблицу импортов:

  • Сначала создается DLL.
  • Затем берется оригинальный исполняемый файл (например, сервер A2).
  • В этот исполняемый файл в определенные адреса, где расположены функции сервера А2, вставляется код из DLL, изменяя эти функции.

Author: Vladimir Chebotarev aka ex-lend
Edit: igroglaz, As Bestos, Zidane

Server modification to support more than 2 billions of gold coins

Пришел в голову простой способ реализации хранения более 2G денег.

Суть в следующем:
1) Делаем программу с базой, которая будет хранить соответствие (login name, char id) <-> money
2) Добавляем команды:

  • #money_get XXX — забирает из вышеперечисленной программы деньги данного персонажа
  • #money_put XXX — делает обратный процесс, то есть добавляет деньги в программу и убавляет у персонажа
  • #money_info — пишет на экране баланс дополнительного счета персонажа

В итоге можно создавать для каждого персонажа бесконечное хранилище бабла. Правда использовать единовременно можно не больше 2G. Но зато все будет работать без глюков и не нужно модифицировать клиент (в отличие от реализации нативного варианта).

Возможные трудности реализации:

  • Как вставить код в сервер?
    Нужно сделать dll, экспортировать в ней подпрограмму. C помощью программы add_dll из раздела Файлы можно воткнуть вызов подпрограммы из dll в любое место.
  • Как добавить новую команду?
    Ищем в дизассемблере #ready/#set latency/#show latency. Эти коды доступны клиентам. Потом идем по обращениям к ним. Оказываемся в месте, где обрабатываются коды. То куда идет команда - функция поиска подстроки в строчке, пришедшей от клиента. Добавьте свою команду аналогичным образом.
  • Откуда вытащить char id и имя логина?
    В упомянутой подпрограмме обработки команд arg_0 - указатель на экземпляр класса Player. Имя логина лежит по адресу [[arg_0] + 0xA78]. Идентификатор персонажа - два двойных слова: [[arg_0] + 0x10], [[arg_0] + 0x14].
  • Как работать с количеством денег игрока?
    Метод Player'а - a2server.exe:00534AC1, типа вызова stdcall, аргумент 1 - число добавляемых денег, аргумент 2 - флаг, если 1, то клиент не увидит сообщения "Вы подняли сколько-то там бабла". В ecx само собой записывается указатель на Player. Если посмотреть на его внутренности, то выяснится, что деньги лежат по адресу +0x3C относительно Player.
  • Как отправить игроку сообщение?
    Метод a2server.exe:0051CD89. В ecx нужно записать 006C3A08. Тип вызова stdcall. Аргумент 1 - указатель на указатель на строку (char **), аргумент 2 - Player, которому написать сообщение, или 0, если нужно отправить его всем.

Дополнение статьи:

Я смотрю экзешник в hex editor’e обычном и он начинается с 0 (адрес)… А в гидре тоже самое, но начинается с 004000. Почему?

Zidane: Потому что hex-редактор просто просматривает любой файл, а в гидре он «загружен» в соответствии с заголовками PE. Ну и по идее, если говорить про старые режимы работы проца(времена дос, 16 бит) — возможно на адреса от 0 было что-то «замаплено».

As Bestos: В хекс-редакторе оффсет от начала файла В гидре оффсет от начала файла + от базового адреса указанного в PE заголовке.

Offset — это оператор ассемблера, который передает в какой-нибудь регистр смещение определенных данных относительно начала сегмента данных. Фактически адрес начала каких-то данных.

Теперь разберем пример:

    // добавить денег игроку
    void __declspec(naked) GiveMoney(byte* pptr, unsigned long count,
                                     unsigned long flags)
    {
        __asm
        {
            push    ebp
            mov        ebp, esp
            push    [ebp+0x10]
            push    [ebp+0x0C]
            mov        ecx, [ebp+0x08]
            mov        edx, 0x00534AC1
            call    edx
            mov        esp, ebp
            pop        ebp
            retn
        }
    }

As Bestos:

в стеке (esp) хранятся аргументы 0x8 pptr 0xC count 0x10 flags код запихивает два последних в стек, а первый в ecx и вызывает функцию по адресу 0x00534AC1 которая судя по всему и есть собственно гив мани внутри а2 она уже вытаскивает их опять из стека и делает гив мани с какими именно соглашениями имеем дело — мне не очень понятно для самой GiveMoney аргументы идут через стек а для вызываемой через call edx один идет в регистр ecx, так что я хз. перед вызовом функции GiveMoney имеем

push flags
push long
push pptr

в стеке получается 0x0 pptr 0x4 count 0x8 flags при вызове GiveMoney в стек закидывается ret addr и адрес аргументов сдвигается >0x4 pptr и ещё в самой GiveMoney в стек накидывается push ebp >0x8 pptr _declspec(naked) говорит не делать вот это

Перед вызовом функции вставляется код, называемый прологом (англ. prolog) и выполняющий следующие действия:

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

восстановление значений регистров, сохранённых кодом пролога;
очистка стека (от локальных переменных функции).

при вызове GiveMoney во кстати глянь в гидре шо там в 0x00534AC1…


Что такое hi.com ?

p.bat

for %%A in (bug_0*.xck) do call p %%A ..\a2server.exe

патчи эти тупо ищут кусочек, его затирают и ровно такой же по размеру вставляют кусочек вместо.

в патче судя по всему старый байт и новый байт. Например:

00085A76: 85 90
00085A77: C0 90
00085A78: 0F 90

90 это NOP; так что какой-то код затирается nop’ами.


 

int __declspec(naked) __stdcall this_call_impl()
{
    __asm
    {
        pop     eax     // pop esp
        pop     eax     // pop eax from the wrapper
        pop     edx     // pop method ptr
        pop     ecx     // pop this
        push    eax     // restore esp from call to the wrapper
        jmp     edx     // jump to the method
    }
}