Как создать elf файл

Обновлено: 07.07.2024

просто любопытно. Это, очевидно, не очень хорошее решение для фактического программирования, но, скажем, я хотел сделать исполняемый файл в Bless (hex editor).

моя архитектура x86. Какую очень простую программу я могу сделать? Привет миру? Бесконечная петля? Похожие на этой вопрос, но в Linux.

Как упоминалось в моем комментарии, вы по существу будете писать свой собственный ELF-заголовок для исполняемого файла, устраняя ненужные разделы. Есть еще несколько необходимых разделов. Документация по адресу Muppetlabs-TinyPrograms делает справедливую работу, объясняя этот процесс. Для удовольствия, вот несколько примеров:

эквивалент /bin / true (45 байт):

классический 'Привет, Мир!'(160 байт):

Не забудьте сделать их исполняемыми.

Декомпилируйте NASM hello world и поймите каждый байт в нем

стандарты

ELF указывается LSB:

LSB в основном ссылается на другие стандарты с незначительными расширениями, в частности:

generic (оба по SCO):

удобное резюме можно найти по адресу:

его структура может быть рассмотрена читаемым человеком способом с помощью утилит, таких как readelf и objdump .

создать пример

давайте сломаем минимальный запускаемый Linux x86-64 пример:

  • NASM 2.10.09
  • Binutils версии 2.24 (содержит ld )
  • Ubuntu 14.04

мы не используем программу на C, так как это усложнит анализ, это будет Уровень 2 :-)

Hexdumps

глобальный файл структура

файл ELF содержит следующие части:

заголовок ELF. Указывает на положение таблицы заголовка раздела и таблицы заголовка программы.

таблица заголовка раздела (необязательно для исполняемого файла). У каждого есть e_shnum заголовки разделов, каждый из которых указывает на положение раздела.

N секций, с N <= e_shnum (необязательный на исполняемый файл)

таблица заголовка программы (только для исполняемого файла). У каждого есть e_phnum заголовки программ, каждый из которых указывает на положение сегмента.

N сегментов, с N <= e_phnum (необязательно для исполняемого файла)

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

эльф заголовок

самый простой способ наблюдать за заголовком:

байт в объектном файле:

0 0: EI_MAG = 7f 45 4c 46 = 0x7f 'E', 'L', 'F' : эльф магия номер

0 4: EI_CLASS = 02 = ELFCLASS64 : 64 бит elf

0 5: EI_DATA = 01 = ELFDATA2LSB : big endian data

0 6: EI_VERSION = 01 : версия формата

0 7: EI_OSABI (только в обновлении 2003) = 00 = ELFOSABI_NONE : никаких расширений.

0 8: EI_PAD = 8x 00 : зарезервировано байт. Должно быть установлено в 0.

1 0: e_type = 01 00 = 1 (обратный порядок байтов) = ET_REl : формат перемещаемых

на исполняемом файле это 02 00 на ET_EXEC .

1 2: e_machine = 3e 00 = 62 = EM_X86_64 : архитектура AMD64

1 4: e_version = 01 00 00 00 : должно быть 1

1 8: e_entry = 8x 00 : точка входа адреса выполнения или 0, если нет применимо как для объектного файла, так как нет точки входа.

на исполняемом файле это b0 00 40 00 00 00 00 00 . TODO: на что еще мы можем это установить? Ядро, похоже, помещает IP непосредственно в это значение, оно не жестко закодировано.

2 0: e_phoff = 8x 00 : смещение таблицы заголовка программы, 0 если нет.

40 00 00 00 на исполняемом файле, т. е. он запускается сразу после заголовка ELF.

2 8: e_shoff = 40 7x 00 = 0x40 : смещение файла таблицы заголовка раздела, 0 если нет.

3 0: e_flags = 00 00 00 00 TODO. Arch specific.

3 4: e_ehsize = 40 00 : размер этого заголовка elf. TODO почему это поле? Как она может меняться?

3 6: e_phentsize = 00 00 : размер каждого заголовка программы, 0 если нет.

38 00 on исполняемый файл: это 56 байт

3 8: e_phnum = 00 00 : количество записей заголовка программы, 0 если нет.

02 00 на исполняемом файле: есть 2 записи.

3 A: e_shentsize и e_shnum = 40 00 07 00 : размер заголовка раздела и количество записей

3 E: e_shstrndx ( Section Header STRing iNDeX ) = 03 00 : индекс .

заголовок раздела таблица

массив Elf64_Shdr структуры.

каждая запись содержит метаданные о данном разделе.

e_shoff заголовка ELF дает начальную позицию, 0x40 здесь.

e_shentsize и e_shnum из заголовка ELF говорят, что у нас есть 7 записей, каждая 0x40 байт.

таким образом, таблица принимает байты от 0x40 до 0x40 + 7 + 0x40 - 1 = 0x1FF.

readelf -S hello_world.o :

struct в лице каждой записи:

разделы

90 0: sh_addr = 8x 00 : в каком виртуальном адресе будет размещен раздел во время выполнения, 0 если не помещено

90 8: sh_offset = 00 02 00 00 00 00 00 00 = 0x200 : число байт от начала программы до первого байта в этом разделе

a0 0: sh_size = 0d 00 00 00 00 00 00 00

если мы возьмем 0xD байта, начиная с sh_offset 200, видим:

АХА! Так наши "Hello world!" строка находится в разделе данных, как мы сказали, чтобы быть на NASM.

как только мы закончим hd , мы будет выглядеть так:

также обратите внимание, что это был плохой выбор раздела: хороший компилятор C поместил бы строку в .rodata вместо этого, потому что он доступен только для чтения, и это позволит для дальнейшей ОС процессы оптимизации.

