4. PE-файлы: секции
4.1. Заголовки секций
Сразу после заголовка PE в файле располагается массив заголовков секций. Его размер задается полем NumberOfSections заголовка файла. Каждый заголовок секции состоит из 0x28 байт и имеет следующую структуру:
| Смещение (hex) | Размер | Тип | Название | Описание |
|---|---|---|---|---|
| 00 | 8 | CHAR[8] | Name | Название секции. |
| 08 | 4 | DWORD | Misc.VirtualSize | Размер секции в памяти. Если это значение больше SizeOfRawData, то секция дополняется в памяти нулевыми байтами. |
| 0C | 4 | DWORD | VirtualAddress | RVA секции в памяти. |
| 10 | 4 | DWORD | SizeOfRawData | Размер секции в файле. Всегда кратен FileAlignment из необязательного заголовка. Если секция содержит только неинициализированные данные, то это поле равно нулю. |
| 14 | 4 | DWORD | PointerToRawData | Смещение в файле до начала данных секций. Всегда кратно FileAlignment из необязательного заголовка. Если секция содержит только неинициализированные данные, то это поле равно нулю. |
| 18 | 4 | DWORD | PointerToRelocations | В исполняемых файлах это поле всегда равно нулю. |
| 1С | 4 | DWORD | PointerToLinenumbers | В исполняемых файлах это поле всегда равно нулю. |
| 20 | 2 | WORD | NumberOfRelocations | В исполняемых файлах это поле всегда равно нулю. |
| 22 | 2 | WORD | NumberOfLinenumbers | В исполняемых файлах это поле всегда равно нулю. |
| 24 | 4 | DWORD | Characteristics | Атрибуты секции. |
Прокомментируем некоторые поля.
Название секции
Название секции содержит от 0 до 8 ASCII-символов. Вместо константы 8 можно использовать определение IMAGE_SIZEOF_SHORT_NAME. Если длина названия меньше 8 символов, то оно дополняется нулевыми байтами. Если оно состоит ровно из 8 символов, то завершающего нулевого байта нет. Важно отметить, что название секции, вообще говоря, никак не соотносится с ее содержимым. Каждый компилятор использует свое собственное соглашение о именовании секций, поэтому полагаться на название секции при ее анализе нельзя. Единственно надежным способом определить, что содержит данная секция, является анализ ее атрибутов и содержащихся в ней каталогов данных.
Атрибуты секции
32-битовое поле Characteristics содержит набор флагов, описывающих содержимое данной секции. Секции исполняемого файла могут иметь следующие атрибуты:
| Название | Значение | Описание |
|---|---|---|
| IMAGE_SCN_CNT_CODE | 0x00000020 | Секция содержит исполняемый код. |
| IMAGE_SCN_CNT_INITIALIZED_DATA | 0x00000040 | Секция содержит инициализированные данные. |
| IMAGE_SCN_CNT_UNINITIALIZED_DATA | 0x00000080 | Секция содержит неинициализированные данные. |
| IMAGE_SCN_MEM_DISCARDABLE | 0x02000000 | Секция может быть удалена из памяти после создания процесса. |
| IMAGE_SCN_MEM_NOT_CACHED | 0x04000000 | Секция не может кэшироваться. |
| IMAGE_SCN_MEM_NOT_PAGED | 0x08000000 | Секция не может выгружаться в файл подкачки. |
| IMAGE_SCN_MEM_SHARED | 0x10000000 | Все копии данного файла могут иметь один общий экземпляр этой секции. По-видимому, используется только для секций данных динамических библиотек. |
| IMAGE_SCN_MEM_EXECUTE | 0x20000000 | Секцию можно исполнять как программный код. |
| IMAGE_SCN_MEM_READ | 0x40000000 | Секцию можно читать. |
| IMAGE_SCN_MEM_WRITE | 0x80000000 | В секцию можно писать. |
4.2. Содержимое секций
Сами секции располагаются в файле после всех заголовков секций. Каждая секция выравнена на границу FileAlignment из необязательного заголовка.
При анализе содержимого секций следует учитывать, что это содержимое может быть разнородным. Например, одна секция может содержать и исполняемый код, и таблицу импорта.
4.3. Доступ к секциям
Для получения указателя на заголовок первой секции можно использовать следующий макрос, определенный в WINNT.H:
где pHeader – указатель на заголовок PE, возвращаемый функцией GetHeader.
Для перебора всех заголовков секций удобно использовать такой цикл:
Теперь мы можем привести алгоритм, позволяющий пересчитать любой RVA в смещение относительно начала файла. Нам потребуются две функции. Первая ищет секцию, содержащую данный RVA, а вторая преобразует RVA в указатель на соответствующую ему позицию в файле.
В качестве примера приведем программу, которая печатает для каждого непустого каталога данных его RVA, размер и позицию в файле.
Статья [0x01] Исследуем Portable Executable (EXE-файл) [Формат PE-файла]
Давайте проведём аналогию между квартирой и PE-файлом. У каждой квартиры есть свой этаж, своя дверь, прихожая, гостинная, кладовка, свои комнаты, также у каждой квартиры есть своя схема планировки. Вся информация о квартире и сама эта квартира хранится в PE-файле. Взглянем на структуру исполняемого файла, а потом разберём основные части.
Как я и сказал, заголовки хранят необходимую информацию для загрузки PE-файла. Поэтому данный заголовок является обязательным для загрузки PE-файла, хоть и не несёт в себе большой смысловой информации.
Заголовок состоит из полей, как список состоит из пунктов свойств. Каждый пункт хранит в себе какое-либо значение. Естественно, в файле всё это представленно в байтовом представлении. Не все поля нужны для загрузки (запуска) PE-файла. Поэтому комментировать и рассматривать мы будем только поля, необходимые для загрузки файла в память.
Вот его структура на языке C/C++.
Я выделил самым большим красный прямоугольником область DOS-заголовка. Здесь мы можем увидеть байты в шестнадцатеричном представлении.
Дальше у нас по списку PE-заголовок, который на самом деле, состоит из трёх частей: сигнатуры, файлового подзаголовка и дополнительного подзаголовка.
Вот его структура на языке C/C++:
На C/C++ структура данного заголовка выглядит так:
Структура дополнительного заголовка представлена следующий C/C++ кодом.
Вот структура каталога на языке C/C++:
А вот идентификаторы (порядковый номер в DataDirectory):
По сути, в таблице секций просто зафиксирована информация о секциях.
Вот и всё, мы закончили изучать заголовки. Теперь мы приступаем к изучению секций. По сути, секции являются простыми последовательными блоками данных. Они следуют друг за другом и у них нет определенного формата, так как их характеристики описаны в таблице секций. А вот формат данных, в этих секциях, зависят от типа информации, которая в них хранится. Секции, как я уже сказал, можно представить в виде комнат. Также, их можно представить и как в виде коробок с информацией. Размер каждой секции зафиксирован в таблице секций, поэтому секции должны быть определённого размера, а для этого их дополняют NULL-байтами (00). Вот и всё, что касается секций.
На этом всё. Спасибо за внимание. Если у Вас есть какие-либо вопросы или вы обнаружите неточности в статье, прошу отписаться в комментариях. Буду рад ответить на все ваши вопросы.
Гиперион: реализация PE-шифровщика
Данный документ освещает теоретические аспекты работающих во время выполнения шифровщиков и описывает эталонную реализацию Portable Executables (PE) [1]: файлового формата динамических библиотек (DLL), объектных файлов и обычных исполняемых файлов Windows.

