• 15 апреля стартует «Курс «SQL-injection Master» ©» от команды The Codeby

    За 3 месяца вы пройдете путь от начальных навыков работы с SQL-запросами к базам данных до продвинутых техник. Научитесь находить уязвимости связанные с базами данных, и внедрять произвольный SQL-код в уязвимые приложения.

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

    Запись на курс до 25 апреля. Получить промодоступ ...

Статья [0x06] Исследуем Portable Executable [DLL инъекции]


Салют, друзья! На дворе 2018 год, а мы с вами начинаем изучать DLL инъекции :). Не смотря на то, что эта техника уже много раз заезжена и всем известна, я всё же внесу свои 5 копеек, так как в будующих статьях мы будем частенько прибегать к этой технике и я просто не мог пропустить её.

DLL Injection
Что же за DLL-инъекции, то такие? Для начала, нужно рассказать, что такое DLL.

DLL (Dynamic Link Library) - исполняемый модуль, в котором хранятся функции, используемые программой. Да! Их просто вынесли в отдельный исполняемый модуль. DLL'ки загружаются программой, а после этого используются функции, которые экспортирует DLL. Пример DLL - kernel32.dll в нём хранятся основные функции Windows API.

При загрузке/выгрузке DLL вызывается определённая функция этой DLL. Её обычно называют инициализирующей и по умолчанию она носит имя DllMain.

Вернёмся к нашим баранам, а точнее DLL инъекциям. На самом деле, DLL инъекция - это один из способов выполнения стороннего кода в контексте другого процесса. Говоря проще, это техника внедрения кода в уже загруженный произвольный исполняемый файл. Зачем она нужна? Ну, рассмотрим самые частые случаи использования DLL:
  • Иногда нам может понадобиться внедрить в процесс собственный код (функцию) (для использования самой программой, для перехват API, для перехвата других функций и т.д.).
  • Иногда очень важно получить прямой доступ к памяти нужного нам процесса (для модифицирования содержимого памяти либо чтения), не смотря на то что существуют WriteProcessMemory и ReadProcessMemory WinApi функции.
  • Скрытие вредоносного кода в легитимном процессе.
  • и т.д. и т.п.
Есть много различных способов проведения DLL инъекций, но мы рассмотрим самый базовый - загрузка DLL с помощью LoadLibrary через удалённый поток. Минус этого способа - его легко задетектить.

Теория

dll_injection_1.png


Основная идея DLL инъекции - загрузить DLL. Для этого может использоваться огромное количество способов, вплоть до ручной загрузки DLL (этот способ называется Reflected DLL Injection). Но как я сказал выше, мы рассмотрим самый быстрый и простой способ - загрузку с помощью LoadLibrary через удалённый поток.

В принципе, процесс внедрения DLL можно разделить на несколько шагов:

1. Открываем процесс (получаем доступ к целевому процессу) с помощью WinApi функции OpenProcess.

open_process.png

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

2. Выделяем память для имени DLL, которую мы хотим внедрить.

virtual_alloc.png

Для чего нам эта память нужна? Для записи в эту память имени DLL (а точнее, путь к DLL). Зачем нам нужен путь к DLL в целевом процессе я объясню позже. Память можно выделить с помощью функции .

3. Записываем путь к DLL по выделенной памяти

write_process_memory.png

Записываем путь к DLL. Зачем туда записывать путь к DLL? Дело в том, что функция LoadLibrary, которая как раз таки и загрузит нашу DLL'ку принимает указатель на строку DLL (адрес строки). Внедрять DLL мы собираемся в процесс hello.exe, поэтому запускать LoadLibrary придётся также в hello.exe. Для этого, соответственно, нам нужна строка в самом hello.exe (запомните, что у всех процессов своё адресное пространство). Сделать это можно с помощью функции .

4. Запускаем LoadLibrary

load_library.png

Вся магия DLL инъекций скрыта как-раз таки на этом этапе, а предыдущие шаги были подготовительными. В данном этапе, мы вызываем функцию , которая и загружает нашу DLL'ку.

После всех наших действий, автоматически запускается инциализирующая функция нашей DLL, которая выполняет свою грязную работу. Ну-с, приступим к практике.

Практика
Для закрепления теории, предлагаю написать простой консольный DLL инжектор, который загрузит нужную нам DLL в нужный нам процесс.

1543653173755.png

Для начала, создадим проект.

