> 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/imports-exports/export-table.md).

# Export Table

## **Exports**

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

Давайте рассмотрим, как это работает более подробно.

## **Секция .edata**

Эта секция содержит информацию о функциях и данных, которые экспортирует модуль (обычно DLL). Она используется для того, чтобы другие модули или программы могли обращаться к этим экспортированным функциям и данным.

Когда-то для таблицы экспорта выделялась персональная секция .edata, но теперь этого правила практически никто не придерживается.

## **Export Directory Table**

Информация об экспортируемых DLL начинается с IMAGE\_ EXPORT\_DIRECTORY, также известная под именем export directory table.

Вот так выглядит эта структура:

```cpp
typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;
    DWORD   TimeDateStamp;
    WORD    MajorVersion;
    WORD    MinorVersion;
    DWORD   Name;
    DWORD   Base;
    DWORD   NumberOfFunctions;
    DWORD   NumberOfNames;
    DWORD   AddressOfFunctions;     
    DWORD   AddressOfNames;         
    DWORD   AddressOfNameOrdinals;  
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
```

* Characteristics - Export Flags. В спецификации указанно, то что она зарезервирована и должна быть равна 0 . Может не иметь явного применения в PE-файлах
* TimeDateStamp - Время и дата создания экспортных данных.
* MajorVersion - Номер основной версии.
* MinorVersion - Минорный номер версии.
* Name - RVA на адрес , где хранится имя в ASCII-строке, содержит имя DLL.
* Base - Ordinal Base - Начальное значение порядковых номеров для функций, которые находятся в Export Address Table. Если `Base` равно 100, то функции в `Export Address Table` будут соответствовать порядковым номерам, начиная с 100. Обычно это значение равно 1.
* NumberOfFunctions - количество функций этой DLL.
* NumberOfNames - Количество записей в Export Name Pointer Table.
* AddressOfFunctions - RVA на Export Address Table.
* AddressOfNames - RVA на Export Name Pointer Table.
* AddressOfNameOrdinals - RVA на Export Ordinal Table

Мы можем получить ExportDirectoryTable с помощью DataDirectory из OptionalHeader. Для этого используется константа IMAGE\_DIRECTORY\_ENTRY\_EXPORT.&#x20;

Получение IMAGE\_DATA\_DIRECTORY для Export Directory Table.

```cpp
IMAGE_DATA_DIRECTORY exportDirectoryTable =
	ntHeaderDll->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
```

## **Export Address Table**

Таблица адресов представляет собой массив из RVA, которые ссылаются на точку входа, экспортируемые данные или абсолютные значения.

Каждая запись в таблице адресов экспорта — это поле, которое использует один из двух форматов, приведённых в следующей таблице:

* Export RVA — это непосредственно RVA на адрес.
* Forwarder RVA — указатель на ASCII-строку в разделе экспорта. В этой строке указывается имя DLL и имя экспорта (например, "MYDLL.expfunc") или имя DLL и порядковый номер экспорта (например, "MYDLL.#27").

Если адрес не равен строке, то это Export RVA, иначе это Forwarded RVA.

Чтобы получить EAT, нам нужно просто взять RVA на EAT из IMAGE\_EXPORT\_DIRECTORY.

Получив IMAGE\_DATA\_DIRECTORY, как я описывал выше, мы можем получить RVA на IMAGE\_EXPORT\_DIRECTORY.\
К этому RVA необходимо прибавить наш базовый адрес.

Напомню, если читаете DLL , как файл, все эти RVA , нужно переводить в offest.

{% embed url="<https://stackoverflow.com/questions/9955744/getting-offset-in-file-from-rva>" %}

Я столкнулся с ситуацией, когда RVA, был равен offset.

И сейчас объясню, почему это произошло.

Как я и говорил выше, .edata секции не используется в большинстве случаев. Поэтому вся таблица экспорта записывается в секцию .rdata.&#x20;

Если посмотреть в CFF Explorer , можно увидеть , что для Kernel32.dll в windows 11, начало Virtual Address равна Raw Address. Это показывает, то что все офсеты , будут равны RVA.&#x20;

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

Для Windows 10 , эти значения отличались, поэтому в случае чтение физически DLL с системы, тут надо было бы переводить RVA в offset.

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

Так , давайте перейдем дальше.

Чтобы получить все адреса функций из DLL, необходимо определить их количество и, используя массив `address_function`, перебрать все функции.

