Статья DLL Inject [1] - методы внедрения

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

1. Введение
2. Привилегия Debug в токенах пользователей
3. Варианты инъекций DLL
4. Заключение



1. Введение

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

1. Мод для расширения функционала ПО. Горячие "Hot-Patch" фиксы позволяют исправлять ошибки в приложениях. Или-же кастомизация интерфейса, как это делает например программа "MacType". Она внедряет свою DLL в процессы Win, чтобы улучшить стандартный механизм сглаживания шрифтов. Аналогичным образом поступает и "PuntoSwitcher", для автоматической смены раскладки клавиатуры Ru/Eng.​
2. Антивирусы EDR/XDR - вот кто легальные пользователи этой технологии. В офисах аверов кипит работа по внедрению своих либ, чтобы проверять подозрительные файлы в песочнице "Sandbox" и отслеживать их поведение. Это позволяет шпионить за выделением памяти, вызовами API, и другими действиями малвари в реальном времени. Продукты класса EDR (CrowdStrike, Kaspersky) используют API-Hook внутри системных процессов, для блокировки атак на-лету.​
3. В игровой индустрии фишка применяется для внедрения оверлеев в код, чтобы рисовать поверх интерфейса FPS-счётчики и отправлять юзеру всякие уведомления, без модификации файлов самой игры. Это касается и загрузки всевозможных модов, для изменения логики игры.​

Да, на лицо вмешательство в работу других приложений, но майки официально предоставляют API SetWindowsHookEx() и CreateRemoteThread() для этих целей, признавая легитимность данной технологии. Однако если код использует инжект не будучи драйвером безопасности или антивирусом, это вызывает у нас подозрения. Поэтому в корпоративной среде техники подобного рода применяются с осторожностью, чтобы не нарушить системную политику безопасности.


2. Привилегии Debug и Backup в токенах пользователей

Понятно, что подсистема безопасности Win не позволит кому-попало резвиться в системе на своё усмотрение - для этого как минимум потребуются правами админа, а в большинстве случаях и привилегия отладки SeDebugPrivilege. Но проблема в том, что функция AdjustTokenPrivileges() может только вкл/откл привилегии, которые имеются у текущего юзера в токене. Если привилегия изначально не была назначена подсистемой безопасности lsass.exe при входе пользователя в сессию, мы не сможем её включить!

Ознакомиться со списком своих привилегий можно командой whoami /priv. Здесь видно, что у смертного юзера нет SeDebug, а потому AdjustTokenPrivileges() будет возвращать ошибку ERROR_NOT_ALL_ASSIGNED=0x514. То-есть мы можем включать только те, которые имеются в этом списке:

UserPriv.webp

А вот лист дефолтных прав админа, и мы можем активировать любую из них, в том числе дарующие нам полную свободу Debug и Backup:

AdminPriv.webp

При таких раскладах, если обычному юзеру потребуется привилегия SeDubug (а для Dll-Inject она необходима), он может через DuplicateTokenEx() скоммуниздеть токен админа, после чего включать уже в нём нужные привилегии. Другой вариант - это явное разрешение отладки в системной политике безопасности, правда манипуляции с этими флагами доступны только самому админу, так что круг замыкается. Для этого жмём Win+R и вводим secpol.msc, после чего идём по сл.пути:

Локальные политики --> Назначения прав пользователям --> Отладка программ --> Добавить пользователя..

Обычно там только админу прописан мандат, но если админ (за шоколадку) добавит и "Пользователя", то в списке юзера выше под номером(7) появится и SeDebugPrivilege:

secpol.webp


3. Варианты реализации DLL-Inject

Классический метод инъекций заключается в цепочке вызовов:
OpenProcess() --> VirtualAllocEx() --> WriteProcessMemory() --> CreateRemoteThread()

Как одноразовый шприц такой приём может и подойдёт, но на долгосрочную перспективу его уже не натянешь. Это наследие прошлого, и начиная с Win7 если и работает, то с большим ограничением. После того как племя служб и сервисов перекочевало в закрытую сессию(0), перечисленная выше триада уже не может до них дотянуться, и нужно искать альтернативные подходы.