Давайте напишем функцию, с помощью которой мы получим PID используя имя процесса. Здесь мы получаем список всех процессов, а потом проходясь по этому списку, сравниваем имя процесса в списке с именем нужного нам процесса. Если мы нашли процесс, то возвращает его PID.
C++:
DWORD GetPidByProcessName(LPTSTR lpszProcessName)
{
    HANDLE hSnapshot;
    PROCESSENTRY32 pe32 = { 0 };

    pe32.dwSize = sizeof(PROCESSENTRY32);

    hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
    if (hSnapshot == INVALID_HANDLE_VALUE)
    {
        throw std::exception("Can't create the snapshot!");
    }

    if (Process32First(hSnapshot, &pe32))
    {
        do
        {
            if (_tcscmp(pe32.szExeFile, lpszProcessName) == 0)
            {
                return pe32.th32ProcessID;
            }
        }
        while (Process32Next(hSnapshot, &pe32));
    }

    throw std::exception("Can't find the process!");
}

Для этой функции нам понадобиться подключить два заголовочных файла:
C++:
#include <Windows.h>
#include <TlHelp32.h>

Ну и объявим две константы. Одну для имени DLL, другую для имени процесса (инъектить DLL'ку мы будем в наш замученный hello.exe).
C++:
#define TARGET_PROCESS "hello.exe"
#define TARGET_DLL     "D:\\test.dll"

Теперь, добавим следующий код в главную функцию. Здесь мы получаем PID процесса и получаем DLL, а также размер строки с путём до DLL.
C++:
    char  sDLLName[] = TARGET_DLL;
    DWORD dwSize     = sizeof(sDLLName)+1;

    DWORD dwProcessID;
    try
    {
        dwProcessID = GetPidByProcessName(_TEXT(TARGET_PROCESS));
    }
    catch (std::exception& e)
    {
        std::wcout << "[ERROR] " << e.what() << std::endl;
        getchar();
        return 1;
    }
    std::wcout << "[INFO] ProcessID = " << dwProcessID << std::endl;

Так, мы получили PID процесса. А для чего же он нам нужен? Конечно же, чтобы открыть процесс! (весь последующий код добавляйте в функцию main (или _tmain) прим. автора). Первым пунктом инъекции у нас идёт открытие процесса. Взглянем на функцию открытия процесса поподробнее.
Протопип функции:
C++:
HANDLE OpenProcess(
  DWORD dwDesiredAccess,
  BOOL  bInheritHandle,
  DWORD dwProcessId
);
  • dwDesiredAccess
    Права доступа, которые мы хотим получить открыв процесс. Установим PROCESS_ALL_ACCESS, чтобы получить все права на процесс.
  • bInheritHandle
    Флаг наследования дескриптора. Установим его в FALSE.
  • dwProcessId
    Идентификатор процесса (Process ID или PID)
Давайте теперь напишем код, который откроет нужный нам процесс.
C++:
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessID);
    if (hProcess == NULL)
    {
        std::wcout << "[ERROR] Can't open the target process!" << std::endl;
        CloseHandle(hProcess);
        getchar();
        return 1;
    }

Что нам нужно сделать после открытия процесса? Правильно! Выделить память для пути к DLL. Взглянем на функцию VirtualAllocEx поподробнее, ведь именно с помощью неё мы и выделим память!
C++:
LPVOID WINAPI VirtualAllocEx(
  _In_     HANDLE hProcess,
  _In_opt_ LPVOID lpAddress,
  _In_     SIZE_T dwSize,
  _In_     DWORD  flAllocationType,
  _In_     DWORD  flProtect
);

  • hProcess
    Дескриптор процесса, в котором мы хотим выделить память
  • lpAddress
    Адрес, по которому хотим выделить память. Если он нам не важен, то мы можем установить NULL.
  • dwSize
    Размер выделяемой памяти.
  • flAllocationType
    Тип выделения
  • flProtect
    Права на выделенную память.