b0 0: sh_addralign = 04 = TODO: почему это выравнивание необходимо? Это только для sh_addr , или также для символов внутри sh_addr ?

b0 8: sh_entsize = 00 = раздел не содержать таблицу. Если != 0, это означает, что раздел содержит таблицу записей фиксированного размера. В этом файле мы видим из readelf выведите, что это относится к .symtab и .rela.text разделы.

.текстовый раздел

теперь, когда мы сделали один раздел вручную, давайте закончим и используем readelf -S других разделов.

.text является исполняемым, но не записываемым: если мы попытаемся написать на него Linux с падениями. Давайте посмотрим, действительно ли у нас есть какой-то код:

если мы grep b8 01 00 00 на hd , мы видим, что это происходит только в 00000210 , что и говорится в разделе. И размер 27, который также совпадает. Поэтому мы должны говорить о правильном разделе.

это выглядит как правильный код: write затем exit .

самая интересная часть-line a что делает:

для передачи адреса строки в системный вызов. В настоящее время 0x0 - это просто заполнитель. После связывания происходит, он будет изменен, чтобы содержать:

это изменение возможно из-за данных .

sht_strtab и атрибут

разделы sh_type == SHT_STRTAB называют строка таблицы.

они содержат null разделенный массив веревка.

такие разделы используются другими разделами при использовании имен строк. В разделе using говорится:

  • какую таблицу строк они используют
  • что такое индекс в целевой таблице строк, где начинается строка

например, у нас может быть таблица строк, содержащая: TODO: она должна начинаться с ?

и если другой раздел хочет использовать строку d e f , они должны указывать на index 5 этого раздела (письмо d ).

примечательные разделы таблицы строк:

.shstrtab

тип раздела: sh_type == SHT_STRTAB .

общее имя: таблица строк заголовка раздела.

название раздела .shstrtab зарезервирован. Стандарт гласит:

этот раздел содержит имена разделов.

на этот раздел указывает e_shstrnd поле самого заголовка ELF.

строковые индексы этого раздела указывают на sh_name поле заголовков разделов, которые обозначают строки.

в этом разделе нет SHF_ALLOC отмечен, поэтому он не будет отображаться в исполняющей программе.

если мы посмотрим на названия других разделов, мы увидим, что все они содержат номера, например номером 7 .

затем каждая строка заканчивается, когда найден первый символ NUL, например, символ 12 is сразу после .text .

.symtab

тип раздела : sh_type == SHT_SYMTAB .

общее имя: таблица символов.

Сначала отметим, что:

на SHT_SYMTAB разделы, эти цифры означают, что:

  • строки, которые дают имена символов, находятся в разделе 5, .strtab
  • данные переселение в разделе 6, .rela.text

хороший инструмент высокого уровня для разборки этого раздела:

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

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

запись 1 имеет ELF64_R_TYPE == STT_FILE . ELF64_R_TYPE продолжение внутри st_info .

10 8: st_name = 01000000 = символ 1 в .strtab , который до делает hello_world.asm

этот информационный файл может использоваться компоновщиком для решения, к какому сегменту идти.

10 12: st_info = 04

биты 0-3 = ELF64_R_TYPE = Type = 4 = STT_FILE : основная цель этой записи-использовать st_name to Укажите имя файла, который сгенерировал этот объектный файл.

биты 4-7 = ELF64_ST_BIND = обязательного = 0 = STB_LOCAL . Требуемое значение для STT_FILE .

10 13: st_shndx = индекс заголовка раздела таблицы символов = f1ff = SHN_ABS . Требуется для STT_FILE .

20 0: st_value = 8x 00 : требуется для значения для STT_FILE

20 8: st_size = 8x 00 : не выделены размере

теперь из readelf , мы быстро интерпретируем остальные.

есть две такие записи, одна из которых указывает на .data , а другой .text (индексы разделе 1 и 2 ).

TODO какова их цель?

затем идут самые важные символы:

hello_world строка в (индекс 1). Это значение равно 0: оно указывает на первый байт этого раздела.

_start обозначен GLOBAL видимость, так как мы написали:

в NASM. Это необходимо, поскольку она должна рассматриваться как точка входа. В отличие от C, по умолчанию метки NASM являются локальными.

hello_world_len указывает на специальные st_shndx == SHN_ABS == 0xF1FF .

0xF1FF выбирается так, чтобы не конфликтовать с другими разделами.

st_value == 0xD == 13 какое значение мы сохранили там в сборке: длина строки Hello World! .

это означает, что перемещение не повлияет на это значение: это константа.

это небольшая оптимизация, которую наш ассемблер делает для нас и которая имеет поддержку ELF.

если бы мы использовали адрес hello_world_len в любом месте ассемблер не смог бы отметить его как SHN_ABS , и компоновщик будет иметь дополнительную работу по перемещению на нем позже.

Раздел sht_symtab на исполняемый

по умолчанию NASM помещает .symtab на исполняемом файле, а также.

это используется только для отладки. Без символов мы полностью слепы и должны все перестроить.

вы можете снять его с objcopy , и исполняемый файл, все равно будет работать. Таких программ называется лишен исполняемые файлы.

.strtab

содержит строки для символа таблица.

в этом разделе sh_type == SHT_STRTAB .

он указал sh_link == 5 на .

это означает, что это ограничение уровня ELF, что глобальные переменные не могут содержать символы NUL.

.Рела.текст

тип раздела: sh_type == SHT_RELA .

общее имя: раздел переезд.

.rela.text держит данные перемещения, которые говорят, как адрес должен быть изменен, когда окончательный исполняемый файл связан. Это указывает на байты текстовой области, которые должны быть изменены при связывании происходит указать на правильные места памяти.

