Анатомия эльфов 2. Разбираем устройство ELF-файлов в подробностях

Содержание статьи

  • Виды связывания
  • Статическое связывание
  • Динамическое связывание во время загрузки файла
  • Динамическое связывание во время исполнения файла
  • Продолжаем разбираться с секционным представлением ELF-файла
  • Секция .shstrtab
  • Символьные секции
  • Динамическое связывание и секции .plt и .got
  • Секция .got
  • Секция .plt
  • Атаки с помощью перезаписи .got и защита от них
  • Секции .note.gnu.build-id, .note.ABI-tag и .note.gnu.property
  • Секция .plt.sec и отличия в секциях .plt и .plt.got
  • Секции .rel. и .rela.
  • Секция .dynamic
  • Секции .gnu.version, gnu.version_r и gnu.version_d
  • Секция .gnu.hash
  • Выводы

Ис­полня­емые фай­лы в ОС семей­ства Linux име­ют доволь­но слож­ную струк­туру. Важ­ный аспект в работе таких при­ложе­ний — вза­имо­дей­ствие с внеш­ними биб­лиоте­ками и вызыва­емы­ми из них фун­кци­ями. Сегод­ня мы погово­рим о том, как обес­печива­ется такое вза­имо­дей­ствие и каким обра­зом исполня­емый файл может быть свя­зан с биб­лиоте­ками.

info

Это про­дол­жение статьи «Ана­томия эль­фов. Раз­бира­емся с внут­ренним устрой­ством ELF-фай­лов», в которой мы начали изу­чать сек­реты фор­мата исполня­емых ELF-фай­лов. В ней мы опре­дели­лись с инс­тру­мен­тари­ем ана­лиза, соз­дали нес­коль­ко подопыт­ных экзем­пля­ров ELF-фай­лов, разоб­рались с фор­матом заголов­ка ELF-фай­ла, узна­ли про таб­лицы заголов­ков сек­ций и сег­ментов, а так­же заг­лянули внутрь некото­рых сек­ций и сег­ментов.

 

Виды связывания

Ос­новная проб­лема, воз­ника­ющая при ком­понов­ке исполня­емо­го фай­ла, — опре­деле­ние адре­сов вызыва­емых в прог­рамме фун­кций, рас­положен­ных во внеш­них биб­лиоте­ках. Если для фун­кций, которые опре­деле­ны в самом исполня­емом фай­ле, такой проб­лемы не наб­люда­ется (адре­са этих фун­кций опре­деля­ются уже на эта­пе ком­пиляции), то внеш­ние биб­лиоте­ки могут находить­ся в памяти по боль­шому сче­ту где угод­но. Это получа­ется бла­года­ря воз­можнос­ти фор­мировать позици­онно незави­симый код. С ходу, на эта­пе ком­пиляции, опре­делить адрес той или иной фун­кции, содер­жащей­ся в такой биб­лиоте­ке, невоз­можно. Это мож­но сде­лать либо ста­тичес­ки (вклю­чив нуж­ные биб­лиоте­ки непос­редс­твен­но в ELF-файл), либо динами­чес­ки (во вре­мя заг­рузки или выпол­нения прог­раммы).

Ис­ходя из это­го, мож­но выделить три вида свя­зыва­ния исполня­емо­го ELF-фай­ла с биб­лиоте­ками:

  • ста­тичес­кое свя­зыва­ние;
  • ди­нами­чес­кое свя­зыва­ние во вре­мя заг­рузки фай­ла;
  • ди­нами­чес­кое свя­зыва­ние во вре­мя исполне­ния фай­ла.

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

Для ста­тичес­ки лин­куемых биб­лиотек исполь­зует­ся рас­ширение .a (в Windows это фай­лы с рас­ширени­ем .lib), для динами­чес­ких — рас­ширение .so (в Windows это фай­лы .dll).

 

Статическое связывание

