Спосіб примусового завантаження DLL в адресний простір процесу

Автор: Сторожових Сергій Agnitum Ltd. Джерело: RSDN Magazine #3-2007
ВисновокСписок литературы
спосіб

В даний час механізм примусового завантаження DLL використовується в багатьох програмних продуктах. До таких продуктів належать різні утиліти, що дозволяють відстежувати нюанси функціонування програм, а також програмні комплекси, що модифікують поведінку програм, наприклад, з метою забезпечення комп'ютерної безпеки.

Виходячи з зазначених сфер застосування механізму, можна сформулювати вимоги до його реалізації: по-перше, механізм повинен бути застосовним до будь-якого процесу в системі, а по-друге, повинен активуватися на ранньому етапі запуску процесу, що цікавить. Причому у разі застосування механізму в програмних комплексах захисту інформації важливо, щоб завантаження DLL відбувалося в автоматичному режимі, «прозоро» для користувача.

Пропонований спосіб примусового завантаження DLL

Для того щоб виконувалася вимога прозорого завантаження впроваджуваної DLL в момент запуску процесу, що цікавить, необхідно перехоплювати процедуру створення процесів в системі. Зазначимо, що перехоплення з допомогою модифікації таблиці системних викликів у роботі розглядається, т.к. реалізація такого перехоплення неможлива в 64-бітових версіях Windows (див. Kernel Patch Protection).

Перехоплення процедури створення процесу

Розглянемо коротко основні етапи запуску процесу з погляду вибору відповідного місця для вбудовування механізму примусового завантаження DLL.

1. Створення та ініціалізація об'єкта «процес» виконавчої підсистеми.

2. Створення та ініціалізація об'єкта «потік» виконавчої підсистеми.

  • Повідомлення про запуск нового процесу та первинного потоку(див. PsSetCreateProcessNotifyRoutine(Ex), PsSetCreateThreadNotifyRoutine ). Зазначимо, що процедури повідомлення про дані події виконуються в контексті батьківського процесу, а сам первинний потік ще не запущений і частково неініціалізований, що суттєво обмежує можливості та робить недоцільним вбудовування примусового завантаження DLL на цьому етапі.
  • Включення черги АРС. Це дозволяє поставити чергу чергу відкладену процедуру, яка буде виконана в контексті потоку в заданому режимі (ядра або користувача).

3. Запуск первинного потоку як ядра.

  • Додавання в чергу поточного потоку APC режиму користувача LdrInitializeThunk , щоб при виході з режиму ядра завершити ініціалізацію процесу, в тому числі, зробити завантаження статично пов'язаних DLL.
  • Повідомлення про завантаження образів (див. PsSetLoadImageNotifyRoutine ) виконуваного файлу процесу та ntdll.dll. Повідомлення про завантаження ntdll.dll є оптимальним місцем для вбудовування механізму примусового завантаження DLL, т.к. по-перше, відбувається у контексті первинного потоку процесу. По-друге, процес і потік завершили свою ініціалізацію як ядра. І, нарешті, це остання подія, про яку виконавча підсистема розсилає повідомлення перед переходом потоку в режим користувача і початком виконання користувальницького коду.

4. Ініціалізація процесу у режимі користувача. Починається при доставці потоку APC режиму користувача LdrInitializeThunk.

  • Ініціалізація завантажувача.
  • Завантаження статично пов'язаних DLL.
  • Виклик точок входу ( DllMain ) завантажених DLL.

5. Передача керування на точку входу образу процесу.

ПРИМІТКА

Реалізація механізму примусового завантаження DLL передбачає використання драйвера режиму ядра. Зазначимо, що у 64-бітних версіях Windows драйвер повинен мати цифровий підпис.

Завантаження впроваджуваної DLL за допомогою APC

Тому доцільно завантажити inject.dll в режимі користувача за допомогою системного сервісу, що надається ntdll.dll, а саме: LdrLoadDll. Для цього пропонується:

а) У момент отримання повідомлення про завантаження ntdll.dll відобразити в пам'ять процесу код процедури-перехідника (позначимо як LoadInjectDllThunk), призначення якої полягає у виклику системного сервісу LdrLoadDll.

б) поставити в чергу потоку власний APC користувача режиму, при диспетчеризації якого управління перейде LoadInjectDllThunk (див. малюнок 1). Зазначимо, що на момент отримання повідомлення у черзі потоку вже знаходиться системний APC LdrInitializeThunk.

ПРИМІТКА

APC (Asynchronous Procedure Call) – асинхронний виклик процедури. Спеціальний об'єкт ядра, який служить асинхронного виконання процедури у контексті певного потоку. Існує три типи APC: користувальницький, нормальний та спеціальний режим ядра. У цій роботі використовуються лише користувацькі APC, які виконуються строго в режимі користувача, коли потік перебуває у стані тривожного очікування.

завантаження
Малюнок 1. Загальна схема завантаження DLL, що впроваджується.

Розглянемо докладніше суть запропонованого методу. Перед тим, як поставити в чергу потоку власний APC LoadInjectDllThunk, потрібно виконати низку підготовчих дій.

Код перехідника повинен оперувати даними та покажчиками на функції, що передаються як параметрифункції-перехідника.

2. Оскільки для завантаження inject.dll в режимі користувача передбачається використання сервісів, що надаються ntdll.dll, то спочатку необхідно отримати покажчики на відповідні функції (LdrLoadDll та ін.) і сформувати контекст, який згодом буде переданий як параметр у функцію-перехідник LoadInjectDllThunk при диспетчеризації APC.

3. Після того, як підготовча робота виконана, APC ставиться в чергу поточного потоку.

4. Після виконання вищеперелічених дій у черзі потоку повинні знаходитися два APC режиму користувача, які будуть виконані по черзі при переході потоку в режим користувача (див. малюнок 2).