в основном, он переводит текст объекта, содержащий адрес заполнителя 0x0:

к фактическому исполняемому коду, содержащему окончательный 0x6000d8:

на него указал sh_info = 6 of the .

readelf -r hello_world.o выдает:

раздел не существует в исполняемом файле.

на struct является:

370 0: r_offset = 0xC: адрес в .text чей адрес это перемещение изменит

370 8: r_info = 0x200000001. Содержит 2 поля:

  • ELF64_R_TYPE = 0x1: значение зависит от точной архитектуры.
  • ELF64_R_SYM = 0x2: индекс раздела, на который указывает адрес, так что .data который находится в индексе 2.

amd64 ABI говорит, что тип 1 называется R_X86_64_64 и что он представляет собой операцию S + A где:

  • S : значение символа в объектном файле, здесь 0 потому что мы указываем на 00 00 00 00 00 00 00 00 of movabs x0,%rsi
  • A : добавление, присутствует в поле r_added

этот адрес добавляется в раздел, на котором выполняется перемещение.

эта операция перемещения действует в общей сложности 8 байт.

380 0: r_addend = 0

Итак, в нашем примере мы заключаем, что новый адрес будет: S + A = .data + 0 , и таким образом первый вещь в разделе данных.

таблица заголовка программы

отображается только в исполняемом файле.

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

исполняемый файл создается компоновщиком из объектных файлов. Основные задания, которые выполняет компоновщик:

определите, какие разделы объектных файлов будут входить в какие сегменты выполнимый.

в Binutils это сводится к разбору скрипта компоновщика и работе с кучей значений по умолчанию.

вы можете получить сценарий компоновщика, используемый с ld --verbose , и установите пользовательский с ld -T .

сделать перемещение по тексту разделов. Это зависит от того, как несколько разделов помещаются в память.

readelf -l hello_world.out выдает:

на заголовке ELF, e_phoff , e_phnum и e_phentsize сказал нам, что есть 2 заголовка программы, которые начинаются с 0x40 и 0x38 байт длиной каждый, поэтому они:

Вообще говоря .c или .cpp , Непосредственно сгенерированные файлы должны быть .o файлами. Если они находятся на платформе linux, они будут скомпилированы gcc для создания общих объектных файлов (динамических библиотек) и исполняемых файлов. На платформе linux исполняемый файл не имеет суффикса. ,

image

Процесс компиляции выглядит следующим образом:

Что касается процесса компиляции, я рекомендую следующую статью:

В дополнение к связыванию этой вещи, эта статья немного более детальна, особенно о перемещении и введении таблицы символов в статической ссылке, более динамической ссылке PIC (адрес независимый код) технологии, GOT , PLT Существует простое введение в знания, которое очень полезно для понимания динамического связывания, кроме того, генерация статически связанных исполняемых файлов и генерация динамически связанных исполняемых файлов также очень детальны, и они прекрасно понимают стадию линковки понимания процесса компиляции помощь;

Формат файла 0x02 ELF

Прежде всего, мы дадим диаграмму формата файла ELF. Существует грубая модель того, что описано позже. Мы дадим следующие различия между каждой частью и связью между сегментами и разделами:

image

1. ELF заголовок файла

Использовать readelf -h <filename> Команда может просмотреть заголовок файла ELF, кроме того, может быть использован readelf -h Посмотреть другие команды. readelf - очень полезная команда для анализа формата файла ELF.
Структура заголовка файла ELF приведена ниже:

Значение каждой переменной в структуре четко указано, и стоит отметить, что последняя переменная e_shstrndx Это относится к хранению каждого Section название Section из Section header в Section header table Смещение, если для Section Друзьям, которые не очень знакомы, возможно, здесь не придется путаться, об этом мы поговорим позже Section Я буду говорить об этом снова. Кроме того, вы можете просмотретьРуководство ELFУзнайте больше

2. Заголовок программы ELF

Заголовок программы ELF представляет собой описание сегмента в двоичном файле и является необходимой частью загрузки программы. Сегмент анализируется, когда ядро ​​загружает программу.В заголовке программы описывается расположение соответствующего сегмента в памяти и как он отображается в памяти. Вы можете обратиться к заголовку ELF e_phoff (смещение таблицы заголовков программы) Получить таблицу заголовков программ. Таблица заголовков программ состоит из нескольких записей заголовков программ. Каждая запись заголовка программы описывает свой соответствующий тип сегмента и хранит информацию, относящуюся к сегменту. Структура заголовка программы 32-битного файла ELF приведена ниже, а несколько структур заголовка программы сохранены в таблице заголовков программы:

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

  • PT_LOAD: Исполняемый файл содержит хотя бы один сегмент PT_LOAD. Этот тип сегмента будет загружен или отображен в памяти. Исполняемый файл, который требует динамического связывания, обычно включает код test Сегментируйте и храните глобальные переменные и информацию о динамических ссылках data Раздел.
  • PT_DYNAMIC: Этот тип сегмента уникален для исполняемых файлов динамической ссылки и содержит некоторую информацию, необходимую динамическому компоновщику. В основном он включает список общих библиотек, которые необходимо связать во время работы программы, адрес глобальной таблицы смещений (GOT) и информацию о записях о перемещении. Это будет обсуждаться позже при обсуждении динамических ссылок.
  • PT_INTERPВ этом хранилище сегментов обычно хранится местоположение динамического компоновщика, то есть местоположение интерпретатора программы. Такие как: /lib/linux-ld.so.2 。
  • PT_PHDR: Этот раздел сохраняет положение и размер самой таблицы заголовков программы. Таблица заголовков программ хранит описание всех заголовков программ в файле.

Может быть просмотреноРуководство ELFЧтобы узнать больше о пункте.

