Как стать автором
Обновить

Создание прокси-dll для проверок эксплуатации dll hijack

Время на прочтение7 мин
Количество просмотров9.6K
Когда я исследую безопасность ПО, то одним из пунктов проверки является работа с динамическими библиотеками. Атаки типа DLL hijack («подмена dll» или «перехват dll») встречаются очень редко. Скорее всего, это связано с тем, что и разработчики Windows добавляют механизмы безопасности для предотвращения атак, и разработчики софта аккуратнее относятся к безопасности. Но тем интереснее ситуации, когда целевое ПО уязвимо.

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

Чтобы загрузка библиотеки прошла корректно, необходимо выполнить ряд условий: битность исполняемого файла и библиотеки должна совпадать и, если библиотека загружается при старте приложения, то dll должна экспортировать все те функции, которые это приложение ожидает импортировать. Часто одного импорта мало — очень желательно, чтобы приложение продолжило свою работу после загрузки dll. Для этого необходимо, чтобы у подготовленной библиотеки функции работали так же, как и у оригинальной. Реализовать это проще всего, просто передавая вызовы функций из одной библиотеки в другую. Вот именно такие dll называют прокси-dll.



Под катом будет несколько вариантов создания таких библиотек — как в виде кода, так и утилитами.

Небольшой теоретический обзор


Загрузка библиотек чаще происходит с помощью функции LoadLibrary, в которую передается имя библиотеки. Если вместо имени передать полный путь, то приложение попытается загрузить именно указанную библиотеку. Например, вызов LoadLibrary(“C:\Windows\system32\version.dll”) приведет к загрузке именно указанной dll. Или, если библиотека не будет существовать, то не будет загружена.

Немного занудства
Если в приложение уже загружена некоторая dll, то повторно она не будет загружаться. Учитывая, что именно version.dll загружается при старте почти любого exe-файла, то на самом деле вызов выше реально ничего не загрузит. Но мы все же рассматриваем общий случай, рассматривайте пример как вызов некоторой абстрактной библиотеки.

Совсем другое дело, если написать LoadLibrary(“version.dll”). В обычной ситуации результат будет ровно такой же, как в предыдущем случае — загрузится C:\Windows\system32\version.dll, но не все так просто.

Сначала будет произведен поиск библиотеки, который пойдет в следующем порядке:

  1. Папка с исполняемым файлом
  2. Папка C:\Windows\System32
  3. Папка C:\Windows\System
  4. Папка C:\Windows
  5. Папка, установленная как текущая для приложения
  6. Папки из переменной окружения PATH

Еще немного занудства
При запуске 32-битных приложений в 64-битной системе все обращения C:\Windows\system32 будут пробрасываться к C:\Windows\SysWOW64. Это просто для точности описания, с точки зрения атакующего разница не особо важна.

При запуске exe-файла ОС загружает все библиотеки из секции импорта файла. В общем смысле можно считать, что ОС принуждает файл к вызову LoadLibrary, передавая все те имена библиотек, которые написаны в секции импорта. Поскольку в 99,9% случаев там именно имена, а не пути, то при старте приложения все загружаемые библиотеки будут искаться в системе.

Из списка мест поиска dll реально нам важны два пункта — 1 и 6. Если мы подложим version.dll в ту же папку, откуда запускается файл, то вместо системного будет загружен именно подложенный. Такая ситуация практически не встречается, поскольку, если есть возможность подложить библиотеку, то, скорее всего, есть возможность и заменить сам исполняемый файл. Но все же такие ситуации возможны. Например, если исполняемый файл находится в доступной для записи папке и является сервисом с автостартом, то его нельзя изменить пока сам сервис работает. Или запускаемый файл перед стартом проверяется извне по контрольной сумме, то заменять файл все равно не вариант. А вот положить библиотеку рядом — будет вполне реально.

Возможно, нельзя создавать файлы рядом с исполняемы файлом, но можно создавать папки. В такой ситуации может сработать механизм WinSxS redirect (aka “DotLocal”).

Кратко о DotLocal
В манифесте файла может быть прописана зависимость от библиотеки конкретной версии. В таком случае при старте исполняемого файла (например, пусть это будет application.exe) ОС проверит существование папки с именем application.exe.local в той же папке, что и сам файл. В этой папке должна быть вложенная папка со сложным именем типа amd64_microsoft.windows.common-controls_6595b64144ccf1df_6.0.9600.19291_none_6248a9f3ecb5e89b внутри которой уже библиотека comctl32.dll. Имя библиотеки и информация для имени папки должна быть указана в манифесте, здесь же просто пример из первого попавшегося процесса. Если папок или файла не будет, то библиотека будет взята из C:\Windows\WinSxS. В примере — C:\Windows\WinSxS\amd64_microsoft.windows.common-controls_6595b64144ccf1df_6.0.9600.19291_none_6248a9f3ecb5e89b\comctl32.dll.

Но и это скорее исключение, нежели правило. А вот ситуации, когда поиск dll доходит до 6 номера в списке — вполне реальны. Если приложение попытается загрузить dll, которой нет в системе или рядом с файлом, то все поиски будут доходить до 6 пункта, в котором, потенциально, могут оказаться доступные для записи папки.

Например, типовая установка Python чаще всего происходит в папку C:\Python (или близкую). Сам установщик питона предлагает добавить свои папки в системную переменную PATH. В итоге имеем хороший плацдарм для начала атаки — папка доступна для записи всем пользователям и любая попытка загрузить несуществующую библиотеку дойдет до поиска в путях из PATH.

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

