GrigorenkoPV / pe-parser

Small .EXE & .DLL parser

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Задание 5. Разборщик Portable Executable

Существует несколько форматов исполняемых файлов. Один из таких форматов — Portable Executable (PE), используемый в преимущественно в операционной системе Windows. В этой лабораторной работе вам предстоит разобраться с устройством файлов формата PE, а также написать разборщик файлов такого формата.

Полезные материалы:

В результате выполнения задания у вас получится исполняемый файл pe-parser (в этот раз не PE, а ELF). Он должен принимать два аргумента командной строки — название операции и путь к исполняемому файлу. Операции и примеры будут приведены ниже.

В рамках этого задания вам не потребуется запускать какие-либо PE-файлы.

В директории examples находятся примеры — они помогут вам при написании и отладке кода. Также к каждому примеру приложен исходный код на языке C.

Выполняйте задание в ветке pe.

Часть 1. Проверка сигнатуры Portable Executable

Разработчики формата PE решили сохранить обратную совместимость с MS-DOS и исполняемыми файлами DOS MZ — любой исполняемый PE-файл одновременно является корректным файлом формата DOS MZ.

Однако, разработчики программ, как правило, не заботятся об обратной совместимости с DOS, поэтому большинство PE-файлов начинаются с заглушки — она выводит сообщение о том, что исполняемый файл не может быть запущен под DOS:

> 1.EXE
This program cannot be run in DOS mode

Такая заглушка может иметь произвольный размер, поэтому в файле существует специальное поле — четыре байта, начиная с адреса 0x3C, обозначают позицию, на которой начинается сигнатура PE-файла. В корректном исполняемом PE-файле начиная с этой позиции должны быть расположены четыре символа: ['P', 'E', '\0', '\0'].

Реализуйте операцию is-pe, которая по исходному файлу проверяет, является ли он исполняемым файлом формата Portable Executable. В этом задании достаточно проверить лишь сигнатуру файла.

  • Если был передан PE-файл, выведите PE и выйдите с кодом 0.
  • Если был передан не PE-файл, выведите Not PE и выйдите с кодом 1.

Пример исполнения программы:

$ ./pe-parser is-pe ./examples/1/1.exe
PE
$ ./pe-parser is-pe ./pe-parser
Not PE
$

Напишите Makefile для сборки вашей программы. Необходимо реализовать две цели — all (собирает файл pe-parser в корневой директории) и clean (удаляет артефакты сборки).

Вы можете добавить иные правила на ваше усмотрение.

Для запуска тестов добавьте ещё одну цель:

validation-pe-tests: all
	python3 -m tests ValidatingPeTestCases -f

Часть 2. Список зависимостей исполняемого файла

Следующая операция, которую мы реализуем — import-functions. Она позволит вывести функции, которые требуются для работы исполняемого файла, а также DLL-библиотеки, экспортирующие их.

К примеру, второй пример использует функцию __stdio_common_printf из библиотеки api-ms-win-crt-stdio-l1-1-0.dll.

В этой части задания мы реализуем просмотр списка библиотек.

В этой и следующих частях вы можете считать, что вам всегда необходимо обрабатывать только корректные PE-файлы в формате PE32+. Поведение вашего решения на некорректных файлах проверяться не будет. В частности, не нужно исполнять код из задания 1 перед выводом списка зависимостей.

Рассмотрим структуру PE-файла. Сразу за подписью идёт 20-байтовый заголовок COFF. В нём ничего важного мы не найдём.

Сразу после него идёт Optional Header размером 240 байт — в нём, среди прочего, находится и информация о таблице импорта. Первые два байта этого заголовка определяют один из двух форматов PE — PE32 для 32-битных систем или 64-битный PE32+. В рамках этого задания будем разбирать только формат PE32+.

В Optional Header нас интересуют два поля — адрес и размер таблицы импорта. Оба этих поля имеют размер 4 байта. Они идут друг за другом по смещению 0x78 от начала заголовка. Однако, не всё так просто — вместо настоящей позиции нам дают relative virtual address (RVA) — смещение относительно начала исполняемого файла в оперативной памяти после его загрузки. Назовём этот адрес import_table_rva. Нам же нужно получить позицию этой таблицы в нашем файле.

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

Размер каждого заголовка — 40 байт. В заголовке нас интересует размер секции в виртуальном адресном пространстве (назовём это значение section_virtual_size) по смещению 0x8, виртуальный адрес начала секции (section_rva, 0xC) и указатель на начало данных секции в файле (section_raw, 0x14). Нетрудно понять, что наша таблица импорта будет расположена в этой секции, если import_table_rva находится где-то между section_rva и section_rva + section_virtual_size. Как правило, таблица импорта находится в секции .rdata или .idata, но компилятор может поместить таблицу в произвольное место.

Теперь мы можем найти, где же начинается наша таблица импорта:

import_raw = section_raw + import_table_rva - section_rva

Сама таблица импорта (Import Directory Table) состоит из набора зависимостей. Каждая зависимость описывается 20-байтовой записью. Четыре байта, начиная со смещения 0xC, содержат RVA имени библиотеки.

Последняя запись — пустая, состоит из двадцати нулевых байтов.

Таким образом, ваша реализация операции import-functions на этом этапе должна находить Import Directory Table и извлекать из неё названия DLL-файлов, которые требуются для запуска исполняемого файла:

$ ./pe-parser import-functions examples/2/2.exe
VCRUNTIME140.dll
api-ms-win-crt-runtime-l1-1-0.dll
api-ms-win-crt-math-l1-1-0.dll
api-ms-win-crt-stdio-l1-1-0.dll
api-ms-win-crt-locale-l1-1-0.dll
api-ms-win-crt-heap-l1-1-0.dll
KERNEL32.dll
$

Запустите тесты, дописав соответствующую цель в Makefile:

import-dll-tests: all
	python3 -m tests ImportDllTestCases -f

Часть 3. Список импортируемых функций

В Windows существует несколько способов импортировать функции. Мы в рамках задания рассмотрим лишь один из них — импорт по имени функции. Дополним операцию import-functions выводом имён импортируемых функций.

Первые четыре байта каждой записи в Import Directory Table — RVA другой таблицы. Она называется Import Lookup Table и как раз содержит информацию об именах необходимых функций.

Import Lookup Table в файлах PE32+ состоит из записей размером 8 байт. Самый старший бит отвечает за способ импорта — значение 0 означает импорт по имени функции, а 1 — импорт по номеру внутри библиотеки. Мы рассматриваем только случай со старшим битом 0 (записи с битом 1 пропускаются). В таких записях младший 31 бит содержит RVA названия функции.

Как и в случае с Import Directory Table, последняя запись всегда состоит из 8 нулевых байт.

Добавьте в реализацию функции import-functions вывод имён функций — под соответствующим этим функциям названием библиотеки выведите названия импортируемых из неё функций с отступом в 4 пробела. Порядок вывода библиотек и функций не важен (однако, выводить функции нужно именно после нужной библиотеки).

$ ./pe-parser import-functions examples/2/2.exe
VCRUNTIME140.dll
    __current_exception
    __current_exception_context
    memset
    __C_specific_handler
api-ms-win-crt-stdio-l1-1-0.dll
    __stdio_common_vfprintf
    __acrt_iob_func
    __p__commode
    _set_fmode
api-ms-win-crt-runtime-l1-1-0.dll
    _register_onexit_function
    _crt_atexit
    terminate
    _seh_filter_exe
    _set_app_type
    _cexit
    _register_thread_local_exe_atexit_callback
    __p___argv
    __p___argc
    _c_exit
    _exit
    exit
    _initterm_e
    _initterm
    _get_initial_narrow_environment
    _initialize_narrow_environment
    _configure_narrow_argv
    _initialize_onexit_table
api-ms-win-crt-math-l1-1-0.dll
    __setusermatherr
api-ms-win-crt-locale-l1-1-0.dll
    _configthreadlocale
api-ms-win-crt-heap-l1-1-0.dll
    _set_new_mode
KERNEL32.dll
    GetCurrentThreadId
    RtlLookupFunctionEntry
    RtlVirtualUnwind
    UnhandledExceptionFilter
    SetUnhandledExceptionFilter
    GetModuleHandleW
    IsDebuggerPresent
    InitializeSListHead
    GetSystemTimeAsFileTime
    RtlCaptureContext
    GetCurrentProcessId
    QueryPerformanceCounter
    IsProcessorFeaturePresent
    TerminateProcess
    GetCurrentProcess
$

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

Тесты к этой части можно запустить с помощью цели в Makefile:

import-function-tests: all
	python3 -m tests ImportFunctionTestCases -f

Часть 4*. Список экспортируемых функций (+2 балла)

Помните, что бонус сдаётся только вместе с основным заданием.

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

В этом задании вам необходимо реализовать операцию export-functions. Она должна выводить список всех экспортируемых библиотекой функций в любом порядке.

$ ./pe-parser export-functions ./examples/3/3.dll
sum_three_ints
sum_two_doubles
sum_two_ints
$ 

Запустите тесты, добавив ещё одну цель в Makefile:

export-function-tests: all
	python3 -m tests ExportFunctionTestCases -f

About

Small .EXE & .DLL parser


Languages

Language:Rust 67.8%Language:Python 28.5%Language:Makefile 3.7%