Здесь все дос­таточ­но прос­то. Внеш­няя биб­лиоте­ка лин­кует­ся с исполня­емым фай­лом, обра­зуя с ним еди­ное целое. Если обра­тить­ся к при­меру из пре­дыду­щей статьи (файл с хел­ловор­лдом example.c), то для того, что­бы сде­лать из него прог­рамму, ста­тичес­ки свя­зан­ную с биб­лиоте­кой glibc, нуж­но наб­рать в кон­соли сле­дующее:

gcc -o example_static_linked -static example.c

В ито­ге получим исполня­емый файл со ста­тичес­ки при­лин­кован­ной к нему биб­лиоте­кой glibc. Если ты обра­тишь вни­мание на раз­мер получен­ного фай­ла, то уви­дишь, что он сущес­твен­но боль­ше, чем раз­мер фай­лов example_pie и example_no_pie, которые были ском­пилиро­ваны методом динами­чес­кой лин­ковки с биб­лиоте­кой glibc. У меня, нап­ример, получи­лось целых 872 Кбайт для ста­тичес­кой лин­ковки, в то вре­мя как динами­чес­кая дала все­го 17.

Собс­твен­но говоря, это и есть основной недос­таток ста­тичес­кого свя­зыва­ния. Нес­мотря на то что из glibc мы исполь­зуем все­го одну фун­кцию puts(), при ста­тичес­ком свя­зыва­нии при­ходит­ся тащить в исполня­емый файл еще мно­го чего ненуж­ного. Так­же мож­но отме­тить еще один, не сов­сем явный недос­таток ста­тичес­кой лин­ковки: если появ­ляет­ся новая вер­сия биб­лиоте­ки (в которой, нап­ример, устра­нена та или иная уяз­вимость), то нам при­дет­ся переком­пилиро­вать прог­рамму уже с новой вер­сией нуж­ной нам биб­лиоте­ки. Если это­го не делать, то наша прог­рамма будет поль­зовать­ся фун­кци­ями, в которых уяз­вимость не устра­нена.

Пос­мотреть тип свя­зыва­ния в исполня­емом фай­ле мож­но, при­менив ути­литу file или ldd.

Оп­ределя­ем тип свя­зыва­ния в ELF-фай­ле (в дан­ном слу­чае видим, что при­мене­на ста­тичес­кая лин­ковка)

Об­рати вни­мание, что для при­мера example_static из пре­дыду­щей статьи ути­лита file покажет динами­чес­кую лин­ковку. Все дело в том, что в этом слу­чае мы ста­тичес­ки лин­ковали с прог­раммой нашу самопис­ную биб­лиоте­ку lib_static_example.a, в которой содер­жится фун­кция hello_world_function(). Одна­ко в этой фун­кции исполь­зует­ся фун­кция puts(), которая берет­ся из биб­лиоте­ки glibc, свя­зан­ной с lib_static_example.a уже динами­чес­ки.

 

Динамическое связывание во время загрузки файла

Как мы уже говори­ли, бла­года­ря позици­онно незави­симо­му коду динами­чес­кие (или раз­деля­емые) биб­лиоте­ки могут быть заг­ружены в память один раз, а все нуж­дающиеся в этой биб­лиоте­ке прог­раммы ста­нут поль­зовать­ся этой раз­деля­емой копи­ей биб­лиоте­ки. На эта­пе ком­понов­ки исполня­емо­го фай­ла адре­са, по которым будут заг­ружены динами­чес­кие биб­лиоте­ки, неиз­вес­тны, и поэто­му адре­са содер­жащих­ся в них фун­кций опре­делить невоз­можно.

В этом слу­чае динами­чес­кий ком­понов­щик (если пом­нишь, то путь к нему лежит в сек­ции .interp) опре­деля­ет адре­са нуж­ных фун­кций и перемен­ных в ходе заг­рузки прог­раммы в память. Если говорить точ­нее, во мно­гих слу­чаях даже не в ходе заг­рузки, а во вре­мя пер­вого обра­щения к фун­кции, но об этом более под­робно погово­рим чуть ниже.

 

Динамическое связывание во время исполнения файла