Первый вариант. Честная прокси-библиотека


Начнем с относительно простого — сделаем честную прокси-библиотеку. Честность в данном случае подразумевает, что все функции в dll будут прописаны явно, и для каждой функции будет написан вызов функции с тем же именем из оригинальной библиотеки. Работа с такой библиотекой будет полностью прозрачна для вызываемого кода: если тот вызывает некоторую функцию, то он получит корректный ответ, результат и все, что там побочно должно произойти.

Вот ссылка на готовый пример (github) библиотеки version.dll.

Основные моменты кода:

  • Все прототипы функций из таблицы экспорта оригинальной библиотеки честно описаны.
  • Загружается оригинальная библиотека и все вызовы наших функций пробрасываются в нее.

Удобно тем, что приложение продолжает корректно работать, не испытывая никаких «спецэффектов». Неудобно тем, что пришлось написать кучу однообразного кода для каждой из функций, причем тщательно проверяя совпадение прототипов.

Второй вариант. Упрощаем написание кода


Когда имеешь дело с библиотекой типа version.dll, где таблица импорта небольшая, всего 17 функций, и прототипы простые, то честная прокси-библиотека — хороший выбор.



А вот если прокси для библиотеки, например, bcrypt, то все сложнее. Вот ее таблица импорта:



57 функций! Причем вот пара примеров прототипов:




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

Упростить код можно, если немного схитрить с функциями. Объявим все функции в библиотеке как __declspec(naked), а в теле — код на ассемблере, который просто сделает jmp на функцию из оригинальной библиотеки. Это позволит нам не использовать длинные прототипы, а поставить везде простые объявления без параметров вида:

void foo()

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

Пример (github) библиотеки version.dll с таким подходом.

Основные моменты:

  • Загружается оригинальная библиотека, и все вызовы наших функций пробрасываются в нее. Тела функций и загрузка обернуты в макросы.

Удобно и корректной работой приложения и тем, что даже большое количество функций легко описывается, благодаря макросам. Неудобно тем, что довольно неожиданные грабли в x64. Visual Studio (где-то, начиная с 2012, если я правильно помню) запрещает в 64-битном коде использовать naked и asm-вставки. При написании прокси «с нуля», необходимо для каждой функции проконтролировать, что она описана в def-файле, что загружается оригинал и описано тело функции.

Третий вариант. Выкидываем тело вообще


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

void nop() {}

Такая библиотека будет загружена приложением, но не будет работать. При вызове любой из функций будет, скорее всего, порван стек или случится еще какая-то гадость. Но это не всегда и плохо — если, например, цель dll-инъекции просто запустить код с нужными правами, то достаточно исполнить полезную нагрузку из DllMain прокси-библиотеки и тут же тихо завершить работу приложения. В таком случае до реального вызова функций дело не дойдет и ошибок-падений не появится.

Пример на гитхабе, опять для version.dll.

Основные моменты кода:

  • Все функции из def-файла ссылаются на одну nop-функцию.

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

Четвертый вариант. Возьмем готовые утилиты


Писать dll это хорошо, но не всегда удобно и не очень быстро, поэтому стоит рассмотреть автоматизированные варианты.

Можно пойти по пути старых вирусов — взять библиотеку, прокси которой хотим сделать, создать в ней исполняемую секцию кода, записать туда полезную нагрузку и поменять точку входа на эту секцию. Не самый простой способ, потому что можно что-то сломать ненароком, придется писать на ассеблере, вспоминать устройство PE-файла. Это не наш путь.

Для эксплуатации dll hijack мы добавим еще один dll hijack.



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

«Хей, ты же заменил загрузку одной библиотеки другой. В чем смысл? Все равно надо будет кодить dll!». Все правильно, но смысл все же есть. Теперь к библиотеке с полезной нагрузкой будет меньше требований. Имя можно задать любое, главное экспортировать всего одну функцию, у которой может быть любой прототип. Главное имя библиотеки и функции вписать в таблицу импорта.

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

Модифицировать таблицу импорта можно многими редакторами PE, например CFF explorer или pe-bear. Для себя я написал небольшую утилиту на C#, которая правит таблицу без лишних телодвижений. Исходники на гитхабе, бинарь в разделе Release.

Заключение


В статье я постарался раскрыть основные способы создания прокси-dll, которыми пользовался сам. Осталось только рассказать, как защищаться.

Универсальных рекомендаций не так много:

  • Не храните исполняемые файлы, особенно запускаемые с высокими правами, в папках доступных для записи пользователям.
  • Лучше сначала найти и проверить существование библиотеки, прежде чем делать LoadLibrary.
  • Посмотрите на существующие способы защиты, доступные в ОС. Например, в Windows 10 можно задать флаг PreferSystem32 чтобы поиск dll начинался не с папки с исполняемым файлом, а с system32.

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

UPD: По советам комментаторов напоминаю о том, что выбирать библиотеку нужно аккуратно и внимательно. Если бибилиотека входит в список KnownDlls или имя похоже на MinWin (ApiSetSchema, api-ms-win-core-console-l1-1-0.dll — вот это вот все), то скорее всего перехватить ее не удастся из-за особенносей обработки таких dll в ОС.
Теги:
Хабы:
+18
Комментарии21

Публикации

Информация

Сайт
amonitoring.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия