MyTetra Share
Делитесь знаниями!
Изучение файлов ELF в Linux
Время создания: 20.01.2023 10:38
Текстовые метки: linux, elf, исполняемый файл, бинарник, структура, формат
Раздел: Компьютер - Программирование - Ассемблеры - Архитектура Intel
Запись: xintrea/mytetra_syncro/master/base/1674200284oxsf0sndcm/text.html на raw.github.com

Анализ и изучение файлов ELF в Linux

Для ана­лиза ELF-фай­лов (Executable and Linkable Format) в Linux существует большой арсенал встро­енных инструментов и ути­лит:

  • readelf — ути­лита позволяет в удо­бочи­таемом виде отобразить всю информацию ELF-фай­лов;
  • hexdump — позволяет просматривать файл в шес­тнад­цатерич­ном пред­став­лении;
  • strings — может отобразить име­на всех импорти­руемых (или экспор­тиру­емых) фун­кций, а так­же биб­лиотек, из которых данные фун­кции импорти­рова­ны и еще мно­го другой полезной информации;
  • ldd — поз­воля­ет выводить име­на раз­деля­емых биб­лиотек, из которых импорти­руют­ся те или иные фун­кции, исполь­зуемые иссле­дуемой прог­раммой;
  • nm — позволяет получить информацию в виде таб­лицы имен из сос­тава отла­доч­ной информа­ции, которая добав­ляет­ся в ELF-фай­лы при их ком­пиляции (эта отла­доч­ная информа­ция с помощью коман­ды strip может быть уда­лена из фай­ла, и в этом слу­чае ути­лита nm ничем не поможет);
  • objdump — спо­соб­на вывес­ти информа­цию и содер­жимое всех эле­мен­тов иссле­дуемо­го фай­ла, в том чис­ле и в дизас­сем­бли­рован­ном виде.

Часть перечис­ленно­го (кро­ме hexdump и ldd) вхо­дит в сос­тав пакета GNU Binutils. Если это­го пакета в сис­теме нет, его лег­ко уста­новить. К при­меру, в Ubuntu это выг­лядит сле­дующим обра­зом:



sudo apt install binutils



В прин­ципе, имея все перечис­ленное, мож­но уже прис­тупать к ана­лизу и иссле­дова­нию ELF-фай­лов без прив­лечения допол­нитель­ных средств. Для боль­шего удобс­тва и наг­ляднос­ти мож­но добавить к нашему инс­тру­мен­тарию извес­тный в кру­гах реверс‑инже­неров дизас­сем­блер IDA (interactive disassembler) в вер­сии Freeware (этой вер­сии для наших целей будет более чем дос­таточ­но, хотя ник­то не зап­реща­ет вос­поль­зовать­ся вер­сиями Home или Pro, если есть воз­можность за них зап­латить).



Ана­лиз заголов­ка файл ELF в IDA


Так­же неп­лохо было бы исполь­зовать вмес­то hexdump что‑то поудоб­нее, нап­ример 010 Editor или wxHex Editor. Пер­вый hex-редак­тор — дос­той­ная аль­тер­натива Hiew для Linux (в том чис­ле и бла­года­ря воз­можнос­ти исполь­зовать в нем боль­шое количес­тво шаб­лонов для раз­личных типов фай­лов, сре­ди них и шаб­лон для пар­синга ELF-фай­лов). Одна­ко он небес­плат­ный (сто­имость лицен­зии начина­ется с 49,95 дол­лара, при этом есть 30-днев­ный три­аль­ный пери­од).



Ана­лиз заголов­ка файл ELF в 010 Editor


Го­воря о допол­нитель­ных инс­тру­мен­тах, которые облегча­ют ана­лиз ELF-фай­лов, нель­зя не упо­мянуть Python-пакет lief. Исполь­зуя этот пакет, мож­но писать Python-скрип­ты для ана­лиза и модифи­кации не толь­ко ELF-фай­лов, но и фай­лов PE и MachO. Ска­чать и уста­новить этот пакет получит­ся тра­дици­онным для Python-пакетов спо­собом:


pip install lief


Наши подопытные

В Linux (да и во мно­гих дру­гих сов­ремен­ных UNIX-подоб­ных опе­раци­онных сис­темах) фор­мат ELF исполь­зует­ся в нес­коль­ких типах фай­лов.

Ис­полня­емый файл — содер­жит все необ­ходимое для соз­дания сис­темой обра­за про­цес­са и запус­ка это­го про­цес­са. В общем слу­чае это инс­трук­ции и дан­ные. Так­же в фай­ле может при­сутс­тво­вать опи­сание необ­ходимых раз­деля­емых объ­ектных фай­лов, а так­же сим­воль­ная и отла­доч­ная информа­ция. Исполня­емый файл может быть позици­онно зависи­мым (в этом слу­чае он гру­зит­ся всег­да по одно­му и тому же адре­су, для 32-раз­рядных прог­рамм обыч­но это 0x8048000, для 64-раз­рядных — 0x400000) и позици­онно незави­симым исполня­емым фай­лом (PIE — Position Independent Execution или PIC — Position Independent Code). В этом слу­чае адрес заг­рузки фай­ла может менять­ся при каж­дой заг­рузке. При пос­тро­ении позици­онно незави­симо­го исполня­емо­го фай­ла исполь­зуют­ся такие же прин­ципы, как и при пос­тро­ении раз­деля­емых объ­ектных фай­лов.

