Блог → Общая структура PE-файла. Часть 2

Итак, продолжаем с того же места, где остановились вчера. Что нам может потребоваться при анализе PE-файла? Поле AddressOfEntryPoint содержит адрес, точнее, относительный адрес точки входа в программу. Кстати относительный адрес начала кода хранится в следующем поле - BaseOfCode. В свою очередь, значение поля BaseOfCode отсчитывается от адреса, по которому должна загружаться программа. А этот адрес хранится в поле ImageBase. Кстати, уважаемый читатель, интересно, почему смещение точки входа, скажем, находится в "необязательной" части заголовка? Может ли существовать программа без точки входа?

Дополнительную информацию о том, что представляет собой анализируемый файл, можно найти в поле Subsystem. Идентификаторы значений, которые может принимать это поле, начинаются с "IMAGE_SUBSYSTEM_". Я уверен, что комментарии, приведенные к этим идентификаторам, полностью объясняют назначение этого поля.

Итак, мы знаем, по какому адресу файл будет загружен в память, и знаем смещение начала кода относительно начального образа загрузки... Но всё это позволит нам в лучшем случае дизассемблировать файл (не завидую я тому, кто будет разбирать такой модуль!) и вообще ничего не скажет о других данных (помимо кода), которые могут находиться в исполняемом файле. Для того, чтобы получить информацию о местонахождении этих данных, обратите внимание на последнее поле "необязательного" заголовка. Оно фактически представляет собой массив из 16 элементов, потому что значение IMAGE_NUMBEROF_DIRECTORY_ENTRIES определено следующим образом:

#define IMAGE_NUMBEROF_DIRECT0RYENTRIES 16


Каждым элементом этого массива является структура, описание которой приведено ниже:

typedef struct _IMAGE_DATA_DIRECTORY {

DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY,

*PIMAGE_DATA_DIRECTORY;


Видно, что в этой структуре определяется относительный адрес и размер каких-то данных. Ещё бы узнать, что это за данные! Но как известно, кто ищет, тот всегда найдет! Если поискать строку "#define IMAGE_DIRECTORY_ENTRY_EXPORT", можно найти перечень определений:

#define IMAGE_DIRECTORY_ENTRY_EXPORT
0 // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT
1 // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOORCE
2 // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION
3 // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY
4 // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC
5 // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG
6 // Debug Directory

// IMAGE_DIRECTORY_ENTRY_COPYRIGHT
// 7 (x86 usage)

#define IMAGE_DIRECTORY_ ENTRY_ARCHITECTURE
7 // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR
8 // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS
9 // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_L0AD_C0NFIG
10 // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_B0UND_IMP0RT
11 // Bound Import Directory in headers
#define IMAGE_DIRECTORYENTRY_IAT
12 // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT
13 // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR
14 // COM Runtime descriptor


Эти числа используются в качестве индекса массива для того, чтобы определить, адрес и размер каких данных записан в структуре типа IMAGE_DATA_DIRECTORY. Другими словами, в нулевом элементе массива находятся данные об экспорте, в первом - данные об импорте, во втором - данные о ресурсах, ну и так далее. Таким образом, мы знаем почти всё, что можно "выжать" из заголовков. Теперь вспомним, что в "обязательном" заголовке присутствует поле NumberOfSections, которое определяет количество разделов в файле. Оказывается, непосредственно за заголовками находится таблица секций. Это видно из следующего макроса, находящегося в файле winnt.h:

#define IMAGE_FIRST_SECTION (ntheader)
(PIMAGE_SECTION_HEADER)
((UINT_PTR)ntheader +
FIELD_0FFSET( IMAGE NT_HEADERS,
OptionalHeader ) +
((PIMAGE_NT_HEADERS)(ntheader))->
FileHeader.SizeOfOptionalHeader
))


Число элементов этой таблицы и определяет поле NumberOfSections. Естественно, в каждом элементе этой таблицы находится не сама секция, а только её заголовок. Заголовок каждой секции описывается структурой типа IMAGE_SECTION_HEADER:

typedef Struct _IMAGE_SECTION_HEADER {

BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
) Misc;
DWORD VirtualAddress;
DWORD SizeOfRawData;
DWORD PointerToRawData;
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
) IMAGE_SECTION_HEADER,

*PIMAGE_SECTION_HEADER;


Попутно замечу, что размер заголовка секции определяется в файле winnt.h таким образом:

#define IMAGE_SIZEOF_SECTION_HEADER 40


Первое поле, Name, является именем этой секции. По каким-то неизвестным мне причинам перед названием большинства секций ставится точка, хотя встречаются и имена без точек. Наверное, это просто обычай, которому стараются следовать, не более того. Кстати, обратим внимание на то определение, которое используется в описании массива Name:

#define IMAGE_SIZEOF_SHORTNAME 8


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

Ну а теперь самое важное - в этом заголовке находится ключ, позволяющий сопоставить данные, хранящиеся в файле на диске, и данные, которые окажутся в загруженном в память файле. Поле VirtualAddress определяет, по какому смещению относительно начального адреса загрузки файла будут загружены данные, находящиеся в секции. С другой стороны, поле PointerToRawData определяет смещение этих данных в файле относительно начала файла на диске. Назовем разность между этими двумя величинами дельтой. Если требуется определить, по какому смещению относительно начала файла (не образа файла, обратите внимание) находится то или иное данное, то из смещения относительно начального адреса загрузки, необходимо просто вычесть нашу дельту. Следует обратить внимание на то, что значение дельты от секции к секции может изменяться, поэтому её значение следует вычислять для каждой секции заново.

И, наконец, после таблицы секций следуют непосредственно секции. Их также часто называют разделами и сегментами. Про них следует сказать особо! При просмотре различных исполняемых файлов достаточно часто можно встретить такие имена, как .edata, .idata, .rsrc и прочие. Сначала я предположил, что в секции .edata хранятся данные экспорта, в .idata - импорта, в .rsrc - ресурсов, и так далее. Однако, проанализировав массу файлов, я пришел к выводу, что никакой зависимости между названием секции и содержащимися в ней данными нет, а данные могут располагаться где угодно. Программист может сам управлять расположением данных при помощи директивы #pragma data_seg. Для того, чтобы определить, в каком месте файла располагаются необходимые данные, следует обратиться к полю DataDirectory "необязательного" заголовка.

Итак, общая структура PE-файла представлена ниже:

- IMAGE_DOS_HEADER
- IMAGE_NT_HEADERS
--- Signature
--- IMAGE_FILE_HEADER
--- IMAGE_OPTIONAL_HEADER
- Section Table
- Sections


К настоящему моменту мы может проанализировать состав заголовков и таблицы разделов PE-файла и выбрать оттуда достаточно много информации о файле. Однако, разумеется, гораздо больше информации можно извлечь, проанализировав данные, которые находятся в "чёрном ящике", который на схеме выше назван "Sections". В следующих статьях я попробую рассказать о том, как именно выбрать из секций ту информацию, которая поможет в анализе программы. А пока что, прощаюсь в вами, мой дорогой читатель. До новых встреч в блоге!