Категория > Новости > Анатомия эльфов 2. Разбираем устройство ELF-файлов в подробностях - «Новости»
Анатомия эльфов 2. Разбираем устройство ELF-файлов в подробностях - «Новости»4-09-2022, 00:02. Автор: Руслан |
Анатомия эльфов. Разбираемся с внутренним устройством ELF-файлов», в которой мы начали изучать секреты формата исполняемых ELF-файлов. В ней мы определились с инструментарием анализа, создали несколько подопытных экземпляров ELF-файлов, разобрались с форматом заголовка ELF-файла, узнали про таблицы заголовков секций и сегментов, а также заглянули внутрь некоторых секций и сегментов.Виды связыванияОсновная проблема, возникающая при компоновке исполняемого файла, — определение адресов вызываемых в программе функций, расположенных во внешних библиотеках. Если для функций, которые определены в самом исполняемом файле, такой проблемы не наблюдается (адреса этих функций определяются уже на этапе компиляции), то внешние библиотеки могут находиться в памяти по большому счету где угодно. Это получается благодаря возможности формировать позиционно независимый код. С ходу, на этапе компиляции, определить адрес той или иной функции, содержащейся в такой библиотеке, невозможно. Это можно сделать либо статически (включив нужные библиотеки непосредственно в ELF-файл), либо динамически (во время загрузки или выполнения программы). Исходя из этого, можно выделить три вида связывания исполняемого ELF-файла с библиотеками:
Если обратиться к миру Windows, то там наблюдается примерно такая же картина и основные принципы функционирования этих видов связывания с внешними библиотеками аналогичны. Для статически линкуемых библиотек используется расширение .a (в Windows это файлы с расширением .lib), для динамических — расширение .so (в Windows это файлы .dll). Статическое связываниеЗдесь все достаточно просто. Внешняя библиотека линкуется с исполняемым файлом, образуя с ним единое целое. Если обратиться к примеру из предыдущей статьи (файл с хелловорлдом
gcc -oexample_static_linked -staticexample.c
В итоге получим исполняемый файл со статически прилинкованной к нему библиотекой glibc. Если ты обратишь внимание на размер полученного файла, то увидишь, что он существенно больше, чем размер файлов Собственно говоря, это и есть основной недостаток статического связывания. Несмотря на то что из glibc мы используем всего одну функцию Посмотреть тип связывания в исполняемом файле можно, применив утилиту file или ldd. Определяем тип связывания в ELF-файле (в данном случае видим, что применена статическая линковка) Обрати внимание, что для примера Динамическое связывание во время загрузки файлаКак мы уже говорили, благодаря позиционно независимому коду динамические (или разделяемые) библиотеки могут быть загружены в память один раз, а все нуждающиеся в этой библиотеке программы станут пользоваться этой разделяемой копией библиотеки. На этапе компоновки исполняемого файла адреса, по которым будут загружены динамические библиотеки, неизвестны, и поэтому адреса содержащихся в них функций определить невозможно. В этом случае динамический компоновщик (если помнишь, то путь к нему лежит в секции Динамическое связывание во время исполнения файлаРазделяемые библиотеки могут быть загружены в память и во время выполнения программы. В этом случае приложение обращается к динамическому линковщику с просьбой загрузить и прилинковать динамическую библиотеку. В Linux для этого предусмотрены системные функции Если покопаться во внутренностях Windows, там можно обнаружить аналогичные API-функции: Этот вид связывания (как в Linux, так и в Windows) используется достаточно редко, во многих случаях его применяют для того, чтобы скрыть от исследователей истинную функциональность программы. Продолжаем разбираться с секционным представлением ELF-файлаВ предыдущей статье мы рассмотрели несколько секций, которые может содержать ELF-файл, однако изучили мы их далеко не все. Сегодня мы продолжим исследовать этот вопрос и посмотрим в том числе, какие секции в ELF-файле предусмотрены для организации связывания и разрешения адресов функций, которые содержатся в разделяемых библиотеках. Секция .shstrtabЭта секция представляет собой массив строк, заканчивающихся нулем, с именами всех секций ELF-файла. Указанная таблица позволяет различным утилитам (например, таким, как
readelf -x.shstrtab example_pie
Секция .shstrtab в шестнадцатеричном представлении Символьные секцииКак можно догадаться по названию, символьные секции хранят какие‑то символы. В нашем случае под символами понимаются имена функций и переменных. Эти имена используются в качестве символьных имен для представления определенного местоположения в файле или в памяти. Все это вместе и образует то, что мы называем символами функций и данных. (Да, мы привыкли считать, что символ, как правило, занимает одно знакоместо в виде буквы, цифры или знака препинания, однако здесь это не так.) Чтобы посмотреть информацию о символах, можно воспользоваться уже знакомой нам утилитой readelf и набрать в консоли что‑нибудь вроде этого:
readelf -s -Wexample_pie
На выходе увидим содержимое двух секций Вывод символьной информации из ELF-файла Секция .symtab Для начала необходимо отметить, что наличие этой секции в ELF-файле необязательно. Более того, в большинстве встречающихся в дикой природе файлов она отсутствует. Основное ее назначение — помощь при отладке программы, в то время как для исполнения файла она не требуется. По умолчанию эта секция создается во время компиляции программы, однако ее можно удалить с помощью команды
strip example_pie
Теперь, если попытаться посмотреть символьную информацию в этом файле, будет выведено только содержимое секции Вывод символьной информации из ELF-файла, на который воздействовали утилитой strip Все же, хоть эта секция и необязательна в файле, остановимся на ней чуть подробнее. Каждая запись этой секции представляет собой структуру вида В указанной секции содержатся все символы, которые компоновщик использует как во время компиляции, так и во время выполнения приложения. В нашем примере Функции main() и puts() в секции .symtab Функции С функцией Содержимое секции
nm man
Если мы попробуем посмотреть содержимое секции Функции main() и puts() в секции .symtab в ELF-файле со статической компоновкой На рисунке мы видим, что у функции При связывании нескольких библиотек в ходе компоновки одного ELF-файла в нескольких библиотеках может оказаться определена функция с одинаковым именем (то есть символы этой функции в двух или более библиотеках будут совпадать). В этом случае компоновщик должен выбрать одну из этих функций. Когда есть одна функция с сильными символами и одна или несколько функций со слабыми символами, будет выбрана функция с сильными символами. Если имеется несколько одинаковых функций со слабыми символами, компоновщик выберет случайным образом. Если же в ходе компоновки обнаружится две или более одинаковые функции с сильными символами, то компоновка прервется и будет констатирована ошибка. В данном случае в библиотеке glibc символы многих стандартных функций определены с низким приоритетом (слабые символы). Это делается для того, чтобы дать возможность программистам написать собственную библиотеку с переопределением некоторых стандартных функций (и, соответственно, с определением символов этих переопределенных функций как сильных). Затем они смогут использовать вместе и библиотеку glibc, и свою библиотеку с переопределенными функциями. При этом, поскольку у переопределенных функций символы имеют более высокий приоритет, будут вызываться именно они, а не те, которые определены в glibc. Более подробно про сильные и слабые символы можно почитать в документации. Секция .dynsym Записи в данной секции имеют такую же структуру, что и в секции Если внимательно изучить секции Секция Секции .strtab и .dynstr Указанные секции содержат непосредственно строковые значения символов, на которые указывает значение Посмотреть содержимое этих секций, так же как и для секции Вывод содержимого секций .dynstr и .strtab из ELF-файла в шестнадцатеричном и строковом виде Более подробно про символьные секции ELF-файлов можно почитать в документации Oracle. Динамическое связывание и секции .plt и .gotПри динамическом связывании во время загрузки в большинстве случаев разрешение находящихся в разделяемых библиотеках адресов функций происходит чуть позже, не в сам момент запуска приложения, а во время первого обращения к неразрешенному адресу при вызове необходимой функции. Таким образом реализуется так называемое позднее (или отложенное) связывание. Для чего это нужно? Позднее связывание позволяет не тратить без необходимости время на разрешение адресов при запуске программы. Для ее функционирования может потребоваться много функций из разделяемых библиотек, и определять их адреса именно тогда, когда это действительно необходимо, — вполне рациональное решение. В операционных системах семейства Linux режим позднего связывания реализуется динамическим компоновщиком по умолчанию. Можно заставить динамический компоновщик производить разрешение адресов функций из разделяемых библиотек непосредственно во время загрузки программы, задав переменную среды
export LD_BIND_NOW=1
Повторюсь, необходимость использовать режим немедленного связывания возникает крайне редко, разве что если требуется обеспечить гарантированную производительность в системах с режимами, близкими к режимам реального времени. Для начала рассмотрим базовый принцип позднего связывания в ELF-файлах, который был реализован изначально. После чего поговорим о том, какие изменения были внесены в этот базовый принцип, когда появились новые технологии защиты программ от атак. Для большей наглядности немного изменим наш «хелловорлд» и добавим в него еще одну функцию (например,
Чтобы получить ELF-файл с базовым принципом позднего связывания, откомпилируем данный пример c использованием опции
gcc -oexample_exit_notrack -fcf-protection=none example_exit.c
Об опциях Итак, в базовом варианте в ELF-файлах позднее связывание реализуется с помощью двух специальных секций:
Секция .gotНачнем с секции Перейти обратно к новости |