Мы можем пройти readelf -l <filename> Команда для просмотра таблицы заголовков программы файла выглядит следующим образом:

image

Среди них мы видим несколько распространенных типов заголовков программ, которые мы представили выше: среди них запись типа LOAD и смещение 0x0 - это программный заголовок текстового сегмента, а другой тип LOAD - программный заголовок сегмента данных. ,

3. Заголовок раздела ELF

Здесь мы сначала поговорим о связи и разнице между Секцией и Сегментом. Многие люди путают это. Сегменты являются неотъемлемой частью каждого выполнения программы, код или данные в каждом сегменте будут разделены на различные разделы. То есть раздел является более подробным разделением исполняемого файла в памяти относительно сегмента. Когда мы представим типы разделов позже, это может быть полностью отражено.

Каждый раздел совпадает с разделом и имеет собственный заголовок. Мы называем заголовок раздела (Таблица заголовков разделов). Многие заголовки разделов составляют таблицу заголовков разделов. Если исполняемый файл имеет таблицу заголовков разделов, вы можете обратиться к заголовку файла ELF. e_shoff Чтобы получить его смещение в файле. Почему мы говорим, что если она существует, поскольку таблица заголовков разделов не нужна для выполнения программы, программа все еще может нормально работать без таблицы заголовков разделов, роль заголовка раздела состоит в том, чтобы описать положение и размер соответствующего раздела, который в основном используется для связывания и отладки. Поэтому мы выбросили его из памяти .so Файл может не иметь заголовка раздела.

Хотя в двоичном файле может отсутствовать заголовок раздела, это не означает, что раздел не существует. Ссылка на раздел через заголовок раздела просто невозможна.Для отладчика и декомпилятора гораздо меньше справочной информации. По умолчанию исполняемые программы поставляются с заголовками разделов, но в какой-то момент, чтобы предотвратить декомпиляцию программы хакерами, используйте инструменты, такие как IDA и objdump, для получения соответствующей информации. ,

Ниже мы даем структуру заголовка 32-битного раздела ELF:

Мы можем использовать readelf -l <filename> Командой, чтобы получить взаимосвязь между каждым сегментом и сечением, соединяем рисунок следующим образом:

image

Строка, соответствующая 02, - это различные типы разделов, содержащихся в текстовом разделе. Ниже мы приводим несколько общих типов разделов:

  • .text: Раздел .text содержит инструкции кода программы, которые существуют в текстовом разделе, и соответствующий тип SHT_PROGBITS
  • .rodata: Раздел .rodata сохраняет данные только для чтения, поскольку он доступен только для чтения, его можно поместить только в раздел только для чтения и он существует в текстовом разделе. Например, на языке c printf("hello world\n") Эта инструкция размещена в разделе родата, соответствующий тип SHT_PROGBITS
  • .plt: Раздел .plt содержит соответствующий код, необходимый для динамического компоновщика для вызова функций, импортированных из общей библиотеки. Существует в текстовом разделе. Соответствующий тип SHT_PROGBITS
  • .data: Раздел .data содержит данные, такие как инициализированные глобальные переменные. Существует в разделе данных, соответствующий тип SHT_PROGBITS
  • .bss: Раздел .bss сохраняет такие данные, как неинициализированные глобальные переменные. Существует в разделе данных, соответствующий тип SHT_NOBITS
  • .got.plt: Раздел .got содержит глобальную таблицу смещений. Раздел .got вместе с разделом .plt обеспечивает доступ к функциям, импортированным из общей библиотеки. Динамический компоновщик изменяет раздел .got во время выполнения. Существует в данных, соответствующий тип SHT_PROGBITS
  • .dynsym: Раздел .dynsym содержит информацию о динамических символах, импортированную из общей библиотеки. Существует в текстовом разделе, соответствующий тип SHT_DYNSYM
  • .dynstr: Раздел .dynstr содержит таблицу строк динамических символов. Существует в текстовом разделе
  • .rel*: Он сохраняет соответствующую информацию о перемещении, которая описывает, как добавить или изменить часть содержимого или зеркальное отображение файла объекта ELF при компоновке или запуске. Существует в текстовом разделе, соответствующий тип SHT_REL
  • .symbol: Он сохраняет все символы двоичного файла, который мы называем таблицей символов. Существует в текстовом разделе, соответствующий тип SHT_SYMTAB
  • .strtab: Он хранит все строки в двоичном файле, который мы называем таблицей строк. На содержание таблицы будет ссылаться таблица символов. Существует в текстовом разделе, соответствующий тип SHT_STRTAB
  • .shstrtab: Этот раздел предназначен для сохранения строки имени всех заголовков разделов в заголовке файла Elf. e_shstrndx Эта переменная ссылается на смещение заголовка раздела в таблице заголовка раздела. Мы можем получить строку имени каждого раздела, получив позицию раздела. Существует в текстовом разделе, соответствующий тип SHT_STRTAB

Мы представим некоторые основные типы разделов, если вы хотите узнать больше, вы можете проверитьРуководство ELF

Четыре, символ эльфа

Символы являются символическими ссылками на определенные коды или данные.Многочисленные символы образуют таблицу символов для двоичного файла. Например, у нас есть printf("hello world\n") Этот оператор затем символ сохраняет смещение своей строки имени в таблице символов, его адрес или смещение, размер и другую информацию.