Мир открывает столько возможностей, что зацикливаться на одних концепциях просто глупо. Идеи витают в воздухе, пока кто-нибудь не ухватит их, после чего все остальные дружной гурьбой копи-пастят. Но это как грабить уже ограбленный банк, который оцепила кругом полиция. Забавно, что каждое такое ограбление ослабляет охрану остальных объектов, а потому нужно найти наименее охраняемый периметр. Поскольку к каждому сарайчику часового не приставишь, значит простор для творческих манёвров у нас всё-таки есть.

Во-первых, можно поместить свой код в асинхронную очередь потока APC (Asynchronous Procedure Call) - этим занимается функция QueueUserAPC() из Kernel32. Например, когда при вызове CreateFile() мы указываем FILE_FLAG_OVERLAPTED, возвращаемый дескриптор будет поддерживать асинхронные операции R/W с файлом, т.е. последующие Read/WriteFile() будут возвращать управление сразу, не дожидаясь фактического окончания чтения/записи в файл или устройство. При этом система пропишет запрос именно в асинхронную очередь потока, вызвав под катом:

C-подобный:
QueueUserAPC(
  [in] PAPCFUNC   pfnAPC,   ;// Указатель на функцию для исполнения
  [in] HANDLE     hThread,  ;// Дескриптор целевого потока
  [in] ULONG_PTR  dwData    ;// Можно передать параметр в функцию выше
);

Она просто помещает запрос в АРС-очередь целевого треда, но не прерывает его принудительно. Для выполнения, в целевом потоке должен быть установлен флаг Аlertable. Это реализуется такими функами как SleepEx() и WaitForSingleObjectEx() с параметром bAlertable =1. В дефолте всегда имеется хоть один поток в процессе, который имеет данный флаг. Обычно перед инжектом ищат подходящих пациентов (разрядность, и флаг Alertable у потока), для чего можно заюзать отладчик WinDbg:

Код:
0: kd> !process 0 3 akelpad.exe
PROCESS fffffa8010c78320
    SessionId: 1   Cid: 0a4c  Peb: 7efdf000  ParentCid: 0534
    Image    : AkelPad.exe

THREAD fffffa8010f1bb50  Cid 0a4c.0a50  Teb: 7efdb000  Non-Alertable
            fffffa800ffd88c0  SynchronizationEvent

THREAD fffffa8010812b50  Cid 0a4c.0a58  Teb: 7efd8000  Alertable  <-------//
            fffffa8010d071e0  SynchronizationTimer
            fffffa80109c6b60  SynchronizationTimer
            fffffa80107ae640  SynchronizationTimer

Если-же нужно программно определить сигнальный/Alert поток или нет, можно прочитать его контекст через GetThreadContext(), и проверить бит(14) "NestedTask" в его регистре флагов EFLAGS. Ну или вызвать бронебойную NtQueryInformationThread() с классом ThreadAlertable =0x2b. Однако оба этих способа не надёжны на последних версиях Win10/11.

Функция QueueUserAPC() - это более продвинутый метод инжекта DLL в процессы. Она может заставить целевой поток вызвать LoadLibrary(), а наличие параметра "dwData" в ней позволяет передать в LoadLibrary() строку с именем требуемой библиотеки DLL. Соответственно если натравить параметр на свою либу, то она загрузиться в целевой поток. Другими словами, когда необходимо выполнить определённый код в конкретном потоке, задача может быть инкапсулирована в функцию APC, и поставлена в очередь.

А вот ещё вариант для внедрения кода в чужие процессы.
Начиная с Висты, в нативной Ntdll.dll появилась расширенная(Ex) функция создания потоков NtCreateThreadEx(), которой не было в ХР. Основное её отличие от предыдущей заключается в том, что она может внедрять либы даже в процессы за пределами своей сессии - как правило это закрытая сессия(0):

