Способ принудительной загрузки DLL в адресное пространство процесса

Автор: Сторожевых Сергей
Agnitum Ltd.

Источник: RSDN Magazine #3-2007
Опубликовано: 14.11.2007
Версия текста: 1.0
Введение
Предлагаемый способ принудительной загрузки DLL
Перехват процедуры создания процесса
Загрузка внедряемой DLL с помощью APC
Форсирование доставки APC
WOW64
Заключение
Список литературы

При решении многих задач системного программирования зачастую бывает необходимо загрузить динамически подключаемую библиотеку (DLL) в адресное пространство другого процесса, с целью исследования либо изменения его поведения. В данной статье показан способ, позволяющий внедрить DLL в любой процесс (в том числе защищенный) на самом раннем этапе его создания.

Введение

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

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

На сегодняшний день известно множество способов, позволяющих осуществить задачу принудительной загрузки DLL в адресное пространство процесса. Пожалуй, самыми распространенными являются регистрация в ключе реестра AppInit_DLLs и использование CreateRemoteThread.

Ни приведенные выше, ни иные известные автору способы реализации механизма принудительной загрузки DLL не удовлетворяют сформулированным выше требованиям. Это и послужило толчком к разработке нового способа, позволяющего автоматически внедрить DLL в любой процесс до начала исполнения процессом пользовательского кода. Способ должен быть применим на Windows 2000/XP/2003/Vista (x64).

Предлагаемый способ принудительной загрузки DLL

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

Перехват процедуры создания процесса

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

1. Создание и инициализация объекта «процесс» исполнительной подсистемы.

2. Создание и инициализация объекта «поток» исполнительной подсистемы.

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

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

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);
  //// Получить указатель на LoadInjectDllThunk//
  loadInjectDllThunkRoutine = FindExportedRoutineByName(
                              viewBase,
                              &loadInjectDllThunkRoutineName);

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

VOID
LoadInjectDllThunk(
  IN PVOID LdrContext,
  IN PVOID SystemArgument1,
  IN PVOID SystemArgument2
 )
{
  [...]

  //// Загрузить внедряемую DLL вызовом LdrLoadLibrary из NTDLL//
  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);

  //// Получить указатель на LdrLoadLibrary из NTDLL//
  ldrContext->LoadDllRoutine = FindExportedRoutineByName(
                    ImageInfo->ImageBase,
                    &loadDllRoutineName);

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

        //
        // Инициализировать и поставить в очередь 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.

        //
        // Флаг UserApcPending будет установлен в KTHREAD текущего потока,
        // т.к. его очередь APC пользовательского режима не пуста (в момент 
        // вызова в очереди находится по крайней мере APC LoadInjectDllThunk).
        //
          // Нулевой интервал. Не играет роли в данном случае.
  LARGE_INTEGER interval = {0};  
  KeDelayExecutionThread(
    UserMode,        // Режим ожидания
    TRUE,            // Alertable?
    &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-битные библиотеки:

В 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;

  //// Инициализировать и поставить в очередь потоку, исполняющемуся// в контексте WOW64-процесса, 32-битную APC режима пользователя//
  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 NT, говорит о его универсальности. В пользу универсальности способа свидетельствует также возможность одновременного функционирования в системе нескольких механизмов, выполненных на его основе.

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

В качестве ограничений предложенного способа можно выделить следующие:

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

Список литературы

  1. Джеффри Рихтер. Windows для профессионалов. – СПб: Питер, 2001. - 752 с.
  2. 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.
  3. Albert Almeida. Inside NT's Asynchronous Procedure Call


Эта статья опубликована в журнале RSDN Magazine #3-2007. Информацию о журнале можно найти здесь