Для большинства разделяемых библиотек и динамически связанных исполняемых файлов существуют две таблицы символов: .dynsym и .symtab. Разница между ними заключается в том, что .dynsym сохраняет глобальные / динамические символы, которые ссылаются на символы внешнего файла, такие как библиотечные функции, такие как printf. И .symtab сохраняет все символы программы. В дополнение к символам в .dynsym есть такие символы, как локальные функции, то есть. Но почему существует таблица .dynsym для таблицы символов .symtab? Это потому, что .dynsym будет загружен в память при запуске программы, что необходимо для запуска программы, а .symtab - нет. Символы в dynsym анализируются только во время работы программы и являются единственными символами, необходимыми динамическому компоновщику во время выполнения. Таблица символов .symtab используется только для компоновки и отладки. Иногда для экономии места таблица символов .symtab удаляется из двоичного файла.

Структура записи символа 32-битного файла ELF приведена ниже, которая составляет таблицу символов:

Формат файла ELF здесь почти такой же, и об этом меньше говорят, но сам файл ELF не может быть закончен в одной или двух статьях. Нужно потратить больше времени, чтобы понять, вы можете посмотретьРуководство ELFЭто очень подробно. Наконец, приведен код синтаксического анализатора файлов ELF. Вы также можете реализовать следующее, что очень полезно для понимания:

Есть в мире вещи, которые мы принимаем как нечто само собой разумеющееся, хотя они являются истинными шедеврами. Одними из таких вещей являются утилиты Linux, такие, как ls и ps. Хотя они обычно воспринимаются как простые, это оказывается далеко не так, если мы заглянем внутрь. И таким же оказывается ELF, Executable and Linkable Format. Формат файлов, который используется повсеместно, но мало кто его понимает. Это краткое руководство поможет вам достичь понимания.


Прочтя это руководство, вы изучите:

  • Зачем нужен формат ELF и для каких типов файлов он используется
  • Структуру файла ELF и детали его формата
  • Как читать и анализировать бинарное содержимое файла ELF
  • Какие инструменты используются для анализа бинарных файлов

Что представляет собой файл ELF?

ELF — это сокращение от Executable and Linkable Format (формат исполняемых и связываемых файлов) и определяет структуру бинарных файлов, библиотек, и файлов ядра (core files). Спецификация формата позволяет операционной системе корректно интерпретировать содержащиеся в файле машинные команды. Файл ELF, как правило, является выходным файлом компилятора или линкера и имеет двоичный формат. С помощью подходящих инструментов он может быть проанализирован и изучен.

Зачем изучать ELF в подробностях?

Перед тем, как погрузиться в технические детали, будет не лишним объяснить, почему понимание формата ELF полезно. Во-первых, это позволяет изучить внутреннюю работу операционной системы. Когда что-то пошло не так, эти знания помогут лучше понять, что именно случилось, и по какой причине. Также возможность изучения ELF-файлов может быть ценна для поиска дыр в безопасности и обнаружения подозрительных файлов. И наконец, для лучшего понимания процесса разработки. Даже если вы программируете на высокоуровневом языке типа Go, выа всё равно будет лучше знать, что происходит за сценой.

Итак, зачем изучать ELF?

  • Для общего понимания работы операционной системы
  • Для разработки ПО
  • Цифровая криминалистика и реагирование на инциденты (DFIR)
  • Исследование вредоносных программ (анализ бинарных файлов)

От исходника к процессу

Какую бы операционную систему мы не использовали, необходимо каким-то образом транслировать функции исходного кода на язык CPU — машинный код. Функции могут быть самыми базовыми, например, открыть файл на диске или вывести что-то на экран. Вместо того, чтобы напрямую использовать язык CPU, мы используем язык программирования, имеющий стандартные функции. Компилятор затем транслирует эти функции в объектный код. Этот объектный код затем линкуется в полную программу, путём использования линкера. Результатом является двоичный файл, который может быть выполнен на конкретной платформе и конкретном типе CPU.

Прежде, чем начать

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

Анатомия ELF-файла

Распространённым заблуждением является то, что файлы ELF предназначены только для бинарных или исполняемых файлов. Мы уже сказали, что они могут быть использованы для частей исполняемых файлов (объектного кода). Другим примером являются файлы библиотек и дампы ядра (core-файлы и a.out файлы). Спецификация ELF также используется в Linux для ядра и модулей ядра.


Структура

В силу расширяемости ELF-файлов, структура может различаться для разных файлов. ELF-файл состоит из:


заголовок ELF

Как видно на скриншоте, заголовок ELF начинается с «магического числа». Это «магическое число» даёт информацию о файле. Первые 4 байта определяют, что это ELF-файл (45=E,4c=L,46=F, перед ними стоит значение 7f).

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

Класс

После объявления типа ELF, следует поле класса. Это значение означает архитектуру, для которой предназначен файл. Оно может равняться 01 (32-битная архитектура) или 02 (64-битная). Здесь мы видим 02, что переводится командой readelf как файл ELF64, то есть, другими словами, этот файл использует 64-битную архитектуру. Это неудивительно, в моей машине установлен современный процессор.

Данные

Далее идёт поле «данные», имеющее два варианта: 01 — LSB (Least Significant Bit), также известное как little-endian, либо 02 — MSB (Most Significant Bit, big-endian). Эти значения помогают интерпретировать остальные объекты в файле. Это важно, так как разные типы процессоров по разному обрабатывают структуры данных. В нашем случае используется LSB, так как процессор имеет архитектуру AMD64.

Эффект LSB становится видимым при использовании утилиты hexdump на бинарном файле. Давайте посмотрим заголовок ELF для /bin/ps.

Мы видим, что пары значений другие, из-за интерпретации порядка данных.

Версия

Затем следует ещё одно магической значение «01», представляющее собой номер версии. В настоящее время имеется только версия 01, поэтому это число не означает ничего интересного.

OS/ABI

Каждая операционная система имеет свой способ вызова функций, они имеют много общего, но, вдобавок, каждая система, имеет небольшие различия. Порядок вызова функции определяется «двоичным интерфейсом приложения» Application Binary Interface (ABI). Поля OS/ABI описывают, какой ABI используется, и его версию. В нашем случае, значение равно 00, это означает, что специфические расширения не используются. В выходных данных это показано как System V.