Раз­деля­емые биб­лиоте­ки могут быть заг­ружены в память и во вре­мя выпол­нения прог­раммы. В этом слу­чае при­ложе­ние обра­щает­ся к динами­чес­кому лин­ковщи­ку с прось­бой заг­рузить и при­лин­ковать динами­чес­кую биб­лиоте­ку. В Linux для это­го пре­дус­мотре­ны сис­темные фун­кции dlopen(), dlsym() и dlclose(), пер­вая заг­ружа­ет раз­деля­емую биб­лиоте­ку, вто­рая ищет в ней нуж­ную фун­кцию, третья зак­рыва­ет ее файл.

Ес­ли покопать­ся во внут­реннос­тях Windows, там мож­но обна­ружить ана­логич­ные API-фун­кции: LoadLibrary() и GetProcAddress() (либо LdrLoadDll() и LdrGetProcAddress()).

Этот вид свя­зыва­ния (как в Linux, так и в Windows) исполь­зует­ся дос­таточ­но ред­ко, во мно­гих слу­чаях его при­меня­ют для того, что­бы скрыть от иссле­дова­телей истинную фун­кци­ональ­ность прог­раммы.

 

Продолжаем разбираться с секционным представлением ELF-файла

В пре­дыду­щей статье мы рас­смот­рели нес­коль­ко сек­ций, которые может содер­жать ELF-файл, одна­ко изу­чили мы их далеко не все. Сегод­ня мы про­дол­жим иссле­довать этот воп­рос и пос­мотрим в том чис­ле, какие сек­ции в ELF-фай­ле пре­дус­мотре­ны для орга­низа­ции свя­зыва­ния и раз­решения адре­сов фун­кций, которые содер­жатся в раз­деля­емых биб­лиоте­ках.

 

Секция .shstrtab

Эта сек­ция пред­став­ляет собой мас­сив строк, закан­чива­ющих­ся нулем, с име­нами всех сек­ций ELF-фай­ла. Ука­зан­ная таб­лица поз­воля­ет раз­личным ути­литам (нап­ример, таким, какreadelf) находить име­на сек­ций. Для прос­мотра этой сек­ции в сим­воль­ном или шес­тнад­цатерич­ном виде мож­но исполь­зовать опции -p или -x ути­литы readelf соот­ветс­твен­но. Нап­ример, вот так:

readelf -x .shstrtab example_pie

Сек­ция .shstrtab в шес­тнад­цатерич­ном пред­став­лении 

Символьные секции

Как мож­но догадать­ся по наз­ванию, сим­воль­ные сек­ции хра­нят какие‑то сим­волы. В нашем слу­чае под сим­волами понима­ются име­на фун­кций и перемен­ных. Эти име­на исполь­зуют­ся в качес­тве сим­воль­ных имен для пред­став­ления опре­делен­ного мес­тополо­жения в фай­ле или в памяти. Все это вмес­те и обра­зует то, что мы называ­ем сим­волами фун­кций и дан­ных. (Да, мы при­вык­ли счи­тать, что сим­вол, как пра­вило, занима­ет одно зна­комес­то в виде бук­вы, циф­ры или зна­ка пре­пина­ния, одна­ко здесь это не так.)

Что­бы пос­мотреть информа­цию о сим­волах, мож­но вос­поль­зовать­ся уже зна­комой нам ути­литой readelf и наб­рать в кон­соли что‑нибудь вро­де это­го:

readelf -s -W example_pie

На выходе уви­дим содер­жимое двух сек­ций .symtab и .dynsym.

Вы­вод сим­воль­ной информа­ции из ELF-фай­лаСекция .symtab

Для начала необ­ходимо отме­тить, что наличие этой сек­ции в ELF-фай­ле необя­затель­но. Более того, в боль­шинс­тве встре­чающих­ся в дикой при­роде фай­лов она отсутс­тву­ет. Основное ее наз­начение — помощь при отладке прог­раммы, в то вре­мя как для исполне­ния фай­ла она не тре­бует­ся. По умол­чанию эта сек­ция соз­дает­ся во вре­мя ком­пиляции прог­раммы, одна­ко ее мож­но уда­лить с помощью коман­ды strip, нап­ример так:

strip example_pie