Пе­реме­щаемый файл — содер­жит инс­трук­ции и дан­ные, при этом они могут быть ста­тичес­ки свя­заны с дру­гими объ­ектны­ми фай­лами, в резуль­тате чего получа­ется раз­деля­емый объ­ектный или исполня­емый файл. К это­му типу отно­сят­ся объ­ектные фай­лы ста­тичес­ких биб­лиотек (как пра­вило, для ста­тичес­ких биб­лиотек имя начина­ется с lib и при­меня­ется рас­ширение *.a), одна­ко, как мы уже говори­ли, рас­ширение в Linux прак­тичес­ки ничего не опре­деля­ет. В слу­чае ста­тичес­ких биб­лиотек это прос­то дань тра­диции, а работос­пособ­ность биб­лиоте­ки будет обес­печена с любым име­нем и любым рас­ширени­ем.

Раз­деля­емый объ­ектный файл — содер­жит инс­трук­ции и дан­ные, может быть свя­зан с дру­гими переме­щаемы­ми фай­лами или раз­деля­емы­ми объ­ектны­ми фай­лами, в резуль­тате чего будет соз­дан новый объ­ектный файл. Такие фай­лы могут выпол­нять фун­кции раз­деля­емых биб­лиотек (по ана­логии с DLL-биб­лиоте­ками Windows). При этом в момент запус­ка прог­раммы на выпол­нение опе­раци­онная сис­тема динами­чес­ки свя­зыва­ет эту раз­деля­емую биб­лиоте­ку с исполня­емым фай­лом прог­раммы, и соз­дает­ся исполня­емый образ при­ложе­ния. Опять же тра­дици­онно раз­деля­емые биб­лиоте­ки име­ют рас­ширение *.so (от англий­ско­го Shared Object).

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

Для наших изыс­каний нам желатель­но иметь все воз­можные вари­анты исполня­емых фай­лов из перечис­ленных выше, чем мы сей­час и зай­мем­ся.

Создание исполняемого файла

Не будем выдумы­вать что‑то свер­хориги­наль­ное, а оста­новим­ся на клас­сичес­ком хел­ловор­лде на С:



1

2

3

4

5

#include

int main(int argc, char* argv[]) {

  printf("Hello world");

  return 0;

}


Ком­пилиро­вать это дело мы будем с помощью GCC. Сов­ремен­ные вер­сии Linux, как пра­вило, 64-раз­рядные, и вхо­дящие в их сос­тав по умол­чанию средс­тва раз­работ­ки (в том чис­ле и ком­пилятор GCC) генери­руют 64-раз­рядные при­ложе­ния. Мы в сво­их иссле­дова­ниях не будем отдель­но вни­кать в 32-раз­рядные ELF-фай­лы (по боль­шому сче­ту отли­чий от 64-раз­рядных ELF-фай­лов в них не очень мно­го) и основные уси­лия сос­редото­чим имен­но на 64-раз­рядных вер­сиях прог­рамм. Если воз­никнет желание поэк­спе­римен­тировать с 32-раз­рядны­ми фай­лами, то при ком­пиляции в GCC нуж­но добавить опцию -m32, при этом, воз­можно, пот­ребу­ется уста­новить биб­лиоте­ку gcc-multilib. Сде­лать это мож­но при­мер­но вот так:



1

sudo apt-get install gcc-multilib


Итак, назовем наш хел­ловорлд example.c (кста­ти, здесь как раз один из нем­ногих слу­чаев, ког­да в Linux рас­ширение име­ет зна­чение) и нач­нем с исполня­емо­го позици­онно зависи­мого кода:



1

gcc -no-pie example.c -o example_no_pie


Как можно догадаться, опция -no-pie как раз и говорит ком­пилято­ру соб­рать не позици­онно незави­симый код.

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

В целом мож­но выделить четыре эта­па работы GCC:

  • преп­роцес­сирова­ние;
  • тран­сля­ция в ассем­блер­ный код;
  • пре­обра­зова­ние ассем­блер­ного кода в объ­ектный;
  • ком­понов­ка объ­ектно­го кода.

Что­бы пос­мотреть на про­межу­точ­ный резуль­тат, к при­меру в виде ассем­блер­ного кода, исполь­зуй в GCC опцию -S:



1

gcc -S -masm=intel example.c


Следует об­ратить вни­мание на два момен­та. Пер­вый — мы в дан­ном слу­чае не зада­ем имя выход­ного фай­ла с помощью опции -o (GCC сам опре­делит его из исходно­го, добавив рас­ширение *.s, что и озна­чает при­сутс­твие в фай­ле ассем­блер­ного кода). Вто­рой момент — опция -masm=intel, которая говорит о том, что ассем­блер­ный код в выход­ном фай­ле необ­ходимо генери­ровать с исполь­зовани­ем син­такси­са Intel (по умол­чанию будет син­таксис AT&T, мне же, как и, навер­ное, боль­шинс­тву, син­таксис Intel бли­же). Так­же в этом слу­чае опция -no-pie не име­ет смыс­ла, пос­коль­ку ассем­блер­ный код в любом слу­чае будет оди­нако­вый, а перено­симость обес­печива­ется на эта­пе получе­ния объ­ектно­го фай­ла и сбор­ки прог­раммы.

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



1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

.file "example.c"

  .intel_syntax noprefix

  .text

  .section  .rodata

.LC0:

  .string "Hello world"

  .text

  .globl  main

  .type main, @function

main:

.LFB0:

  .cfi_startproc

  endbr64

  push  rbp

  .cfi_def_cfa_offset 16

  .cfi_offset 6, -16

  mov rbp, rsp

  .cfi_def_cfa_register 6

  lea rdi, .LC0[rip]

  call  puts@PLT

  mov eax, 0

  ...


Так же нужно об­ратить вни­мание на стро­ку call puts@PLT. Это вызов си-ш­ной фун­кции puts(). Несмотря на то что в исходни­ке мы при­мени­ли фун­кцию printf(), ком­пилятор самос­тоятель­но про­вел неболь­шую опти­миза­цию и заменил ее puts(), пос­коль­ку в printf() мы не исполь­зовали какие‑либо специфи­като­ры фор­матиро­вания вывода стро­ки, а puts() работа­ет быс­трее, чем printf().


В целом поэтап­ная работа GCC при ком­пиляции фай­ла exmple.c пред­став­лена в виде схе­мы на рисунке.



Эта­пы про­цес­са ком­пиляции фай­ла example.c


Ис­поль­зуя опции -E, -S и -c, мож­но оста­новить про­цесс ком­пиляции в нуж­ном мес­те и зафик­сировать резуль­тат каж­дого эта­па в виде выход­ного фай­ла.

Да­лее сде­лаем позици­онно незави­симый исполня­емый ELF-файл (здесь все прос­то и никаких допол­нитель­ных опций не нуж­но):



1

gcc example.c -o example


Так­же для раз­нооб­разия сто­ит написать хел­ловорлд на ассем­бле­ре с исполь­зовани­ем сис­темных вызовов Linux, а не фун­кций из сиш­ных биб­лиотек. Исполь­зуем для это­го Fasm. Сов­сем недав­но мы уже обра­щались к это­му язы­ку (прав­да, под Windows) и разоб­рались, что это такое. Сегод­ня при­меним получен­ные зна­ния в Linux. Для прос­тоты возь­мем при­мер прог­раммы hello64.asm, которая идет в ком­плек­те с



1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

Fasm:

format ELF64 executable 3

segment readable executable

entry $

mov  edx, msg_size ;размер строки

lea  rsi, [msg]

mov  edi, 1        ; Номер стандартного потока вывода stdout

mov  eax, 1        ; Код вызова системной функции sys_write

syscall

xor  edi, edi      ; Код возврата из системной функции sys_exit

mov  eax, 60       ; Код вызова системной функции sys_exit

syscall

segment readable writeable

msg db 'Hello world!', 0xA

msg_size = $-msg      ; Длина выводимой строки


Ском­пилиру­ем это все с помощью Flat Assembler:



1

fasm hello64.asm example_asm


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

Создание перемещаемого файла и разделяемую библиотеку

Для переме­щаемо­го или раз­деля­емо­го (в виде динами­чес­кой биб­лиоте­ки) ELF-фай­ла необ­ходимо нем­ножко изме­нить наш хел­ловорлд:



1

2

3

4

5

#include

extern int hello_world_function() {

  printf("Hello world\n");

  return 0;

};


На­зовем этот исходник, к при­меру, exmple_lib.c и ском­пилиру­ем (без лин­ковки) с при­мене­нием опции -c:



1

gcc -c example_lib.c -o example_lib.o


Да­лее напишем и выпол­ним в тер­минале сле­дующее:



1

ar rc libstatic_example.a example_lib.o


Ес­ли поискать в Интернете наз­начение ути­литы ar, то можно уви­деть, что это архи­ватор. Так сло­жилось, что по сво­ему пря­мому пред­назна­чению (соз­давать архи­вы из фай­лов) он при­меня­ется край­не ред­ко, а вот для соз­дания ста­тичес­ки при­лино­ковы­ваемых биб­лиотек — поч­ти всег­да. Дело в том, что ста­тичес­кие биб­лиоте­ки (те самые, с рас­ширени­ем *.a и пре­фик­сом lib в име­ни фай­ла) — это не что иное, как архив, сос­тоящий из нес­коль­ких ском­пилиро­ван­ных (без лин­ковки) ELF-фай­лов, а так­же информа­ции об индекса­ции этих фай­лов для быс­тро­го поис­ка нуж­ных фун­кций при общей лин­ковке при­ложе­ния. Кста­ти, про­индекси­ровать нашу биб­лиоте­ку мож­но такой вот коман­дой:


ranlib libstatic_example.a


Итак, с переме­щаемым ELF разоб­рались, теперь сде­лаем раз­деля­емый. Возь­мем уже готовый ском­пилиро­ван­ный без лин­ковки объ­ектный файл example_lib.o и скор­мим его GCC с опци­ей -shared:



1

gcc -shared example_lib.o -o libdynamic_example.so


Пос­коль­ку Linux в слу­чае необ­ходимос­ти ищет биб­лиоте­ки в спе­циаль­но отве­ден­ных катало­гах, то получив­ший­ся в резуль­тате файл нуж­но сло­жить в какой‑либо из этих катало­гов, к при­меру /usr/lib или /lib:



1

sudo cp libdynamic_example.so /usr/lib


Те­перь нуж­но про­верить работос­пособ­ность наших биб­лиотек. Для это­го напишем прог­рамму, которая будет вызывать фун­кцию hello_world_function() из этих биб­лиотек:



1

2

3

4

5

int hello_world_function();

int main(int argc, char* argv[]) {

  int return_code = hello_world_function();

  return return_code;

}


Ском­пилиру­ем ее со ста­тичес­кой биб­лиоте­кой:



1

gcc example_ext_func.c lib_static_example.a -o example_static


И сде­лаем то же самое с динами­чес­кой биб­лиоте­кой:



1

gcc example_ext_func.c -ldynamic_example -o example_dynamic


Об­рати вни­мание, что при ком­пиляции с раз­деля­емой биб­лиоте­кой не ука­зыва­ется ее пре­фикс lib, а так­же рас­ширение и пред­варя­ется все это опци­ей -l.

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

Структура ELF-файла

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



Об­щая струк­тура 64-раз­рядно­го ELF-фай­ла


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

Спе­цифи­кацию фор­мата ELF-фай­лов мож­но взять здесь, а отли­чия 64-раз­рядных «эль­фов» от 32-раз­рядных пос­мотреть вот здесь.