Версия ABI

При необходимости, может быть указана версия ABI.

Машина

Также в заголовке указывается ожидаемый тип машины (AMD64).

Поле типа указывает, для чего предназначен файл. Вот несколько часто встречающихся типов файлов.

CORE (значение 4)
DYN (Shared object file), библиотека (значение 3)
EXEC (Executable file), исполняемый файл (значение 2)
REL (Relocatable file), файл до линковки (значение 1)

Смотрим полный заголовок

Хотя некоторые поля могут быть просмотрены через readelf, их на самом деле больше. Например, можно узнать, для какого процессора предназначен файл. Используем hexdump, чтобы увидеть полный заголовок ELF и все значения.


(вывод hexdump -C -n 64 /bin/ps)

Выделенное поле определяет тип машины. Значение 3e — это десятичное 62, что соответствует AMD64. Чтобы получить представление обо всех типах файлов, посмотрите этот заголовочный файл.

Хотя вы можете делать всё это в шестнадцатиричном дампе, имеет смысл использовать инструмент, который сделает работу за вас. Утилита dumpelf может быть полезна. Она показывает форматированный вывод, соответствующий заголовку ELF. Хорошо будет изучить, какие поля используются, и каковы их типичные значения.

Теперь, кгда мы объяснили значения этих полей, время посмотреть на то, какая реальная магия за ними стоит, и перейти к следующим заголовкам!

Данные файла

Помимо заголовка, файлы ELF состоят из трёх частей.

  • Программные заголовки или сегменты
  • Заголовки секций или секции
  • Данные

Заголовки программы

Файл ELF состоит из нуля или более сегментов, и описывает, как создать процесс, образ памяти для исполнения в рантайме. Когда ядро видит эти сегменты, оно размещает их в виртуальном адресном пространстве, используя системный вызов mmap(2). Другими словами, конвертирует заранее подготовленные инструкции в образ в памяти. Если ELF-файл является обычным бинарником, он требует эти программные заголовки, иначе он просто не будет работать. Эти заголовки используются, вместе с соответствующими структурами данных, для формирования процесса. Для разделяемых библиотек (shared libraries) процесс похож.



Программный заголовок в бинарном ELF-файле

Мы видим в этом примере 9 программных заголовков. Сначала трудно понять, что они означают. Давайте погрузимся в подробности.

GNU_EH_FRAME

Это сортированная очередь, используемая компилятором GCC. В ней хранятся обработчики исключений. Если что-то пошло не так, они используются для того, чтобы корректно обработать ситуацию.

GNU_STACK

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

Если сегмент GNU_STACK отсутствует, используется исполняемый стек. Утилиты scanelf и execstack показывают детали устройства стека.

  • dumpelf (pax-utils)
  • elfls -S /bin/ps
  • eu-readelf –program-headers /bin/ps

Секции ELF

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

Заголовки секции определяют все секции файла. Как уже было сказано, эта информация используется для линковки и релокации.

Секции появляются в ELF-файле после того, как компилятор GNU C преобразует код С в ассемблер, и ассемблер GNU создаёт объекты.

Как показано на рисунке вверху, сегмент может иметь 0 или более секций. Для исполняемых файлов существует четыре главных секций: .text, .data, .rodata, и .bss. Каждая из этих секций загружается с различными правами доступа, которые можно посмотреть с помощью readelf -S.

Содержит исполняемый код. Он будет упакован в сегмент с правами на чтение и на исполнение. Он загружается один раз, и его содержание не изменяется. Это можно увидеть с помощью утилиты objdump.

Инициализированные данные, с правами на чтение и запись.

.rodata

Инициализированные данные, с правами только на чтение. (=A).

Неинициализированные данные, с правами на чтение/запись. (=WA)

  • dumpelf
  • elfls -p /bin/ps
  • eu-readelf –section-headers /bin/ps
  • readelf -S /bin/ps
  • objdump -h /bin/ps

Группы секций

Некоторые секции могут быть сгруппированы, как если бы они формировали единое целое. Новые линкеры поддерживают такую функциональность. Но пока такое встречается не часто.

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

Статические и динамические бинарные файлы

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

Если вы хотите проверить, является ли файл статическим или динамическим, используйте команду file. Она покажет что-то вроде этого:


Чтобы определить, какие внешние библиотеки использованы, просто используйте ldd на том же бинарнике:

Совет: Чтобы посмотреть дальнейшие зависимости, лучше использовать утилиту lddtree.

Инструменты анализа двоичных файлов

Если вы хотите анализировать ELF-файлы, определённо будет полезно сначала посмотреть на существующие инструменты. Существуют тулкиты для обратной разработки бинарников и исполняемого кода. Если вы новичок в анализе ELF-файлов, начните со статического анализа. Статический анализ подразумевает, что мы исследуем файлы без их запуска. Когда вы начнёте лучше понимать их работу, переходите к динамическому анализу. Запускайте примеры и смотрите на их реальное поведение.

Популярные инструменты

Radare2

Тулкит Radare2 создан Серджи Альваресом (Sergi Alvarez). Число 2 подразумевает, что код был полностью переписан по сравнению с первой версией. Сейчас он используется многими исследователями, для изучения работы кода.

Программные пакеты

Большинство Linux-систем имеют установленный пакет binutils. Другие пакеты могут помочь вам увидеть больше информации. Правильный тулкит упростит вашу работу, особенно если вы занимаетесь анализом ELF-файлов. Я собрал здесь список пакетов и утилит для анализа ELF-файлов.

