Форматы ELF и PE EXE
В данном обзоре мы будем говорить только о 32-х битной версии этого формата, ибо 64-х битная нам пока ни к чему.
Любой файл формата ELF (в том числе и объектные модули этого формата) состоит из следующих частей:
- Заголовок ELF файла;
- Таблица программных секций (в объектных модулях может отсутствовать);
- Секции ELF файла;
- Таблица секций (в выполняемом модуле может отсутствовать);
- Ради производительности в формате ELF не используются битовые поля. И все структуры обычно выравниваются на 4 байта.
Теперь рассмотрим типы, используемые в заголовках ELF файлов:
Тип | Размер | Выравнивание | Комментарий |
---|---|---|---|
Elf32_Addr | 4 | 4 | Адрес |
Elf32_Half | 2 | 2 | Беззнаковое короткое целое |
Elf32_Off | 4 | 4 | Смещение |
Elf32_SWord | 4 | 4 | Знаковое целое |
Elf32_Word | 4 | 4 | Беззнаковое целое |
unsigned char | 1 | 1 | Безнаковое байтовое целое |
Теперь рассмотрим заголовок файла:
#define EI_NIDENT 16 struct elf32_hdr { unsigned char e_ident[EI_NIDENT]; Elf32_Half e_type; Elf32_Half e_machine; Elf32_Word e_version; Elf32_Addr e_entry; /* Entry point */ Elf32_Off e_phoff; Elf32_Off e_shoff; Elf32_Word e_flags; Elf32_Half e_ehsize; Elf32_Half e_phentsize; Elf32_Half e_phnum; Elf32_Half e_shentsize; Elf32_Half e_shnum; Elf32_Half e_shstrndx; };
Массив e_ident содержит в себе информацию о системе и состоит из нескольких подполей.
struct { unsigned char ei_magic[4]; unsigned char ei_class; unsigned char ei_data; unsigned char ei_version; unsigned char ei_pad[9]; }
- ei_magic - постоянное значение для всех ELF файлов, равное { 0x7f, 'E', 'L', 'F'}
- ei_class - класс ELF файла (1 - 32 бита, 2 - 64 бита который мы не рассматриваем)
- ei_data - определяет порядок следования байт для данного файла (этот порядок зависит от платформы и может быть прямым (LSB или 1) или обратным (MSB или 2)) Для процессоров Intel допустимо только значение 1.
- ei_version - достаточно бесполезное поле, и если не равно 1 (EV_CURRENT) то файл считается некорректным.
В поле ei_pad операционные системы хранят свою идентификационную информацию. Это поле может быть пустым. Для нас оно тоже не важно.
Поле заголовка e_type может содержать несколько значений, для выполняемых файлов оно должно быть ET_EXEC равное 2
e_machine - определяет процессор на котором может работать данный выполняемый файл (Для нас допустимо значение EM_386 равное 3)
Поле e_version соответствует полю ei_version из заголовка.
Поле e_entry определяет стартовый адрес программы, который перед стартом программы размещается в eip.
Поле e_phoff определяет смещение от начала файла, по которому располагается таблица программных секций, используемая для загрузки программ в память.
Не буду перечислять назначение всех полей, не все нужны для загрузки. Лишь еще два опишу.
Поле e_phentsize определяет размер записи в таблице программных секций.
И поле e_phnum определяет количество записей в таблице программных секций.
Таблица секций (не программных) используется для линковки программ. мы ее рассматривать не будем. Так же мы не будем рассматривать динамически линкуемые модули. Тема эта достаточно сложная, для первого знакомства не подходящая. :)
Теперь про программные секции. Формат записи таблицы программных секций таков:
struct elf32_phdr { Elf32_Word p_type; Elf32_Off p_offset; Elf32_Addr p_vaddr; Elf32_Addr p_paddr; Elf32_Word p_filesz; Elf32_Word p_memsz; Elf32_Word p_flags; Elf32_Word p_align; };
Подробнее о полях.
- p_type - определяет тип программной секции. Может принимать несколько значений, но нас интересует только одно. PT_LOAD (1). Если секция именно этого типа, то она предназначена для загрузки в память.
- p_offset - определяет смещение в файле, с которого начинается данная секция.
- p_vaddr - определяет виртуальный адрес, по которому эта секция должна быть загружена в память.
- p_paddr - определяет физический адрес, по которому необходимо загружать данную секцию. Это поле не обязательно должно использоваться и имеет смысл лишь для некоторых платформ.
- p_filesz - определяет размер секции в файле.
- p_memsz - определяет размер секции в памяти. Это значение может быть больше предыдущего. Поле p_flag определяет тип доступа к секциям в памяти. Некоторые секции допускается выполнять, некоторые записывать. Для чтения в существующих системах доступны все.
Загрузка формата ELF.
С заголовком мы немного разобрались. Теперь я приведу алгоритм загрузки бинарного файла формата ELF. Алгоритм схематический, не стоит рассматривать его как работающую программу.
int LoadELF (unsigned char *bin) { struct elf32_hdr *EH = (struct elf32_hdr *)bin; struct elf32_phdr *EPH; if (EH->e_ident[0] != 0x7f || // Контролируем MAGIC EH->e_ident[1] != 'E' || EH->e_ident[2] != 'L' || EH->e_ident[3] != 'F' || EH->e_ident[4] != ELFCLASS32 || // Контролируем класс EH->e_ident[5] != ELFDATA2LSB || // порядок байт EH->e_ident[6] != EV_CURRENT || // версию EH->e_type != ET_EXEC || // тип EH->e_machine != EM_386 || // платформу EH->e_version != EV_CURRENT) // и снова версию, на всякий случай return ELF_WRONG; EPH = (struct elf32_phdr *)(bin + EH->e_phoff); while (EH->e_phnum--) { if (EPH->p_type == PT_LOAD) memcpy (EPH->p_vaddr, bin + EPH->p_offset, EPH->p_filesz); EPH = (struct elf32_phdr *)((unsigned char *)EPH + EH->e_phentsize)); } return ELF_OK; }
По серьезному стоит еще проанализировать поля EPH->p_flags, и расставить на соответствующие страницы права доступа, да и просто копирование здесь не подойдет, но это уже не относится к формату, а к распределению памяти. Поэтому сейчас об этом не будем говорить.
Формат PE.
Во многом он аналогичен формату ELF, ну и не удивительно, там так же должны быть секции, доступные для загрузки.
Как и все в Microsoft :) формат PE базируется на формате EXE. Структура файла такова:
- 00h - EXE заголовок (не буду его рассматривать, он стар как Дос. :)
- 20h - OEM заголовок (ничего существенного в нем нет);
- 3сh - смещение реального PE заголовка в файле (dword).
- таблица перемещения stub;
- stub;
- PE заголовок;
- таблица объектов;
- объекты файла;
stub - это программа, выполняющаяся в реальном режиме и производящая какие-либо предварительные действия. Может и отсутствовать, но иногда может быть нужна.
Нас интересует немного другое, заголовок PE.
Структура его такая:
struct pe_hdr { unsigned long pe_sign; unsigned short pe_cputype; unsigned short pe_objnum; unsigned long pe_time; unsigned long pe_cofftbl_off; unsigned long pe_cofftbl_size; unsigned short pe_nthdr_size; unsigned short pe_flags; unsigned short pe_magic; unsigned short pe_link_ver; unsigned long pe_code_size; unsigned long pe_idata_size; unsigned long pe_udata_size; unsigned long pe_entry; unsigned long pe_code_base; unsigned long pe_data_base; unsigned long pe_image_base; unsigned long pe_obj_align; unsigned long pe_file_align; // ... ну и еще много всякого, неважного. };
Много всякого там находится. Достаточно сказать, что размер этого заголовка - 248 байт.
И главное что большинство из этих полей не используется. (Кто так строит?) Нет, они, конечно, имеют назначение, вполне известное, но моя тестовая программа, например, в полях pe_code_base, pe_code_size и тд содержит нули но при этом прекрасно работает. Напрашивается вывод, что загрузка файла осуществляется на основе таблицы объектов. Вот о ней то мы и поговорим.
Таблица объектов следует непосредственно после PE заголовка. Записи в этой таблице имеют следующий формат:
struct pe_ohdr { unsigned char o_name[8]; unsigned long o_vsize; unsigned long o_vaddr; unsigned long o_psize; unsigned long o_poff; unsigned char o_reserved[12]; unsigned long o_flags; };
- o_name - имя секции, для загрузки абсолютно безразлично;
- o_vsize - размер секции в памяти;
- o_vaddr - адрес в памяти относительно ImageBase;
- o_psize - размер секции в файле;
- o_poff - смещение секции в файле;
- o_flags - флаги секции;
Вот на флагах стоит остановиться поподробнее.
- 00000004h - используется для кода с 16 битными смещениями
- 00000020h - секция кода
- 00000040h - секция инициализированных данных
- 00000080h - секция неинициализированных данных
- 00000200h - комментарии или любой другой тип информации
- 00000400h - оверлейная секция
- 00000800h - не будет являться частью образа программы
- 00001000h - общие данные
- 00500000h - выравнивание по умолчанию, если не указано иное
- 02000000h - может быть выгружен из памяти
- 04000000h - не кэшируется
- 08000000h - не подвергается страничному преобразованию
- 10000000h - разделяемый
- 20000000h - выполнимый
- 40000000h - можно читать
- 80000000h - можно писать
Опять таки не буду с разделяемыми и оверлейными секциями, нас интересуют код, данные и права доступа.
В общем, этой информации уже достаточно для загрузки бинарного файла.
Загрузка формата PE.
int LoadPE (unsigned char *bin) { struct elf32_hdr *PH = (struct pe_hdr *) (bin + *((unsigned long *)&bin[0x3c])); // Конечно комбинация не из понятных... просто берем dword по смещению 0x3c // И вычисляем адрес PE заголовка в образе файла struct elf32_phdr *POH; if (PH == NULL || // Контролируем указатель PH->pe_sign != 0x4550 || // сигнатура PE {'P', 'E', 0, 0} PH->pe_cputype != 0x14c || // i386 (PH->pe_flags & 2) == 0) // файл нельзя запускать! return PE_WRONG; POH = (struct pe_ohdr *)((unsigned char *)PH + 0xf8); while (PH->pe_obj_num--) { if ((POH->p_flags & 0x60) != 0) // либо код либо инициализированные данные memcpy (PE->pe_image_base + POH->o_vaddr, bin + POH->o_poff, POH->o_psize); POH = (struct pe_ohdr *)((unsigned char *)POH + sizeof (struct pe_ohdr)); } return PE_OK; }
Это опять таки не готовая программа, а алгоритм загрузки.
И опять таки многие моменты не освещаются, так как выходят за пределы темы.
Но теперь стоит немного поговорить про существующие системные особенности.
Системные особенности.
Не смотря на гибкость средств защиты, имеющихся в процессорах (защита на уровне таблиц дескрипторов, защита на уровне сегментов, защита на уровне страниц) в существующих системах (как в Windows, так и в Unix) полноценено используется только страничная защита, которая хотя и может уберечь код от записи, но не может уберечь данные от выполнения. (Может быть, с этим и связано изобилие уязвимостей систем?)
Все сегменты адресуются с нулевого линейного адреса и простираются до конца линейной памяти. Разграничение процессов производится только на уровне страничных таблиц.
В связи с этим все модули линкуются не с начальных адресов, а с достаточно большим смещением в сегменте. В Windows используется базовый адрес в сегменте - 0x400000, в юникс (Linux или FreeBSD) - 0x8048000.
Некоторые особенности так же связаны со страничной организацией памяти.
ELF файлы линкуются таким образом, что границы и размеры секций приходятся на 4-х килобайтные блоки файла.
А в PE формате, не смотря на то, что сам формат позволяет выравнивать секции на 512 байт, используется выравнивание секций на 4к, меньшее выравнивание в Windows не считается корректным.