Те­перь, если попытать­ся пос­мотреть сим­воль­ную информа­цию в этом фай­ле, будет выведе­но толь­ко содер­жимое сек­ции .dynsym.

Вы­вод сим­воль­ной информа­ции из ELF-фай­ла, на который воз­дей­ство­вали ути­литой strip

Все же, хоть эта сек­ция и необя­затель­на в фай­ле, оста­новим­ся на ней чуть под­робнее.

Каж­дая запись этой сек­ции пред­став­ляет собой струк­туру вида Elf32_Sym или Elf64_Sym. Внут­реннее устрой­ство этой струк­туры (как, впро­чем, и содер­жимое всех осталь­ных струк­тур и зна­чений кон­стант ELF-фай­лов) мож­но пос­мотреть в фай­ле /usr/include/elf.h.

В ука­зан­ной сек­ции содер­жатся все сим­волы, которые ком­понов­щик исполь­зует как во вре­мя ком­пиляции, так и во вре­мя выпол­нения при­ложе­ния. В нашем при­мере example_pie сре­ди все­го, что содер­жится в дан­ной сек­ции, мож­но уви­деть сим­воль­ное имя зна­комой нам фун­кции main(), которая при­сутс­тву­ет в любой прог­рамме, а так­же сим­воль­ное имя фун­кции puts().

Фун­кции main() и puts() в сек­ции .symtab

Фун­кции main() соот­ветс­тву­ет адрес 0x1149, и имен­но с это­го адре­са фун­кция будет начинать­ся пос­ле заг­рузки фай­ла в память перед выпол­нени­ем. Так­же вид­но, что раз­мер фун­кции main() сос­тавля­ет 27 байт, ее тип — FUNC (то есть фун­кция), а сама фун­кция раз­меща­ется в сек­ции с номером 16 (это сек­ция .text, в которой находит­ся непос­редс­твен­но исполня­емый код прог­раммы).

С фун­кци­ей puts() такой опре­делен­ности не отме­чает­ся: нет ни адре­са, ни сек­ции. Так про­исхо­дит потому, что фун­кция puts() находит­ся в биб­лиоте­ке glibc и, как мы уже говори­ли, адрес этой фун­кции на эта­пе ком­пиляции опре­делить нель­зя.

Со­дер­жимое сек­ции .symtab так­же мож­но пос­мотреть и с помощью коман­ды nm. Что­бы узнать под­робнос­ти исполь­зования этой коман­ды, набери в тер­минале

nm man

Ес­ли мы поп­робу­ем пос­мотреть содер­жимое сек­ции .symtab ELF-фай­ла, в котором при­мене­но ста­тичес­кое свя­зыва­ние, то уви­дим, что, помимо уже зна­комых нам сим­волов фун­кций main() и puts(), в сек­ции при­сутс­тву­ет боль­шое количес­тво сим­волов дру­гих фун­кций, вхо­дящих в сос­тав биб­лиоте­ки glibc.

Фун­кции main() и puts() в сек­ции .symtab в ELF-фай­ле со ста­тичес­кой ком­понов­кой

На рисун­ке мы видим, что у фун­кции main() приз­нак свя­зыва­ния (который содер­жится в поле st_info струк­туры Elf32_Sym или Elf64_Sym) име­ет зна­чение STB_GLOBAL, а фун­кция puts() — зна­чение STB_WEAK. Зна­чение приз­нака свя­зыва­ния сим­вола, рав­ное STB_WEAK, говорит о том, что дан­ный сим­вол име­ет самый низ­кий при­ори­тет при свя­зыва­нии (так называ­емый сла­бый сим­вол). Сим­волы с дру­гими приз­наками свя­зыва­ния, нап­ример STB_LOCAL или STB_GLOBAL, име­ют более высокий при­ори­тет (их называ­ют силь­ными сим­волами).

