• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Обфускация API-вызовов | Трушный метод

dopeware

CD-диск
Пользователь
Регистрация
28.09.2019
Сообщения
11
Реакции
6
Здравствуйте, многоуважаемые форумчане.
Сегодня я хочу поведать вам о банальной технологии обфускации API вызвов. Эта методика сильно усложнит жизнь начинающему реверсеру и немного уменьшит вес вашего софта.
Для того, что бы скрыть вызовы WinApi функций из под нашей программы - мы будем парсить таблицу экспорта нужных нам библиотек своими ручками.
Думаю, у вас сейчас есть один вопрос - а как мы получим дескриптор нужной нам библиотеки?

Всё просто. В каждый процесс в ОС Windows по дефолту грузятся несколько библиотек. Основные - Kernel32.dll и Ntdll.dll, в зависимости от нахождения в эмуляции ( Wow64 ) могут грузиться и другие,
однако это нам не нужно в контексте данного гайда.

В системе Windows есть структура под названием PEB. ( Process Environment Block, блок информации о процессе. Подробнее - https://en.wikipedia.org/wiki/Process_Environment_Block )
Оттуда мы сможем взять дескриптор модуля Kernel32.dll, подтянуть из его таблицы экспорта LoadLibrary, а затем грузить нужные нам библиотеки, доставая из них нужные функции.
Для некоторых вышеописанное может показаться сложной задачей, однако поверьте, всё банально проосто. Сейчас я вам это наглядно покажу.
Давайте напишем функцию, которая вернёт нам дескриптор модуля Kernel32.dll.

Код:
HMODULE GetKernel32()
{
    __asm
    {
        mov ebx, FS:[0x30] // PEB для 32-битных приложений всегда лежит в регистре FS по смещению 0x30, получаем его.
        mov ebx, [ebx + 0x0C] // Получаем указатель на структуру PEB_LDR_DATA.
        mov ebx, [ebx + 0x14] // Получаем указатель на первую запись в InMemoryOrderModuleList.
        mov ebx, [ebx] // Получаем указатель на вторую запись в InMemoryOrderModuleList. ( ntdll.dll )
        mov ebx, [ebx] // Получаем указатель на третью запись в InMemoryOrderModuleList. ( kernel32.dll )
        mov eax, [ebx + 0x10] // Получаем адрес нужного нам модуля.
    }
}


Теперь давайте приступим к реализации самой функции для парсинга таблицы экспорта нужных нам модулей.

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

Садимся кодить, в процессе написания функции буду оставлять в ней коментарии, дабы смысл был понятен всем :

Код:
LPVOID pGetProcAddress(
            IN HMODULE hModule,
            IN LPSTR lpFunctionName
            )
{

    if (hModule == NULL || lpFunctionName == NULL) return NULL; // Если один из параметров равен нулю - возвращаем ноль.

    PIMAGE_DOS_HEADER IDH = (PIMAGE_DOS_HEADER)hModule; // Получаем указатель на DOS заголовок модуля. Подробнее - тут : https://www.nirsoft.net/kernel_struct/vista/IMAGE_DOS_HEADER.html
    PIMAGE_NT_HEADERS INH = (PIMAGE_NT_HEADERS)((DWORD)IDH + IDH->e_lfanew); // Получаем указатель на NT заголовок модуля. Подробнее - тут : https://www.nirsoft.net/kernel_struct/vista/IMAGE_NT_HEADERS.html
    // P.S : e_lfanew - зарахдкоренное смещение до NT заголовка.
    PIMAGE_EXPORT_DIRECTORY IED = (PIMAGE_EXPORT_DIRECTORY)((DWORD)IDH + INH->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress); // Получаем указатель на таблицу экспорта модуля.

    PDWORD AddressOfFunctions = (PDWORD)((DWORD)IDH + IED->AddressOfFunctions); // Получаем указатель на таблицу с адресами функций.
    PDWORD AddressOfNames = (PDWORD)((DWORD)IDH + IED->AddressOfNames); // Получаем указатель на таблицу с именами функций.
    PWORD AddressOfNameOrdinals = (PWORD)((DWORD)IDH + IED->AddressOfNameOrdinals); // Получаем указатель на таблицу с порядковыми номерами функций. ( Ординалами ).
    DWORD dwOrdinal = 0; // Сюда будет ложится порядковый номер функции.

    // Теперь проходимся в цикле по списку имён функций, экспортируемых данным модулем.

    for (DWORD i = 0; i < IED->NumberOfNames; i++)
    {
        LPSTR lpApiName = (LPSTR)((DWORD)IDH + AddressOfNames[i]); // Получаем имя текущей функции.

        if (strcmp(lpFunctionName, lpApiName) == 0)    // Если имя текущей идентично имени функции, которую мы ищем - сохраняем её порядковый номер и выходим из цикла.
        {
            dwOrdinal = AddressOfNameOrdinals[i];
            break;
        }
    }

    LPBYTE lpFunctionVA = (LPBYTE)((DWORD)IDH + AddressOfFunctions[dwOrdinal]); // Получаем адрес функции.

    // Вот незадача. Функция может переадресовыватся в другую библиотеку. Такое понятие называется - Function Forwarding. Это сделано для обратной совместимости между старым ПО и новыми системами.
    // Однако мы сейчас заставим нашу функцию обрабатывать функции, которые форвардятся.
    // Если функция форвардится - переменная lpFunctionVA будет содержать значение следующего вида : Название библиотеки, куда ссылается функция.Название функции
    // Например : HeapAlloc из Kernel32.dll даст нам результат ntdll.RtlAllocateHeap и так далее.

    if (lpFunctionVA > (LPBYTE)IED && lpFunctionVA < (LPBYTE)IED + INH->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size) // Определяем, форвардится ли функция.
    {
        char szDllName[MAX_PATH + 1] = { 0 }; // Буффер для получения названия динамической библиотеки, в которую ссылается функция.
        DWORD dwCounter = 0; // Счётчик, нужный нам для цикла.

        while (lpFunctionVA[dwCounter] != '.')
        {
            szDllName[dwCounter] = lpFunctionVA[dwCounter];
            dwCounter++;
        }

        LPSTR lpForwardedFunctionName = (LPSTR)(lpFunctionVA + (dwCounter + 1)); // Извлекаем название функции. + 1 - дабы обойти точку.
        HMODULE hKernel32 = GetKernel32(); // Получаем дескриптор модуля Kernel32.

        typedef HMODULE(WINAPI* xLoadLibraryA)(LPCSTR lpLibFileName); // Обьявляем прототип функции LoadLibraryA.
        xLoadLibraryA pLoadLibraryA = (xLoadLibraryA)pGetProcAddress(hKernel32, "LoadLibraryA"); // Получаем адрес LoadLibraryA.

        HMODULE hForwardedLibrary = pLoadLibraryA(szDllName); // Загружаем библиотеку, на которую ссылается функция.

        return pGetProcAddress(hForwardedLibrary, lpForwardedFunctionName); // Возвращаем адрес уже настоящей функции.
    }

    return lpFunctionVA; // Возвращаем адрес функции, если она не форвардится.
}

И теперь давайте попробуем практически использовать вышеприведённый мной код, неявно вызовем месседжбокс :

Код:
HMODULE hKernel32 = GetKernel32();

    typedef HMODULE(WINAPI* xLoadLibraryW)(LPCWSTR lpLibFileName);
    typedef int(WINAPI* xMessageBoxW)(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType);

    xLoadLibraryW pLoadLibraryW = (xLoadLibraryW)pGetProcAddress(hKernel32, "LoadLibraryW");

    HMODULE hUser32 = pLoadLibraryW(L"user32.dll");

    xMessageBoxW pMessageBoxW = (xMessageBoxW)pGetProcAddress(hUser32, "MessageBoxW");

    pMessageBoxW(NULL, L"Hello, my nickname is dopeware!", NULL, MB_OK);
 
Статья неполноценная, есть много подводных камней, про которые, если у меня руки дойдут, я напишу
Например? Тут нет поддержки переадрессации по ординалу, разве что. А так вроде бы всё в порядке.
 
Например? Тут нет поддержки переадрессации по ординалу, разве что. А так вроде бы всё в порядке.
APISET.
 
APISET.

Не заметил твоё сообщение выше. Да, есть такое. Но раз многоуважаемый a1d говорит про " много подводных камней " - хотелось бы послушать его.
 
Кстати есть еще мутная тема с защитой адресов функций экспорта, чето типа или рядом с control flow guard(подобная таблица в лоад конфиг), там вроде суть в том что чтобы получить адрес апи модуль должен иметь на это права(а так адреса пошифрованы чтоли)...сча лень смотреть как оно там зоветься.
Кто в курсе про это отпишитесь.
 
Для kernel32 можно просто сделать pop eax, и потом с кратностью 64 дойдем до РЕ
В этом походе можно запросто споткнуться об гранулярность и раскровить себе нос.
Я поясню о чем я.
Мне приходилось сталкиваться с версией системной библиотеки где секции были выровнены на гранулярность(64кб) и неиспользуемое пространство не было предствлено...уже не вспомню что это было или ntdll или ntos.
Просто следует учитывать что между пе хидером и первой секцией может быть пусто. Тоеретически если идти с кратностью 64 то проблем быть не должно...но это так себе допущение.
Опять же по кратному адресу можно словить MZ которое вовсе не пе хидер а значит нужно делать экзамен пе хидера.
Все же лучше брать базовый адрес из пеб.
 
Последнее редактирование:
В этом походе можно запросто споткнуться об гранулярность и раскровить себе нос.
Я поясню о чем я.
Мне приходилось сталкиваться с версией системной библиотеки где секции были выровнены на гранулярность(64кб) и неиспользуемое пространство не было предствлено...уже не вспомню что это было или ntdll или ntos.
Просто следует учитывать что между пе хидером и первой секцией может быть пусто. Тоеретически если идти с кратностью 64 то проблем быть не должно...но это так себе допущение.
Опять же по кратному адресу можно словить MZ которое вовсе не пе хидер а значит нужно делать экзамен пе хидера.
Все же лучше брать базовый адрес из пеб.
Все верно. Дойдем до MZ и парсим до PE
 
Если я буду писать на x64, то inline assembly уже нельзя, по крайней мере в студии, так что чтобы получить peb придется писать отдельные asm файлы или использовать интринсики:
Код:
    auto tempAddr = __readgsqword(0x60);
    void* PEB = reinterpret_cast<void*>(tempAddr);
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх