DLL hijacking - техника, которая может дать множество преимуществ: повыситься до NT AUTHORITY/SYSTEM, получить исполнение от лица привилегированного пользователя, действовать от лица легитимного приложения и т. д.

Естественно, перед тем как ее осуществить, нужно найти подходящее приложение и библиотеку, которую можно подменить.

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

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

1. Сбор списка библиотек, теоретически подверженных подмене

Для начала разберемся, какие библиотеки приложение использует во время работы.

Сразу на ум приходит таблица импорта (Import Address Table) в заголовке PE исполняемого файла.

Приложение также может явно загрузить библиотеку с помощью семейства функций из Kernel32.dll: LoadLibraryA, LoadLibraryW, LoadLibraryExA, LoadLibraryExW.

Если присмотреться пристальнее к тому, как устроены исполняемые файлы и что делает загрузчик, список пополняется:

  1. Библиотеки отложенной загрузки (delay-load imports)

  2. Библиотеки, на которые ведут forwarders

  3. Библиотеки, загружаемые до Kernel32.dll

И, разумеется, это не полный список.

Однако следить за каждым вариантом - не самая удобная стратегия. Есть ли у них общий знаменатель?

Большая часть динамических загрузок в итоге приходит к вызову LdrLoadDll из ntdll.dll:

NTSYSAPI
NTSTATUS
NTAPI
LdrLoadDll(
    Inopt_ PCWSTR DllPath,
    Inopt_ PULONG DllCharacteristics,
    In PCUNICODE_STRING DllName,
    Out PVOID *DllHandle
);

Таким образом, перехватив вызовы этой функции и сохранив DllName, мы можем собрать обширный список библиотек.

Как можно организовать перехват?

Для этого можно внедрить трамплин в начало функции LdrLoadDll и перенаправить его на собственную хук-функцию, которая будет логировать название загружаемой библиотеки.

Пример реализации можно найти здесь.

Итого, стратегия сбора следующая:

  1. Разрабатываем библиотеку Hooker.dll, которая:

    1. при загрузке в процесс устанавливает хук на функцию LdrLoadDll

    2. хук записывает аргумент DllName в файл

  2. Разрабатываем консольное приложение, которое:

    1. создает процесс с необходимым исполняемым файлом в состоянии suspended

    2. процесс создается в рамках объекта Job

    3. в созданном процессе выделяет память и записывает туда путь к Hooker.dll

    4. создает в процессе поток с функцией LoadLibraryA и аргументом - адресом выделенной памяти со строкой

    5. возобновляет процесс

    6. по истечении таймаута завершает его

Схематичное отображение инжектора
Схематичное отображение инжектора

Какой выбрать таймаут - вопрос практики. Часто достаточно дать приложению поработать 30 секунд, чтобы поймать явные вызовы.

В результате получается Injector, который после отработки оставляет файл со списком библиотек.

2. Auto DLL hijacking

Теперь, когда у нас есть список библиотек, нужно понять, какие из них реально удастся подменить.

На практике список бывает большим, поэтому без автоматизации не обойтись.

Какие требования мы ставим?

Необходимо средство, которое для произвольного приложения может подменить произвольную библиотеку.

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

Соответственно, запись содержит RVA к коду функции и ее имя для поиска по таблице.

Однако иногда вместо кода запись может ссылаться на строку ASCII. Это и есть механизм перенаправления (forwarders).

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

В качестве примера приведена таблица Kernel32.dll. Когда приложение импортирует функцию AcquireSRWLockExclusive, загрузчик загружает Kernel32.dll, видит перенаправление, подгружает ntdll.dll и ищет функцию RtlAcquireSRWLockExclusive.

Теперь вернемся к нашей исходной задаче. Есть приложение, которое использует некоторую библиотеку.

Мы хотим подменить ее и внедриться в это взаимодействие. Что если воспользоваться механизмом перенаправления?

Мы создадим библиотеку, которая будет называться так же, как и та, которую хотим подменить, и будет экспортировать те же функции, но перенаправлять их на настоящую библиотеку.

Полезную нагрузку поместим в DllMain. В качестве минимальной нагрузки используем небольшую функцию, которая создает файл с расширением .injected, чтобы сигнализировать о том, что подмена произошла.

В результате получим следующую схему:

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

Elliot Killick в своей статье описал способ разместить полезную нагрузку в DllMain с учетом блокировки LoaderLock и исполнить ее без вреда для процесса.

Итак, первым шагом компилируем библиотеку с полезной нагрузкой в DllMain без таблицы экспорта.

Далее создаем свою таблицу экспорта: формируем секцию, корректно размещаем в ней данные и указываем в заголовке PE адрес этой секции как адрес таблицы экспорта.

Содержимое секции:

  1. _IMAGE_EXPORT_DIRECTORY

  2. имя целевой библиотеки

  3. массив ординалов

  4. массив RVA на имена функций

  5. массив RVA на код функций (в нашем случае - на строки forwarders)

  6. имена функций

  7. строки forwarders

Архитектура проксирующей библиотеки
Архитектура проксирующей библиотеки

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

Заключение

Таким образом, мы сумели решить следующие задачи:

  • для любой используемой приложением библиотеки создавать проксирующую библиотеку, причем в автоматическом режиме

  • для произвольного приложения собирать список используемых им библиотек

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