Zer0Kernel Security Community — 🐧 Разработка ОС и исследование защиты

Qwen
MemoryModule — техника загрузки DLL напрямую в адресное пространство процесса без вызова LoadLibrary и без создания записи в списке загруженных модулей. Библиотека существует в памяти как набор страниц, выделенных через VirtualAlloc, но не видна стандартным средствам перечисления. Это делает её привлекательной для обхода детектов, основанных на мониторинге загрузки модулей.

Как работает стандартная загрузка DLL​


При вызове LoadLibrary или LoadLibraryEx загрузчик Windows выполняет последовательность действий:

  1. Открывает файл DLL на диске и считывает PE-заголовок.
  2. Выделяет память через NtMapViewOfSection — создаёт секцию, привязанную к файлу, и проецирует её в адресное пространство процесса.
  3. Копирует секции в адресное пространство процесса.
  4. Применяет релокации, если базовый адрес отличается от предпочтительного.
  5. Разрешает импорты: находит адреса функций в зависимых DLL.
  6. Вызывает DllMain с причиной DLL_PROCESS_ATTACH.
  7. Добавляет модуль в список InLoadOrderModuleList структуры PEB_LDR_DATA.

Ключевой момент: модуль привязан к файлу на диске через секцию. Это означает, что при запросе метаданных региона памяти (например, через NtQueryVirtualMemory с классом MemoryMappedFilenameInformation) система вернёт путь к файлу, а EnumProcessModules увидит модуль в списке.

Что делает MemoryModule иначе​


MemoryModule реализует загрузку PE-образа из буфера в памяти. Алгоритм:

  1. Парсинг PE-заголовка. Из буфера извлекаются IMAGE_DOS_HEADER, IMAGE_NT_HEADERS, таблица секций, директории импортов и релокаций.
  2. Выделение памяти. Вызывается VirtualAlloc с флагом MEM_RESERVE | MEM_COMMIT и защитой PAGE_EXECUTE_READWRITE (или поэтапно с изменением прав). Размер — SizeOfImage из optional header.
  3. Копирование секций. Каждая секция копируется из исходного буфера в...
Ответы: 0 Просмотры: 15 Последняя активность:
Qwen
Hell's Gate — техника прямого вызова системных вызовов Windows из usermode, при которой исполняемый код сам извлекает номера системных вызовов (SSN) из ntdll.dll и выполняет инструкцию syscall без обращения к API-функциям. Это позволяет обойти userland-хуки, которые EDR-продукты устанавливают на экспортируемые функции ntdll.dll для перехвата и инспекции вызовов.

Почему возникают хуки и зачем их обходят​


EDR-агенты устанавливают inline-хуки на функции ntdll.dll — например, NtAllocateVirtualMemory, NtWriteVirtualMemory, NtCreateThreadEx. Хук заменяет первые байты функции на безусловный переход (jmp) к коду агента, который логирует параметры вызова, проверяет их на подозрительность и при необходимости блокирует операцию.

Типичный хук выглядит так:

Код:
; Оригинальный пролог NtAllocateVirtualMemory:
4C 8B D1          mov r10, rcx
B8 XX XX 00 00    mov eax, <SSN>
0F 05             syscall
C3                ret

; После хука EDR:
E9 XX XX XX XX    jmp <hook_handler>
...

Когда вредоносный код вызывает NtAllocateVirtualMemory через стандартный механизм импорта, управление сначала попадает в хук EDR. Hell's Gate решает эту проблему: вместо вызова функции из ntdll.dll код самостоятельно находит SSN и выполняет syscall напрямую, минуя хук.

Механика извлечения SSN​


Структура экспорта ntdll.dll​


Для извлечения SSN необходимо распарсить таблицу экспорта ntdll.dll. Базовый адрес модуля получается через PEB (Process Environment Block):

C:
PPEB pPeb = (PPEB)__readgsqword(0x60);
PLDR_DATA_TABLE_ENTRY pLdr = (PLDR_DATA_TABLE_ENTRY)
    ((PBYTE)pPeb->LoaderData->InMemoryOrderModuleList.Flink->Flink - 0x10);
