[Урок] Чтение Import/Export Table с помощью SourcePawn

Rostu

Добрая душа
Сообщения
986
Реакции
622
В данной статье я хочу рассказать вам о том, как можно с помощью языка SourcePawn можно прочитать таблицу импорта определенного dll файла.

Что необходимо знать:
Необходимо иметь базовые представления о том, что такое PE-Header исполняемого файла/библиотеки

Какие инструменты будут использоваться:
1) Некоторый функции Другое - [INC] Memory Extended
2) Голый SourcePawn

Предыстория
У вас, возможно, возник вопрос: Для чего все это, разве нельзя обойтись без всего этого?
Ответ:
Вам необходимо взять адрес g_pMemAlloc. Какие шаги необходимы для получения его?

1) Необходимо найти функцию в которой используется тот самый g_pMemAlloc.
2) Узнать сигнатуру данной функции.
3) Создать gamedata + узнать смещение до g_pMemAlloc внутри функции.
4) Использовать в самом плагине gamedata и получить адрес g_pMemAlloc.

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

Начало
Все начинается с PE формата, адрес которой = базовому адресу dll, а так же с этой иллюстрации, которая должна быть у вас перед глазами до конца вступительного текста
Для получения адреса таблицы импорта, нам необходимо дойти до этой части иллюстрации
2020-04-15_15-24-31.png

Первое что необходимо сделать, это получить PE заголовок, адрес которого находится что по смещению 0x3C
2020-04-15_15-36-43.png
.
Второй наш шаг, по иллюстрации, узнать, каков размер всего PE заголовка => Он является = 0x78, так же добавляем 0x08 к нашему смещению т.к нам необходимо пропустить таблицу экспорта
2020-04-15_15-48-55.png

В итоге смещение до адреса таблицы = 0x80, но как это выглядит в SourcePawn? -
C-подобный:
    Address pBase = GetModuleHandle("engine");

    Address PE = pBase + view_as<Address>(LoadFromAddress(pBase + view_as<Address>(0x3C), NumberType_Int32)); // Адрес PE заголовка

    Address importTable = pBase +  view_as<Address>(LoadFromAddress(PE + view_as<Address>(0x80), NumberType_Int32)); // Адрес таблицы импорта
    Address importTableSize = view_as<Address>(LoadFromAddress(PE + view_as<Address>(0x84), NumberType_Int32)); // Размер таблицы импорта
Теперь у нас есть тот самый адрес таблицы импорта, но что же делать дальше с этим?
Для этого необходимо погрузиться глубже....

Чтение таблицы импорта
Когда я задавался вопросом, что вообще делать дальше - я нашел статью на хабре. Из нее становится ясно, что нам необходимо воссоздать на SourcePawn структуру IMAGE_IMPORT_DESCRIPTOR (Автор предложил смотреть структуру в WINNT.h - но это не обязательно, все это есть в документации => PE Format - Win32 apps)

Но до конца, что это такое, помогла вот эта статья, которая расставила все на свои места - Import x64

На основе полученной информации у меня получилась такая структура
C-подобный:
#define CALC_STRUCT_OFFSET(%0) view_as<Address>(%0 * 0x04)
#define STRUCT_SIZE(%0) (sizeof(%0) * 0x04)

enum struct ImportDiscription
{
    Address originalFirstThunk;
    int timeDateStamp;
    int forwarderChaine;
    Address dllName;
    Address firstThunk;

    void FillInfo(Address base, Address addr)
    {
        this.originalFirstThunk = base + view_as<Address>(LoadFromAddress(addr + CALC_STRUCT_OFFSET(ImportDiscription::originalFirstThunk), NumberType_Int32));
        this.timeDateStamp      = LoadFromAddress(addr + CALC_STRUCT_OFFSET(ImportDiscription::timeDateStamp), NumberType_Int32);
        this.forwarderChaine       = LoadFromAddress(addr + CALC_STRUCT_OFFSET(ImportDiscription::forwarderChaine), NumberType_Int32);
        this.dllName            = base + view_as<Address>(LoadFromAddress(addr + CALC_STRUCT_OFFSET(ImportDiscription::dllName), NumberType_Int32));
        this.firstThunk         = base + view_as<Address>(LoadFromAddress(addr + CALC_STRUCT_OFFSET(ImportDiscription::firstThunk), NumberType_Int32));
    }
    void GetName(char[] sBuffer, int iMaxLength)
    {
        ReadString(this.dllName, sBuffer, iMaxLength);
    }
}

