> For the complete documentation index, see [llms.txt](https://ymmfty0.gitbook.io/ymmfty0/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://ymmfty0.gitbook.io/ymmfty0/pe-file-format/headers/dos-header-and-dos-stub-and-rich-header.md).

# DOS Header and DOS Stub and Rich Header

## **Введение**

В предыдущей статье, я рассказывал о том , что такое PE файл и какую структуру он имеет. В данной статье мы углубимся в DOS Header, DOS Stub и Rich Header, чтобы понять как это все работает внутри с использованием С++.

## DOS Header.

Заголовок DOS (также называемый заголовком MS-DOS) — это 64-байтовая структура, которая находится в начале PE-файла. Она служит для обеспечения совместимости с более старыми версиями Windows и операционными системами MS-DOS.&#x20;

При попытке запустить PE файл в среде DOS , вместо основной программы выполнится заглушка ( DOS Stub), которая выдаёт сообщение: "This program cannot be run in DOS mode." Однако её можно модифицировать, чтобы она делала что-то другое. Без этого заголовка, если вы попытаетесь загрузить исполняемый файл в MS-DOS, он не будет загружен и просто выдаст общую ошибку.

Хотя современные операционные системы Windows не используют заголовок DOS напрямую, его наличие остаётся обязательным для корректной структуры PE-файла.

### DOS Header: Struct

Заголовок DOS представляет собой структуру данных, которая определяется следующим образом:

```cpp
typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
    WORD   e_magic;                     // Magic number
    WORD   e_cblp;                      // Bytes on last page of file
    WORD   e_cp;                        // Pages in file
    WORD   e_crlc;                      // Relocations
    WORD   e_cparhdr;                   // Size of header in paragraphs
    WORD   e_minalloc;                  // Minimum extra paragraphs needed
    WORD   e_maxalloc;                  // Maximum extra paragraphs needed
    WORD   e_ss;                        // Initial (relative) SS value
    WORD   e_sp;                        // Initial SP value
    WORD   e_csum;                      // Checksum
    WORD   e_ip;                        // Initial IP value
    WORD   e_cs;                        // Initial (relative) CS value
    WORD   e_lfarlc;                    // File address of relocation table
    WORD   e_ovno;                      // Overlay number
    WORD   e_res[4];                    // Reserved words
    WORD   e_oemid;                     // OEM identifier (for e_oeminfo)
    WORD   e_oeminfo;                   // OEM information; e_oemid specific
    WORD   e_res2[10];                  // Reserved words
    LONG   e_lfanew;                    // Offset to the NT header
  } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
```

Эта структура важна для загрузчика PE в MS-DOS. Однако для загрузчика PE в системах Windows значение имеют только некоторые её элементы. Поэтому мы рассмотрим лишь ключевые части структуры, пропустив менее значимые.

* **`e_magic`:** Это первый элемент заголовка DOS. Это `WORD`, поэтому он занимает 2 байта. Его обычно называют магическим числом. Он имеет фиксированное значение `0x5A4D` или `MZ` в ASCII и служит подписью, которая отмечает файл как исполняемый файл MS-DOS.
* **`e_lfanew`:** Это последний элемент структуры заголовка DOS. Он расположен по смещению `0x3C` в заголовке DOS и содержит смещение к началу заголовков NT. Этот элемент важен для загрузчика PE в системах Windows, поскольку он сообщает загрузчику, где искать заголовок файла.

### e\_magic

Давайте разберёмся с `e_magic`.\
Начало PE-файла начинается с этой структуры. Первые два байта всегда имеют значения `0x4D` и `0x5A`, которые являются сигнатурным значением заголовка DOS.

Рассмотрим простой код на C++, который будет получать базовый адрес текущего процесса. С помощью отладчика мы посмотрим содержимое начала этого файла.&#x20;

**Примечание:** Ниже будет код, который будет вручную разбирать структуру dos\_header , будем использовать смещение и так далее, чтобы просто более подробно разобраться как все это работает.

Вот код:

```cpp
#include <Windows.h>
#include <iostream>

int main()
{
	PVOID baseAddr = GetModuleHandle(NULL);
	std::cout << "Base addr from for current process: " << std::hex << baseAddr << std::endl;

}
```

Давайте разберем его:

* Получаем базовый адрес текущего процесса:

```cpp
PVOID baseAddr = GetModuleHandle(NULL);
```

В документации [GetModuleHandleA](https://learn.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getmodulehandlea?redirectedfrom=MSDN), говорится , если мы передаем в параметр значение NULL, то возвращается дескриптор файла, который используется в текущем процессе

* Выводим базовый адрес в шестнадцатеричном формате

```cpp
std::cout << “Base addr from for current process: ” << std::hex << baseAddr << std::endl;
```

Передаём полученный адрес в поле Address и видим, что начало нашего файла действительно является 0x4D и 0x5A.

<figure><img src="/files/FM0lHZPjxo8zGIxbVq91" alt=""><figcaption></figcaption></figure>

Чтобы вывести значение, мы будем использовать вот такой код:

```cpp
PBYTE pBaseAddr = static_cast<PBYTE>(baseAddr);
std::cout << "e_magic value: "  << *reinterpret_cast<WORD*>(pBaseAddr) << std::endl;
```

Разберем предыдущий код:&#x20;

Тут происходит лишь статическое преобразование, чтобы было легче манипулировать адресом на уровне байтов. Это просто-напросто удобно.

```cpp
PBYTE pBaseAddr = static_cast<PBYTE>(baseAddr);
```

Далее возникает небольшая сложность для тех, у кого мало опыта в использовании C++.\
Для начала разберёмся: значение `e_magic` имеет тип данных `WORD` (двухбайтовое значение). Чтобы получить значение из адреса в памяти, нам нужен указатель на этот тип данных. Для этого необходимо преобразовать адрес в указатель типа `WORD`.

```cpp
reinterpret_cast<WORD*>(pBaseAddr)
```

Так как мы преобразовали наш адрес в двухбайтовый указатель `WORD`, нам нужно разыменовать его, чтобы получить данные по этому адресу.

```cpp
*reinterpret_cast<WORD*>(pBaseAddr)
```

Вот так можно все это представить:

```cpp
reinterpret_cast<WORD*>(baseAddr) points to: 0x00400000 
*reinterpret_cast<WORD*>(baseAddr) reads: 0x5A4D (because of little-endian order)
```

**Результат кода**:

Как мы видим, значение вывода действительно результат будет MZ == 4D 5A.

<figure><img src="/files/MFQHpK0qqrqMfZAC0TlW" alt=""><figcaption></figcaption></figure>

### e\_lfanew

e-lfanew - это значение является последний членом DOS заголовка, он находится по смещению `0x3C` , как и говорилось ранее.

<figure><img src="/files/xklCtM1KMzAFK6UHcKUn" alt=""><figcaption></figcaption></figure>

Имея базовый адрес процесса, мы можем сложить этот адрес и офсет, и получать адрес в памяти, которое хранит значение `e_lfanew`

Давайте перейдем к коду:

```cpp
PBYTE pDosHdrAddr = pBaseAddr;
LONG eLfaNew = *reinterpret_cast<LONG*>(pDosHdrAddr + 0x3c);
std::cout << "e_lfanew value: " << eLfaNew << std::endl;
```

Ничего нового тут нет, для простоты использование, мы создали переменную pDosHdrAddr и инициализировали значением pBaseAddr. Здесь `pDosHdrAddr` указывает на базовый адрес процесса, что позволяет нам работать с заголовком DOS.

```cpp
PBYTE pDosHdrAddr = pBaseAddr;
```

Далее тут Происходит небольшая арифметика, чтобы получить адрес на значение `e_lfanew` мы должны к базовому адресу прибавить офсет `0x3c` , который указывает на `e_lfanew`

```cpp
pDosHdrAddr + 0x3c
```

Значение `e_lfanew` имеет тип данных LONG ( 4 байтовый тип данных)&#x20;

После чего просто выводим значение

<figure><img src="/files/DR7PkvxKCZ7ejhX9erCv" alt=""><figcaption></figcaption></figure>

Давайте удостоверимся и передадим файл в PE-Bear

<figure><img src="/files/zmAHg8Sfa4vaXaxe9bIn" alt=""><figcaption></figcaption></figure>

И как мы видим по результату, действительно , значение который получили при выводе сходится с значением , которое мы получили из PE-Bear.

#### Get DOS Header: Easy Way

Теперь, когда мы рассмотрели способ ручного получения всех офсетов, я бы хотел показать, как это можно сделать с использованием структуры, чтобы процесс стал более удобным и лаконичным.

```cpp
PVOID baseAddr = GetModuleHandle(NULL);
std::cout << "Base addr for current process: " << std::hex << baseAddr << std::endl;

auto dosHeader = reinterpret_cast<PIMAGE_DOS_HEADER>(baseAddr);
std::cout << "DOS Header: e_magic -> " << std::hex << dosHeader->e_magic << std::endl;
std::cout << "DOS Header: e_lfanew -> " << std::hex << dosHeader->e_lfanew << std::endl;
```

Как и говорилось ранее, мы просто получаем дескриптор процесса для получения начального адреса текущего процесса и преобразуем этот адрес в структуру `IMAGE_DOS_HEADER`. Это возможно, потому что заголовок DOS всегда находится в начале PE-файла.

## DOS Stub

DOS Stub — это небольшая часть кода, которая присутствует в каждом PE-файле и служит для вывода сообщения об ошибке по умолчанию: “This program cannot be run in DOS mode.”, если исполняемый файл запускается в среде DOS.

Это сообщение может быть изменено пользователем во время компиляции. Это всё, что нам нужно знать о заглушке DOS.

Давайте разберёмся, как можно получить адрес DOS Stub.

### **Вычисляем офсет**

Так как `e_lfanew` использует тип данных `LONG` для хранения значения (которое занимает 4 байта), чтобы вычислить адрес начала DOS Stub, нужно выполнить следующие шаги:

1. Взять адрес заголовка DOS (`DOS Header`).
2. Прибавить смещение `0x3C`, которое указывает на начало `e_lfanew`.
3. Прибавить ещё 4 байта (размер типа данных `LONG`), чтобы перейти за значение `e_lfanew`.

Именно эта позиция будет началом адреса DOS Stub. Чтобы упростить вычисления, мы складываем `0x3C + 4` и получаем итоговое смещение `0x40`. Таким образом, адрес DOS Stub находится по смещению `0x40` от начала PE-файла.

<figure><img src="/files/4iXE32YwouvrAy4f6O9I" alt=""><figcaption></figcaption></figure>

Давайте сравним это с PE-Bear

<figure><img src="/files/ogHUHJY4Kqfa7TlJ2tfS" alt=""><figcaption></figcaption></figure>

И мы видим, что значение, отображаемое в Debug в памяти, совпадает со значением, которое мы получили из PE-Bear.

## Rich Header

Rich Header — это недокументированная структура в PE-файле, содержащая информацию о сборке и компиляции файла. Эта структура расположена между DOS Stub и началом NT-заголовка.

Rich Header состоит из фрагмента данных, зашифрованного методом XOR, ключевого слова `Rich` и ключа XOR (checksum), который используется для расшифровки первой части заголовка.

В конце заголовка присутствует сигнатура `Rich` или `0x68636952`, что позволяет легко идентифицировать Rich Header. Сразу после ключевого слова `Rich` следует контрольная сумма, которая служит как ключ XOR для расшифровки остальной части заголовка.

**Важно отметить, что сигнатура `Rich` присутствует только в исполняемых файлах VC++ и не встречается в сборках .NET**.

В начале заголовка находится 4-байтовое значение `DanS`, также зашифрованное с помощью XOR, которое содержит сигнатурное значение (`0x536e6144`). После значения `DanS` следуют три нулевых `DWORD` для выравнивания, которые, по моим наблюдениям, равны контрольной сумме в конце заголовка. Затем идут зашифрованные данные, содержащие метаданные о сборке, где каждая запись состоит из пары `DWORD`.

<figure><img src="/files/LoLvlyP232WinQ5ivyil" alt=""><figcaption></figcaption></figure>

Синим квадратом я отметил, начало структуру Rich Header, то есть значение DanS. Розовым я отметил , checksum значение, и красным самое значение "Rich"

### **Вычисляем офсет**

Самый простой способ определить смещение до сигнатурного значения `Rich` — это метод перебора. Что нам это даст? После сигнатурного значения `Rich` хранится 4-байтовое значение CheckSum. Этот CheckSum также повторяется несколько раз в начале заголовка Rich. Найдя это значение, мы можем продолжить перебор до тех пор, пока не найдём первое совпадение, а затем отнять 4 байта, чтобы определить начало структуры Rich.

Для выполнения этой задачи необходимо знать размер DOS Stub. Это можно легко определить, используя значение `e_lfanew` из DOS Header, которое указывает смещение (от начала файла) до начала NT Header. DOS Stub находится между DOS Header и NT Header. Чтобы вычислить размер DOS Stub, следует отнять `e_lfanew` от размера структуры DOS Header.

Формула для вычисления размера DOS Stub:

```
DOS Stub Size = e_lfanew − Size of DOS Header
```

Я написал небольшую функцию, которая помогает найти начало RichHdr

```cpp
DWORD GetOffsetToRichHdr(const PBYTE& pBaseAddr )
{
	LONG eLfaNew = *reinterpret_cast<LONG*>(pBaseAddr + 0x3c);
	int dosStubSize = eLfaNew - 0x40;

	const DWORD RICH_SIGNATURE = 0x68636952;
	DWORD offset = 0;
	
	if (dosStubSize <= sizeof(DWORD))
	{
		std::cout << "Size of dos stub cannot be less sizeof DWORD" << std::endl;
		return 0;
	}
	for (int i = 0; i < dosStubSize / sizeof(DWORD); ++i)
	{
		offset = 0x40 + (i * sizeof(DWORD));
		DWORD richSignatureVal = *reinterpret_cast<DWORD*>(pBaseAddr + offset);
		if (richSignatureVal == RICH_SIGNATURE){
			break;
		}
	}

	const LONG checkSumSign = *reinterpret_cast<DWORD*>(pBaseAddr + offset + sizeof(DWORD));
	std::cout << "CheckSumSign is 0x" << std::hex << checkSumSign << std::endl;

	for (int i = 0; i < dosStubSize / sizeof(DWORD); ++i)
	{
		offset = 0x40 + (i * sizeof(DWORD));
		DWORD checkSumSignVal = *reinterpret_cast<DWORD*>(pBaseAddr + offset);
		if (checkSumSignVal == checkSumSign)
		{
			return offset - sizeof(DWORD);
		}
	}

	std::cerr << "RichHdr not found in DOS stub." << std::endl;
	return 0;
}
```

В целом, код объяснять, думаю, не надо, но если будут вопросы, пожалуйста, напишите мне в Telegram.

### **Декодируем данные**

Rich Header в PE-файлах хранит данные, закодированные с использованием XOR. Для декодирования этих данных нам нужно использовать checksum, который находится в конце Rich Header.

**Какие данные нам нужны?**

Первые 4 байта — это значение `DanS`, далее идут три четырёхбайтовых значения, которые, как мы ранее рассказывали, служат для выравнивания. После них уже идут данные до сигнатуры `Rich`.

Формула:

```
StartToData = Offset to RichHdr + 16 байт  
```

Пример вывода:

```cpp
DWORD64 CompIdData = *reinterpret_cast<DWORD64*>(pBaseAddr + offsetToRichHdr + 16);
std::cout << std::hex << CompIdData << std::endl;
```

Офсет до Rich Header мы просто получаем при помощи функции выше:

```cpp
DWORD offsetToRichHdr = GetOffsetToRichHdr(pBaseAddr);
```

Теперь надо узнать количество элементов CompId.

<figure><img src="/files/99gtq7UrYsG1WMeD1eyO" alt=""><figcaption></figcaption></figure>

Чтобы найти количество элементов, нам нужно знать смещение до Rich Signature от того момента, где начинается первый элемент `CompId`.

**Как понять, где начинается `CompId`?**

Первый элемент — это `DanS`, который занимает 4 байта. Следующие три значения — это `XorMask`. Таким образом, мы добавляем `4 * sizeof(DWORD)`, что равно 16 байтам.

<figure><img src="/files/SwkZGTgak9LLhImJxbtL" alt=""><figcaption></figcaption></figure>

**Поиск офсета до Rich Signature:**

```cpp
DWORD offset = 0;
DWORD richSignatureVal = 0;

DWORD offsetToCompData = 4 * sizeof(DWORD);

do{
	offset += sizeof(DWORD);
	richSignatureVal = *reinterpret_cast<DWORD*>(pBaseAddr + offsetToRichHdr + offsetToCompData + offset);
	
}while (richSignatureVal != 0x68636952);
```

Мы нашли смещение до Rich Signature. Теперь нам нужно определить их количество.\
Как это сделать?

Зная, что каждая пара `CompId` представляет собой два `DWORD` (или `DWORD64`), разделите смещение на `sizeof(DWORD64)`.

**Формула:**\
Где `Offset to Rich Signature` определяется методом, который я указал выше.

```
CompDataCount = (Offset ro rich signature / sizeof(DWORD64) )
```

**Код для получения элементов CompId:**

```cpp
size_t compDataLength = offset / sizeof(DWORD64) ;

std::vector<DWORD64> compIds{};

for (size_t i = 0; i < compDataLength; ++i)
{
	DWORD64 compId = *reinterpret_cast<DWORD64*>(pBaseAddr + offsetToRichHdr + offsetToCompData + i * (sizeof(DWORD64)));
	compIds.push_back(compId);
}
```

**Расшифровка данных CompId:**

После того как мы нашли все CompIds , нам теперь стоит их расшифровать При первой попытки я использовал XorMask , как просто DWORD значение , которое находится после значения DanS или в конце заголовка.\
Как показано в статье <https://ntcore.com/files/richsign.htm>

<figure><img src="/files/m3KFqxreHZw3YSshFx1G" alt=""><figcaption></figcaption></figure>

Но результата никакого не было, изучив исходники PE-Bear, я обнаружил, то что для вычисление XorMask используется вот такой метод:

{% embed url="<https://github.com/hasherezade/bearparser/blob/9287f21afd7c884108146bfa4358d7de31e7eb13/parser/pe/RichHdrWrapper.cpp#L219>" %}

**Вычисление XorMask:**

```cpp
DWORD64 xorVal2 = xorKey | ((DWORD64)xorKey << sizeof(DWORD) * 8);
```

Давайте пошагово разберем, что здесь происходит

```cpp
((DWORD64)xorKey << sizeof(DWORD) * 8)
```

Сдвигает значение `xorKey`, преобразованное в `DWORD64`, влево на 32 бита. Это эквивалентно умножению на `2^32` (или 0x100000000).

```cpp
xorKey | ((DWORD64)xorKey << sizeof(DWORD) * 8)
```

Операция побитового "или" (OR) `|` объединяет два значения:

* Первое значение — это `xorKey` в исходном виде.
* Второе — это `xorKey`, сдвинутое влево на 32 бита.

Что в итоге? Получается, что `xorVal2` становится 64-битным значением, где нижние 32 бита — это `xorKey`, и верхние 32 бита — это тоже `xorKey`.

Например, если `xorKey` равен `0x12345678`, то после всех этих манипуляций значение будет выглядеть как `0x1234567812345678`.

Почему так? Когда `xorKey` типа `DWORD` преобразуется в `DWORD64`, оно занимает младшие 32 бита, а старшие 32 бита заполняются нулями. Например, `0x12345678` превращается в `0x0000000012345678`. Затем мы сдвигаем `xorKey` влево на 32 бита, получая `0x1234567800000000`. После этого объединяем эти два значения с помощью операции OR, и итоговый результат — `0x1234567812345678`.

Данные `CompId` фактически можно представить в виде структуры такого вида:

```cpp
typedef struct _RICH_COMP_ID
{
	WORD CV;
	WORD prodId;
	DWORD count;
} RICH_COMP_ID, * PRICH_COMP_ID;
```

{% embed url="<https://github.com/hasherezade/bearparser/blob/9287f21afd7c884108146bfa4358d7de31e7eb13/parser/include/bearparser/pe/pe_undoc.h#L16>" %}

Конечный код:

```cpp
typedef struct _RICH_COMP_ID
{
	WORD CV;
	WORD prodId;
	DWORD count;
} RICH_COMP_ID, * PRICH_COMP_ID;

const DWORD xorKey = *reinterpret_cast<DWORD*>(pBaseAddr + offsetToRichHdr + sizeof(DWORD));
std::cout << "XorKey: " << std::hex << xorKey << std::endl;

DWORD64 xorVal2 = xorKey | ((DWORD64)xorKey << sizeof(DWORD) * 8);
std::cout << "XorKey: " << std::hex << xorVal2 << std::endl;

for (const auto& data : compIds)
{
	DWORD64 my_num = static_cast<DWORD64>(data) ^ (xorVal2);
	PRICH_COMP_ID myCompId = reinterpret_cast<PRICH_COMP_ID>(&my_num);

	std::cout << "CompId: " << std::dec << myCompId->CV << "."
			  << std::dec << myCompId->prodId << "."
			  << std::dec << myCompId->count << std::endl;
}                                                    
```

Проверив с PE-Bear , результат оказался верным

<figure><img src="/files/krFY2uO0AaTl5S55cZCa" alt=""><figcaption></figcaption></figure>

## **Итог**

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

Для практического понимания работы с заголовком PE-файла, вы можете ознакомиться с кодом в моем репозитории Github: <https://github.com/ymmfty0/PEFormatTutorial/tree/master/DosRichHeader>

**Дополнения для изучения:**&#x20;

<https://ntcore.com/files/richsign.htm&#x20>;

<https://0xrick.github.io/win-internals/pe3/&#x20>;

<https://learn.microsoft.com/en-us/windows/win32/debug/pe-format>