elfutils
/usr/bin/eu-addr2line
/usr/bin/eu-ar – альтернатива ar, для создания и обработки архивных файлов
/usr/bin/eu-elfcmp
/usr/bin/eu-elflint – проверка на соответствие спецификациям gABI и psABI
/usr/bin/eu-findtextrel – поиск релокаций текста
/usr/bin/eu-ld – комбинирует объектный и архивные файлы
/usr/bin/eu-make-debug-archive
/usr/bin/eu-nm – показывает символы объектного и исполняемого файлов
/usr/bin/eu-objdump – показывает информацию из объектного файла
/usr/bin/eu-ranlib – создаёт индекс архивных файлов
/usr/bin/eu-readelf – показывает ELF-файл в читаемой форме
/usr/bin/eu-size – показывает размер каждой секции (text, data, bss, etc)
/usr/bin/eu-stack – показывает стек текущего процесса или дампа ядра
/usr/bin/eu-strings – показывает текстовые строки (как утилита strings)
/usr/bin/eu-strip – удаляет таблицу символов из файла ELF
/usr/bin/eu-unstrip – добавляет символы и отладочную информацию в бинарник
Примечание: пакет elfutils будет хорошим началом, он содержит большинство утилит для анализа

elfkickers
/usr/bin/ebfc – компилятор языка Brainfuck
/usr/bin/elfls – показывает программные заголовки и заголовки секций с флагами
/usr/bin/elftoc – преобразует бинарник в программу на С
/usr/bin/infect – утилита, инжектирующая дроппер, создаёт файл setuid в /tmp
/usr/bin/objres – создаёт объект из обычных или бинарных данных
/usr/bin/rebind – изменяет связывание и видимость символов в ELF-файлах
/usr/bin/sstrip – удаляет ненужные компоненты из ELF-файла
Примечание: автор пакета ELFKickers сфокусирован на манипулировании ELF-файлами, что позволяет вам получить больше информации при работе с «неправильными» ELF-бинарниками

pax-utils
/usr/bin/dumpelf – дамп внутренней структуры ELF
/usr/bin/lddtree – как ldd, с установкой уровня показываемых зависимостей
/usr/bin/pspax – выводит ELF/PaX информацию о запущенных процессах
/usr/bin/scanelf – широкий диапазон информации, включая подробности PaX
/usr/bin/scanmacho – показывает подробности бинарников Mach-O (Mac OS X)
/usr/bin/symtree – показывает символы в виде дерева
Примечание: некоторые утилиты в этом пакете могут рекурсивно сканировать директории, и идеальны для анализа всего содержимого директории. Фокус сделан на инструментах для исследования подробностей PaX. Помимо поддержки ELF, можно извлекать информацию из Mach-O-бинарников.


prelink
/usr/bin/execstack – можно посмотреть или изменить информацию о том, является ли стек исполняемым
/usr/bin/prelink – релоцирует вызовы в ELF файлах, для ускорения процесса

Часто задаваемые вопросы

Что такое ABI?

ABI — это Бинарный Интерфейс Приложения (Application Binary Interface) и определяет, низкоуровневый интерфейс между операционной системой и исполняемым кодом.

Что такое ELF?

ELF — это Исполняемый и Связываемый Формат (Executable and Linkable Format). Это спецификация формата, определяющая, как инструкции записаны в исполняемом коде.

Как я могу увидеть тип файла?

Используйте команду file для первой стадии анализа. Эта команда способна показать подробности, извлечённые из «магических» чисел и заголовков.

Заключение

Файлы ELF предназначены для исполнения и линковки. В зависимости от назначения, они содержат необходимые сегменты и секции. Ядро ОС просматривает сегменты и отображает их в память (используя mmap). Секции просматриваются линкером, который создаёт исполняемый файл или разделяемый объект.

Файлы ELF очень гибкие и поддерживаются различные типы CPU, машинные архитектуры, и операционные системы. Также он расширяемый, каждый файл сконструирован по-разному, в зависимости от требуемых частей. Путём использования правильных инструментов, вы сможете разобраться с назначением файла, и изучать содержимое бинарных файлов. Можно просмотреть функции и строки, содержащиеся в файле. Хорошее начало для тех, кто исследует вредоносные программы, или понять, почему процесс ведёт себя (или не ведёт) определённым образом.

Ресурсы для дальнейшего изучения

Если вы хотите больше знать про ELF и обратную разработку, вы можете посмотреть работу, которую мы выполняем в Linux Security Expert. Как часть учебной программы, мы имеем модуль обратной разработки с практическими лабораторными работами.

Для тех из вас, кто любит читать, хороший и глубокий документ: ELF Format и документ за авторством Брайана Рейтера (Brian Raiter), также известного как ELFkickers. Для тех, кто любит разбираться в исходниках, посмотрите на документированный заголовок ELF от Apple.

Совет:
если вы хотите стать лучше в анализе файлов, начните использовать популярные инструменты анализа, которые доступны в настоящее время.

Содержание

История


Структура ELF-файла с точки зрения компоновщика и системного загрузчика

ELF был призван упростить и стандартизировать разработку, предоставляя программистам чёткую и понятную структуру файла.

Стандарт формата ELF различает несколько типов файлов:

  • Перемещаемый файл — хранит инструкции и данные, которые могут быть связаны с другими объектными файлами. Результатом такой связи может быть разделяемый объектный файл или исполняемый файл. К этому типу относятся объектные файлы статических библиотек.
  • Разделяемый объектный файл — также содержит инструкции и данные и может быть связан с другими перемещаемыми файлами и разделяемыми объектными файлами, в результате чего будет создан новый объектный файл, либо при запуске программы на выполнение операционная система может динамически связать его с исполняемым файлом программы, в результате чего будет создан исполняемый образ программы. В последнем случае речь идет о разделяемых библиотеках.
  • Исполняемый файл — содержит полное описание, позволяющее системе создать образ процесса. В том числе: инструкции, данные, описание необходимых разделяемых объектных файлов и необходимую символьную и отладочную информацию.

