|  | ||||||||||||||||||||||||||||||
|   |  | 
 | 
 | |||||||||||||||||||||||||||
|  | ||||||||||||||||||||||||||||||
| 
 | 
 C++ немного практикиИсточник: kaimi Тем, кто мало знаком с PE-форматом, или совсем с ним не знаком, наверное, было непросто понять то море информации, которое было излито в моей предыдущей статье, поэтому я решил пока немного отложить изучение экспортов, импортов и прочих служебных таблиц в PE-файлах. В этой статье мы займемся практикой: напишем программку, которая выведет список секций произвольного исполняемого файла и некоторую информацию о них. Впоследствии, когда дойдет дело до написания упаковщика, этим кодом мы воспользуемся, ведь упаковывать мы будем как раз данные секций.  Писать будем на C++. Никаких извратов не будет, поэтому код должен быть понятен, тем более, я его досконально прокомментирую. //библиотека ввода-вывода для вывода информации в консоль #include <iostream> //библиотека для работа с файлами #include <fstream> //вспомогательная библиотека для выравнивания, форматирования вывода и т.д. #include <iomanip> //конечно, нам потребуются структуры из Windows.h //но ничто, в общем-то, не мешает их перенести прямо в код и скомпилировать это под линукс :) #include <Windows.h> Далее - несколько макросов, которые предоставил Крис Касперски в своей статье про формат PE. Мы будем чаще всего использовать ALIGN_UP - макрос для выравнивания числа на заданную границу. #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) Итак, тело главной функции. В качестве единственного аргумента нашей программе будет передаваться путь к исполняемому файлу для анализа. int main(int argc, const char* argv[])
{
//если аргумент не передали - выведем пример использования и выйдем
	if(argc != 2)
	{
		std::cout << "Usage: sectons.exe pe_file" << std::endl;
		return 0;
	}Теперь пришла пора открыть файл, имя которого нам передали через консоль. //откроем файл формата 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. Считаем ее и немного проверим. 	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-файла (например, не проверяю выравнивания), потому что они сейчас не являются критичными. //Переходим на структуру 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, но я сделал это руками, чтобы было понятнее: //позиция в файле таблицы секций - это размер всех заголовков полностью
//(включая дос-стаб, если он есть и все дата директории, если они есть)
	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" автоматически. std::cout << std::hex << std::showbase << std::left; Теперь начнем читать таблицу секций. Количество секций лежит в IMAGE_NT_HEADERS.FileHeader.NumberOfSections. 	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;
		}Дальше я добавил всевозможные проверки корректности таблицы секций. Разберем их. //во-первых, "сырой" размер данных и виртуальный размер секции
//не могут быть одновременно нулевыми
		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);Если вам сейчас трудно вспомнить, что это всё такое - виртуальный размер, реальный адрес, выравнивание, то советую вернуться к первой статье и всё повторить. //Проверим, что виртуальное пространство секции не вылетает за пределы виртуального пространства всего 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;
			}
		}Пришло время вывести информацию о секции - раз уж она прошла все проверки :) //имя секции может иметь размер до 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). 
 |  | |||||||