Как перехватить доступ к файловой системе внутри dlopen()?

#glibc #dlopen

#glibc #dlopen

Вопрос:

Я хочу перехватить весь доступ к файловой системе, который происходит внутри dlopen(). Поначалу это казалось бы LD_PRELOAD или -Wl,-wrap, было бы жизнеспособным решением, но у меня возникли проблемы с их работой по некоторым техническим причинам:

  • ld.so уже сопоставил свои собственные символы к моменту обработки LD_PRELOAD. Для меня не критично перехватывать начальную загрузку, но _dl_* рабочие функции в это время разрешены, поэтому будущие вызовы проходят через них. Я думаю LD_PRELOAD , уже слишком поздно.

  • Каким-то образом malloc обходит проблему выше, потому malloc() что внутри ld.so у него нет функционала free() , он просто вызывает memset() .

  • Рабочие функции файловой системы, например __libc_read() , содержащиеся в ld.so , являются статическими, поэтому я не могу их перехватить -Wl,-wrap,__libc_read .

Все это может означать, что мне нужно создать свой собственный ld.so непосредственно из исходного кода, а не связывать его с оболочкой. Проблема в том, что оба libc и rtld-libc создаются из одного и того же источника. Я знаю, что макрос IS_IN_rtld определяется при сборке rtld-libc , но как я могу гарантировать, что существует только одна копия статических структур данных при экспорте функции открытого интерфейса? (Это вопрос системы сборки glibc, но я не нашел документации по этим деталям.)

Есть ли лучшие способы проникнуть внутрь dlopen() ?

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

Комментарии:

1. Это не ответ на ваш вопрос, поэтому я не публикую его как один, но в целом вы не можете сделать это надежно: можно получить доступ к файловой системе, вызвав системный вызов напрямую, не проходя через интерфейс динамической библиотеки. Если у вас нет абсолютного контроля над тем, как была скомпилирована библиотека, которую вы пытаетесь загрузить, вам может не повезти. Такие программы, как fakeroot, которые используют этот метод, большую часть времени работают нормально, а в некоторых ситуациях ужасно терпят неудачу.

2. Тем не менее, вы можете заставить это работать, запустив код вашей динамической библиотеки в ее собственном процессе и используя ptrace для перехвата сами системные вызовы. Я сделал это с большим успехом, и это позволяет полностью избежать всей ерунды с разделяемой библиотекой. Но это требует, чтобы вы полностью перепроектировали свою логику, чтобы иметь главный процесс, который выполняет ptrace, и подчиненный процесс, который выполняет динамическую библиотеку.

3. Ну, мне нужно dlopen / dlsym для правильной работы, но для доступа к файловой системе по-другому. В частности, в средах высокой производительности, таких как Blue Gene, все операции, связанные с файловым дескриптором ядра, отправляются с узлов ввода-вывода вычислительных узлов. Это вызывает серьезную проблему конкуренции при высоком параллелизме узлов. Например, загрузка приложения на Python, которое ссылается на несколько скомпилированных разделяемых библиотек, занимает около 4 часов на 65 тыс. ядер. Излишне говорить, что люди не в восторге от сжигания четверти миллиона часов ядра для загрузки своей программы.

4. Чтобы исправить это, я реализовал интерфейс ввода-вывода ( open , read , mmap , и т.д.) с использованием коллективов MPI. Это нормально для загрузки байт-кода Python, но разделяемые библиотеки должны пройти dlopen , и у меня возникли проблемы с вызовом моей реализации внутри dlopen .

5. Я подозреваю, что вам придется написать свою собственную реализацию dlopen(). Это ужас. (Мы сделали это там, где я работаю на своей повседневной работе.) Я был бы склонен попробовать трюк с ptrace; это не так много кода, и это позволит вам запускать стандартную версию кода, включая стандартную dlopen() , но ваш сервер мониторинга следит за процессом и переопределяетвызовы файловой системы делают свое дело. Тем не менее, это замедляет системные вызовы, но если вы привязаны к процессору, это может не быть проблемой. Смотрите quequero.org/Intercepting_with_ptrace() .

Ответ №1:

казалось бы, LD_PRELOAD или -Wl,-wrap были бы жизнеспособными решениями

--wrap Решение не может быть жизнеспособным: оно работает только во время (статического) соединения, а ваши ld.so и libc.so.6 и libdl.so.2 все уже связаны, так что теперь слишком поздно использовать --wrap .

Это LD_PRELOAD могло бы сработать, за исключением … ld.so учитывает тот факт, что dlopen() вызывает open() внутреннюю деталь реализации. Таким образом, он просто вызывает внутреннюю __open функцию, минуя PLT , и вашу способность вставлять open ее.

Каким-то образом malloc обходит проблему

Это потому libc , что поддерживает пользователей, которые реализуют свои собственные malloc (например, для целей отладки). Таким образом, вызов, например calloc , из dlopen , проходит PLT и может быть вставлен через LD_PRELOAD .

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

Что будет ld.so делать перестроенный? Я думаю, вы хотите, чтобы он вызывал __libc_open (in libc.so.6 ), но это не может сработать по очевидной причине: это то ld.so , что open s libc.so.6 в первую очередь (при запуске процесса).

Вы могли бы перестроить ld.so вызов __open , заменив вызов на open вызов. Это приведет ld.so к прохождению PLT и подвергнет его LD_PRELOAD интерпозиции.

Если вы пойдете по этому пути, я предлагаю вам не перезаписывать систему ld.so своей новой копией (вероятность ошибки и сделать систему не загружаемой слишком велика). Вместо этого установите его, например /usr/local/my-ld.so , и затем свяжите свои двоичные -Wl,--dynamic-linker=/usr/local/my-ld.so файлы с.

Другая альтернатива: исправление во время выполнения. Это немного взлом, но вы можете (как только получите контроль в main) просто сканировать .text оф ld.so и искать CALL __open инструкции. Если ld.so не удалено, то вы можете найти как внутренние __open , так и функции, которые вы хотите исправить (например open_verify , in dl-load.c ). Как только вы найдете интересное CALL , mprotect страница, которая его содержит, будет доступна для записи, и исправьте адрес вашего собственного посредника (который, в свою очередь, может вызвать __libc_open , если это необходимо), затем mprotect верните его обратно. Любое будущее dlopen() теперь будет проходить через ваш interposer.

Комментарии:

1. Первая идея полезна, но переключение на PLT вызовы в dlopen() привело к сбоям segfaults, поэтому мы рассмотрим второй вариант…