Рисунок 1: краткий обзор потока работ PE-шифровщика
1 Введение
Шифровщик, работающий во время выполнения (runtime-crypter), получает на вход исполняемые файлы и преобразует их в шифрованную версию (сохраняя их первоначальное поведение). Зашифрованный файл расшифровывает себя при запуске и запускает свое исходное содержимое. Данный подход позволяет разворачивать вредоносные исполняемые файлы в защищенных средах: основанные на шаблонах антивирусные решения обнаруживают сигнатуры подозрительных файлов и блокируют их выполнение. Зашифрованная же копия не содержит известной сигнатуры, ее содержимое нельзя подвергнуть эвристическому анализу, поэтому она нормально выполняется без вмешательства антивирусного сканера. Другие использования шифровщика – защита двоичных файлов от реверсинга и уменьшение размера исполняемых файлов (при замене шифрования сжатием).
Данный документ освещает теоретические аспекты работающих во время выполнения шифровщиков и описывает эталонную реализацию Portable Executables (PE) [1]: файлового формата динамических библиотек (DLL), объектных файлов и обычных исполняемых файлов Windows. Реализация шифрования исполняемых файлов Windows требует общего понимания следующих аспектов:
Мы представим ориентированное на новичков введение в эти две важные темы в разделе 2. Затем в разделе 3 мы рассмотрим и объясним эталонную реализацию PE-шифровщика Гиперион для 32-битных исполняемых файлов, которая может быть разделена на две части (см. рисунок 1): шифровщик и контейнер. Шифровщик (более подробно рассмотрен в разделе 3.1) получает на входе двоичный файл в формате PE, копирует его целиком в память, вычисляет его контрольную сумму и дописывает ее в начало файла. Затем генерируется случайный ключ, который используется для шифрования контрольной суммы и входного файла алгоритмом AES-128 [2]. Наконец, зашифрованный результат копируется в секцию данных контейнера.
Контейнер (более подробно описанный в разделе 3.2) действует как дешифровщик и загрузчик PE: он копирует содержимое ранее зашифрованного файла в память, расшифровывает его и запускает. Ключ шифрования в контейнере отсутствует, поэтому контейнеру приходится подбирать его из ограниченного множества возможных ключей, проверяя правильность ключа с помощью контрольной суммы. На первый взгляд это кажется недостатком, поскольку зашифрованный исполняемый файл нуждается в дополнительном времени на дешифровку при запуске. С другой стороны, это хорошая защита от статического и динамического анализа.
Для сравнения, внедрение зашифрованного входного файла в двоичную форму контейнера (container.exe) потребует исправления больших фрагментов контейнера вручную (базы образа, размера секций и т. д.). Наш подход перекладывает эти задачи на компилятор, который уменьшает сложность шифровщика и упрощает его расширение и поддержку.
Некоторые аспекты вроде полиморфизма и антиэвристики в нашей реализации все еще отсутствуют. Поэтому мы представим и обсудим дальнейшие направления работы в разделе 4.
2 Структура PE-файлов и загрузчик Windows PE
Данный раздел описывает формат PE-файлов Windows и то, как они загружаются в память. Существует много работ по данной теме, и мы полагаем, что читатель имеет по крайней мере некоторое базовое представление о таких принципах работы современных операционных систем, как виртуальная память, системные вызовы и т. д. Поэтому мы дадим лишь краткое введение в важные элементы exe-файлов Windows в следующей таблице:
Представленная таблица показывает структуру PE-образа, но не структуру PE-файла, загружаемого в память. Каждый исполняемый файл windows начинается с MZ-заглушки. Данная заглушка – это программа MSDOS, которая отображает сообщение «You can not run this program in DOS mode» (эту программу нельзя запустить в режиме DOS) или нечто подобное. Таким образом, при запуске в среде MSDOS загрузчик исполняемых файлов распознает DOS-заголовок, отобразит сообщение и завершит работу приложения.
Заголовок MZ-заглушки содержит дополнительный (и последний) элемент – указатель на заголовок Windows PE, начинающийся с магического значения «P, E, 0x0, 0x0», за которым следует заголовок файла образа (image file header). Заголовок файла образа имеет фиксированный размер и содержит информацию о поддерживаемых типах машин (например, архитектуры x86 [4] или ARM [5]), флаги (указывающие на то, является ли файл динамической библиотекой) и т. п. Важными полями для реализации PE-шифровщика являются общее количество секций и размер опционального заголовка. Они необходимы для разбора PE-заголовка, поскольку общее количество секций и количество записей в data directory не фиксировано.
За заголовком файла образа следует опциональный заголовок (который, несмотря на название, не является необязательным). Он содержит различную информацию вроде размера исполняемого кода, размера данных и т. д. (см. [1] для подробностей). Важными полями в опциональном заголовке являются база образа и размер образа. Мы уже упоминали, что контейнер PE-шифровщика действует как дешифровщик и PE-загрузчик. Раз так, контейнеру нужно выделять память по базовому адресу образа (использование базового адреса необязательно, если входной файл имеет таблицу релокаций – relocation table) в размере, равном значению поля «размер образа». Далее расшифрованный файл копируется в зарезервированную область и запускается.
Data directory – также часть опционального заголовка образа. По существу это список, который содержит адреса и размеры таблицы релокаций, таблиц экспорта и импорта и т. д. Наиболее важной записью для PE-шифровщика является указатель на таблицу импорта. Таблица импорта содержит список имен API-функций (Application Programming Interface). API-функции находятся в DLL и используются приложениями для взаимодействия с операционной системой (например, приложение, которое хочет отобразить message box должно вызвать API-функцию MessageBox() из user32.dll). Таблица импорта по сути содержит имена DLL, имена API-функций и пустой список указателей на функции. Контейнеру нужно разобрать таблицу импорта расшифрованного входного файла, загрузить соответствующие DLL, получить адреса нужных API-функций и записать их в список указателей на функции.
Следующая часть PE-заголовка – список заголовков секций. Секции содержат данные и код, и каждая секция имеет соответствующий заголовок. Заголовок секции содержит ее имя, некоторые флаги (чтение, запись, выполнение и т. д.), адрес секции и ее размер. Размер секции состоит из размера сырых данных и виртуального размера. Размер сырых данных представляет размер секции в PE-образе (например, на жестком диске), а виртуальный размер – это размер секции после загрузки в память. Поле адреса также состоит из двух значений: виртуального адреса и указателя на сырые данные. Опять же, указатель на сырые данные – это адрес секции в рамках PE-образа, а виртуальный адрес – это адрес секции после загрузки в память. За последним заголовком секции следуют сами секции.
Мы описали структуру PE-файлов и теперь рассмотрим различные задачи, стоящие перед PE-загрузчиком. Прежде, чем мы сможем описать PE-загрузчик, нам нужно обсудить механизмы адресации в PE-файле: 32-битные и 64-битные исполняемые файлы Windows имеют почти одинаковые PE-заголовки. Есть лишь одно отличие: в зависимости от архитектуры некоторые адреса (например, точка входа) имеют размер 32 или 64 бита. Шифровщик, который описан в данном документе, поддерживает только 32-битные испольняемые файлы и, потому, мы остановимся на 32-битном PE-заголовке.
Другой важный аспект – абсолютная и относительная адресации: большинство записей в PE-заголовке являются относительными виртуальными адресами (RVA). С другой стороны, код в исполняемом файле Windows может использовать абсолютную адресацию и полагать, что PE-файл загружается по базовому адресу образа. Если PE-файл загружается в иное место памяти, приходится использовать таблицу релокаций (которая не является обязательной для обычных exe-файлов), чтобы исправить абсолютные адреса. Теперь мы опишем базовый принцип работы PE-загрузчика Windows:
Это лишь базовое и упрощенное описание для лучшего понимания следующих разделов. Некоторые продвинутые темы в нем были пропущены, и мы затронем их в разделе 4.
3 Гиперион
Гиперион можно разделить на две части: шифровщик и контейнер. Взаимодействие обоих компонентов показано на рисунке 2 и подробно описано в следующих двух разделах.
3.1 Шифровщик
Шифровщик – это консольное приложение написанное на C/C++. Оно шифрует входной файл и внедряет его в контейнер. Сперва мы использовали предварительно скомпилированный контейнер. Внедрить входной файл в двоичный контейнер довольно сложно, поскольку PE-заголовок контейнера приходится сильно модифицировать.
Рисунок 2: Детализированный поток выполнения PE-шифровщика
Более того, может оказаться, что входной файл не содержит таблицы релокаций и перед выполнением должен быть загружен по указанному базовому адресу образа. В данном случае контейнер должен быть загружен по базовому адресу входного файла и перезаписан входным файлом после шифрования. При этом приходится исправлять поле «база образа» PE-заголовка контейнера. Данная модификация базы образа влечет за собой обновление каждого элемента в контейнере (кода или данных), который использует абсолютную адресацию.
Однако, нам удалось избежать указанных проблем, и в данном документе представлен новый поток выполнения для PE-шифровщика, работающего при запуске: мы внедряем зашифрованный входной файл в исходный ассемблерный код контейнера. Затем вызывается FASM для генерации исполняемого файла контейнера. Преимуществом здесь является то, что больше не требуется модификации PE-заголовка, исправления абсолютных адресов и т. д. Все это теперь делает Fasm. Таким образом, шифровщик зависит от следующих компонентов:
Мы описали общую структуру шифровщика и теперь рассмотрим детали его реализации: шифровщик запускается пользователем и получает в качестве параметров пути до входного и выходного файлов. Входной файл копируется в память и проверяется:
Затем PE-заголовок анализируется и из него извлекаются поля «база образа» и «размер образа». Разбираются также и некоторые другие значения (например, заголовки секций), однако они не важны для потока работ шифровщика и просто выводятся на экран в качестве дополнительной информации для пользователя.
Следующий шаг – это шифрование входного файла. Сначала вычисляется его контрольная сумма размером 4 байта и добавляется в начало буфера памяти, вмещающего содержимое входного файла. AES – это блочный шифр, и каждый блок имеет размер 16 байт. Таким образом, размер входного буфера увеличивается до значения, кратного 16 (дополнительное пространство заполняется нулями). После модификации буфера входного файла генерируется случайный ключ шифрования. Гиперион использует алгоритм шифрования AES-128, размер ключа которого равен 16 байтам. Контейнеру приходится подбирать ключ шифрования, что потребовало бы большого количества времени, если бы использовалось все возможное пространство ключей. Поэтому пространство ключей уменьшено, и ключ генерируется с помощью следующего алгоритма:
Листинг 1: Алгоритм генерации ключа AES
1 unsigned char key [ AES KEY SIZE ];
2 for ( int i = 0; i
В листинге использованы две важные константы, которые определены в исходном коде шифровщика: KEY SIZE и KEY RANGE. KEY SIZE определяет фактический размер ключа в байтах и может иметь значение от 0 до 15 (неиспользованные байты заполняются нулями). Максимальное значение каждого элемента ключа определяется константой KEY RANGE, которая может иметь значение в диапазоне от 0 до 255.
После генерации ключа входной файл шифруется. Мы используем реализацию AES для Fasm [6] и компилируем ее в виде DLL, чтобы сделать доступной для нашей реализации шифровщика на C/C++. Шифровщик загружает данную DLL, API-функцию aesEncrypt() и шифрует буфер с содержимым входного файла (содержащий контрольную сумму, сам файл и дополнение до размера, кратного 16) с помощью сгенерированного ключа. Зашифрованный файл конвертируется в следующее ASCII-представление:
Листинг 2: Зашифрованный входной файл, преобразованный в массив Fasm
Содержимое данного листинга совместимо с Fasm, хранится в файле input.asm и копируется в папку с исходным кодом контейнера. Затем база образа, размер образа, KEY SIZE и KEY RANGE также конвертируются в формат, совместимый с Fasm (семантика соответствующего Fasm-кода описана в разделе 3.2) и копируется в папку с исходным кодом контейнера, поскольку эти данные необходимы во время выполнения (для подробностей см. раздел 3.2).
База образа входного файла конвертируется в следующий Fasm-формат (предположим, что базовый адрес образа равен 0x1000000), сохраняется в файле imagebase.asm, который копируется в папку исходных кодов контейнера:
Листинг 3: База образа входного файла, записанная в синтаксисе Fasm
1 format PE GUI 4.0 at 0x1000000
Размер образа конвертируется в строку следующего формата (предположим, что его значение равно 0x8000) и хранится в файле sizeofimage.asm:
Листинг 4: Размер образа входного файла, записанный в синтаксисе Fasm
Наконец, KEY SIZE и KEY RANGE конвертируются в следующий исходный код Fasm:
Листинг 5: Константы пространства ключей, конвертированные в синтаксис Fasm
1 REAL KEY SIZE equ 6
2 REAL KEY RANGE equ 4
Далее шифровщик вызывает исполняемый файл Fasm, компилирует исходный код контейнера и генерирует зашифрованную версию входного файла.
3.2 Контейнер
Контейнер по сути действует как дешифровщик и PE-загрузчик и написан на Fasm. Листинг 6 содержит часть исходного кода файла main.asm и демонстрирует общую структуру контейнера. Main.asm начинается с оператора include, который похож на соответствующую директиву препроцессора языка C и вставляет в код содержимое листинга 3. Вставленный фрагмент заставляет Fasm генерировать заданный формат PE-файла, который загружается по указанному базовому адресу. Благодаря этому действию мы можем убедиться, что контейнер всегда загружается по базовому адресу образа зашифрованного входного файла.
Строка 4 содержит оператор entry, который используется в fasm для достижения адреса точки входа. За ней следует подключение нескольких файлов, которые подобны заголовочным файлам C/C++ для важных API-функций и библиотек. Строка 7 подключает файл aes.inc, который является реализацией AES в Fasm [6]. Он используется в шифровщике для шифрования, а в контейнере для дешифрования.
Рисунок 3: Контейнер в памяти перед расшифровкой
Рисунок 4: Контейнер в памяти после дешифровки
4 Заключение и направления дальнейшей работы
Данный документ описывает основные принципы Гипериона – PE-шифровщика, работающего во время выполнения. Полный исходный код будет опубликован на домашней странице Nullsecurity под открытой лицензией.
5 Благодарности
Я хотел бы выразить благодарность всей команде Nullsecurity за поддержку данной работы. Особое спасибо Келси за корректировку ошибок и вдохновляющие беседы.
Статья [0x02] Исследуем Portable Executable [Загрузка PE-файла]
А после этого происходит преобразование (транслирование) адресов физической памяти (ОЗУ), к адресам в виртуальной памяти. Этот процесс изображён ниже.
Так зачем же ImageBase процессу? Почему он не может быть, скажем, равен 0? Почему по умолчанию он равен 0x400000? Так же было бы гораздо проще!
Также в виртуальной памяти хранятся загруженные Dynamic Linked Libraries (DLLs) и различные структуры, а именно, PEBs (Process Environment Blocks) и TEBs (Thread Environment Blocks). Эти структуры хранят различную информацию о процессе и потоках.
1.2. После этого, происходит считывание поля e_lfanew и переход к разбору PE-заголовка
Для каждой секции из PE-файла считывается блок размером SizeOfRawData (секция) по смещению PointerToRawData, после этого этот блок будет загружен в виртуальную память по адресу ImageBase + VirtualAddress с различными характеристиками.
Сначала, по адресу базовой загрузки (ImageBase) происходит проецирование всех заголовков PE-файла.
После разбора всех заголовков, разбора таблицы секций, проецирования файла в память и импорта функций начинается исполнение программы по адресу ImageBase + AddressOfEntryPoint. Обычно, AddressOfEntryPoint указывает на секцию кода (.text), которая содержит машинные инструкции, но это всего лишь соглашение и оно может быть несоблюдено.
Ну, на этом всё. Если есть какие-либо вопросы или поправки, буду рад если вы напишите комментарий. В следующей статье мы «вручную» создадим свой EXE-файл с использованием лишь одного Hex-редактора. После окончательного изучения PE мы начнём изучать его с точки зрения информационной безопасности.







