Когда я исследую безопасность программного обеспечения, одной из контрольных точек является работа с динамическими библиотеками.
Атаки перехвата DLL («перехват dll» или «перехват dll») случаются очень редко.
Скорее всего, это связано с тем, что разработчики Windows добавляют механизмы безопасности для предотвращения атак, а разработчики программного обеспечения более внимательно относятся к безопасности.
Но более интересны ситуации, когда целевое программное обеспечение уязвимо.
Кратко описывая атаку, DLL hijack — это создание ситуации, при которой какой-то исполняемый файл пытается загрузить 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, но не всё так просто.
Сначала будет выполнен поиск в библиотеке, который будет происходить в следующем порядке: хорошо :
- Папка с исполняемым файлом
- Папка C:\Windows\System32
- Папка C:\Windows\System
- Папка C:\Windows
- Папка установлена как текущая для приложения
- Папки из переменной среды PATH
При запуске exe-файла ОС загружает все библиотеки из раздела импорта файлов.
В общем смысле можно считать, что ОС заставляет файл вызывать LoadLibrary, передавая все те имена библиотек, которые прописаны в разделе импорта.
Поскольку в 99,9% случаев речь идет об именах, а не путях, то при запуске приложения в системе будет производиться поиск всех загруженных библиотек.
Из списка мест для поиска dll нам очень важны два пункта - 1 и 6. Если мы поместим version.dll в ту же папку, из которой запускается файл, то вместо системной будет именно посадил тот, который загружен.
Такая ситуация практически не возникает, так как если есть возможность добавить библиотеку, то, скорее всего, можно и заменить сам исполняемый файл.
Но такие ситуации все же возможны.
Например, если исполняемый файл находится в доступной для записи папке и является службой автозапуска, то его нельзя изменить во время работы самой службы.
Либо запускаемый файл перед запуском проверяется извне по контрольной сумме, тогда замена файла все равно не вариант. А вот разместить рядом библиотеку было бы вполне реально.
Возможно, вы не сможете создавать файлы рядом с исполняемым файлом, но можете создавать папки.
В такой ситуации может сработать механизм перенаправления WinSxS (он же «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 (или аналогичной).
Сам установщик Python предлагает добавить ваши папки в системную переменную PATH. В результате мы имеем хороший плацдарм для начала атаки — папка доступна для записи всем пользователям и любая попытка загрузить несуществующую библиотеку приведет к перебору по путям из PATH. Теперь, когда теория раскрыта, давайте посмотрим на создание полезной нагрузки — самих прокси-библиотек.
Первый вариант. Честная прокси-библиотека
Начнем с относительно простой вещи — сделаем честную прокси-библиотеку.Честность в данном случае подразумевает, что все функции в dll будут написаны явно, и для каждой функции будет написан одноименный вызов функции из исходной библиотеки.
Работа с такой библиотекой будет полностью прозрачна для вызываемого кода: если он вызовет определенную функцию, то получит правильный ответ, результат и все остальное, что там должно произойти.
Вот ссылка на готовый пример ( github ) версия библиотеки.
dll. Основные моменты кода:
- Честно описаны все прототипы функций из оригинальной таблицы экспорта библиотеки.
- Загружается исходная библиотека и в нее передаются все вызовы наших функций.
Неудобный потому что мне пришлось написать кучу монотонного кода для каждой из функций, тщательно проверяя совпадения прототипов.
Второй вариант. Упрощение написания кода
Когда вы имеете дело с такой библиотекой, как version.dll, где таблица импорта небольшая, всего 17 функций, а прототипы простые, то честная прокси-библиотека — хороший выбор.
А вот если прокси для библиотеки, например bcrypt, то тут все сложнее.
Вот таблица импорта:
57 функций! Вот пара примеров прототипов:
Скажем так, нет ничего невозможного, но делать честный прокси для такой библиотеки не очень приятно.
Упростить код можно, если немного схитрить с функциями.
Объявим все функции в библиотеке как __declspec(naked), а в теле будет ассемблерный код, который просто сделает jmp для функции из оригинальной библиотеки.
Это позволит нам не использовать длинные прототипы, а везде размещать простые объявления без параметров типа: пустота Фу() Когда приложение вызывает нашу функцию, прокси-библиотека не будет выполнять никаких манипуляций с регистрами или стеком, позволяя исходной функции выполнять всю работу должным образом.
Пример ( github ) библиотеки version.dll с таким подходом.
Основные моменты:
- Загружается исходная библиотека, и в нее передаются все вызовы наших функций.
Тела функций и загрузка заключены в макросы.
Неудобный потому что это довольно неожиданные грабли в x64. Visual Studio (где-то с 2012 года, если я правильно помню) запрещает использовать голые и ассемблерные вставки в 64-битный код. При написании прокси «с нуля» необходимо для каждой функции проверять, что она описана в def-файле, загружен оригинал и описано тело функции.
Третий вариант. Мы вообще выбрасываем тело
Использование голого предполагает другой вариант. Вы можете создать таблицу импорта, которая связывает все функции с одной реальной строкой кода: недействительный ноп() {} Такая библиотека будет загружена приложением, но работать не будет. При вызове любой из функций скорее всего порвется стек или произойдет еще какая-нибудь неприятная вещь.Но это не всегда плохо — если, например, цель внедрения dll — просто запустить код с необходимыми правами, то достаточно выполнить полезную нагрузку из прокси-библиотеки DllMain и сразу спокойно закрыть приложение.
.
В этом случае до фактического вызова функций дело не дойдет и никаких сбоев не возникнет. Пример на github , опять же для version.dll. Основные моменты кода:
- Все функции из файла def ссылаются на одну функцию nop.
Четвертый вариант. Возьмем готовые утилиты
Написание dll — это хорошо, но это не всегда удобно и не очень быстро, поэтому стоит рассмотреть автоматизированные варианты.Мы можем пойти по пути старых вирусов — взять библиотеку, которую хотим проксировать, создать в ней исполняемый участок кода, записать туда полезную нагрузку и изменить точку входа в этот раздел.
Не самый простой способ, ведь можно что-то ненароком сломать, придется писать на ассемблере, помнить структуру PE-файла.
Это не наш путь.
Для работы с библиотекой захвата мы добавим еще одну библиотеку захвата.
Это относительно легко сделать.
Давайте скопируем библиотеку, которую мы хотим проксировать, и добавим в таблицу импорта этой копии какую-нибудь dll с произвольной функцией.
Теперь загрузка будет идти по цепочке — при запуске исполняемого файла будет загружаться прокси-dll, которая сама будет загружать указанную библиотеку.
«Эй, ты заменил загрузку одной библиотеки на другую.
В чем смысл? Вам все равно придется кодировать DLL!» Все правильно, но все равно имеет смысл.
Теперь требований к библиотеке полезной нагрузки будет меньше.
Имя можно задать любое, главное экспортировать только одну функцию, прототип которой может быть любым.
Введите основное имя библиотеки и функцию в таблицу импорта.
И может быть одна библиотека с полезной нагрузкой на все случаи жизни.
Вы можете изменить таблицу импорта с помощью многих редакторов PE, например CFF explorer или pe-bear. Для себя я написал небольшую утилиту на C#, которая без лишней возни редактирует таблицу.
Источники на github , двоичный в разделе Выпускать .
Заключение
В статье я постарался раскрыть основные методы создания прокси-dll, которые использовал сам.Остаётся только рассказать, как защититься.
Универсальных рекомендаций не так уж и много:
- Не храните исполняемые файлы, особенно те, которые запускаются с высокими правами, в папках, доступных для записи пользователем.
- Лучше сначала найти и проверить наличие библиотеки, прежде чем делать LoadLibrary.
- Посмотрите существующие методы защиты, доступные в ОС.
Например, в Windows 10 вы можете установить Флаг PreferSystem32 чтобы поиск dll начинался не из папки с исполняемым файлом, а из system32.
УПД: По советам комментаторов, напоминаю, что выбирать библиотеку нужно внимательно и внимательно.
Если библиотека включена в список KnownDlls или имя похоже на MinWin (ApiSetSchema, api-ms-win-core-console-l1-1-0.dll — и все), то, скорее всего, сделать это не удастся.
перехватить его из-за особенностей обработки таких dll в ОС.
Теги: #информационная безопасность #программирование #Windows #C++ #Системное программирование #Перехват Dll #Внедрение DLL
-
У Меня Оборот Нулевой
19 Oct, 24 -
Роль Математики В Машинном Обучении
19 Oct, 24