Что же такое #define CALC_STRUCT_OFFSET(%0) view_as<Address>(%0 * 0x04) и зачем это нужно?
В SourcePawn при попытке узнать смещение элемента в enum struct, например, к dllName, вернется значение = 0x03, но у dllName для корректного чтения информации из памяти, необходимо, чтобы смещение = 0x12, по этому, каждое смещение структуры мы умножаем на 4.
Такая же история, если попытаться узнать размер всей структуры - SourcePawn вернет 0x05 - по этому, мы так же умножаем размер на 4 с помощью STRUCT_SIZE

Далее нам необходимо узнать, сколько всего динамических библиотек задействовано, это можно сделать так: Размер таблицы импорта / размер структуры IMAGE_IMPORT_DESCRIPTOR (У нас это ImportDiscription)
На SourcePawn это выглядит так int iSize = view_as<int>(importTableSize) / STRUCT_SIZE(ImportDiscription);

Остается понять то, как найти во всем этом, нужную нам переменную/функцию. Я не буду описывать весь процесс, ведь это описано в статье, которую я уже упоминал чуть выше => выглядит это примерно так, как изображено на иллюстрации ниже (Вырезал из статьи)
2020-04-15_16-39-42.png

Цель статьи, ведь, показать - как это реализовать на SourcePawn.
Код поиска выглядит так:

C-подобный:
Address GetProcAddress(const char[] sLibrary, const char[] sName )
{
    return GetServerOS() == OS_Windows ? GetProcAddressWindows(sLibrary, sName) : GetProcAddressLinux(sLibrary, sName);
}
Address GetProcAddressWindows(const char[] sLibrary, const char[] sName )
{
    Address pBase = GetModuleHandle(sLibrary);

    if(pBase == Address_Null)
    {
        return Address_Null;
    }

    Address PE = pBase + view_as<Address>(LoadFromAddress(pBase + view_as<Address>(0x3C), NumberType_Int32));

    Address importTable = pBase +  view_as<Address>(LoadFromAddress(PE + view_as<Address>(0x80), NumberType_Int32));
    Address importTableSize = view_as<Address>(LoadFromAddress(PE + view_as<Address>(0x84), NumberType_Int32));
    int iSize = view_as<int>(importTableSize) / STRUCT_SIZE(ImportDiscription);

    ImportDiscription disc;
    //char sDll[64];
    char sFunction[256];

    for(int x = 0; x < iSize; x++)
    {
        Address addr = importTable + view_as<Address>((STRUCT_SIZE(ImportDiscription) * x));
        disc.FillInfo(pBase, addr);
        // disc.GetName(sDll, sizeof sDll);  Это нигде не используется - но для демонстрации было оставлено

        for(Address offset = Address_Null; offset >= Address_Null; offset += view_as<Address>(0x04)) // endless cycle
        {
            Address pFunctionStr = view_as<Address>(LoadFromAddress(disc.originalFirstThunk + offset, NumberType_Int32));

            if(pFunctionStr == Address_Null)
            {
                break;
            }

            pFunctionStr += pBase;

            ReadString(pFunctionStr + view_as<Address>(0x02), sFunction, sizeof sFunction);

            if(!strcmp(sName, sFunction))
            {
                return view_as<Address>(LoadFromAddress(disc.firstThunk + offset, NumberType_Int32));
            }
            
        }
    }

    return Address_Null;
}
Address GetProcAddressLinux(const char[] sLibrary, const char[] sName )
{
//....
}

Я бы хотел остановится на этом участке кода и рассказать поподробнее, что тут происходит.
C-подобный:
            ReadString(pFunctionStr + view_as<Address>(0x02), sFunction, sizeof sFunction);

            if(!strcmp(sName, sFunction))
            {
                return view_as<Address>(LoadFromAddress(disc.firstThunk + offset, NumberType_Int32));
            }

Почему я делаю смещение на 0x02 - можно понять из вот этого - PE Format - Win32 apps .
В памяти это выглядит так (Т.е первые два байта - это просто индекс, а далее идет то, что нам необходимо - название)
2020-04-15_16-50-29.png
Позже сравниваем названия - и если оно совпадает - возвращаем нужный нам адрес.

Пример работоспособности
C-подобный:
#include <MemoryEx/ServerLibrary>

#define CALC_STRUCT_OFFSET(%0) view_as<Address>(%0 * 0x04)
#define STRUCT_SIZE(%0) (sizeof(%0) * 0x04)

enum struct ImportDiscription
{
    Address originalFirstThunk;
    int timeDateStamp;
    int forwarderChaine;
    Address dllName;
    Address firstThunk;

    void FillInfo(Address base, Address addr)
    {
        this.originalFirstThunk = base + view_as<Address>(LoadFromAddress(addr + CALC_STRUCT_OFFSET(ImportDiscription::originalFirstThunk), NumberType_Int32));
        this.timeDateStamp      = LoadFromAddress(addr + CALC_STRUCT_OFFSET(ImportDiscription::timeDateStamp), NumberType_Int32);
        this.forwarderChaine       = LoadFromAddress(addr + CALC_STRUCT_OFFSET(ImportDiscription::forwarderChaine), NumberType_Int32);
        this.dllName            = base + view_as<Address>(LoadFromAddress(addr + CALC_STRUCT_OFFSET(ImportDiscription::dllName), NumberType_Int32));
        this.firstThunk         = base + view_as<Address>(LoadFromAddress(addr + CALC_STRUCT_OFFSET(ImportDiscription::firstThunk), NumberType_Int32));
    }
    void GetName(char[] sBuffer, int iMaxLength)
    {
        ReadString(this.dllName, sBuffer, iMaxLength);
    }
}

public void OnPluginStart()
{
    Address pProcId = GetProcAddress("server", "GetCurrentProcessId");

    PrintToServer("pProcId = 0x%X offsetoriginal = 0x%X time off = %d sizeof = %d", pProcId, ImportDiscription::originalFirstThunk, ImportDiscription::timeDateStamp, sizeof(ImportDiscription));

    if(pProcId == Address_Null)
    {
        return;
    }

    StartPrepSDKCall(SDKCall_Static);
    PrepSDKCall_SetAddress(pProcId);
    PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain);

    Handle h = EndPrepSDKCall();
    int iProcID = SDKCall(h);

    PrintToServer("CurrentProcessId = %d", iProcID);
    delete h;
}
Address GetProcAddress(const char[] sLibrary, const char[] sName )
{
    return GetServerOS() == OS_Windows ? GetProcAddressWindows(sLibrary, sName) : GetProcAddressLinux(sLibrary, sName);
}
Address GetProcAddressWindows(const char[] sLibrary, const char[] sName )
{
    Address pBase = GetModuleHandle(sLibrary);

    if(pBase == Address_Null)
    {
        return Address_Null;
    }

    Address PE = pBase + view_as<Address>(LoadFromAddress(pBase + view_as<Address>(0x3C), NumberType_Int32));

    Address importTable = pBase +  view_as<Address>(LoadFromAddress(PE + view_as<Address>(0x80), NumberType_Int32));
    Address importTableSize = view_as<Address>(LoadFromAddress(PE + view_as<Address>(0x84), NumberType_Int32));
    int iSize = view_as<int>(importTableSize) / STRUCT_SIZE(ImportDiscription);

    ImportDiscription disc;
    //char sDll[64];
    char sFunction[256];

    for(int x = 0; x < iSize; x++)
    {
        Address addr = importTable + view_as<Address>((STRUCT_SIZE(ImportDiscription) * x));
        disc.FillInfo(pBase, addr);
        //disc.GetName(sDll, sizeof sDll);


        for(Address offset = Address_Null; offset >= Address_Null; offset += view_as<Address>(0x04)) // endless cycle
        {
            Address pFunctionStr = view_as<Address>(LoadFromAddress(disc.originalFirstThunk + offset, NumberType_Int32));

            if(pFunctionStr == Address_Null)
            {
                break;
            }

            pFunctionStr += pBase;

            ReadString(pFunctionStr + view_as<Address>(0x02), sFunction, sizeof sFunction);
            
            if(!strcmp(sName, sFunction))
            {
                return view_as<Address>(LoadFromAddress(disc.firstThunk + offset, NumberType_Int32));
            }
            
        }
    }

    return Address_Null;
}
stock Address GetProcAddressLinux(const char[] sLibrary, const char[] sName )
{
 // ...
}