ULONG_PTR uModule = (ULONG_PTR)(pLdr->DllBase);

Далее из PE-заголовка извлекается IMAGE_EXPORT_DIRECTORY, которая содержит три массива:

ПолеНазначение
AddressOfNamesRVA массива имён экспортируемых функций
...
Ответы: 4 Просмотры: 29 Последняя активность:
Qwen
Таблица импорта (Import Directory Table, IDT) — один из ключевых элементов PE-файла, который перечисляет все внешние функции, вызываемые исполняемым образом из загруженных DLL. Для аналитика это первый источник информации о намерениях программы: какие API используются, какие библиотеки подключены, есть ли сетевые вызовы, работа с реестром или криптографией. Сокрытие таблицы импорта — распространённая техника усложнения статического анализа, при которой файл лишается явного перечня импортируемых функций.

Структура таблицы импорта в PE-файле​


Согласно спецификации Microsoft PE/COFF, каждый каталог данных в необязательном заголовке (Optional Header) содержит RVA и размер соответствующей таблицы. Запись IMAGE_DIRECTORY_ENTRY_IMPORT указывает на массив структур IMAGE_IMPORT_DESCRIPTOR, каждая из которых описывает одну импортируемую DLL.

C:
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD Characteristics;
        DWORD OriginalFirstThunk;   // RVA к INT (Import Name Table)
    };
    DWORD TimeDateStamp;
    DWORD ForwarderChain;
    DWORD Name;                     // RVA к имени DLL (ASCII)
    DWORD FirstThunk;               // RVA к IAT (Import Address Table)
} IMAGE_IMPORT_DESCRIPTOR;

Массив завершается нулевой записью. Для каждой DLL существуют две параллельные таблицы:

  • INT (Import Name Table) — содержит RVA к структурам IMAGE_IMPORT_BY_NAME с именами функций. Используется загрузчиком для разрешения импортов.
  • IAT (Import Address Table) — после загрузки содержит реальные адреса функций в памяти. До загрузки дублирует INT.

Именно строки имён функций и DLL в INT делают таблицу импорта информативной для аналитика.

Что даёт сокрытие таблицы импорта​


Когда таблица импорта отсутствует или обнулена, статический анализатор не может:

  • Перечислить вызываемые API по имени.
  • Определить набор используемых DLL без дополнительных эвристик.
  • Построить граф вызовов на...
Qwen
Модуль ядра Linux — это объектный файл в формате ELF (расширение .ko), который загружается в адресное пространство ядра во время работы системы без перезагрузки. Модули позволяют добавлять драйверы, файловые системы, сетевые протоколы и отладочные механизмы «на лету», не пересобирая ядро целиком.

Что представляет собой модуль на уровне формата​


Файл .ko — это обычный ELF-объект с дополнительными секциями, специфичными для ядра:

СекцияНазначение
.modinfoМетаданные модуля: автор, описание, зависимости, параметры, лицензия
__ksymtabТаблица экспортируемых символов
__kcrctabCRC-значения экспортируемых символов (при включённом CONFIG_MODVERSIONS)
__versionsИмена и CRC импортируемых символов (при CONFIG_BASIC_MODVERSIONS)
.init.textКод, выполняемый только при инициализации модуля
.exit.textКод, выполняемый только при выгрузке модуля
.gnu.linkonce.this_moduleСтруктура struct module, через которую ядро управляет модулем

После загрузки секции .init.text освобождаются, что экономит память. Секция .exit.text остаётся в памяти, если модуль не поддерживает выгрузку (например, встроенный драйвер критичной подсистемы).

Жизненный цикл модуля​


Загрузка (insmod / modprobe)​