Напишем код, реализующий это, а также как обычно не забудем проверить на ошибки:
C++:
    LPVOID lpDllName = VirtualAllocEx(hProcess, NULL, dwSize,
        MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    if (lpDllName == NULL)
    {
        std::wcout << "[ERROR] Can't allocate memory in the target process!"
            << std::endl;
        CloseHandle(hProcess);
        getchar();
        return 1;
    }
    std::wcout << "[INFO] Memory allocated at 0x" << lpDllName << std::endl;

Зачем выделять? Чтобы записать сюда имя DLL. Изучим функцию WriteProcessMemory, с помощью которой мы и запишем имя DLL'ки в нужный нам процесс по нужному нам адресу памяти.

Код:
BOOL WINAPI WriteProcessMemory(
  _In_  HANDLE  hProcess,
  _In_  LPVOID  lpBaseAddress,
  _In_  LPCVOID lpBuffer,
  _In_  SIZE_T  nSize,
  _Out_ SIZE_T  *lpNumberOfBytesWritten
);

  • hProcess
    Дескриптор процесса, в память которого мы и хотим записать нужные нам данные.
  • lpBaseAddress
    Адрес в памяти процесса, по которому мы хотим записать данные.
  • lpBuffer
    Указатель на буфер, который мы хотим записать.
  • nSize
    Размер буфера (или сколько байтов нам нужно записать).
  • lpNumberOfBytesWritten
    Указатель на целочисленную переменную, в которую будет записано число записанных байт.

Перейдём к коду. Здесь мы запишем имя DLL'ки и проверим на ошибки.
C++:
    BOOL bRes = WriteProcessMemory(hProcess, lpDllName, sDLLName,
        dwSize, NULL);
    if (!bRes)
    {
        std::wcout << "[ERROR] Can't write the dll name in the target process!"
            << std::endl;
        VirtualFreeEx(hProcess, lpDllName, dwSize, MEM_RELEASE);
        CloseHandle(hProcess);
        getchar();
        return 1;
    }

Зачем записывать? Чтобы загрузить DLL, конечно. Но перед этим, нам нужно получить адрес функции LoadLibraryA (с помощью двух функций, GetModuleHandle и GetProcAddress), которая и загрузит нашу DLL'ку, а уже потом создать сам удалённый поток. Что такое удалённый поток? Проще говоря, с помощью удалённого потока мы можем вызвать любую нужную нам функцию в нужном нам процессе передав нужные нам параметры.

C++:
    LPVOID lpLoadLibrary = GetProcAddress(hKernel32, "LoadLibraryA");
    if (lpLoadLibrary == NULL)
    {
        std::wcout << "[ERROR] Can't get the LoadLibraryA address!"
            << std::endl;
        VirtualFreeEx(hProcess, lpDllName, dwSize, MEM_RELEASE);
        CloseHandle(hProcess);
        getchar();
        return 1;
    }

    HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0,
        LPTHREAD_START_ROUTINE(lpLoadLibrary), lpDllName, 0, 0);
    if (hThread == NULL)
    {
        std::wcout << "[ERROR] Can't create remote thread!"
            << std::endl;
        VirtualFreeEx(hProcess, lpDllName, dwSize, MEM_RELEASE);
        CloseHandle(hProcess);
        getchar();
        return 1;
    }

Ну-с, в принципе, на этом процесс инъекции и заканчивается. Добавим финальные штрихи:
C++:
    std::wcout << "[+] DLL Injected!" << std::endl;
   
    CloseHandle(hThread);
    VirtualFreeEx(hProcess, lpDllName, dwSize, MEM_RELEASE);
    CloseHandle(hProcess);
    getchar();
    return 0;

Финальный код выглядит так:
C++:
#include <Windows.h>
#include <TlHelp32.h>
#include <iostream>

#define TARGET_PROCESS "hello.exe"
#define TARGET_DLL     "D:\\test.dll"

DWORD GetPidByProcessName(LPTSTR lpszProcessName)
{
    HANDLE hSnapshot;
    PROCESSENTRY32 pe32 = { 0 };

    pe32.dwSize = sizeof(PROCESSENTRY32);

    hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
    if (hSnapshot == INVALID_HANDLE_VALUE)
    {
        throw std::exception("Can't create the snapshot!");
    }

    if (Process32First(hSnapshot, &pe32))
    {
        do
        {
            if (_tcscmp(pe32.szExeFile, lpszProcessName) == 0)
            {
                return pe32.th32ProcessID;
            }
        }
        while (Process32Next(hSnapshot, &pe32));
    }

    throw std::exception("Can't find the process!");
}