C-подобный:
NTSTATUS NtCreateThreadEx(
    OUT  ThreadHandle,      ;// Указатель на переменную под дескриптор потока
    IN   DesiredAccess,     ;// Маска доступа к потоку (например, THREAD_ALL_ACCESS)
    IN   ObjectAttributes,  ;// Указатель на структуру OBJECT_ATTRIBUTES (может быть NULL)
    IN   ProcessHandle,     ;// Дескриптор процесса, в котором создается поток
    IN   StartRoutine,      ;// Указатель на функцию, которая будет выполняться в потоке
    IN   Argument,          ;// Аргумент, передаваемый функции StartRoutine
    IN   CreateFlags,       ;// Флаги: 0 = стартовать поток сразу, 2 = ждать ResumeThread()
    IN   ZeroBits,          ;// --\  Связанные со стеком параметры,
    IN   StackSize,         ;//    > обычно выставляем все в нуль,
    IN   MaximumStackSize,  ;// --/  т.е. как у родительского процесса.
    IN   AttributeList      ;// Список атрибутов для потока (может быть NULL)
);

Возвращаемое значение: 0 = OK
----------------------
STATUS_ACCESS_DENIED  (0xC0000022): Доступ запрешён.
STATUS_INVALID_HANDLE (0xC0000008): Неверный дескриптор процесса.

;// https://ntdoc.m417z.com/ntcreatethreadex
;// Флаги
;//-------------------
THREAD_CREATE_FLAGS_CREATE_SUSPENDED       = 0x01
THREAD_CREATE_FLAGS_SKIP_THREAD_ATTACH     = 0x02
THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER     = 0x04
THREAD_CREATE_FLAGS_LOADER_WORKER          = 0x10
THREAD_CREATE_FLAGS_SKIP_LOADER_INIT       = 0x20
THREAD_CREATE_FLAGS_BYPASS_PROCESS_FREEZE  = 0x40

;// Атрибуты
;//-------------------
PS_ATTRIBUTE_CLIENT_ID       =  0x10003   ;// запрос ID жертвы
PS_ATTRIBUTE_TEB_ADDRESS     =  0x10004   ;// запрос адреса TEB жертвы
PS_ATTRIBUTE_IDEAL_PROCESSOR =  0x10005   ;// нет описания
PS_ATTRIBUTE_UMS_THREAD      =  0x10006   ;// нет описания

Вот алгоритм действий:

1. CreateToolhelp32Snapshot() - получить ID целевого процесса.​
2. OpenProcess() - открыть процесс, для этого и нужна привилегия SeDebug.​
3. VirtualAllocEx() - выделить память в целевом процессе под путь к dll.​
4. WriteProcessMemory() - записать путь к dll в выделенную память.​
5. GetProcAddress() - получить адреса LoadLibrary() и NtCreateThreadEx().​
6. NtCreateThreadEx() - инжект, передав в StartRoutine адрес LoadLibrary(), а в Argument линк на путь к DLL в памяти целевого процесса.​

Будем считать, что DLL мы внедрили в чужой процесс. Но кто и каким образом будет вызывать теперь из неё функции? Секрет в формате самой библиотеки, в аргумент "fdwReason" которой система отправляет 4 типа сообщений - нам-же достаточно только одного DLL_PROCESS_ATTACH из четырёх, которое придёт при подключении либы в адресное пространство процесса-жертвы. Вот возможный вариант оформления самой библиотеки для инжекта (просто боксит мессагу типа "всё ок"):

C-подобный:
format   pe gui 6.0 DLL
include 'win32ax.inc'
entry    DllEntryPoint

;//--------
.code
proc DllEntryPoint uses ebx esi edi, hinstDLL, fdwReason, lpvReserved
        cmp     [fdwReason], DLL_PROCESS_ATTACH  ;// Бинго!
        jne     @done
    
;// При загрузке DLL показываем сообщение
        invoke  MessageBox,0,<'DLL-Inject was successfully!',0>,\
                             <'Injection Success!',0>,0
@done:  mov     eax, TRUE
        ret
endp

;//--------
section '.idata' import data readable writeable
library  user32,'user32.dll'
include 'api\user32.inc'

;//--------
section '.reloc' fixups data readable discardable
   if $=$$
      dd 0, 8   ;// Фиктивная секция релоков для корректной работы
   end if

Таким образом, никто из неё не будет вызывать функции (здесь даже экспорта нет), а пайлоад сработает сам на автомате.


3.1. Обобщение методов DLL-инъекций

CreateRemoteThread() - классический метод.
Поддерживается всеми версиями Win, но легко обнаруживается античитами из-за характерного паттерна: новый поток создаётся в процессе, который его не создавал. Плюсы: простота реализации + высокая надёжность. Минусы: легко детектится, оставляя явные следы.

QueueUserAPC() - асинхронный вызов.
Метод использует существующие потоки целевого процесса. Он добавляет вызов в очередь APC потока, который выполняется, когда поток перейдёт в "Аlertable" состояние. Плюсы: не создаёт новых потоков (сложнее обнаружить). Минусы: требует, чтобы в процессе был поток в alertable состоянии (не все процессы подходят).

NtCreateThreadEx() - метод Native API.
Поскольку многие системы мониторинга проверяют только Win32-API, этот метод может остаться незамеченным. Плюсы: меньше отслеживается античитами, минусы: зависимость от недокументированных структур, а потому может сломаться с обновлениями Win.

SetWindowsHookEx() - перехват сообщений.
Данная техника использует легитимный механизм Win-хуков. При установке, ОС сама загружает DLL с обработчиком в процессы. Плюсы: использует легитимный механизм (не создаёт подозрительных потоков). Минусы: ограничен процессами только с графическим интерфейсом GUI.

Thread Hijacking - перехват потока.
Более сложный метод. Инжектор приостанавливает существующий поток в целевом процессе, сохраняет его контекст (регистры, стек), подменяет RIP/EIP на адрес шелла, дожидается выполнения, а затем восстанавливает исходное состояние потока. Плюсы: скрытный метод (не создаёт новых объектов ядра). Минусы: сложность реализации, и риск угробить процесс.

Reflective DLL Injection - ручной маппинг
Этот метод не вызывает LoadLibrary() вообще. Вместо этого инжектор загружает DLL в память стороннего процесса полностью вручную: т.е. сам обрабатывает секции, выполняет релоки, резолвит импорты, вызывает точку-входа DllMain(), и т.д. Плюсы: не оставляет следов в списке загруженных модулей процесса. Минусы: крайне сложная реализация, т.к. требует глубокого понимания внутреннего формата PE-файла.


4. Выводы

Инжект - мощный инструмент в арсенале разработчика. На fasm эта техника реализуется достаточно прямолинейно, сохраняя всю гибкость и контроль, которые даёт ассемблер. Понимание механизмов инъекции критически важно и для защитников: антивирусы и системы обнаружения вторжений активно мониторят характерные паттерны этих методов. Изучая инъекцию мы не только расширяем свои возможности как разработчика, но и глубже понимаем принципы безопасности Win в целом. Важно помнить, что это палка с двумя концами - при работе всегда нужно соблюдать законодательство и лицензионные соглашения.

Разрешено: Отладка собственных программ, разработка модов к играм с разрешения разработчика, создание инструментов мониторинга для корпоративного использования.
Запрещено: Взлом чужого ПО, создание читов для онлайн-игр (нарушает пользовательское соглашение), разработка вредоносного ПО.

По понятным причинам код готового инжектора не выкладываю, тем более что информации и так достаточно получилось. В сл.части рассмотрим, как можно найти и удалить с любых процессов внедрённые в них описанными выше способами сторонние либы - там и будет полный код анти-инжекта. Всем удачи, пока!
 
Изучая dll инъекции обнаружил интересную штуку: если изначально прямо вписать функции OpenProcess, VirtualAlloc, WriteProcessMemory, CreateRemoteThread, встроенный антивирусник windows ругается (на какие именно комбинации этих функций уже не помню). Но если их вызывать с помощью GetProcAddress т.е. узнаем адресс нужной функции и вызываем ее простым call, то уже все окей)
 
  • Нравится
Реакции: Сергей Попов
Мы в соцсетях:

Взломай свой первый сервер и прокачай скилл — Начни игру на HackerLab

🚀 Первый раз на Codeby?
Гайд для новичков: что делать в первые 15 минут, ключевые разделы, правила
Начать здесь →
🔴 Свежие CVE, 0-day и инциденты
То, о чём ChatGPT ещё не знает — обсуждаем в реальном времени
Threat Intel →
💼 Вакансии и заказы в ИБ
Pentest, SOC, DevSecOps, bug bounty — работа и проекты от проверенных компаний
Карьера в ИБ →

HackerLab