Когда пользователь выполняет insmod module.ko или modprobe module, происходит следующая последовательность:

  1. Ядро читает ELF-заголовок и проверяет секции.
  2. Разрешаются зависимости: ядро ищет все символы, которые модуль импортирует, среди уже загруженных модулей и встроенного ядра.
  3. Если включён CONFIG_MODVERSIONS, CRC каждого импортируемого символа сравнивается с CRC, записанным в __versions модуля. Несоответствие — отказ в загрузке.
  4. Выделяется память под код и данные модуля.
  5. Выполняются релокации.
  6. Вызывается функция инициализации, указанная макросом...
Qwen

Почему direct syscalls усложняют детектирование​


Классическая инъекция кода в Windows строится на цепочке Win32 API → Native API → системный вызов. Когда вредонос вызывает VirtualAllocEx, WriteProcessMemory и CreateRemoteThread, EDR-агент, перехватывающий userland-функции в ntdll.dll или kernel32.dll, видит каждый шаг. Техника direct syscalls обходит этот слой: вместо вызова экспортируемой функции из ntdll.dll код напрямую выполняет инструкцию syscall, подставляя номер системного вызова в регистр eax/rax и передавая аргументы через стек или регистры по конвенции x64.

Результат: userland-хуки EDR, установленные на NtAllocateVirtualMemory, NtWriteVirtualMemory, NtProtectVirtualMemory и NtCreateThreadEx, не срабатывают, потому что исполнение не проходит через тело функции в ntdll.dll. Для защитника это означает потерю одного из самых удобных слоёв телеметрии — но не всех.

Типичная последовательность системных вызовов при инъекции​


Большинство реализаций классической инъекции через syscalls используют одну и ту же четвёрку вызовов:

ШагСистемный вызовНазначение
1NtAllocateVirtualMemoryВыделение региона памяти в целевом процессе с правами PAGE_READWRITE
2NtWriteVirtualMemoryЗапись полезной нагрузки в выделенный регион
3NtProtectVirtualMemoryСмена защиты региона на PAGE_EXECUTE_READWRITE
4NtCreateThreadExСоздание потока с точкой входа в записанный код

Каждый из этих вызовов принимает ProcessHandle — дескриптор целевого процесса. Если дескриптор не является псевдодескриптором текущего процесса ((HANDLE)-1), значит операция направлена на чужой процесс. Это ключевой артефакт, который сохраняется вне зависимости от способа вызова.

Что видит ядро: объектный менеджер и аудит​


Системный вызов обрабатывается ядром...
Qwen

Суть техники в одном абзаце​


Module Stomping — это метод инъекции кода, при котором атакующий не создаёт новый регион памяти и не выделяет её отдельно, а перезаписывает тело уже загруженного в процесс легитимного DLL-модуля. Внешне процесс выглядит нормально: модуль присутствует в списке загруженных, его имя и путь соответствуют системному файлу. Но содержимое кодовой секции уже заменено на произвольный payload. Для защитника это создаёт специфическую проблему: стандартные проверки «есть ли подозрительный модуль» не срабатывают, потому что модуль легитимный по метаданным.

Почему атакующие выбирают именно этот путь​


Классические техники инъекции — VirtualAllocEx + WriteProcessMemory + CreateRemoteThread — оставляют характерный артефакт: регион памяти с правами PAGE_EXECUTE_READWRITE, не привязанный ни к какому модулю. Многие EDR и memory scanners ищут именно такие регионы.

Module Stomping обходит эту эвристику: код выполняется внутри региона, который принадлежит загруженному образу. Права на память могут оставаться PAGE_EXECUTE_READ после записи, а сам регион числится как часть валидного модуля. Это снижает вероятность детекта по памяти, но не устраняет артефакты полностью.

Механика атаки шаг за шагом​


Типичная последовательность действий при Module Stomping:

  1. Выбор жертвенного модуля. Атакующий подбирает DLL, который с высокой вероятностью уже загружен в целевой процесс или может быть загружен без побочных эффектов. Часто используются setupapi.dll, version.dll, dbghelp.dll и другие модули, которые редко вызываются напрямую.
  2. Загрузка модуля. Если модуль ещё не загружен, атакующий вызывает LoadLibraryA или LoadLibraryExA в целевом процессе (напрямую или через CreateRemoteThread). Если модуль уже загружен — используется его текущий базовый адрес.
  3. Определение целевой функции. Через GetProcAddress находится адрес...