Заголовок ELF-файла

Как мы уже выяс­нили, любой ELF-файл начина­ется с заголов­ка, который пред­став­ляет струк­туру, содер­жащую информа­цию о типе фай­ла, его раз­ряднос­ти, типе архи­тек­туры, вер­сии ABI (Application Binary Interface), а так­же о том, где в фай­ле искать все осталь­ное. Фор­мат струк­тур заголов­ка как для 32-раз­рядных, так и для 64-раз­рядных ELF-фай­лов (как, впро­чем, и фор­маты всех осталь­ных струк­тур ELF-фай­лов) мож­но пос­мотреть в фай­ле:



1

/usr/include/elf.h


Пер­вые 16 байт заголов­ка (мас­сив e_ident) слу­жат для иден­тифика­ции ELF-фай­ла. Пер­вые четыре бай­та — это магичес­кая кон­стан­та, сос­тоящая из бай­та 0x7f, за которым идут ASCII-коды сим­волов E, L и F. По наличию этих бай­тов заг­рузчик Linux (или, к при­меру, ути­лита file) опре­деля­ет, что перед ним имен­но ELF-файл (в PE-фай­лах Windows ана­логич­ную фун­кцию выпол­няет ком­бинация из ASCII-кодов сим­волов M и Z). Сле­дующие в этом мас­сиве бай­ты в фай­ле elf.h обоз­нача­ются такими кон­стан­тами:

  • EI_CLASS — байт опре­деля­ет раз­рядность ELF-фай­ла (зна­чение 0x01 соот­ветс­тву­ет 32 раз­рядам, зна­чение 0x02 — 64);
  • EI_DATA — зна­чение это­го бай­та опре­деля­ет по­рядок сле­дова­ния бай­тов дан­ных при раз­мещении их в памяти (Little Endian или Big Endian). Архи­тек­тура x86 исполь­зует раз­мещение бай­тов Little Endian, поэто­му зна­чение это­го бай­та будет рав­но 0x01;
  • EI_VERSION — вер­сия спе­цифи­кации ELF-фор­мата. Кор­рек­тное зна­чение в нас­тоящее вре­мя — 0x01;
  • EI_OSABI и EI_ABIVERSION опре­деля­ют вер­сию дво­ично­го интерфей­са и опе­раци­онную сис­тему, для которой откомпи­лиро­ван файл. Для Linux пер­вый из этих двух бай­тов обыч­но име­ет зна­чение 0x00 или 0x03, вто­рой — 0x00. Общую спе­цифи­кацию ABI (так называ­емую ABI System V) мож­но взять от­сюда, рас­ширение этой спе­цифи­кации для архи­тек­туры i386 (это все, что каса­ется 32-раз­рядных ELF-фай­лов) лежит здесь. Рас­ширение для x86-64 (это для 64-раз­рядных прог­рамм) — тут;
  • EI_PAD в нас­тоящее вре­мя не несет никакой наг­рузки и запол­нено нулевы­ми зна­чени­ями (в мес­тах с индекса­ми от 9 по 15).

Да­лее пос­ле мас­сива e_ident рас­положе­ны сле­дующие поля:

  • e_type — зна­чение это­го поля, как мож­но пред­положить, опре­деля­ет тип ELF-фай­ла (име­ются в виду те типы, о которых мы говори­ли перед соз­дани­ем при­меров для нашего иссле­дова­ния). Некото­рые инте­ресу­ющие нас в пер­вую оче­редь зна­чения это­го поля:
    • ET_EXEC — исполня­емый файл (зна­чение рав­но 0x02). Дан­ное зна­чение исполь­зует­ся толь­ко для позици­онно зависи­мых исполня­емых ELF-фай­лов (нап­ример, тех, которые были ском­пилиро­ваны GCC с опци­ей -no-pie);
    • ET_REL — переме­щаемый файл (зна­чение в этом слу­чае — 0x01);
    • ET_DYN — раз­деля­емый объ­ектный файл (зна­чение рав­но 0x03). Дан­ное зна­чение харак­терно как для динами­чес­ки под­клю­чаемых биб­лиотек (тех самых, которые обыч­но име­ют рас­ширение *.so), так и для позици­онно незави­симых исполня­емых фай­лов. Как они раз­лича­ются, мы обсу­дим чуть поз­же;
  • e_machine — зна­чени­ем это­го поля опре­деля­ется архи­тек­тура, для которой собс­твен­но и соз­дан ELF-файл. Пос­коль­ку мы в пер­вую оче­редь говорим об архи­тек­туре x86 в 64-раз­рядном исполне­нии, то зна­чение это­го поля будет EM_X86_64 (рав­но 0x42). Понят­но, что мож­но встре­тить и дру­гое зна­чение, нап­ример EM_386 (для 32-раз­рядно­го слу­чая архи­тек­туры x86, рав­но 0x03) или, к при­меру, EM_ARM (для про­цес­соров ARM и рав­ное 0x28);
  • e_version — дуб­лиру­ет зна­чение бай­та EI_VERSION из мас­сива e_dent;
  • e_entry — точ­ка вхо­да в ELF-файл, с которой начина­ется выпол­нение прог­раммы. Для позици­онно зависи­мых фай­лов здесь лежит абсо­лют­ный вир­туаль­ный адрес начала выпол­нения прог­раммы, для позици­онно незави­симо­го кода сюда пишет­ся сме­щение отно­ситель­но вир­туаль­ного адре­са начала обра­за ELF-фай­ла, заг­ружен­ного в память;
  • e_phoff — сме­щение начала заголов­ков прог­рамм (обра­ти вни­мание: здесь, в отли­чие от точ­ки вхо­да, сме­щение отно­ситель­но начала фай­ла, а не вир­туаль­ный адрес);
  • e_shoff — сме­щение начала заголов­ков сек­ций (так­же отно­ситель­но начала фай­ла);
  • e_flags — это поле содер­жит фла­ги, спе­цифич­ные для кон­крет­ной архи­тек­туры, для которой пред­назна­чен файл. В нашем слу­чае (име­ется в виду архи­тек­тура x86) поле име­ет зна­чение 0x00;
  • e_ehsize — раз­мер заголов­ка ELF-фай­ла (для 32-раз­рядно­го он равен 52 бай­там, для 64-раз­рядно­го — 64 бай­там);
  • e_phentsize — раз­мер одной записи в раз­деле заголов­ков прог­рамм (раз­мер одно­го заголов­ка);
  • e_phnum — чис­ло записей в раз­деле заголов­ков прог­рамм (чис­ло заголов­ков прог­рамм);
  • e_shentsize — раз­мер одной записи в раз­деле заголов­ков сек­ций (раз­мер одно­го заголов­ка);
  • e_shnum — чис­ло записей в раз­деле заголов­ков сек­ций (чис­ло заголов­ков прог­рамм);
  • e_shstrndx — это поле содер­жит индекс (то есть поряд­ковый номер в раз­деле заголов­ков сек­ций) заголов­ка одной из сек­ций, которая называ­ется .shstrtab. Эта сек­ция содер­жит име­на всех осталь­ных сек­ций ELF-фай­ла в кодиров­ке ASCII с завер­шающим нулем.

Что­бы пос­мотреть на заголо­вок ELF-фай­ла воочию, вос­поль­зуем­ся ути­литой readelf (здесь опция -W ука­зыва­ет, что необ­ходимо выводить пол­ные стро­ки, без огра­ниче­ния в 80 сим­волов в стро­ке, а опция -h говорит, что вывес­ти нуж­но имен­но заголо­вок):



1

readelf -W -h example_asm


На выходе получим сле­дующую кар­тину.



Вы­вод заголов­ка ELF-фай­ла с помощью readelf


Так­же пос­мотреть на заголо­вок ELF-фай­ла мож­но с помощью любого шес­тнад­цатерич­ного редак­тора (к при­меру, на рисун­ке с hex-редак­тором 010 Editor выделен заголо­вок ELF-фай­ла с нашим хел­ловор­лдом на ассем­бле­ре) или прив­лечь к это­му делу «Иду» (на рисун­ке выше вид­но заголо­вок это­го же фай­ла).

На Python мож­но написать прос­той скрипт (с исполь­зовани­ем lief), который может вывес­ти как заголо­вок пол­ностью, так и отдель­ные его эле­мен­ты:



1

2

3

4

5

6

import lief

elf_object = lief.parse('example_asm')

header = elf_object.header

print(header)

# Отдельно выведем точку входа

print('Entry point: %08x' % header.entrypoint)


Ну и наконец, мож­но написать в тер­минале (в дан­ном слу­чае информа­ции будет нес­коль­ко мень­ше по срав­нению с readelf):



1

objdump -f example_asm




Ре­зуль­тат работы objdump: вывод информа­ции о заголов­ке ELF-фай­ла


Секционное представление ELF-файлов

Код и дан­ные в ELF-фай­ле логичес­ки раз­делены на сек­ции, которые пред­став­ляют собой непере­сека­ющиеся смеж­ные бло­ки, рас­положен­ные в фай­ле друг за дру­гом без про­межут­ков. У сек­ций нет опре­делен­ной общей струк­туры: в каж­дой сек­ции орга­низа­ция раз­мещения дан­ных или кода зависит от ее наз­начения. Более того, некото­рые сек­ции вооб­ще могут не иметь какой‑либо струк­туры, а пред­став­лять собой нес­трук­туриро­ван­ный блок кода или дан­ных. Каж­дая сек­ция опи­сыва­ется сво­им заголов­ком, который хра­нит­ся в таб­лице заголов­ков сек­ций. В заголов­ке перечис­лены свой­ства сек­ции, а так­же мес­тонахож­дение содер­жимого самой сек­ции в фай­ле.

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

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



1

readelf -S -W example_asm


то мы уви­дим, что раз­дела с таб­лицей заголов­ков сек­ций в этом фай­ле нет.



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


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



Таб­лица заголов­ков сек­ций в фай­ле example_pie


Кста­ти, вывес­ти информа­цию из раз­дела заголов­ков сек­ций мож­но и с помощью Python (сов­мес­тно с lief), написав при­мер­но вот такой скрипт:



1

2

3

4

import lief

elf_object = lief.parse('example_pie')

for section in elf_object.sections:

  print('Section {name} - size: {size} bytes'.format(name=section.name, size=section.size))


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

Заголовки секций

Итак, как мы выяс­нили, в заголов­ках сек­ций (если в ELF-фай­ле есть раз­дел с таб­лицей заголов­ков сек­ций) содер­жится информа­ция о свой­ствах и мес­тонахож­дении той или иной сек­ции. Заголов­ки сек­ций пред­став­ляют собой струк­туру, опи­сание которой мож­но най­ти в фай­ле /usr/include/elf.h (там эти струк­туры носят име­на Elf64_Shdr и Elf32_Shdr для 64- и 32-раз­рядных фай­лов соот­ветс­твен­но).

Раз­берем­ся с наз­начени­ем каж­дого из полей этой струк­туры:

  • sh_name — содер­жит индекс име­ни сек­ции (здесь име­ется в виду номер бай­та, с которо­го начина­ется имя сек­ции) в таб­лице строк, которая, в свою оче­редь, содер­жится в сек­ции .shstrtab. Дру­гими сло­вами, все име­на сек­ций хра­нят­ся в спе­циаль­ной сек­ции .shstrtab (о ней мы уже говори­ли, ког­да рас­смат­ривали заголо­вок ELF-фай­ла), а в поле sh_name находит­ся зна­чение сме­щения начала стро­ки с име­нем сек­ции, к которой отно­сит­ся дан­ный заголо­вок, от начала сек­ции sh_strtab;
  • sh_type — тип сек­ции, который опре­деля­ет ее содер­жимое. Из всех воз­можных типов (а они так­же опре­деле­ны в /usr/include/elf.h) наиболь­ший инте­рес пред­став­ляют сле­дующие:
    • SHT_NULL — неис­поль­зуемая (пус­тая) сек­ция. Сог­ласно спе­цифи­кации, пер­вая запись в таб­лице заголов­ков сек­ций дол­жна быть имен­но такой (зна­чение, как нет­рудно догадать­ся, рав­но 0x00);
    • SHT_PROGBITS — сек­ция содер­жит дан­ные или код для выпол­нения (зна­чение рав­но 0x01);
    • SHT_SYMTAB — сек­ция содер­жит так называ­емую таб­лицу ста­тичес­ких сим­волов (под сим­волами в дан­ном слу­чае понима­ются име­на фун­кций или перемен­ных). Каж­дая запись в этой сек­ции пред­став­ляет собой струк­туру Elf64_Sym (или Elf32_Sym для 32-раз­рядных фай­лов), которая опре­деле­на в usr/include/elf.h. Как пра­вило, сек­ция с таб­лицей ста­тичес­ких сим­волов носит имя .symtab, каж­дая запись в этой сек­ции нуж­на для сопос­тавле­ния того или ино­го сим­вола с мес­тонахож­дени­ем фун­кции или перемен­ной, имя которой и опре­деле­но дан­ным сим­волом. Все это в подав­ляющем боль­шинс­тве слу­чаев нуж­но, что­бы облегчить отладку прог­раммы, а непос­редс­твен­но для выпол­нения ELF-фай­ла эта сек­ция не исполь­зует­ся (и во мно­гих слу­чаях пос­ле отладки прог­раммы ее из фай­ла уда­ляют с помощью ути­литы strip). Зна­чение рав­но 0x02;
    • SHT_DYNSYM — таб­лица сим­волов, исполь­зуемая динами­чес­ким ком­понов­щиком при ком­понов­ке прог­раммы (чис­ловое зна­чение рав­но 0x0b). Каж­дая запись этой сек­ции так­же пред­став­ляет собой струк­туру Elf64_Sym (или Elf32_Sym). Как пра­вило, сек­ция с таб­лицей динами­чес­ких сим­волов носит имя .dynsym. Более под­робно о сек­циях .symtab и .dynsym погово­рим чуть поз­же;
    • SHT_STRTAB — в сек­циях такого типа хра­нят­ся стро­ки в кодиров­ке ASCII с завер­шающим нулем (в час­тнос­ти, уже зна­комая нам сек­ция .shstrtab име­ет имен­но такой тип). Зна­чение рав­но 0x03;
    • SHT_REL, SHT_RELA — сек­ции это­го типа содер­жат записи о переме­щени­ях, при­чем фор­мат каж­дой записи опре­делен струк­турами Elf64_Rel (Elf32_Rel) и Elf64_Rela (Elf32_Rela), опять же опи­сан­ными в elf.h. Непос­редс­твен­но с орга­низа­цией переме­щений мы раз­берем­ся чуть поз­же;
    • SHT_DYNAMIC — сек­ция это­го типа хра­нит информа­цию, необ­ходимую для динами­чес­кой ком­понов­ки (чис­ловое зна­чение — 0x06). Фор­мат одной записи в такой сек­ции опи­сыва­ется струк­турой Elf64_Dyn (Elf32_Dyn) в фай­ле elf.h;
    • SHT_NOBITS — сек­ция такого типа не занима­ет мес­та в фай­ле. По сути, наличие такой сек­ции явля­ется дирек­тивой о выделе­нии необ­ходимо­го количес­тва памяти для неини­циали­зиро­ван­ных перемен­ных на эта­пе заг­рузки ELF-фай­ла в память и под­готов­ки его к выпол­нению (обыч­но такая сек­ция носит имя .bss). Чис­ловое зна­чение дан­ной кон­стан­ты рав­но 0x08;
  • sh_flags — содер­жит допол­нитель­ную информа­цию о сек­ции. Из все­го мно­гооб­разия зна­чений в пер­вую оче­редь инте­рес­ны фла­ги:
    • SHF_WRITE — флаг, име­ющий зна­чение 0x01 и говоря­щий о том, что в сек­цию воз­можна запись дан­ных (здесь необ­ходимо учи­тывать воз­можность объ­еди­нить нес­коль­ко фла­гов пос­редс­твом опе­рации «или»);
    • SHF_ALLOC — наличие это­го фла­га говорит о том, что содер­жимое сек­ции дол­жно быть заг­ружено в вир­туаль­ную память при под­готов­ке прог­раммы к выпол­нению (хотя нуж­но пом­нить, что ELF-файл заг­ружа­ется в память с при­мене­нием сег­мен­тно­го пред­став­ления фай­ла, ну а сег­мент есть не что иное, как объ­еди­нение нес­коль­ких сек­ций). Чис­ловое зна­чение рав­но 0x02;
    • SHF_EXECINSTR — такой флаг показы­вает, что сек­ция содер­жит исполня­емый код (зна­чение рав­но 0x04);
    • SHF_STRINGS — эле­мен­ты дан­ных в сек­ции с таким фла­гом сос­тоят из сим­воль­ных строк, завер­шающих­ся нулевым сим­волом (зна­чение — 0x20);
  • sh_addr — вир­туаль­ный адрес сек­ции. Хоть мы и говори­ли, что сек­цион­ное пред­став­ление не исполь­зует­ся при раз­мещении фай­ла в памяти, в некото­рых слу­чаях ком­понов­щику необ­ходимо знать, по каким адре­сам будут раз­мещены те или иные учас­тки кода или дан­ных на эта­пе выпол­нения, что­бы обес­печивать переме­щения. Для это­го и пре­дус­мотре­но дан­ное поле. Если сек­ция не дол­жна гру­зить­ся в память при выпол­нении фай­ла, то зна­чение это­го поля рав­но нулю;
  • sh_offset — сме­щение сек­ции в фай­ле (отно­ситель­но его начала);
  • sh_size — раз­мер сек­ции в бай­тах;
  • sh_linc — содер­жит индекс (в таб­лице заголов­ков сек­ций) сек­ции, с которой свя­зана дан­ная сек­ция;
  • sh_info — допол­нитель­ная информа­ция о сек­ции (зна­чение зависит от типа сек­ции);
  • sh_addralign — здесь зада­ются тре­бова­ния к вырав­ниванию сек­ции в памяти (если содер­жит зна­чения 0x00 или 0x01, то вырав­нивание не тре­бует­ся);
  • sh_entsize — в некото­рых сек­циях, нап­ример таб­лице сим­волов .symtab, лежат записи опре­делен­ных струк­тур (к при­меру, Elf64_Sym), и для таких сек­ций поле содер­жит раз­мер одной записи сек­ции в бай­тах.

Если сек­ция не отно­сит­ся к такому виду, то поле содер­жит нулевое зна­чение.

Назначение и содержимое основных секций

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


Секция .interp

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

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



1

readelf -p .interp -W example_pie




Путь к динами­чес­кому ком­понов­щику в сек­ции `.interp` фай­ла *example_pie*




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




По­пыт­ка вывес­ти сек­цию `.interp` фай­ла с раз­деля­емой биб­лиоте­кой


На­личие (или отсутс­твие) сек­ции .interp как раз и явля­ется одним из отли­чий исполня­емо­го переме­щаемо­го фай­ла от раз­деля­емой биб­лиоте­ки (как мы отме­тили выше, оба этих типа ELF-фай­лов име­ют зна­чение ET_DYN в поле e_type заголов­ка). Вто­рое отли­чие — пра­ва на выпол­нение у фай­ла (те самые, которые уста­нав­лива­ются коман­дой chmod с парамет­ром -x). В подав­ляющем боль­шинс­тве слу­чаев в раз­деля­емой биб­лиоте­ке нет ни сек­ции .interp, ни прав на выпол­нение фай­ла. Из это­го пра­вила могут быть исклю­чения. Нап­ример, биб­лиоте­ка с основны­ми фун­кци­ями С libc (на нынеш­них сис­темах это файл libc.so.6) впол­не запус­кает­ся и выводит информа­цию о себе.



За­пуск раз­деля­емой биб­лиоте­ки `libc`


Секции .init и .fini

В сек­ции .init находит­ся код, выпол­няющий­ся перед запус­ком кода, который начина­ется в точ­ке вхо­да (этот код рас­положен в сек­ции .text, и об этой сек­ции мы погово­рим ниже). Если дизас­сем­бли­ровать какой‑нибудь ELF-файл (нап­ример, исполь­зуя objdump) и пос­мотреть внутрь этой сек­ции, то мож­но уви­деть две фун­кции: _init и __gmon_start__.



Ди­зас­сем­бли­рован­ная сек­ция `.init` (вид­но, что внут­ри лежат фун­кции `_init` и `__gmon_start__`)


За­дача этих фун­кций — ини­циали­зиро­вать и попытать­ся запус­тить про­фили­ров­щик gprof. Что­бы эта попыт­ка ока­залась удач­ной и ELF-файл запус­тился под про­фили­ров­щиком, этот файл дол­жен быть ском­пилиро­ван с опци­ей -pg. В дан­ном слу­чае (если вни­матель­но пос­мотреть на дизас­сем­бли­рован­ный код фун­кции) в регис­тре rax будет находить­ся адрес фун­кции __gmon_start__, которая и вызовет gprof перед выпол­нени­ем основно­го кода прог­раммы. В про­тив­ном слу­чае в rax будет 0, вызова __gmon_start__ и, соот­ветс­твен­но, про­фили­ров­щика, не про­изой­дет, а выпол­нение будет переда­но сра­зу на код в сек­ции text.

Сек­ция .fini содер­жит в себе фун­кцию _fini, которая выпол­няет­ся пос­ле выпол­нения основно­го кода прог­раммы.



Ди­зас­сем­бли­рован­ная сек­ция `.fini`


Секция .text

Здесь как раз и находит­ся весь тот код, ради выпол­нения которо­го и была написа­на прог­рамма и на который ука­зыва­ет поле e_entry заголов­ка ELF-фай­ла. Одна­ко если пос­мотреть дизас­сем­бли­рован­ный лис­тинг этой сек­ции, то воп­реки ожи­дани­ям мы уви­дим, что по адре­су, на который ука­зыва­ет точ­ка вхо­да, лежит не фун­кция main(), а некая фун­кция _start, пос­ле которой при­сутс­тву­ет еще нес­коль­ко фун­кций, выпол­няющих под­готов­ку к запус­ку прог­раммы (нап­ример, deregister_tm_clones, register_tm_clones и frame_dummy).

Фун­кция _start счи­тыва­ет парамет­ры коман­дной стро­ки (если они есть) и вызыва­ет фун­кцию __libc_start_main. И уже эта фун­кция вызыва­ет на выпол­нение фун­кцию main(), где содер­жится основной код прог­раммы.



Ди­зас­сем­бли­рован­ная сек­ция `.text`


Секция .data

Сек­ция для хра­нения ини­циали­зиро­ван­ных перемен­ных, изме­нение которых воз­можно в ходе выпол­нения прог­раммы (соот­ветс­твен­но, эта сек­ция име­ет флаг SHF_WRITE).

Секция .rodata

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

Секция .bss

Сек­ция .bss пред­назна­чена для неини­циали­зиро­ван­ных перемен­ных. Если сек­ции .data и .rodata име­ют тип SHT_PROGBITS, эта сек­ция, как мы уже отме­чали выше, име­ет тип SHT_NOBITS. Дан­ная сек­ция не занима­ет мес­то в ELF-фай­ле, пос­коль­ку и так понят­но, что неини­циали­зиро­ван­ные перемен­ные рав­ны нулю, а хра­нить эти нули в ELF-фай­ле нет никако­го смыс­ла.

Сегментное представление ELF-файлов

Как мы уже говори­ли, сег­мен­тное пред­став­ление исполь­зует­ся ком­понов­щиком при заг­рузке ELF-фай­ла в про­цесс для выпол­нения. Этот тип пред­став­ления дает таб­лица заголов­ков прог­раммы (пов­торюсь, если иссле­дуемый файл не пред­назна­чен для выпол­нения, то эта таб­лица может отсутс­тво­вать). Таб­лица заголов­ков прог­раммы опи­сыва­ется струк­турой Elf32_Phdr или Elf64_Phdr из уже зна­комо­го нам фай­ла /usr/include/elf.h.

В целом сег­мент может вклю­чать в себя ноль и более сек­ций, то есть объ­еди­няет сек­ции в один блок. Здесь может воз­никнуть спра­вед­ливый воп­рос: почему же тог­да в сег­менте может быть ноль сек­ций?

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

Вы­вес­ти информа­цию о сег­ментах мож­но сле­дующим обра­зом:



1

readelf -W --segments example_pie




Вы­вод информа­ции о сег­ментах с помощью `readelf`


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

Итак, основные поля заголов­ка прог­раммы таковы:

  • p_type — это поле опре­деля­ет тип сег­мента. Воз­можные зна­чения так­же пред­став­лены в фай­ле /usr/include/elf.h. Наибо­лее важ­ные зна­чения:
    • PT_HDR — с дан­ного заголов­ка начина­ется таб­лица заголов­ков прог­раммы (как мы уже говори­ли, опи­сан­ный этим заголов­ком сег­мент пус­той);
    • PT_LOAD — сег­мент это­го типа пред­назна­чен для заг­рузки в память на эта­пе под­готов­ки про­цес­са к выпол­нению;
    • PT_INTERP — этот сег­мент содер­жит сек­цию .interp, в которой лежит имя интер­пре­тато­ра, исполь­зуемо­го при заг­рузке ELF-фай­ла;
    • PT_DYNAMIC — в этом сег­менте содер­жится сек­ция .dynamic с информа­цией о динами­чес­ких свя­зях, которая говорит, как раз­бирать и под­готав­ливать ELF-файл к выпол­нению (более под­робно об этой сек­ции погово­рим в сле­дующий раз);
    • PT_GNU_STACK — здесь хра­нит­ся информа­ция о сте­ке, которая опре­деля­ется зна­чени­ем фла­га pt_flags и показы­вает, что стек не дол­жен исполнять­ся (зна­чение pt_flags рав­но PF_R и PF_W). Отсутс­твие дан­ного сег­мента говорит о том, что содер­жимое сте­ка может быть исполне­но (что, как многим навер­няка известно, не есть хорошо);
  • p_flags — опре­деля­ет пра­ва дос­тупа к сег­менту во вре­мя выпол­нения ELF-фай­ла (самые глав­ные зна­чения это­го поля: PF_R — чте­ние, PF_W — запись, PF_X — выпол­нение);
  • p_offset, p_vaddr и p_filesz — зна­чения этих полей ана­логич­ны зна­чени­ям полей sh_offset, sh_addr и sh_size в заголов­ке сек­ции;
  • p_addr — обыч­но зна­чение это­го поля для сов­ремен­ных вер­сий Linux рав­но нулю (хотя изна­чаль­но здесь хра­нил­ся адрес в физичес­кой памяти, по которо­му дол­жен быть заг­ружен сег­мент);
  • p_memsz — раз­мер сег­мента в памяти (если вспом­нить о наличии сек­ции .bss, которая может вхо­дить в сос­тав сег­мента, то ста­нет понят­но, почему раз­мер сег­мента в фай­ле может быть мень­ше, чем раз­мер сег­мента в памяти);
  • p_align — это поле ана­логич­но полю sh_addralign в заголов­ке сек­ции.

Ну а скрипт на Python (с исполь­зовани­ем модуля lief) для прос­мотра информа­ции о сег­ментах ELF-фай­ла может выг­лядеть при­мер­но так:



1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

import lief

from lief import ELF

elf_object = lief.parse('example_pie')

segments = elf_object.segments

for segment in segments:

  sections = segment.sections

  section_names = ", ".join([section.name for section in sections])

  flags_str = ["-"] * 3

  if ELF.SEGMENT_FLAGS.R in segment:

    flags_str[0] = "R"

  if ELF.SEGMENT_FLAGS.W in segment:

    flags_str[1] = "W"

  if ELF.SEGMENT_FLAGS.X in segment:

    flags_str[2] = "X"

  flags_str = "".join(flags_str)

  print(str(segment.type).split(".")[-1], flags_str, section_names)


Заключение

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

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


 
MyTetra Share v.0.65
Яндекс индекс цитирования