спосіб
Малюнок 2. Використання APC для впровадження DLL

Виконуватиметься доставка APC користувальницького режиму чи ні, регулюється спеціальним прапором відкладеної доставки APC у блоці ядра потоку KTHREAD, а саме: UserApcPending. Відповідно, перший раз цей прапор встановлюється у процедурі запуску потоку відразу ж після додавання до черги системної АРС LdrInitializeThunk. Тому при першому переході потоку з режиму ядра в режим користувача цей APC буде вибраний із черги на виконання. Разом з цим прапор UserApcPending скидається, тому під час виконання LdrInitializeThunk не буде доставлятися жодних інших APC режимів користувача. Це означає, що APC LoadInjectDllThunk, доданий нами слідом за системним APC, отримає шанс на виконання лише після завершення процедури LdrInitializeThunk. Перед самим виходом APC-процедура викликає системний сервіс NtTestAlert, всередині якого прапор відкладеного завантаження знову встановлюється, якщо черга APC режиму користувача не порожня. Оскільки у черзі все ще знаходиться APCLoadInjectDllThunk , прапор буде встановлений, і при переході в режим користувача APC буде доставлено.

Форсування доставки APC

Порядок доставки APC важливий, тому що необхідно гарантувати завантаження впроваджуваної DLL на ранньому етапі запуску процесу - до того, як буде виконаний будь-який код користувача. Однак той факт, що APC LoadInjectDllThunk буде доставлений тільки після закінчення роботи LdrInitializeThunk (тобто після того, як будуть завантажені статично-пов'язані DLL і виконані їх точки входу) суперечить цій вимогі.

Однак ядром не експортується системний сервіс NtTestAlert і відповідна процедура KeTestAlertThread, що дозволяє встановити прапор відкладеної доставки APC режиму користувача UserApcPending. Знаючи формат недокументованої структури KTHREAD, можна встановити прапорець вручну. Проте з погляду універсальності розроблюваного способу цей варіант застосовувати не можна, оскільки формат цієї структури змінюється від версії до версії.

Щоб коректно вирішити завдання форсованої доставки APC, необхідно врахувати, що доставка APC режиму користувача можлива при виконанні наступних умов: потік повинен перебувати у стані тривожного очікування, причому режим очікування має бути режимом користувача. Перевести потік у такий стан можна за допомогою однієї з чотирьох процедур: KeWaitForSingleObject, KeWaitForMultipleObjects, KeWaitForMutexObject або KeDelayExecutionThread. Перелічені процедури під час виклику з параметрами Alertable = TRUE, WaitMode = User перевірять наявність APC режиму користувача в черзі, і якщо черга не порожня, встановлять прапор відкладеної доставки APC.

Отже, при надходженні повідомлення про відображення першої зі статично пов'язаних DLL потрібно перевести поточний потік встан тривожного очікування. Тоді при виході з режиму ядра APC LoadInjectDllThunk буде негайно доставлено. Таким чином, завантаження і виконання точки входу бібліотеки inject.dll, що впроваджується, будуть виконані раніше всіх інших, пов'язаних з процесом, DLL.

Малюнок 3 ілюструє описаний вище процес форсованого впровадження inject.dll.

завантаження
Малюнок 3. Форсування завантаження бібліотеки, що впроваджується.

ПРИМІТКА

Отже, пропонований спосіб примусового впровадження DLL за допомогою АРС дозволяє здійснити завантаження бібліотеки автоматично (прозоро для користувача), а форсування доставки APC забезпечує завантаження та виклик точки входу бібліотеки, що впроваджується, до моменту ініціалізації будь-якої іншої бібліотеки.

Реалізація пропонованого способу не відрізняється у 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 відповідно.

У цьому виникає проблема виконання 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-процедуру) . При цьому покажчик на x86-процедуру передається в молодших 32-х бітах контексту створюваного APC, а контекст зазначеної x86-процедури - у старших.

Тоді при диспетчеризації всередині 64-бітної ntdll.dll буде викликана Wow64ApcRoutine, яка здійснить перехід поточного середовища виконання в режим сумісності з x86 і передасть управління процедурі диспетчеризації APC з 32-бітної ntdll.dll.

Таким чином, реалізація способу примусового завантаження DLL за допомогою APC може бути без особливих змін застосована до 32-бітових процесів, що виконуються в середовищі WOW64.

ПОПЕРЕДЖЕННЯ

Форсування доставки APC призводить до порушення доступу всередині wow64.dll т.к. WOW64-емулятор не до кінця інціалізований в момент завантаження статично пов'язаних DLL. Це частковообмежує область застосування способу і може бути предметом подальших досліджень з метою розвитку запропонованого способу.

Висновок

При реалізації запропонованого методу застосовуються тільки системні механізми, а той факт, що метод може бути застосований у всіх підтримуваних ОС сімейства Windows NT, говорить про його універсальність. На користь універсальності способу свідчить можливість одночасного функціонування в системі декількох механізмів, виконаних на його основі.

Як обмеження запропонованого способу можна виділити такі:

  • Впровадження DLL неможливе у захищені процеси у Windows Vista. Проте, оскільки реалізації запропонованого способу використовується драйвер режиму ядра, то надалі дане обмеження може бути усунуто шляхом модифікації структур режиму ядра процесу. Це завдання вже вирішено в утиліті Introducting D-Pin Purr v1.0.
  • Форсування APC неможливе у WOW64-процесах. Усунення цього обмеження можуть бути присвячені подальші дослідження щодо розвитку запропонованого способу.

З урахуванням наведених обмежень запропонований спосіб може бути застосований при створенні програмних продуктів різного класу: засобів моніторингу та налагодження, а також захисту інформації.