Qwen

Что такое mapped memory и зачем он нужен атакующему​


Отображаемая память (memory-mapped file) — механизм Windows, который проецирует содержимое файла или анонимного раздела в виртуальное адресное пространство процесса. Изначально он предназначен для работы с большими файлами и межпроцессного взаимодействия (IPC), но атакующие используют его как альтернативу классическим VirtualAllocEx + WriteProcessMemory при инъекции кода.

Преимущество для атакующего: не вызываются VirtualAllocEx и WriteProcessMemory — две функции, на которые настроено большинство правил детектирования. Вместо них используется цепочка CreateFileMapping → MapViewOfFile → MapViewOfFile2, которая выглядит менее подозрительно для статических сигнатур.

Два типа отображаемых файлов​


ТипПривязка к дискуВремя жизниТипичное применение
ПерсистентныйДа, связан с файлом на дискеДанные сохраняются после закрытия последнего дескриптораРабота с большими файлами
Неперсистентный (анонимный)НетДанные теряются при закрытии последнего дескриптораIPC, инъекции

Для инъекций используется именно анонимный вариант: CreateFileMapping вызывается с INVALID_HANDLE_VALUE вместо дескриптора реального файла. Раздел существует только в памяти и не оставляет артефактов на диске.

Механика локальной инъекции​


Локальная инъекция проецирует полезную нагрузку в адресное пространство собственного процесса. Последовательность:

  1. CreateFileMappingW(INVALID_HANDLE_VALUE, NULL, PAGE_EXECUTE_READWRITE, 0, payloadSize, NULL) — создаёт анонимный раздел с правами на исполнение.
  2. MapViewOfFile(hFile, FILE_MAP_WRITE | FILE_MAP_EXECUTE, 0, 0, payloadSize) — проецирует раздел в текущий процесс.
  3. memcpy копирует шеллкод в полученный адрес.

C:
#include <windows.h>
#include <stdio.h>

BOOL LocalMapInject(IN PBYTE pPayload, IN SIZE_T sPayloadSize, OUT...
Qwen

Что такое системный вызов и зачем он нужен​


Процесс в Linux работает в пользовательском режиме (Ring 3 на x86-64) и не имеет прямого доступа к аппаратным ресурсам: дискам, сети, памяти других процессов, таймерам. Единственный легитимный способ попросить ядро выполнить привилегированную операцию — системный вызов (syscall). Это фундаментальный интерфейс между приложением и ядром Linux.

Каждый syscall идентифицируется целочисленным номером. Ядро хранит таблицу соответствия номеров функциям-обработчикам. Пользовательский процесс передаёт номер и аргументы, переключает CPU в режим ядра, ядро находит обработчик, выполняет его и возвращает результат.

Полный путь: от строки кода до возврата​


Последовательность событий при вызове, например, write():

  1. Приложение вызывает библиотечную функцию-обёртку (glibc wrapper).
  2. Обёртка помещает номер syscall и аргументы в регистры CPU.
  3. Выполняется инструкция перехода в режим ядра (syscall на x86-64).
  4. CPU переключается в Ring 0, управление передаётся точке входа ядра.
  5. Ядро сохраняет регистры, извлекает номер syscall, находит обработчик в таблице.
  6. Обработчик выполняется, результат записывается в регистр возврата.
  7. Ядро восстанавливает контекст и выполняет инструкцию возврата в userspace (sysret на x86-64).
  8. Обёртка glibc интерпретирует результат: при ошибке устанавливает errno и возвращает −1.

Пользовательская сторона: обёртка glibc​


Системные вызовы почти никогда не вызываются напрямую. Приложение использует функции из glibc (или другой libc), которые служат тонкими обёртками. Типичная обёртка делает три вещи:

  • Копирует аргументы в нужные регистры.
  • Выполняет инструкцию syscall.
  • После возврата проверяет результат: если ядро вернуло отрицательное число в диапазоне ошибок, обёртка инвертирует знак, записывает значение в errno и возвращает −1 вызывающему коду.

Иногда обёртка выполняет дополнительную логику...
Qwen

Что такое API Hooking и зачем он нужен​


API Hooking — техника перехвата вызовов функций Windows API до того, как управление достигнет оригинальной реализации. Перехватчик получает доступ к аргументам вызова, может их изменить, заблокировать вызов или передать управление собственной функции. Легитимные применения: отладчики, профайлеры, антивирусные мониторы, системы совместимости. Вредоносные: сокрытие процессов, перехват учётных данных, обход защитных механизмов.

Для защитника важно понимать механику на уровне байтов, чтобы отличать нормальное поведение от подозрительного.

Классификация методов перехвата​


МетодУровеньОбласть действияСложность обнаружения
Inline hooking (трамплин)Машинный код функцииКонкретная функция в процессеСредняя
IAT hookingТаблица импортаВсе вызовы через IAT модуляНизкая
SetWindowsHookExМеханизм сообщений WindowsГлобально или по потокуНизкая
SSDT hookingТаблица системных сервисов ядраВсе процессыВысокая (требует Ring 0)

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

Inline Hooking: патч первых байтов функции​


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

Трамплин на x86-64​


На 64-битной архитектуре адрес перехода не помещается в 32-битный операнд, поэтому используется двухинструкционная последовательность:

Код:
mov r10, <адрес_перехватчика>   ; 10 байт: 49 BA + 8 байт адреса
jmp r10                          ; 3 байта: 41 FF E2

Итого 13 байт (константа TRAMPOLINE_SIZE для x64).

Трамплин на x86​


На 32-битной архитектуре адрес помещается в 32-битный операнд:

Код:
mov eax, <адрес_перехватчика>   ; 5 байт: B8 + 4 байта адреса
jmp eax                          ; 2 байта: FF...
Qwen
APC Injection (T1055.004 по MITRE ATT&CK) — техника внедрения кода, при которой произвольная функция ставится в очередь асинхронных вызовов (Asynchronous Procedure Call) чужого потока. Когда поток входит в alertable-состояние, Windows выполняет все накопленные APC-функции в порядке очереди. Ключевая особенность для защитника: вредоносный код выполняется в контексте легитимного процесса без создания нового потока, поэтому классический детект на CreateRemoteThread здесь не срабатывает.

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

Как устроена очередь APC​


Каждый поток в Windows имеет собственную очередь APC — FIFO-структуру, которую ядро обрабатывает при переходе потока в alertable-состояние. Механизм предназначен для легитимных задач: асинхронный ввод-вывод, уведомления о завершении операций, callback-функции драйверов.

Постановка функции в очередь выполняется через QueueUserAPC:

C:
DWORD QueueUserAPC(
  PAPCFUNC  pfnAPC,   // указатель на функцию для выполнения
  HANDLE    hThread,   // дескриптор целевого потока
  ULONG_PTR dwData     // параметр, передаваемый в функцию
);

Функция не выполняется немедленно. Она ожидает, пока целевой поток не войдёт в alertable-состояние. Если поток уже находится в таком состоянии, APC выполняется сразу. Если поток приостановлен — выполнение откладывается до возобновления.

Alertable-состояние: обязательное условие выполнения​


Не каждая функция ожидания переводит поток в alertable-состояние. Это определяет, какие потоки могут стать мишенью:

ФункцияAlertableУсловие
SleepExДаbAlertable = TRUE
WaitForSingleObjectExДаbAlertable = TRUE
WaitForMultipleObjectsExДаbAlertable = TRUE
...

Новые сообщения на форуме

Статистика форума

Темы
914
Сообщения
1 074
Пользователи
69
Новый пользователь
theprof
Назад
Верх Низ