Формат

Структуру файла можно рассматривать с двух сторон: со стороны компоновщика (линкера) и со стороны загрузчика [Источник 2] . Любой файл состоит из:

Сегмент - это непрерывная область адресного пространства со своими атрибутами доступа. Сегмент кода имеет атрибут исполнения, а сегмент данных - атрибуты чтения и записи. В зависимости от типа сегмента величина выравнивания в памяти может варьироваться от 4h до 1000h байт (для архитектуры x86). В самом ELF-файле сегменты не выравниваются и хранятся плотно прижатыми друг к другу. Ближайший аналог - секции в PE-файлах Windows

Сегмент в ELF-файлах может быть разбит на одну или несколько частей - секций. Типичный кодовый сегмент состоит из секций .init (процедуры инициализации), .plt (секции связок), .text (основной код программы) и .finit (процедуры финализации).

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


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

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

Структура ELF-заголовка

Назначение элементов массива e_ident
Элемент Значение Описание
e_ident[0] '\x7f' Сигнатура
e_ident[1] 'E' Сигнатура
e_ident[2] 'L' Сигнатура
e_ident[3] 'F' Сигнатура
e_ident[4] 1 Размер слова: 0 - неизвестно, 1 - 32 бита, 2 - 64 бита
e_ident[5] 1 Порядок байт: 0 - неизвестно, 1 - little-endian, 2 - big-endian
e_ident[6] 1 Версия формата ELF: 0 - неизвестно, 1 - текущая версия
e_ident[7] 0 ОС и бинарный интерфейс, для Linux - 0
e_ident[8] 0 Версия бинарного интерфейса, для Linux - 0
e_ident[9] - e_ident[15] 0 Зарезервировано
Возможные значения поля e_type
Имя Значение Описание
ET_NONE 0 Отсутствие типа файла
ET_REL 1 Перемещаемый объектный файл
ET_EXEC 2 Исполняемый файл
ET_DYN 3 Динамическая библиотека
ET_CORE 4 Дамп памяти
ET_LOPROC 0xff00 Назначение зависит от процессора
ET_HIPROC 0xffff Назначение зависит от процессора

Таблица заголовков программы

Таблица заголовков программы или таблица заголовков сегментов расположена сразу после заголовка файла и содержит заголовки сегментов, каждый из которых описывает отдельный сегмент программы и его атрибуты, такие как:

  • Тип сегмента и действия операционной системы с данным сегментом.
  • Расположение сегмента.
  • Точка входа сегмента.
  • Размер сегмента.
  • Флаги доступа к сегменту (запись, чтение, выполнение).

Информация, содержащаяся в этой таблице, представляет интерес для ядра системы, так как указывает ему, как собрать образ процесса из сегментов.

Структура таблицы заголовков программы

Некоторые возможные значения поля p_type
Значение Символьное имя Описание
0 PT_NULL Неиспользуемая запись
1 PT_LOAD Сегмент программы, загружаемый в память
2 PT_DYNAMIC Информация для динамического связывания
3 PT_INTERP Загрузчик программ
4 PT_NOTE Дополнительная информация
5 PT_PHDR Информация о таблице заголовков программы

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

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

Структура таблицы заголовков секций

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

Возможные значения поля sh_type
Значение Символьное имя Описание
0 SHT_NULL Пустой заголовок секции
1 SHT_PROGBITS Секции программы (например, код или данные)
2 SHT_SYMTAB Таблица символов
3 SHT_STRTAB Таблица строк
4 SHT_RELA Данные о перемещаемых адресах
5 SHT_HASH Хэш-таблица имен для динамического связывания
6 SHT_DYNAMIC Информация для динамического связывания
7 SHT_NOTE Дополнительная информация
8 SHT_NOBITS Признак того, что секция занимает место в адресном пространстве процесса
9 SHT_REL Дополнительные данные о перемещаемых адресах

Утилиты

Существует множество утилит для работы с файлами elf, основные из них содержатся в наборе программных инструментов GNU Binutils:

  • elfedit — обновляет заголовок файла ELF.
  • objdump — показывает информацию об объектных файлах (в том числе и ELF).
  • readelf — показывает подробную информацию о файле.
  • elfutils — предоставляет альтернативные инструменты для GNU Binutils только для Linux.
  • elfdump — команда для просмотра ELF информации в файле ELF, доступна в Solaris и FreeBSD.

Загрузка исполняемого файла в память


Карта памяти загруженного образа исполняемого файла

При загрузке исполняемого файла в память ELF-заголовок по-умолчанию проецируется по адресу 8048000h, который прописан в его заголовке [Источник 3] . Это базовый адрес загрузки, который может быть изменен на стадии компоновки. Все сегменты проецируются в память в соответствии с виртуальными адресами, прописанными в таблице сегментов, причем виртуальная проекция сегментов всегда непрерывна.

Начиная с адреса 40000000h располагаются совместно используемые библиотеки. Например, ld-linux.so, libm.so и libc.so (ближайший аналог в Windows - kernel32.dll, реализующая Win32 API). За вызов функций операционной системы напрямую отвечает прерывание INT 80h.

Последний гигабайт адресного пространства от адреса C0000000h занимают код и данные операционной системы, к которым можно обращаться посредством прерывания INT 80h или через разделяемые библиотеки. Стек находится в нижних адресах. Он начинается с базового адреса загрузки и увеличивается по направлению к нулевым адресам.

Читайте также: