Тем, кто мало знаком с PE-форматом, или совсем с ним не знаком, наверное, было непросто понять то море информации, которое было излито в моей предыдущей статье, поэтому я решил пока немного отложить изучение экспортов, импортов и прочих служебных таблиц в PE-файлах. В этой статье мы займемся практикой: напишем программку, которая выведет список секций произвольного исполняемого файла и некоторую информацию о них. Впоследствии, когда дойдет дело до написания упаковщика, этим кодом мы воспользуемся, ведь упаковывать мы будем как раз данные секций.
Писать будем на C++. Никаких извратов не будет, поэтому код должен быть понятен, тем более, я его досконально прокомментирую.
Начнем с инклюдов:
1 2 3 4 5 6 7 8 9 |
//библиотека ввода-вывода для вывода информации в консоль #include <iostream> //библиотека для работа с файлами #include <fstream> //вспомогательная библиотека для выравнивания, форматирования вывода и т.д. #include <iomanip> //конечно, нам потребуются структуры из Windows.h //но ничто, в общем-то, не мешает их перенести прямо в код и скомпилировать это под линукс :) #include <Windows.h> |
Далее - несколько макросов, которые предоставил Крис Касперски в своей статье про формат PE. Мы будем чаще всего использовать ALIGN_UP - макрос для выравнивания числа на заданную границу.
1 2 3 |
#define Is2power(x) (!(x & (x - 1))) #define ALIGN_DOWN(x, align) (x & ~(align - 1)) #define ALIGN_UP(x, align) ((x & (align - 1)) ? ALIGN_DOWN(x, align) + align : x) |
Итак, тело главной функции. В качестве единственного аргумента нашей программе будет передаваться путь к исполняемому файлу для анализа.
1 2 3 4 5 6 7 8 |
int main(int argc, const char* argv[]) { //если аргумент не передали - выведем пример использования и выйдем if(argc != 2) { std::cout << "Usage: sectons.exe pe_file" << std::endl; return 0; } |
Теперь пришла пора открыть файл, имя которого нам передали через консоль.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
//откроем файл формата PE в бинарном режиме std::ifstream pefile; pefile.open(argv[1], std::ios::in | std::ios::binary); if(!pefile.is_open()) { //если вдруг его открыть не удалось, то выведем ошибку и выйдем std::cout << "Can't open file" << std::endl; return 0; } //определим размер файла, он нам пригодится дальше pefile.seekg(0, std::ios::end); //для этого переведем файловый указатель чтения в самый конец файла, получим его позицию std::streamoff filesize = pefile.tellg(); //это и будет размер файла в байтах //затем вернем файловый указатель в начало файла pefile.seekg(0); |
Как я писал в предыдущей статье, в самом начале файла должна лежать структура IMAGE_DOS_HEADER. Считаем ее и немного проверим.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
IMAGE_DOS_HEADER dos_header; pefile.read(reinterpret_cast<char*>(&dos_header), sizeof(IMAGE_DOS_HEADER)); if(pefile.bad() || pefile.eof()) { //если вдруг считать не удалось... std::cout << "Unable to read IMAGE_DOS_HEADER" << std::endl; return 0; } //Первые два байта структуры должны быть MZ, но, так как в x86 у нас обратный порядок следования байтов, //мы сравниваем эти байты со значением 'ZM' if(dos_header.e_magic != 'ZM') { std::cout << "IMAGE_DOS_HEADER signature is incorrect" << std::endl; return 0; } //Начало заголовка самого PE-файла (IMAGE_NT_HEADERS) должно быть //выровнено на величину двойного слова (DWORD) //убедимся, что это так if((dos_header.e_lfanew % sizeof(DWORD)) != 0) { //а иначе наш PE-файл некорректен std::cout << "PE header is not DWORD-aligned" << std::endl; return 0; } |
Теперь необходимо считать структуру IMAGE_NT_HEADERS. Я программу писал исключительно под PE32, хотя сделать ее для PE64 или вообще универсальной труда никакого не составляет. Читать будем, соответственно, структуру IMAGE_NT_HEADERS32 (это 32-разрядная версия IMAGE_NT_HEADERS, они все определены в глубине Windows.h). Сейчас я пропускаю множество необходимых проверок полей заголовка PE-файла (например, не проверяю выравнивания), потому что они сейчас не являются критичными.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
//Переходим на структуру IMAGE_NT_HEADERS и готовимся считать ее pefile.seekg(dos_header.e_lfanew); if(pefile.bad() || pefile.fail()) { std::cout << "Cannot reach IMAGE_NT_HEADERS" << std::endl; return 0; } //Читаем //читать будем только часть структуры IMAGE_NT_HEADERS //без дата директорий //они нам и не понадобятся сейчас IMAGE_NT_HEADERS32 nt_headers; pefile.read(reinterpret_cast<char*>(&nt_headers), sizeof(IMAGE_NT_HEADERS32) - sizeof(IMAGE_DATA_DIRECTORY) * 16); if(pefile.bad() || pefile.eof()) { std::cout << "Error reading IMAGE_NT_HEADERS32" << std::endl; return 0; } //Проверяем, что наш файл - PE //сигнатура у него должна быть "PE\0\0" //помним про обратный порядок байтов и проверяем... if(nt_headers.Signature != 'EP') { std::cout << "Incorrect PE signature" << std::endl; return 0; } //Проверяем, что это PE32 if(nt_headers.OptionalHeader.Magic != 0x10B) { std::cout << "This PE is not PE32" << std::endl; return 0; } |
Теперь нам необходимо переместиться к таблице секций, которую мы и будем читать, чтобы получить информацию о секциях исполняемого файла. Можно было бы воспользоваться макросом IMAGE_FIRST_SECTION, но я сделал это руками, чтобы было понятнее:
1 2 3 4 5 6 7 8 9 10 11 |
//позиция в файле таблицы секций - это размер всех заголовков полностью //(включая дос-стаб, если он есть и все дата директории, если они есть) DWORD first_section = dos_header.e_lfanew + nt_headers.FileHeader.SizeOfOptionalHeader + sizeof(IMAGE_FILE_HEADER) + sizeof(DWORD) /* Signature */; //переходим на первую секцию в таблице секций pefile.seekg(first_section); if(pefile.bad() || pefile.fail()) { std::cout << "Cannot reach section headers" << std::endl; return 0; } |
Немного подготовим консоль для удобного вывода информации. Выставим выравнивание текста по левому краю и вывод чисел в 16-ричной системе счисления. std::showbase добавит перед 16-разрядными числами "0x" автоматически.
1 |
std::cout << std::hex << std::showbase << std::left; |
Теперь начнем читать таблицу секций. Количество секций лежит в IMAGE_NT_HEADERS.FileHeader.NumberOfSections.
1 2 3 4 5 6 7 8 9 10 11 |
for(int i = 0; i < nt_headers.FileHeader.NumberOfSections; i++) { //готовим заголовок секции IMAGE_SECTION_HEADER header; //и читаем его pefile.read(reinterpret_cast<char*>(&header), sizeof(IMAGE_SECTION_HEADER)); if(pefile.bad() || pefile.eof()) { std::cout << "Error reading section header" << std::endl; return 0; } |
Дальше я добавил всевозможные проверки корректности таблицы секций. Разберем их.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
//во-первых, "сырой" размер данных и виртуальный размер секции //не могут быть одновременно нулевыми if(!header.SizeOfRawData && !header.Misc.VirtualSize) { std::cout << "Virtual and Physical sizes of section can't be 0 at the same time" << std::endl; return 0; } //если размер инициализированных данных ("сырых") не равен нулю... if(header.SizeOfRawData != 0) { //Проверим, что инициализированные данные секции также не вылетают за пределы нашего PE-файла if(ALIGN_DOWN(header.PointerToRawData, nt_headers.OptionalHeader.FileAlignment) + header.SizeOfRawData > filesize) { std::cout << "Incorrect section address or size" << std::endl; return 0; } //в этой переменной мы сохраним выровненный виртуальный размер секции DWORD virtual_size_aligned; //если виртуальный размер секции был выставлен в ноль, if(header.Misc.VirtualSize == 0) //то ее выровненный виртуальный размер равен ее реальному размеру инициализированных данных, //выровненному на границу SectionAlignment virtual_size_aligned = ALIGN_UP(header.SizeOfRawData, nt_headers.OptionalHeader.SectionAlignment); else //а иначе он равен ее виртуальному размеру, //выровненному на границу SectionAlignment virtual_size_aligned = ALIGN_UP(header.Misc.VirtualSize, nt_headers.OptionalHeader.SectionAlignment); |
Если вам сейчас трудно вспомнить, что это всё такое - виртуальный размер, реальный адрес, выравнивание, то советую вернуться к первой статье и всё повторить.
1 2 3 4 5 6 7 |
//Проверим, что виртуальное пространство секции не вылетает за пределы виртуального пространства всего PE-файла if(header.VirtualAddress + virtual_size_aligned > ALIGN_UP(nt_headers.OptionalHeader.SizeOfImage, nt_headers.OptionalHeader.SectionAlignment)) { std::cout << "Incorrect section address or size" << std::endl; return 0; } } |
Пришло время вывести информацию о секции - раз уж она прошла все проверки :)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
//имя секции может иметь размер до 8 символов char name[9] = {0}; memcpy(name, header.Name, 8); //выводим имя секции std::cout << std::setw(20) << "Section: " << name << std::endl << "=======================" << std::endl; //ее размеры, адреса std::cout << std::setw(20) << "Virtual size:" << header.Misc.VirtualSize << std::endl; std::cout << std::setw(20) << "Raw size:" << header.SizeOfRawData << std::endl; std::cout << std::setw(20) << "Virtual address:" << header.VirtualAddress << std::endl; std::cout << std::setw(20) << "Raw address:" << header.PointerToRawData << std::endl; //и самые важные характеристики std::cout << std::setw(20) << "Characteristics: "; if(header.Characteristics & IMAGE_SCN_MEM_READ) std::cout << "R "; if(header.Characteristics & IMAGE_SCN_MEM_WRITE) std::cout << "W "; if(header.Characteristics & IMAGE_SCN_MEM_EXECUTE) std::cout << "X "; if(header.Characteristics & IMAGE_SCN_MEM_DISCARDABLE) std::cout << "discardable "; if(header.Characteristics & IMAGE_SCN_MEM_SHARED) std::cout << "shared"; std::cout << std::endl << std::endl; } return 0; } |
Вот и все, наша программа готова, и ей можно через консоль скормить любой исполняемый файл (PE32), чтобы получить информацию о его секциях. На первом скриншоте как раз показан вывод этой программы при анализе самой себя, собранной в Visual Studio 2010 в отладочной версии.
Полная версия кода (без комментариев): скачать (txt).
great post loved it
Вам не кажется, что с File Mapping'ом код был бы раз в 5 проще?
Ну напишите, включите туда все проверки, которые включил я, потом сравним. Не думаю, что он был бы проще в 5 раз, разве что совсем немного) Да и я писал на C++ же, без винапи.
нуну, не отмазывайтесь.) можно подумать с++ с потолка узнал о DWORD и IMAGE_NT_HEADERS =)
А это не винапи, это структуры обычные. Их можно и без windows.h объявить.
Спасибо! Не думал, что найду это именно здесь!
Спасибо хороший гайд
Если у кого на Win7 не пашет, меняем 0x108 на IMAGE_NT_OPTIONAL_HDR32_MAGIC.
По крайней мере у меня не работало, покопавшись в MSDN нашел решение проблемы.
Еще забыл добавить, для версии x64 используем IMAGE_NT_HEADERS64 вместо IMAGE_NT_HEADERS32. Вроде по все баги которые у меня полезли.
Спасибо за гайд. =)
http://pastebin.com/index/UU8q1Lwj
И свой вопрос я еще подробно изложил на форуме:
http://www.cyberforum.ru/cpp-beginners/thread1126186.html
по логике при вводе значения 9 в операторе if( x >= 10 ) - возвращаться должно false.
При 9 выведется "Less than 10..". И что с того?
Как правильно запускать программу?
Пишу в консоли sections.exe sections.exe, ошибка: Error reading section header
Запускаешь вроде правильно. Но не исключено, что в примере есть ошибка, тогда надо разбираться. А каким компилятором собирал пример?
Visual C++ 2013.
В коде, где считывается заголовок секции IMAGE_SECTION_HEADER, закоментировал проверку и стало нормально отрабатывать.
Дошли руки проверить. Странно, но у меня всё нормально отрабатывает на бинарнике, собранном в VS2013. Был бы благодарен, если зальёшь куда-нибудь тот бинарник, на котором у тебя не сработало.
/* TODO: Дописать серию статей, сейчас времени нет */
Шел 2018-й год :D
Ну, время так и не появилось :D