- Практическое применение LD_PRELOAD или замещение функций в Linux
- Реальный Use-Case #1: Блокируем mimeinfo.cache в Opera
- Реальный Use-Case #2: Превращаем файл в сокет
- Не совсем реальный Use-Case #3: Перехват C++-методов
- В поисках LD_PRELOAD
- 1. Небольшое отступление для тех, кто не знаком с замещением функций
- 2. Муки поиска
- 2.1. Начнём с простого
- 2.2. Двигаемся дальше
- 2.3. /proc/self/
- 2.4. Вариант от Chokepoint
- 2.5. Syscalls
- Debian GNU\Linux: работа над ошибками
- В здоровом теле, здоровый дух. Как-то так и с компьютерами и софтом.
- Ошибка 1.
- Ошибка 2.
- Ошибка 3
- Ошибка 4.
- Linux etc ld so preload
- ОПИСАНИЕ
- Раскрытие токена rpath
- ПАРАМЕТРЫ
- ОКРУЖЕНИЕ
- Безопасный режим выполнения
- Переменные окружения
Практическое применение LD_PRELOAD или замещение функций в Linux
Всем привет!
В 2010 году, shoumikhin написал замечательную статью Перенаправление функций в разделяемых ELF-библиотеках. Та статья очень грамотно написана, полная, но она описывает более харкордный способ замещения функций. В этой статье, мы будем использовать стандартную возможность динамического линкера — переменную окружения LD_PRELOAD, которая может загрузить вашу библиотеку до загрузки остальных.
Как это работает?
Да очень просто — линкер загружает вашу библиотеку с вашими «стандартными» функциями первой, а кто первый — того и тапки. А вы из своей библиотеки можете загрузить уже реальную, и «проксировать» вызовы, попутно делая что вам угодно.
Реальный Use-Case #1: Блокируем mimeinfo.cache в Opera
Мне очень нравится браузер Opera. А еще я использую KDE. Opera не очень уважает приоритеты приложений KDE, и, зачастую, так и норовит открыть скачанный ZIP-архив в mcomix, PDF в imgur-uploader, в общем, вы уловили суть. Однако, если ей запретить читать файл mimeinfo.cache, то она все будет открывать через «kioclient exec», а он-то уж лучше знает, в чем я хочу открыть тот или иной файл.
Чем может приложение открывать файл? На ум приходят две функции: fopen и open. В моем случае, opera использовала 64-битный аналог fopen — fopen64. Определить это можно, воспользовавшись утилитой ltrace, или просто посмотрев таблицу импорта утилитой objdump.
Что нам нужно для написания библиотеки? Первым делом, нужно составить прототип оригинальной функции.
Судя по man fopen, прототип у этой функции следующий:
И возвращает она указатель на FILE, либо NULL, если файл невозможно открыть. Отлично, пишем код:
Как видите, все просто: объявляем функцию fopen64, загружаем «следующую» (оригинальную), по отношению к нашей, функцию, и проверяем, не открываем ли мы файл «mimeinfo.cache». Компилируем ее следующей командой:
И запускаем opera:
И видим:
Успех!
Реальный Use-Case #2: Превращаем файл в сокет
Есть у меня проприетарное приложение, которое использует прямой доступ к принтеру (файл устройства /dev/usb/lp0). Захотел я написать для него свой сервер в целях отладки. Что возвращает open()? Файловый дескриптор. Что возвращает socket()? Такой же файловый дескриптор, на котором совершенно так же работают read() и write(). Приступаем:
Не совсем реальный Use-Case #3: Перехват C++-методов
С C++ все немного иначе. Скажем, есть у нас класс:
Но функции не будут называться «Testclass::getvar» и «Testclass::setvar» в результирующем файле. Чтобы узнать названия функций, достаточно посмотреть таблицу экспорта:
Это называется name mangling.
Тут есть два выхода: либо сделать библиотеку-перехватчик на C++, описав класс так же, каким он был в оригинале, но, в этом случае, у вас, с большой вероятностью, будут проблемы с доступом к конкретному инстансу класса, либо же сделать библиотеку на C, назвав функцию так, как она экспортируется, в таком случае, первым параметром вам передастся указатель на инстанс:
Вот, собственно, и все, о чем хотелось рассказать. Надеюсь, это будет кому-то полезно.
Источник
В поисках LD_PRELOAD
Эта заметка была написана в 2014-м году, но я как раз попал под репрессии на хабре и она не увидела свет. За время бана я про неё забыл, а сейчас нашёл в черновиках. Думал было удалить, но авось кому пригодится.
В общем, небольшое пятничное админское чтиво на тему поиска «включенного» LD_PRELOAD.
1. Небольшое отступление для тех, кто не знаком с замещением функций
Остальным можно сразу переходить к п.2.
Начнём с классического примера:
Компилируем без каких-либо флагов:
И, ожидаемо, получаем 5 случайных чисел меньше 100:
Но представим, что у нас нет исходного кода программы, а поведение изменить нужно.
Создадим свою библиотеку с собственным прототипом функции, например:
И теперь наш случайный выбор вполне предсказуем:
Этот трюк выглядит ещё более впечатляющим, если мы сначала экспортируем нашу библиотеку через
или предварительно выполним
а затем запустим программу в обычном режиме. Мы не изменили ни строчки в коде самой программы, но её поведение теперь зависит от крошечной функции в нашей библиотеке. Более того, на момент написания программы фальшивый rand даже не существовал.
Что же заставило нашу программу использовать поддельный rand? Разберем по шагам.
Когда приложение запускается, загружаются определенные библиотеки, которые содержат функции необходимые программе. Мы можем посмотреть их используя ldd:
Этот список может быть различен в зависимости от версии OS, но там обязательно должен быть файл libc.so. Именно эта библиотека обеспечивает системные вызовы и основные функции, такие как open, malloc, printf и т. д. Наш rand также входит в их число. Убедимся в этом:
Посмотрим, не изменится ли набор библиотек при использовании LD_PRELOAD
Оказывается установленная переменная LD_PRELOAD заставляет загрузиться нашу ld_rand.so даже, не смотря на то, что программа сама её не требует. И, так как наша функция «rand» загружается раньше чем rand от libc.so, то она и правит балом.
Ok, заменить родную функцию нам удалось, но как бы сделать так, чтобы и её функционал сохранился и некие действия добавились. Модифицируем наш рандом:
Здесь в качестве нашей «добавки» мы лишь печатаем одну строку текста, после чего создаём указатель на исходную функция rand. Для получения адреса этой функции нам потребуется dlsym — это функция из библиотеки libdl, которая найдет наш rand в стеке динамических библиотек. После чего мы вызовем эту функцию и возвратим её значение. Соответственно, нам понадобится добавить «-ldl» при сборке:
И наша программа использует «родной» rand, предварительно исполнив некие непотребные действия.
2. Муки поиска
Зная о потенциальной угрозе, мы хотим обнаружить, что preload был выполнен. Понятно, что лучший способ детектинга — запихнуть его в ядро, но меня интересовали именно варианты определения в юзерспэйсе.
Далее парами пойдут решения для обнаружения и их опровержение.
2.1. Начнём с простого
Как говорилось ранее, указать загружаемую библиотеку можно с помощью переменной LD_PRELOAD или прописав её в файле /etc/ld.so.preload. Создадим два простейших детектора.
Первый — для проверки установленной переменной окружения:
Второй — для проверки открытия файла:
Здесь и далее [+] указывает на успешное обнаружение.
Соответственно, [-] означает обход детектирования.
Насколько же действенен такой обнаружитель? Сначала займёмся переменной окружения:
Аналогично избавляемся и от проверки open:
Да, здесь могут быть использованы другие способы доступа к файлу, такие как, open64, stat и т.д., но, по сути, для их обмана необходимы те же 5-10 строк кода.
2.2. Двигаемся дальше
Выше мы использовали getenv() для получения значения LD_PRELOAD, но есть же и более «низкоуровневый» способ добраться до ENV-переменных. Не будем использовать промежуточные функции, а обратимся к массиву **environ, в котором хранится копия окружения:
Так как здесь мы читаем данные напрямую из памяти, то такой вызов нельзя перехватить, и наш undetect_getenv уже не мешает определить вторжение.
Казалось бы на этом проблема решена? Всё ещё только начинается.
После того как программа запущена, значение переменной LD_PRELOAD в памяти уже не нужно взломщикам, то есть можно считать её и удалить до выполнения каких-либо инструкций. Конечно, править массив в памяти, как минимум, плохой стиль программирования, но разве это может остановить того, кто и так не особо желает нам добра?
Для этого нам понадобится создать свою фейковую функцию init(), в которой перехватим установленный LD_PRELOAD и передать её нашему компоновщику:
2.3. /proc/self/
Однако, память — это не последнее место, где можно обнаружить подмену LD_PRELOAD, есть же ещё и /proc/. Начнём с очевидного /proc/
На самом деле есть универсальное решение для undetect’а **environ и /proc/self/environ. Проблема заключается в «неправильном» поведении unsetenv(env).
Но представим, что мы его не нашли и /proc/self/environ содержит «проблемные» данные.
Сначала попробуем с нашей предыдущей «маскировкой»:
cat использует для открытия файла всё тот же open(), поэтому решение сходно с тем, что уже делалось в п.2.1, но теперь мы создаем временный файл, куда копируем значения истинной памяти без строк содержащих LD_PRELOAD.
И этот этап пройден:
Следующее очевидное место — /proc/self/maps. Задерживаться на нем нет смысла. Решение абсолютно идентичное предыдущему: копируем данные из файла за вычетом строк между libc.so и ld.so.
2.4. Вариант от Chokepoint
Это решение мне особенно понравилось своей простотой. Сравниваем адреса функций, загружаемых непосредственно из libc, и «NEXT»-адреса.
Загружаем библиотеку с перехватом «open()» и проверяем:
Опровержение оказалось ещё проще:
2.5. Syscalls
Казалось бы на этом всё, но ещё побарахтаемся. Если мы направим системный вызов напрямую к ядру, то это позволит обойти весь процесс перехвата. Решение ниже, разумеется, архитектурозависимое (x86_64). Попробуем реализовать для обнаружения открытия ld.so.preload.
И данная задачка имеет решение. Выдержка из man‘а:
ptrace — это средство, позволяющее родительскому процессу наблюдать и контролировать протекание другого процесса, просматривать и изменять его данные и регистры. Обычно эта функция используется для создания точек прерывания в программе отладки и отслеживания системных вызовов.
Родительский процесс может начать трассировку, сначала вызвав функцию fork(2), а затем получившийся дочерний процесс может выполнить PTRACE_TRACEME, за которым (обычно) следует выполнение exec(3). С другой стороны, родительский процесс может начать отладку существующего процесса при помощи PTRACE_ATTACH.
При трассировке дочерний процесс останавливается каждый раз при получении сигнала, даже если этот сигнал игнорируется. (Исключением является SIGKILL, работающий обычным образом.) Родительский процесс будет уведомлен об этом при вызове wait(2), после которого он может просматривать и изменять содержимое дочернего процесса до его запуска. После этого родительский процесс разрешает дочернему продолжать работу, в некоторых случаях игнорируя посылаемый ему сигнал или отправляя вместо этого другой сигнал).
Тем самым, решение заключается в том, чтобы отслеживать процесс, останавливая его перед каждым системным вызовом и, при необходимости, перенаправлять поток в функцию ловушки.
, чьи статьи, исходники и комментарии сделали намного больше, чем я, для того чтобы эта заметка появилась здесь.
Источник
Debian GNU\Linux: работа над ошибками
В здоровом теле, здоровый дух. Как-то так и с компьютерами и софтом.
При этом весь поток всякой чепухи будет литься в консоль. Можно сделать перенаправление вывода в файл в виде:
Ошибка 1.
Создали симлинк на папку которую программулина не находит.
Так же на всякий случай перегенерируем кеш шрифтов:
Ошибка 2.
Увидеть есть ли такая ошибка можно даже простой командой:
На всякий случай убедимся что у нас выбраны нужные локали:
И генерируем сами локали:
Повторно запускаем команду:
И видим что проблема исчезла.
Ошибка 3
Его я поймал все на том же примере:
На самом деле это может относиться и к любой другой библиотеке. Будем проводить расследование:
Ждем пока обновиться и ищем файл:
Как видим, файл относиться к glibc. Баг этот ведет начало с 2007 года или даже раньше.
Немного покопав дальше встречаем сообщение что это совсем не баг, а фича.
Сообщение говорит нам что при присутствии этой библиотеки линковщик будет подгружать не оптимизированную версию библиотеки, даже если будут оптимизированные для CPU версии.
Будем считать что вопрос решен.
Ошибка 4.
Похожа на предыдущую. Но будем копать. Opennet нам говорит о том что:
- /lib/ld.so — динамический связыватель/загрузчик для формата a.out
- /lib/ld-linux.so. — динамический связыватель/загрузчик для формата ELF
- /etc/ld.so.cache — Файл, содержащий скомпилированный список каталогов, в которых производится поиск библиотек и сортированный список библиотек-кандидатов.
- /etc/ld.so.preload — Файл, содержащий список разделённых пробелами динамических ELF библиотек, которые будут загружены перед программой.
Не знаю будет ли правильно ли просто создать этот файл, но файл создадим:
Ошибку это убрало.
Повлияло ли как-то на preload не понятно. Но судя по strace preload никак в худшую сторону не повлиял. Будем считать что еще одну ошибку убрали.
Думаю на сегодня работы над ошибками хватит. Остальными займемся в одном из следующих материалов. Надеюсь кому-то это все помогло.
Источник
Linux etc ld so preload
/lib/ld-linux.so.* [ПАРАМЕТРЫ] [ПРОГРАММА [АРГУМЕНТЫ]]
ОПИСАНИЕ
Для двоичных файлов Linux требуется динамическая компоновка (компоновка во время выполнения), если при сборке программе ld(1) не был передан параметр -static.
Программа ld.so предназначена для обработки двоичных файлов в формате a.out (старый формат); ld-linux.so* предназначена для обработки файлов в формате ELF (/lib/ld-linux.so.1, если используется libc5 и /lib/ld-linux.so.2, если glibc2), который используется последние несколько лет. Обе программы ведут себя одинаково и используют те же самые файлы поддержки и программы ldd(1), ldconfig(8) и /etc/ld.so.conf.
При определении зависимостей общего объекта, динамический компоновщик сначала просматривает каждую строку зависимости в поисках символа косой черты (такое случается, если путь общего объекта с косой чертой был задан во время компоновки). Если косая черта найдена, то строка зависимости воспринимается как путь (относительный или абсолютный), и общий объект загружается исходя из этого пути.
Если в зависимости общего объекта отсутствует косая черта, то поиск выполняется в следующем порядке:
- (только для ELF) В каталогах, указанных в атрибуте DT_RPATH динамического раздела двоичного файла, если он есть и если атрибут DT_RUNPATH не существует. Использование DT_RPATH не рекомендуется.
- В переменной окружения LD_LIBRARY_PATH (если исполняемый файл не выполняется в режиме безопасного выполнения; смотрите далее, в этом случае она игнорируется).
- (только для ELF) В каталогах, указанных в атрибуте DT_RUNPATH динамического раздела двоичного файла, если он есть.
- В кэш-файле /etc/ld.so.cache, содержащем скомпилированный список кандидатов общих объектов, которые ранее были найдены по указанным путям расположения библиотек. Однако, если при сборке двоичного файла компоновщику был указан параметр -z nodeflib, то общие объекты в путях по умолчанию будут пропущены. Общие объекты, установленные в каталоги для аппаратных возможностей (см. далее) имеют больший приоритет, чем остальные общие объекты.
- В каталоге по умолчанию /lib и затем в /usr/lib (на некоторых 64-битных архитектурах путь по умолчанию для 64-битных объектов — /lib64 и затем /usr/lib64). Если при сборке двоичного файла компоновщику был указан параметр -z nodeflib, то этот шаг пропускается.
Раскрытие токена rpath
ld.so распознаёт определённые строки согласно спецификации rpath (DT_RPATH или DT_RUNPATH); эти строки заменяются на
$ORIGIN (или $ ) Она раскрывается в каталог, содержащий программу или общий объект. Таким образом, приложение, расположенное в somedir/app может компилироваться с
для того, чтобы оно могло найти связанный общий объект в somedir/lib и не важно где в иерархии каталогов будет находиться somedir. Это облегчает создание приложений «под ключ», которые вместо установки в специальные каталоги, можно просто распаковать в любой каталог, и они всё равно найдут свои общие объекты.
$LIB (или $ ) Она раскрывается в lib или lib64, в зависимости от архитектуры (например, на x86-64 она заменяется на lib64, а на x86-32 она заменяется на lib). $PLATFORM (или $ ) Преобразуется в строку, соответствующую типу процессора узла (например, «x86_64»). На некоторых архитектурах ядро Linux не предоставляет строку платформы динамическому компоновщику. Значение этой строки берётся из значения AT_PLATFORM вспомогательного вектора (смотрите getauxval(3)).
ПАРАМЕТРЫ
ОКРУЖЕНИЕ
Безопасный режим выполнения
Переменные окружения
LD_ASSUME_KERNEL можно использовать, чтобы заставить динамический компоновщик предполагать, что он работает в системе с другой версией ядра ABI. Например, следующая команда заставляет динамический компоновщик при загрузке общих объектов, требуемых myprog, предполагать, что он запущен на Linux с версией 2.2.5:
В системах, предоставляющих несколько версий общего объекта (в различных каталогах пути поиска) с разными требованиями к минимальной версии ядра ABI, LD_ASSUME_KERNEL может использоваться для выбора версии объекта, которую нужно задействовать (в зависимости от порядка поиска в каталогах). Исторически, свойство LD_ASSUME_KERNEL наиболее часто использовалось при ручном выборе старых реализаций нитей POSIX LinuxThreads в системах, которые предоставляли и LinuxThreads, и NPTL (в последствии стала базовой в таких системах); смотрите pthreads(7).
LD_BIND_NOW (libc5; в glibc начиная с версии 2.1.1) Если переменная содержит непустую строку, то динамический компоновщик будет искать все символы при запуске программы вместо того, чтобы отложить поиск вызовов функций до момента, когда они встретятся в первый раз. Это полезно при отладке. LD_LIBRARY_PATH Список каталогов, в которых ищутся библиотеки ELF при выполнении. Элементы списка разделяются двоеточиями или точками с запятой. Похожа на переменную окружения PATH. Данная переменная игнорируется в режиме безопасного выполнения. LD_PRELOAD Список дополнительных, указанных пользователем, общих объектов ELF для загрузки раньше чем все остальные. Элементы списка разделяются пробелами или двоеточиями. Может использоваться для выборочной замены функций из других общих объектов. Объекты ищутся согласно правилам, указанным в ОПИСАНИИ. В режиме безопасного выполнения предварительная загрузка файлов с символами косой черты не выполняется, а общие объекты из стандартных каталогов поиска загружаются только, если на файле общего объекта включён бит режима set-user-ID. LD_TRACE_LOADED_OBJECTS (только для ELF) Если установлена, то вместо нормального запуска программы будут выданы её динамические зависимости, как если бы она была запущена ldd(1).
Также существует большое количество более или менее полезных переменных, многие из которых устарели или предназначены только для внутреннего использования.
LD_AOUT_LIBRARY_PATH (libc5) Тоже что и LD_LIBRARY_PATH, но только для двоичных файлов в формате a.out. Старые версии ld-linux.so.1 также поддерживают LD_ELF_LIBRARY_PATH. LD_AOUT_PRELOAD (libc5) Тоже что и LD_PRELOAD, но только для двоичных файлов в формате a.out. Старые версии ld-linux.so.1 также поддерживают LD_ELF_PRELOAD. LD_AUDIT (в glibc начиная с 2.4) Определяемый пользователем список общих объектов ELF разделяемых двоеточиями, которые будут загружены раньше всех остальных в отдельном пространстве имён компоновщика (т. е., они не внедряются вместо обычных привязываемых символов, которые могли бы быть в этом процессе). Эти объекты можно использовать для контрольной проверки операций динамического компоновщика. LD_AUDIT игнорируется в режиме безопасного выполнения.
Динамический компоновщик будет уведомлять общие объекты контроля в так называемых точках контроля — например, при загрузке нового общего объекта, поиске символа или при вызове символа из другого динамического объекта — вызывая соответствующую функцию из общего объекта контроля. Подробности смотрите в rtld-audit(7). Интерфейс контроля в значительной степени совместим с представленным в Solaris, описан в его Руководстве по компоновщику и библиотекам (Linker and Libraries Guide) в главе Интерфейс контроля компоновщика во время выполнения (Runtime Linker Auditing Interface).
Источник