Спосіб примусової завантаження DLL в адресний простір процесу
- Автор: сторожових Сергій Agnitum Ltd. джерело: RSDN Magazine # 3-2007
- Вступ
- Пропонований спосіб примусової завантаження DLL
- Перехоплення процедури створення процесу
- Завантаження впроваджуваної DLL за допомогою APC
- Форсування доставки APC
- WOW64
- Висновок
- список літератури
Автор: сторожових Сергій
Agnitum Ltd.
джерело: RSDN Magazine # 3-2007
Опубліковано: 14.11.2007
Виправлено: 10.12.2016
Версія тексту: 1.0
При вирішенні багатьох завдань системного програмування часто буває необхідно завантажити динамічно підключається бібліотеку (DLL) в адресний простір іншого процесу, з метою дослідження або зміни його поведінки. У даній статті показаний спосіб, що дозволяє впровадити DLL в будь-який процес (в тому числі захищений) на самому ранньому етапі його створення.
Вступ
В даний час механізм примусової завантаження DLL використовується в безлічі програмних продуктів. До таких продуктів відносяться різні утиліти, що дозволяють відслідковувати нюанси функціонування додатків, а також програмні комплекси, що модифікують поведінку додатків, наприклад, з метою забезпечення комп'ютерної безпеки.
Виходячи з означених сфер застосування механізму, можна сформулювати вимоги до його реалізації: по-перше, механізм повинен бути застосовний до будь-якого процесу в системі, а по-друге, потрібно активувати на самому ранньому етапі запуску цікавить процесу. Причому, в разі застосування механізму в програмних комплексах захисту інформації, важливо, щоб завантаження DLL відбувалася в автоматичному режимі, «прозоро» для користувача.
На сьогоднішній день відомо безліч способів, що дозволяють здійснити завдання примусової завантаження DLL в адресний простір процесу. Мабуть, найпоширенішими є реєстрація в ключі реєстру AppInit_DLLs і використання CreateRemoteThread .
- Реєстрація в AppInit_DLLs . Зареєстрована подібним чином DLL буде автоматично завантажена при створенні процесу. Відзначимо, що даний спосіб непридатний в разі необхідності впровадження DLL в додатки, які не використовують user32.dll.
- Використання CreateRemoteThread . Суть даного методу полягає в створенні потоку в сюжеті процесі, який і завантажує потрібну DLL. Відзначимо, що на момент впровадження DLL цільової процес уже запущений і функціонує в системі, при цьому процес повинен бути відкритий з правами на створення віддалених потоків і записи в адресний простір процесу, що в сукупності істотно обмежує сферу застосування способу ..
Ні наведені вище, ні інші відомі автору способи реалізації механізму примусової завантаження DLL не задовольняють сформульованим вище вимогам. Це і послужило поштовхом до розробки нового способу, що дозволяє автоматично впровадити DLL в будь-який процес до початку виконання процесом користувацького коду. Спосіб повинен бути застосовний на Windows 2000 / XP / 2003 / Vista (x64).
Пропонований спосіб примусової завантаження DLL
Для того щоб виконувалася вимога прозорою завантаження впроваджуваної DLL в момент запуску цікавить процесу, необхідно перехоплювати процедуру створення процесів в системі. Відзначимо, що перехоплення за допомогою модифікації таблиці системних викликів в роботі не розглядається, тому що реалізація подібного перехоплення неможлива в 64-бітових версіях Windows (див. Kernel Patch Protection ).
Перехоплення процедури створення процесу
Розглянемо коротко основні етапи запуску процесу, з точки зору вибору підходящого місця для вбудовування механізму примусової завантаження DLL.
1. Створення і ініціалізація об'єкта «процес» виконавчої підсистеми.
- Створення віртуального адресного простору процесу. З цього моменту стає можливою завантаження модулів в адресний простір процесу.
- Відображення образів виконуваного файлу і системної бібліотеки (ntdll.dll) на адресний простір процесу.
2. Створення та ініціалізація об'єкта «потік» виконавчої підсистеми.
- Повідомлення про запуск нового процесу і первинного потоку (див. PsSetCreateProcessNotifyRoutine (Ex), PsSetCreateThreadNotifyRoutine). Відзначимо, що процедури повідомлення про дані події виконуються в контексті батьківського процесу, а сам первинний потік ще не запущений і частково неініціалізованих, що істотно обмежує можливості і робить недоцільним вбудовування механізму примусової завантаження DLL на цьому етапі.
- Включення черзі АРС. Це дозволяє поставити в чергу потоку відкладену процедуру, яка буде виконана в контексті потоку в заданому режимі (ядра або користувача).
3. Запуск первинного потоку в режимі ядра.
- Додавання в чергу поточного потоку APC режиму користувача LdrInitializeThunk, з тим, щоб при виході з режиму ядра завершити ініціалізацію процесу, в тому числі, зробити завантаження статично пов'язаних DLL.
- Сповіщення про завантаження образів (див. PsSetLoadImageNotifyRoutine) виконуваного файлу процесу і ntdll.dll. Повідомлення про завантаження ntdll.dll бачиться оптимальним місцем для вбудовування механізму примусової завантаження DLL, тому що по-перше, відбувається в контексті первинного потоку процесу. По-друге, процес і потік завершили свою ініціалізацію в режимі ядра. І, нарешті, це остання подія, про яку виконавча підсистема розсилає повідомлення перед переходом потоку в призначений для користувача режим і початком виконання призначеного для користувача коду.
4. Ініціалізація процесу в режимі користувача. Починається при доставці потоку APC режиму користувача LdrInitializeThunk.
- Ініціалізація завантажувача.
- Завантаження статично пов'язаних DLL.
- Виклик точок входу (DllMain) завантажених DLL.
5. Передача управління на точку входу образу процесу.
Отже, для перехоплення процедури створення процесу і вбудовування механізму примусової завантаження DLL пропонується використовувати повідомлення про відображення в адресний простір процесу системної бібліотеки (ntdll.dll).
ПРИМІТКАРеалізація механізму примусової завантаження DLL передбачає використання драйвера режиму ядра, Відзначимо, що в 64-бітових версіях Windows драйвер повинен мати цифровий підпис.
Завантаження впроваджуваної DLL за допомогою APC
Перехопивши створення процесу в режимі ядра, в адресний простір процесу необхідно завантажити призначену для користувача DLL, що є серйозною проблемою. Ядро Windows NT не надає відповідних сервісів. Тому необхідно або реалізовувати логіку завантажувача самостійно, або відкласти впровадження потрібної DLL (позначимо як inject.dll), поки не відбудеться перехід потоку в режим користувача і не ініціалізується системний завантажувач.
Перший варіант надзвичайно трудомісткий і, на думку автора, не може бути коректно здійснений в силу закритості багатьох структур, які використовуються вбудованим завантажувачем. До того ж при реалізації власного завантажувача в режимі ядра необхідно враховувати, що inject.dll, в свою чергу, може мати великий список статично пов'язаних DLL, які також потрібно буде завантажити і викликати їх точки входу.
Тому доцільно завантажити inject.dll в режимі користувача за допомогою системного сервісу, що надається ntdll.dll, а саме: LdrLoadDll. Для цього пропонується:
а) У момент отримання повідомлення про завантаження ntdll.dll відобразити в пам'ять процесу код процедури-перехідника (позначимо як LoadInjectDllThunk), призначення якої полягає у виклику системного сервісу LdrLoadDll.
б) поставити в чергу потоку власний APC призначеного для користувача режиму, при диспетчеризації якого управління перейде LoadInjectDllThunk (див. малюнок 1). Відзначимо, що на момент отримання повідомлення в черзі потоку вже знаходиться системний APC LdrInitializeThunk.
ПРИМІТКА
APC (Asynchronous Procedure Call) - асинхронний виклик процедури. Спеціальний об'єкт ядра, який служить для асинхронного виконання процедури в контексті певного потоку. Існує три типи APC: призначений для користувача, нормальний і спеціальний режиму ядра. У даній роботі використовуються тільки призначені для користувача APC, які виконуються строго в режимі користувача, коли потік знаходиться в стані тривожного очікування.
Малюнок 1. Загальна схема завантаження впроваджуваної DLL.
Розглянемо докладніше суть запропонованого способу. Перед тим, як поставити в чергу потоку власний APC LoadInjectDllThunk, потрібно виконати ряд підготовчих дій.
1. Відображення модуля (позначимо як thunk.dll), що реалізує функцію-перехідник, в адресний простір процесу. Використання окремого модуля для коду перехідника обумовлено простотою його завантаження для виконання в адресний простір процесу. Модуль повинен бути виконаний у вигляді динамічно-завантажується бібліотеки і експортувати функцію-перехідник (LoadInjectDllThunk). При цьому модуль повинен бути базонезавісімим (інакше необхідно виробляти модифікацію адрес в пам'яті, що містять зміщення щодо передбачуваного адреси завантаження) і не містити таблиці імпорту - тоді його завантаження в режимі ядра стає тривіальним завданням, тому що крім, власне, відображення в адресний простір процесу, не потрібно ніякої додаткової роботи.
status = ZwCreateSection (& section, SECTION_MAP_READ | SECTION_MAP_EXECUTE, NULL, NULL, PAGE_EXECUTE, SEC_IMAGE, imageFileHandle); status = ZwMapViewOfSection (section, NtCurrentProcess (), (PVOID *) & viewBase, 0, 0, NULL, & viewSize, ViewUnmap, 0, PAGE_EXECUTE); loadInjectDllThunkRoutine = FindExportedRoutineByName (viewBase, & loadInjectDllThunkRoutineName);
Код перехідника повинен оперувати даними і покажчиками на функції, що передаються в якості параметрів функції-перехідника.
VOID LoadInjectDllThunk (IN PVOID LdrContext, IN PVOID SystemArgument1, IN PVOID SystemArgument2) {[...] status = LdrContext-> LoadDllRoutine (NULL, NULL, & LdrContext-> InjectDllName, & dllHandle); [...] LdrContext-> TestAlertRoutine (); }
2. Оскільки для завантаження inject.dll в режимі користувача передбачається використання сервісів, що надаються ntdll.dll, то спочатку необхідно отримати покажчики на відповідні функції (LdrLoadDll і ін.) І сформувати контекст, який згодом буде переданий в якості параметра в функцію-перехідник LoadInjectDllThunk при диспетчеризації APC.
status = ZwAllocateVirtualMemory (NtCurrentProcess (), & ldrContext, 0, & regionSize, MEM_COMMIT, PAGE_READWRITE); ldrContext-> LoadDllRoutine = FindExportedRoutineByName (ImageInfo-> ImageBase, & loadDllRoutineName);
3. Після того як підготовча робота виконана, APC ставиться в чергу поточного потоку.
KeInitializeApc (startApc, KeGetCurrentThread (), OriginalApcEnvironment, SpecialApcRoutine, RundownApcRoutine, loadDllThunkRoutine, UserMode, ldrContext); KeInsertQueueApc (startApc, NULL, NULL, 0);
4. Після виконання перерахованих вище дій в черзі потоку повинні знаходитися два APC призначеного для користувача режиму, які будуть виконані по черзі при переході потоку в режим користувача (див. Рисунок 2).
Малюнок 2. Використання APC для впровадження DLL
Виконуватиметься доставка APC призначеного для користувача режиму чи ні, регулюється спеціальним прапором відкладеної доставки APC в блоці ядра потоку KTHREAD, а саме: UserApcPending. Відповідно, перший раз цей прапор встановлюється в процедурі запуску потоку відразу ж після додавання в чергу системного АРС LdrInitializeThunk. Тому при першому переході потоку з режиму ядра в режим користувача цей APC буде обраний з черги на виконання. Разом з цим прапор UserApcPendingсбрасивается, тому під час виконання LdrInitializeThunk не проводитиметься доставка ніяких інших APC режиму користувача. Це означає, що APC LoadInjectDllThunk, доданий нами слідом за системним APC, отримає шанс на виконання тільки після завершення процедури LdrInitializeThunk. Перед самим виходом APC-процедура викликає системний сервіс NtTestAlert, всередині якого прапор відкладеної завантаження знову встановлюється, якщо чергу APC призначеного для користувача режиму не порожня. Оскільки в черзі все ще знаходиться APC LoadInjectDllThunk, прапор буде встановлено, і при переході в режим користувача APC буде доставлений.
Форсування доставки APC
Порядок доставки APC важливий, так як необхідно гарантувати завантаження впроваджуваної DLL на самому ранньому етапі запуску процесу - до того, як буде виконаний будь-який користувацький код. Однак той факт, що APC LoadInjectDllThunk буде доставлений тільки після закінчення роботи LdrInitializeThunk (тобто після того як будуть завантажені статично-пов'язані DLL і виконані їх точки входу) суперечить цій вимозі.
Тому якщо у процесу є статично пов'язані DLL (крім системної ntdll.dll) необхідно форсувати доставку APC LoadInjectDllThunk. Для цього пропонується встановлювати прапор відкладеної доставки APC в момент надходження повідомлення про відображення (ZwMapViewOfSection) в адресний простір процесу першої статично пов'язаної DLL, яке відбувається при вирішенні таблиці імпорту процесу всередині LdrInitializeThunk. Тоді APC LoadInjectDllThunk буде доставлений відразу ж при виході з системного сервісу ZwMapViewOfSection, до того, як LdrInitializeThunk отримає шанс виконати ініціалізацію завантажується DLL. Це дозволить inject.dll завантажитися в адресний простір процесу раніше інших модулів.
Однак ядром не експортується системний сервіс NtTestAlert і відповідна процедура KeTestAlertThread, що дозволяє безпосередньо встановити прапор відкладеної доставки APC режиму користувача UserApcPending. Знаючи формат недокументованою структури KTHREAD, даний прапор можна встановити вручну. Однак з точки зору універсальності розробляється способу цей варіант застосовувати не можна, так як формат даної структури змінюється від версії до версії.
Щоб коректно вирішити задачу форсованої доставки APC, необхідно врахувати, що доставка APC режиму користувача можлива при виконанні наступних умов: потік повинен перебувати в стані тривожного очікування, причому режим очікування повинен бути режимом користувача. Перекласти потік в такий стан можна шляхом виклику однієї з чотирьох процедур: KeWaitForSingleObject, KeWaitForMultipleObjects, KeWaitForMutexObject або KeDelayExecutionThread. Перераховані процедури при виклику з параметрами Alertable = TRUE, WaitMode = User перевірять наявність APC режиму користувача в черзі, і якщо черга не порожня, встановлять прапор відкладеної доставки APC.
Отже, при надходженні повідомлення про відображення першої з статично-пов'язаних DLL потрібно перевести поточний потік в стан тривожного очікування. Тоді при виході з режиму ядра APC LoadInjectDllThunk буде негайно доставлений. Таким чином, завантаження і виконання точки входу впроваджуваної бібліотеки inject.dll будуть виконані раніше за всі інші, пов'язаних з процесом, DLL.
LARGE_INTEGER interval = {0}; KeDelayExecutionThread (UserMode, TRUE, & interval);
Малюнок 3 ілюструє описаний вище процес форсованого впровадження inject.dll.
Малюнок 3. Форсування завантаження впроваджуваної бібліотеки.
Відзначимо ключову особливість способу c форсуванням доставки APC: завантаження впроваджуваної DLL відбувається безпосередньо перед викликом точки входу того модуля, повідомлення про відображення якого було отримано. Це відкриває нові можливості: завантаження впроваджуваної бібліотеки може бути здійснена в строго певний момент до виконання точки входу конкретної, заздалегідь відомої DLL. Це може бути корисним при необхідності відкласти завантаження впроваджуваної DLL до того моменту, як в адресний простір процесу не буде відображений образ певної DLL, що представляє інтерес, наприклад, з точки зору перехоплення. Тоді отримання повідомлення про відображення способу такої DLL буде служити сигналом для постановки в чергу APC і форсування завантаження впроваджуваної бібліотеки.
ПРИМІТКА
Розглядаючи цю можливість запропонованого способу, необхідно враховувати наступний момент: хоча постановка в чергу APC і може бути відкладена до отримання повідомлення про відображення будь-якого модуля, образ перехідника thunk.dll повинен бути відображений в адресний простір процесу раніше, при отриманні повідомлення про завантаження ntdll .dll. Це пов'язано з тим, що відображення образів виконуваного файлу процесу і системної бібліотеки відбувається відокремлено, в момент створення адресного простору процесу, а повідомлення про ці події приходить пізніше, під час запуску первинного потоку процесу, тоді як відображення інших модулів і повідомлення про це відбуваються під об'єктом синхронізації (fast mutex), що забороняє одночасну модифікацію структур, що описують віртуальний адресний простір процесу. Відповідно, відкладене відображення thunk.dll може привести до рекурсивному захоплення об'єкта синхронізації, і, в даному випадку, до взаімоблокіровке.
Отже, пропонований спосіб примусового впровадження DLL за допомогою АРС дозволяє здійснити завантаження бібліотеки автоматично (прозоро для користувача), а форсування доставки APC забезпечить регулярне і виклик точки входу впроваджуваної бібліотеки до моменту ініціалізації будь-який інший бібліотеки.
WOW64
Реалізація запропонованого способу не відрізняється в 32-бітних і 64-бітових версіях Windows 2000 / XP / 2003 / Vista. Однак при завантаженні впроваджуваної бібліотеки в 32-бітний процес в 64-бітових версіях Windows необхідно враховувати особливості функціонування 32-бітних процесів в средеWOW64.
WOW64-емулятор виконується в режимі користувача і знаходиться між 32-бітної ntdll.dll і 64-бітовим ядром, перехоплюючи виклики системних сервісів ядра. WOW64 включає в себе три 64-бітові бібліотеки:
- wow64.dll - ядро інфраструктурі емуляції, перехідник до точок входу ядра;
- wow64win.dll - перехідник до точок входу win32k.sys;
- wow64cpu.dll - емуляція інструкцій процесора x86.
У 32-бітній процес могут буті завантажені только перераховані 64-бітові модулі и 64-бітна ntdll.dll. Тому при впровадженні в 32-бітній процес в 64-бітовіх версіях Windows як впроваджувана inject.dll, так и завантажуваті ее thunk.dll повінні буті 32-бітнімі. 64 і 32-бітові версії цих бібліотек доцільно помістити в каталоги system32 і sysWOW64 відповідно.
При запуску процесу wow64.dll передає управління процедурі ініціалізації всередині 32-бітної ntdll.dll, яка завантажує інші необхідні 32-бітові бібліотеки в адресний простір процесу. Тому в момент оповіщення про завершення завантаження wow64.dll необхідно поставити в чергу APC LoadInjectDllThunk, яка для завантаження 32-бітної inject.dll викличе LdrLoadDll з 32-бітної ntdll.dll.
При цьому виникає проблема виконання 32-бітної APC-процедури, тому що при доставці APC необхідно перевести контекст виконання процесора в режим сумісності з x86 і передати управління 32-бітного коду APC-процедури. Справа в тому, що в 64-бітових версіях Windows будь APC режиму користувача при доставці спочатку діспетчерізіруется 64-бітної ntdll.dll, і 32-бітна APC-процедура не може бути виконана безпосередньо.
У зв'язку з цим wow64.dll експортує недокументовану функцію-перехідник Wow64ApcRoutine (отриману в результаті дослідження wow64.dll, виконаного в ході роботи). Wow64ApcRoutine уможливлює доставку 32-бітних APC потоку процесу, що виконується під WOW64. Для цього при ініціаліцаціі APC призначеного для користувача режиму в якості процедури, яка буде виконана при доставці APC, слід вказати саме Wow64ApcRoutine (а не цільову x86-пpоцедуру). При цьому покажчик на x86-пpоцедуру передається в молодших 32-х бітах контексту створюваного APC, а контекст зазначеної x86-пpоцедури - в старших.
union {struct {ULONG Apc32BitContext; ULONG Apc32BitRoutine; }; PVOID Apc64BitContext; } Wow64ApcContext; apc32BitContext.Apc32BitRoutine = loadDllThunk32BitRoutine; apc32BitContext.Apc32BitContext = ldr32BitContext; KeInitializeApc (startApc, KeGetCurrentThread (), OriginalApcEnvironment, SpecialApcRoutine, RundownApcRoutine, wow64ApcRoutine, UserMode, wow64ApcContext. Apc64BitContext);
Тоді при диспетчеризації всередині 64-бітної ntdll.dll буде викликана Wow64ApcRoutine, яка зробить перехід поточного середовища виконання в режим сумісності з x86 і передасть управління процедурі диспетчеризації APC з 32-бітної ntdll.dll.
Таким чином реалізація способу примусової завантаження DLL за допомогою APC може бути без особливих змін застосована до 32-бітних процесам, що виконуються в середовищі WOW64.
ПОПЕРЕДЖЕННЯ
Форсування доставки APC призводить до порушення доступу всередині wow64.dll, тому що WOW64-емулятор не до кінця інціалізірован в момент завантаження статично пов'язаних DLL. Це частково обмежує сферу застосування способу і може служити предметом подальших досліджень з метою розвитку запропонованого способу.
Висновок
У роботі був запропонований новий спосіб примусової завантаження DLL в адресний простір процесу, що дозволяє:
- автоматично впроваджувати бібліотеку в усі новостворювані процеси в системах Windows 2000 / XP / 2003 / Vista (x64);
- виконати точку входу впроваджуваної DLL до моменту ініціалізації будь-якої іншої завантажується в адресний простір процесу DLL.
При реалізації запропонованого способу використовуються тільки системні механізми, а той факт, що спосіб може бути застосований у всіх підтримуваних ОС сімейства Windows NT, говорить про його універсальності. На користь універсальності способу свідчить також можливість одночасного функціонування в системі декількох механізмів, виконаних на його основі.
Реалізація запропонованого способу може бути без праці модифікована для відкладеного впровадження DLL в момент безпосереднього завантаження в адресний простір процесу конкретного модуля, що свідчить про гнучкості способу.
В якості обмежень запропонованого способу можна виділити наступні:
- Впровадження DLL неможливо в захищені процеси в Windows Vista. Однак, оскільки для реалізації запропонованого способу використовується драйвер режиму ядра, то в подальшому це обмеження може бути усунуто шляхом модифікації структур режиму ядря процесу. Дане завдання вже вирішена в утиліті Introducting D-Pin Purr v1.0 .
- Форсування APC неможливо в WOW64-процесах. Усунення даного обмеження можуть бути присвячені подальші дослідження в частині розвитку запропонованого способу.
З урахуванням наведених обмежень запропонований спосіб може бути застосований при створенні програмних продуктів самого різного класу: засобів моніторингу та налагодження, а також захисту інформації.
список літератури
- Джеффрі Ріхтер. Windows для професіоналів. - СПб: Пітер, 2001. - 752 с.
- Mark E. Russinovich, David A. Solomon. Microsoft Windows Internals, Fourth Edition: Microsoft Windows Server (TM) 2003 Windows XP, and Windows. - Washigton: Microsoft Press, 2005. - 935 p.
- Albert Almeida. Inside NT's Asynchronous Procedure Call