Найти в Дзене
Под капотом ПО

Тайны Windows: Разбираем PE-файлы как настоящий детектив!

Оглавление

Привет! 👋 Вы когда-нибудь задумывались, как Windows понимает, что запускать, когда вы кликаете на game.exe? Всё дело в PE-формате (Portable Executable) - секретном языке Windows для программ и библиотек. Сегодня мы станем цифровыми археологами и разберём его по кирпичикам! 🕵️‍♂️

❓ Что такое PE-файл?

PE (Portable Executable) - это стандартный формат исполняемых файлов в Windows. Под эту "шапку" попадают:

  • .exe (программы)
  • .dll (библиотеки)
  • .sys (драйверы)
  • .ocx (ActiveX-компоненты)

Представьте его как контейнер с инструкциями для ОС: "Где код?", "Какие библиотеки нужны?", "Сколько памяти выделить?". Без PE Windows просто не поймёт, что делать с файлом.

🧩 Анатомия PE-файла: Главные "органы"

Структура напоминает слоёный пирог (или луковицу 😄). Двигаемся от начала файла вглубь:

  1. DOS-заголовок (IMAGE_DOS_HEADER)
  2. Исторический артефакт! Содержит:
  3. Сигнатуру MZ (инициалы создателя - Марка Збиковского).
  4. Поле e_lfanew - ключевой указатель на PE-заголовок (наше "золото"!).
typedef struct _IMAGE_DOS_HEADER {
WORD e_magic; // "MZ" (0x5A4D)
// ... другие поля ...
LONG e_lfanew; // Смещение до PE-заголовка!
} IMAGE_DOS_HEADER;
  1. DOS-Stub
  2. Маленькая программа, которая выводит: "Эта программа не может быть запущена в DOS". Напоминание о древних временах! 💾
  3. PE-сигнатура (4 байта)
  4. Буквы PE\0\0 (код: 0x50450000). Если их нет - файл битый или это не PE!
  5. COFF-заголовок (IMAGE_FILE_HEADER)
  6. Технические характеристики:
  7. Архитектура (x86/x64)
  8. Количество секций
  9. Временная метка компиляции
  10. Признаки (DLL? Исполняемый?).
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; // Архитектура (0x8664 = x64)
WORD NumberOfSections; // Число секций!
DWORD TimeDateStamp; // Когда скомпилирован?
// ... и другие поля ...
} IMAGE_FILE_HEADER;
  1. Опциональный заголовок (IMAGE_OPTIONAL_HEADER)
  2. Несмотря на имя - обязателен для EXE! Содержит "магию":
  3. Magic Number: 0x10B (32-бит) или 0x20B (64-бит).
  4. Entry Point (RVA) - Адрес первой инструкции!
  5. ImageBase - Виртуальный адрес загрузки в память.
  6. Размеры кода/данных.
  7. Data Directories - Таблица указателей на критичные части (импорт, экспорт, ресурсы).

📚 Секции - где хранится "жизнь"

После заголовков идут секции - блоки с разным назначением. Каждая описывается структурой IMAGE_SECTION_HEADER:

typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[8]; // Имя (например, ".text", ".rdata")
DWORD VirtualSize; // Размер в памяти
DWORD VirtualAddress; // RVA (адрес относительно ImageBase)
DWORD SizeOfRawData; // Размер на диске
DWORD PointerToRawData; // Смещение в файле
// ... атрибуты (код/данные/доступ) ...
} IMAGE_SECTION_HEADER;

Основные секции:

  • .text: Код программы (инструкции CPU).
  • .data: Инициализированные данные (глобальные переменные).
  • .rdata: Только для чтения (строки, константы).
  • .idata: Импорт функций (какие DLL и функции используем).
  • .edata: Экспорт функций (если это DLL).
  • .rsrc: Ресурсы (иконки, картинки, меню).

🔍 Читаем PE-файл на C++: Практика!

Вот как можно прочитать базовую информацию о PE-файле:

#include <iostream>
#include <fstream>
#include <Windows.h>
void AnalyzePE(const char* filename) {
std::ifstream file(filename, std::ios::binary);
if (!file) {
std::cerr << "Error opening file!" << std::endl;
return;
}
// Читаем DOS-заголовок
IMAGE_DOS_HEADER dosHeader;
file.read((char*)&dosHeader, sizeof(dosHeader));
// Проверяем сигнатуру "MZ"
if (dosHeader.e_magic != IMAGE_DOS_SIGNATURE) {
std::cerr << "Not a valid PE file!" << std::endl;
return;
}
// Переходим к PE-заголовку
file.seekg(dosHeader.e_lfanew, std::ios::beg);
DWORD peSignature;
file.read((char*)&peSignature, sizeof(peSignature));
// Проверяем "PE\0\0"
if (peSignature != IMAGE_NT_SIGNATURE) {
std::cerr << "PE signature not found!" << std::endl;
return;
}
// Читаем COFF-заголовок
IMAGE_FILE_HEADER fileHeader;
file.read((char*)&fileHeader, sizeof(fileHeader));
// Выводим информацию
std::cout << "🌟 PE File Info 🌟\n";
std::cout << "Architecture: " <<
(fileHeader.Machine == IMAGE_FILE_MACHINE_AMD64 ? "x64" : "x86") << "\n";
std::cout << "Number of Sections: " << fileHeader.NumberOfSections << "\n";
// Читаем Optional Header (32/64 зависит от Machine!)
if (fileHeader.SizeOfOptionalHeader > 0) {
IMAGE_OPTIONAL_HEADER optHeader;
file.read((char*)&optHeader, sizeof(optHeader));
std::cout << "Entry Point (RVA): 0x" << std::hex << optHeader.AddressOfEntryPoint << "\n";
}
}
int main() {
AnalyzePE("C:\\Windows\\notepad.exe");
return 0;
}

Что делает код:

  1. Открывает файл в бинарном режиме.
  2. Проверяет DOS-заголовок и сигнатуру MZ.
  3. Переходит к PE-заголовку через e_lfanew.
  4. Проверяет сигнатуру PE\0\0.
  5. Читает COFF-заголовок и выводит архитектуру/число секций.
  6. Читает Optional Header и показывает точку входа.

💡 Зачем это знать?

  • Отладка: Понимание структуры помогает анализировать сбои.
  • Безопасность: Анализ вредоносных программ (malware) начинается с разбора PE.
  • Оптимизация: Ручная настройка секций может уменьшить размер файла.
  • Reverse Engineering: Без этого - как без карты в джунглях! 🌴

🔚 Заключение

PE-формат - фундамент Windows-программ. Теперь вы знаете его "скелет"! 🦴 Чтобы углубиться:

  • Изучите Data Directories (особенно импорт/экспорт).
  • Поэкспериментируйте с утилитой PEView или CFF Explorer.

У вас есть сила понимать Windows изнутри! 💪 Делитесь своими находками в комментариях - что самое интересное вы обнаружили в PE-файлах? 🚀