2020-04-15_16-59-50.png

Когда нибудь, этот текст дополнится и для linux, если это будет необходимо.
Но, пока что, это моя первая статья на данном форуме, по этому - постарался более подробно все расписать 🤔
 

Вложения

  • importTable.smx
    8 КБ · Просмотры: 4
  • importTable.sp
    3.6 КБ · Просмотры: 5

Rostu

Добрая душа
Сообщения
986
Реакции
622
Чтение Export Table на SourcePawn [Аналог GetProcAddress]
Требования: MemoryEx версии 2.6 и выше
C-подобный:
stock Address GetProcAddressWindows(Address pBase, const char[] sName)
{
    Address pExportTable = pBase + LoadFromAddressEx(WindowsGetPEHeader(pBase) + view_as<Address>(0x78), NumberType_Int32);

    int iNumFunc = LoadFromAddress(pExportTable + view_as<Address>(0x14), NumberType_Int32);

    Address pFunctions     = pBase + LoadFromAddressEx(pExportTable + view_as<Address>(0x1C), NumberType_Int32);
    Address pNames         = pBase + LoadFromAddressEx(pExportTable + view_as<Address>(0x20), NumberType_Int32);
    Address pOrdinals     = pBase + LoadFromAddressEx(pExportTable + view_as<Address>(0x24), NumberType_Int32);

    static char sExport[512];

    Address pStr;
    int iBytes;
    int iStrLen = strlen(sName);
    int iOrdinal;

    for(int x = 0; x < iNumFunc; x++)
    {
        pStr = pBase + LoadFromAddressEx(pNames + view_as<Address>(x * 0x04), NumberType_Int32);
        iBytes = ReadString(pStr, sExport, sizeof sExport);

        if(iBytes == iStrLen)
        {
            if(!strcmp(sName, sExport))
            {
                iOrdinal = LoadFromAddress(pOrdinals + view_as<Address>(x * 0x02), NumberType_Int16);
                return pBase + LoadFromAddressEx(pFunctions + view_as<Address>(iOrdinal * 0x04), NumberType_Int32);
            }
        }
    }

    return Address_Null;
}
C-подобный:
#include <MemoryEx>

public void OnPluginStart()
{
    Address pProcId =  g_hMem.GetProcAddress("kernel32", "GetCurrentProcessId");

    PrintToServer("pProcId = 0x%X", pProcId);

    if(pProcId == Address_Null)
    {
        return;
    }

    StartPrepSDKCall(SDKCall_Static);
    PrepSDKCall_SetAddress(pProcId);
    PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain);

    Handle h = EndPrepSDKCall();
    int iProcID = SDKCall(h);

    PrintToServer("CurrentProcessId = %d", iProcID);
    delete h;
}
2020-04-28_09-45-10.png
 

Dragokas

Добрая душа
Сообщения
229
Реакции
213
Спасибо. Отличная статья!
Вам необходимо взять адрес g_pMemAlloc. Какие шаги необходимы для получения его?
А какое отношение ко всему этому имеет g_pMemAlloc?
Если я правильно понимаю, зная его адрес, тоже можно вычислить начало таблицы импорта?
Если да, то как это примерно делается, т.е. как он вообще соотносится с каким либо из адресов из PE?
 

Rostu

Добрая душа
Сообщения
986
Реакции
622
Спасибо. Отличная статья!

А какое отношение ко всему этому имеет g_pMemAlloc?
Если я правильно понимаю, зная его адрес, тоже можно вычислить начало таблицы импорта?
Если да, то как это примерно делается, т.е. как он вообще соотносится с каким либо из адресов из PE?
Он был взят для примера, код игры обновляется и сигнатура функции, из которой берется g_pMemAlloc - может сломаться
В этой статье я хотел показать то - что все можно делать без этого - просто узнавая адрес с помощью таблицы импорта (Или экспорта, если брать из tier0 библиотеки)
Идет наследие философии из MemoryEx - первые строчки описания
1608241845370.png


Нет, вы не сможете узнать начало таблицы импорта с помощью этого(И не нужно это, вам же нужен адрес интересующей вас вещи, не так ли?)
В самом первом коде в этой статье даны строчки как получить эту информацию
 
Сверху Снизу