```cpp
std::cout << "Number of functions is: " << std::hex << export_directory->NumberOfFunctions << std::endl;  
auto address_function = static_cast<PDWORD>(base_address + export_directory->AddressOfFunctions);  
  
std::cout << "Address of function is: " << std::hex << address_function << std::endl;  
  
for ( auto i = 0; i < export_directory->NumberOfFunctions; ++i )  
{  
    std::cout << "Function: " << std::hex << base_address + address_function[i] << std::endl;  
}
```

Вывод кода:

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

## **Export Name Pointer Table**

Это массива RVA , который указывает на имена с функциями в формате ASCII. Они упорядочены по алфавиту. Если указатель на имя отсутствует в таблице, это значит, что функция не экспортируется по имени.

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

```cpp
auto address_of_names = static_cast<PDWORD>(base_address + export_directory->AddressOfNames);  
  
for ( auto i = 0; i < export_directory->NumberOfFunctions; ++i )  
{  
    std::endl  << "Function name: " << static_cast<char*>(base_address + address_of_names[i]) << std::endl;  
}
```

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

## **Export Ordinal Table**

Таблица ординалов представляет собой массив индексов типа WORD и служит своеобразным связующим звеном между таблицей имен и таблицей адресов.

Если функция, например, `FunctionName`, имеет индекс `1` в таблице имен, то, обратившись к таблице ординалов по этому индексу, мы получим значение, указывающее на индекс в таблице адресов (EAT). Этот индекс в EAT позволит получить RVA, соответствующий адресу данной функции.

Давайте напишем код. Найдем функцию с именем `MessageBox` и выполним её.

```cpp
FARPROC myGetProcAddress(const HMODULE& hModule, const std::string& funcName)
{
	auto base_address = reinterpret_cast<PBYTE>(hModule);
	if (!base_address)
		return nullptr;

	const auto dos_header = reinterpret_cast<PIMAGE_DOS_HEADER>(base_address);;
	if (dos_header->e_magic != IMAGE_DOS_SIGNATURE)
	{
		std::cerr << "error: dos_header->e_lfanew" << std::endl;
		return nullptr;
	}

	const auto nt_header = reinterpret_cast<PIMAGE_NT_HEADERS64>(base_address + dos_header->e_lfanew);;
	if (nt_header->Signature != IMAGE_NT_SIGNATURE)
	{
		std::cerr << "error: nt_header->Signature" << std::endl;
		return nullptr;
	}

	const auto export_data_directory = nt_header->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
	const auto export_directory = reinterpret_cast<PIMAGE_EXPORT_DIRECTORY>(base_address + export_data_directory.VirtualAddress);

	auto address_function = reinterpret_cast<PDWORD>(base_address + export_directory->AddressOfFunctions);
	auto address_of_names = reinterpret_cast<PDWORD>(base_address + export_directory->AddressOfNames);
	auto address_of_ordinals = reinterpret_cast<PWORD>(base_address + export_directory->AddressOfNameOrdinals);

	PVOID msg_addr = nullptr;

	for (auto i = 0; i < export_directory->NumberOfFunctions; ++i)
	{
		const char* func_name_in_dll = reinterpret_cast<char*>(base_address + address_of_names[i]);
		if (strcmp(funcName.c_str(), func_name_in_dll) == 0)
		{
			std::cout << func_name_in_dll << std::endl;
			msg_addr = static_cast<PVOID>(base_address + address_function[address_of_ordinals[i]]);
			break;
		}
	}

	return reinterpret_cast<FARPROC>(msg_addr);
}
```

Код несложный: мы просто проверяем, чтобы имя функции в нашей переменной совпадало с `MessageBoxA`.

```cpp
if ( strcmp( func_name.c_str() ,  func_name_in_dll  ) == 0 )
```

Далее мы просто получаем ординал по индекcу имени и получаем адрес

```cpp
base_address + address_function[address_of_ordinals[i]]
```

Вызываем функцию

```cpp
FARPROC msg_addr = myGetProcAddress(LoadLibrary("user32.dll"), "MessageBoxW");  
if( msg_addr == nullptr )  
    return -1;  
  
const auto function = reinterpret_cast<messageBox>(msg_addr);  
function(nullptr, L"PE FORMAT FILE TUTOR", L"GetProcAddress analog", MB_OK);
```

Ну и вот результат.

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

## **Итог**

Мы рассмотрели, что представляет собой Export Table, изучили используемые в ней структуры и попрактиковались в написании кода.

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