При свя­зыва­нии нес­коль­ких биб­лиотек в ходе ком­понов­ки одно­го ELF-фай­ла в нес­коль­ких биб­лиоте­ках может ока­зать­ся опре­деле­на фун­кция с оди­нако­вым име­нем (то есть сим­волы этой фун­кции в двух или более биб­лиоте­ках будут сов­падать). В этом слу­чае ком­понов­щик дол­жен выб­рать одну из этих фун­кций. Ког­да есть одна фун­кция с силь­ными сим­волами и одна или нес­коль­ко фун­кций со сла­быми сим­волами, будет выб­рана фун­кция с силь­ными сим­волами. Если име­ется нес­коль­ко оди­нако­вых фун­кций со сла­быми сим­волами, ком­понов­щик выберет слу­чай­ным обра­зом. Если же в ходе ком­понов­ки обна­ружит­ся две или более оди­нако­вые фун­кции с силь­ными сим­волами, то ком­понов­ка прер­вется и будет кон­ста­тиро­вана ошиб­ка.

В дан­ном слу­чае в биб­лиоте­ке glibc сим­волы мно­гих стан­дар­тных фун­кций опре­деле­ны с низ­ким при­ори­тетом (сла­бые сим­волы). Это дела­ется для того, что­бы дать воз­можность прог­раммис­там написать собс­твен­ную биб­лиоте­ку с пере­опре­деле­нием некото­рых стан­дар­тных фун­кций (и, соот­ветс­твен­но, с опре­деле­нием сим­волов этих пере­опре­делен­ных фун­кций как силь­ных). Затем они смо­гут исполь­зовать вмес­те и биб­лиоте­ку glibc, и свою биб­лиоте­ку с пере­опре­делен­ными фун­кци­ями. При этом, пос­коль­ку у пере­опре­делен­ных фун­кций сим­волы име­ют более высокий при­ори­тет, будут вызывать­ся имен­но они, а не те, которые опре­деле­ны в glibc. Более под­робно про силь­ные и сла­бые сим­волы мож­но почитать в докумен­тации.

Секция .dynsym

За­писи в дан­ной сек­ции име­ют такую же струк­туру, что и в сек­ции .symtab. Глав­ное отли­чие в том, что в этой сек­ции содер­жатся толь­ко сим­волы фун­кций или перемен­ных, которые необ­ходимы для динами­чес­кой ком­понов­ки. На эту сек­цию коман­да strip никако­го вли­яния не ока­зыва­ет (что, в общем‑то, понят­но). Сек­ция .dynsym име­ется в тех фай­лах, где исполь­зует­ся динами­чес­кое свя­зыва­ние во вре­мя заг­рузки ELF-фай­ла. Соот­ветс­твен­но, если попытать­ся пос­мотреть наличие этой сек­ции в фай­ле со ста­тичес­кой лин­ковкой, то мы ее там не уви­дим.

Ес­ли вни­матель­но изу­чить сек­ции .symtab и .dynsym, мож­но заметить, что в сек­цию .dynsym из сек­ции .symtab переко­чева­ли сим­волы c нулевы­ми адре­сами и неоп­ределен­ными номера­ми сек­ций. Зна­чения этих адре­сов и номеров сек­ций как раз и будут опре­деле­ны во вре­мя заг­рузки прог­раммы.

Сек­ция .symtab име­ет тип SHT_SYMTAB, а сек­ция .dynsym — тип SHT_DYNSYM. Собс­твен­но, дан­ный факт и поз­воля­ет ути­лите strip разоб­рать­ся, что мож­но зачис­тить в ELF-фай­ле, а что нель­зя.

Секции .strtab и .dynstr

Ука­зан­ные сек­ции содер­жат непос­редс­твен­но стро­ковые зна­чения сим­волов, на которые ука­зыва­ет зна­чение st_name из струк­туры Elf32_Sym или Elf64_Sym. Они, как было показа­но выше, явля­ются эле­мен­тами сек­ций .symtab или .dynsym. То есть в сек­циях .symtab и .dynsym непос­редс­твен­но самих стро­ковых зна­чений сим­волов не содер­жится, а при­сутс­тву­ет толь­ко индекс, по которо­му и находит­ся нуж­ное стро­ковое зна­чение сим­вола в сек­циях .strtab или .dynstr (этот индекс как раз и лежит в поле st_name струк­туры Elf32_Sym или Elf64_Sym).

Пос­мотреть содер­жимое этих сек­ций, так же как и для сек­ции .shstrtab, мож­но с исполь­зовани­ем опций -x или -p ути­литы readelf.

Вы­вод содер­жимого сек­ций .dynstr и .strtab из ELF-фай­ла в шес­тнад­цатерич­ном и стро­ковом виде

Бо­лее под­робно про сим­воль­ные сек­ции ELF-фай­лов мож­но почитать в докумен­тации Oracle.

 

Динамическое связывание и секции .plt и .got

При динами­чес­ком свя­зыва­нии во вре­мя заг­рузки в боль­шинс­тве слу­чаев раз­решение находя­щих­ся в раз­деля­емых биб­лиоте­ках адре­сов фун­кций про­исхо­дит чуть поз­же, не в сам момент запус­ка при­ложе­ния, а во вре­мя пер­вого обра­щения к нераз­решен­ному адре­су при вызове необ­ходимой фун­кции. Таким обра­зом реали­зует­ся так называ­емое поз­днее (или отло­жен­ное) свя­зыва­ние.

Для чего это нуж­но? Поз­днее свя­зыва­ние поз­воля­ет не тра­тить без необ­ходимос­ти вре­мя на раз­решение адре­сов при запус­ке прог­раммы. Для ее фун­кци­они­рова­ния может пот­ребовать­ся мно­го фун­кций из раз­деля­емых биб­лиотек, и опре­делять их адре­са имен­но тог­да, ког­да это дей­стви­тель­но необ­ходимо, — впол­не раци­ональ­ное решение. В опе­раци­онных сис­темах семей­ства Linux режим поз­дне­го свя­зыва­ния реали­зует­ся динами­чес­ким ком­понов­щиком по умол­чанию. Мож­но зас­тавить динами­чес­кий ком­понов­щик про­изво­дить раз­решение адре­сов фун­кций из раз­деля­емых биб­лиотек непос­редс­твен­но во вре­мя заг­рузки прог­раммы, задав перемен­ную сре­ды LD_BIND_NOW:

export LD_BIND_NOW=1

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

Для начала рас­смот­рим базовый прин­цип поз­дне­го свя­зыва­ния в ELF-фай­лах, который был реали­зован изна­чаль­но. Пос­ле чего погово­рим о том, какие изме­нения были вне­сены в этот базовый прин­цип, ког­да появи­лись новые тех­нологии защиты прог­рамм от атак.

Для боль­шей наг­ляднос­ти нем­ного изме­ним наш «хел­ловорлд» и добавим в него еще одну фун­кцию (нап­ример, exit()):

#include <stdio.h> #include <stdlib.h> void main(int argc, char* argv[]) { printf("Hello worldn"); exit(0); }

Что­бы получить ELF-файл с базовым прин­ципом поз­дне­го свя­зыва­ния, откомпи­лиру­ем дан­ный при­мер c исполь­зовани­ем опции -fcf-protection=none. Эта опция показы­вает ком­пилято­ру, что нуж­но соб­рать прог­рамму без исполь­зования защит­ной тех­нологии IBT (indirect branch tracking):

gcc -o example_exit_notrack -fcf-protection=none example_exit.c

Об опци­ях gcc мож­но почитать в докумен­тации.

Итак, в базовом вари­анте в ELF-фай­лах поз­днее свя­зыва­ние реали­зует­ся с помощью двух спе­циаль­ных сек­ций:

  • .plt — таб­лица свя­зей и про­цедур (Procedure Linkage Table);
  • .got — таб­лица гло­баль­ных сме­щений (Global Offset Table).

 

Секция .got

Нач­нем с сек­ции .got. Для начала обра­тим вни­мание, что эта сек­ция име­ет тип SHT_PROGBITS (то есть содер­жит либо код, либо дан­ные, и в нашем слу­чае это дан­ные), а так­же флаг SHF_WRITE (то есть ее содер­жимое может менять­ся в ходе выпол­нения прог­раммы).

Источник: xakep.ru

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *