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

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

  • Что нам понадобится
  • Подопытные экземпляры
  • Делаем исполняемые файлы
  • Делаем перемещаемый файл и разделяемую библиотеку
  • Заглядываем внутрь ELF-файла
  • Заголовок ELF-файла
  • Секционное представление ELF-файлов
  • Заголовки секций
  • Назначение и содержимое основных секций
  • Сегментное представление ELF-файлов
  • Заключение

Ес­ли в мире Windows исполня­емые фай­лы пред­став­лены в фор­мате Portable Executable (PE), то в Linux эта роль отве­дена фай­лам в фор­мате Executable and Linkable Format (ELF). Сегод­ня мы заг­лянем внутрь таких фай­лов, нем­ного поис­сле­дуем их струк­туру и узна­ем, как они устро­ены.

Сра­зу отме­чу, что в отли­чие от Windows в Linux от рас­ширения фай­ла не зависит прак­тичес­ки ничего (за сов­сем неболь­шим исклю­чени­ем). Тип и фор­мат фай­ла опре­деля­ется его внут­ренним содер­жимым и наличи­ем тех или иных атри­бутов, поэто­му фай­лы в фор­мате Executable and Linkable Format могут иметь любое рас­ширение.

 

Что нам понадобится

Во­обще, мож­но позави­довать людям, оби­тающим в мире Linux, — как пра­вило, в сис­теме «из короб­ки» идет боль­шое чис­ло ути­лит и прог­рамм, которые в Windows необ­ходимо где‑то искать и уста­нав­ливать допол­нитель­но, да еще и не всег­да бес­плат­но. В нашем слу­чае для ана­лиза ELF-фай­лов в Linux при­сутс­тву­ет впол­не сос­тоятель­ный арсе­нал встро­енных средств и ути­лит:

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

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

sudo apt install binutils

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

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

Так­же неп­лохо было бы исполь­зовать вмес­то 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, одна­ко мы о такого рода фай­лах говорить не будем, пос­коль­ку задача иссле­дова­ния дам­пов и содер­жимого памяти дос­таточ­но объ­емна и тре­бует отдель­ной статьи.

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

 

Делаем исполняемые файлы

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

#include <stdio.h>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. Сде­лать это мож­но при­мер­но вот так:

sudo apt-get install gcc-multilib

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

gcc -no-pie example.c -o example_no_pie

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

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

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

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

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

gcc -S -masm=intel example.c

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

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

.file "example.c" .intel_syntax noprefix .text .section .rodata.LC0: .string "Hello world" .text .globl main .type main, @functionmain:.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 ...

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

Ответить

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