int _tmain(int argc, _TCHAR* argv[])
{
    char  sDLLName[] = TARGET_DLL;
    DWORD dwSize     = sizeof(sDLLName)+1;

    DWORD dwProcessID;
    try
    {
        dwProcessID = GetPidByProcessName(_TEXT(TARGET_PROCESS));
    }
    catch (std::exception& e)
    {
        std::wcout << "[ERROR] " << e.what() << std::endl;
        getchar();
        return 1;
    }
    std::wcout << "[INFO] ProcessID = " << dwProcessID << std::endl;

    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessID);
    if (hProcess == NULL)
    {
        std::wcout << "[ERROR] Can't open the target process!" << std::endl;
        CloseHandle(hProcess);
        getchar();
        return 1;
    }

    LPVOID lpDllName = VirtualAllocEx(hProcess, NULL, dwSize,
        MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    if (lpDllName == NULL)
    {
        std::wcout << "[ERROR] Can't allocate memory in the target process!"
            << std::endl;
        CloseHandle(hProcess);
        getchar();
        return 1;
    }
    std::wcout << "[INFO] Memory allocated at 0x" << lpDllName << std::endl;

    BOOL bRes = WriteProcessMemory(hProcess, lpDllName, sDLLName,
        dwSize, NULL);
    if (!bRes)
    {
        std::wcout << "[ERROR] Can't write the dll name in the target process!"
            << std::endl;
        VirtualFreeEx(hProcess, lpDllName, dwSize, MEM_RELEASE);
        CloseHandle(hProcess);
        getchar();
        return 1;
    }

    HMODULE hKernel32 = GetModuleHandle(_TEXT("kernel32.dll"));  
    if (hKernel32 == NULL)
    {
        std::wcout << "[ERROR] Can't get the handle of kernel32.dll!"
            << std::endl;
        VirtualFreeEx(hProcess, lpDllName, dwSize, MEM_RELEASE);
        CloseHandle(hProcess);
        getchar();
        return 1;
    }

    LPVOID lpLoadLibrary = GetProcAddress(hKernel32, "LoadLibraryA");
    if (lpLoadLibrary == NULL)
    {
        std::wcout << "[ERROR] Can't get the LoadLibraryA address!"
            << std::endl;
        VirtualFreeEx(hProcess, lpDllName, dwSize, MEM_RELEASE);
        CloseHandle(hProcess);
        getchar();
        return 1;
    }

    HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0,
        LPTHREAD_START_ROUTINE(lpLoadLibrary), lpDllName, 0, 0);
    if (hThread == NULL)
    {
        std::wcout << "[ERROR] Can't create remote thread!"
            << std::endl;
        VirtualFreeEx(hProcess, lpDllName, dwSize, MEM_RELEASE);
        CloseHandle(hProcess);
        getchar();
        return 1;
    }

    std::wcout << "[+] DLL Injected!" << std::endl;
   
    CloseHandle(hThread);
    VirtualFreeEx(hProcess, lpDllName, dwSize, MEM_RELEASE);
    CloseHandle(hProcess);
    getchar();
    return 0;
}

Но что мы будем внедрять? А точнее, какую DLL мы будем внедрять? Для этого, я создал простую DLL с именем test.dll и поместил его на диск D. Вот код DLL'ки:
C++:
#include <windows.h>

extern "C" __declspec(dllexport) BOOL APIENTRY DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
    switch (fdwReason)
    {
        case DLL_PROCESS_ATTACH:
            MessageBoxA(NULL, "DLL Injected!", "DLL", MB_OK | MB_ICONINFORMATION);
            break;

        case DLL_PROCESS_DETACH:
            break;

        case DLL_THREAD_ATTACH:
            break;

        case DLL_THREAD_DETACH:
            break;
    }
    return TRUE;
}

Скомпилируем и протестируем!

1543666878509.png

На этом всё, спасибо за внимание!
 
Последнее редактирование:
A

Akroll

Крайне полезная статья, да ещё и на плюсах : D
Соглашусь!

Автор, продолжай писать. Очень интересные и полезные статьи.
А можно где-то почитать дополнительную литературу на данную тематику? Или же книги по плюсам, где разбирают данный вопрос более глубоко.
 
  • Нравится
Реакции: AidBotnet и PingVinich

PingVinich

Технарь
Green Team
19.03.2017
138
505
BIT
0
Соглашусь!

Автор, продолжай писать. Очень интересные и полезные статьи.
А можно где-то почитать дополнительную литературу на данную тематику? Или же книги по плюсам, где разбирают данный вопрос более глубоко.
Есть уникальный в этом отношении сайт firststeps.ru
 

Aleks Binakril

Green Team
04.12.2019
38
5
BIT
0
там такого дзен кодеса не найти - поддерживаю автор продолжай делать мирный атом для нас
 
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!