Восстановление таблицы импорта PE файлов
----------------------------------------
TiTi/BLiZZARD

1. Зачем это нужно?
===================

Привет всем :) Эта статья была написана в результате моей работы над дампером процес-сов. Я обнаружил, что многие компрессоры/шифровальщики делают таблицу импорта (IT - Import Table) непригодной для использования в дампе, и для нормальной работы дампа она должна быть восстановлена. На общеизвестных сайтах по win32asm я не обнаружил статей на эту тему.
Если и Вы интересуетесь этим вопросом, то эта статья сможет Вам помочь.
Например, любой упакованный Petite'ом v2.1 исполняемый файл после сброса на диск образа его памяти требует восстановления IT (точнее корректировки) для нормальной работы dump.exe. (То же самое справедливо для aspack, pepack, pesentry... и т.п.) Именно по-этому функции восстановления IT должны быть в любой программе, снимающей дампы (например, Phoenix Engine от G-RoM/UCF (включенный в ProcDump), или PE Rebuilder, раз-работанном мною и Virogen/PC).
Так как тема очень специфичная и весьма сложная, я буду считать, что Вы уже знакомы со структурой PE файлов (Вам следует прочитать документацию по формату PE-файлов).


2. Некоторые предварительные комментарии
========================================

Для начала, некоторая общая информация о IT и RVA/VA.
RVA (относительное виртуальное смещение таблицы импорта) хранится в соответствующей за-писи PE-заголовка по адресу = [смещение PE-заголовка + 80h]. Поскольку это виртуальное смещение, то оно не соответствует файловому смещению (VA (виртуальный адрес)) таблицы импорта (исключение, если файл был правильно "сброшен" из памяти). Итак, первое, что Вы должны делать для нахождения таблицы импорта в PE-файле - конвертировать ее RVA в соот-ветствующий VA. Для этого существует несколько различных способов: Вы можете написать собственную процедуру, анализирующую каталоги секций и рассчитывающую VA, но простейший вариант - использовать API, специально предназначенный для этого. Эта функция находится в IMAGEHLP.DLL (библиотека, используемая и в win9x, и в NT системах), ее имя - ImageRvaToVa. Вот ее описание (за подробностями обращайтесь к MSDN):

# LPVOID ImageRvaToVa(
# IN PIMAGE_NT_HEADERS NtHeaders,
# IN LPVOID Base,
# IN DWORD Rva,
# IN OUT PIMAGE_SECTION_HEADER *LastRvaSection
#);
#
#Параметры:
# NtHeaders
# Указатель на структуру IMAGE_NT_HEADERS. Сама структура может
# быть получена вызовом функции ImageNtHeader.
# Base
# Задает базовый адрес образа, отображенного в память
# вызовом функции MapViewOfFile.
# Rva
# Задает относительный виртуальный адрес, который нужно конвертировать.
# LastRvaSection
# Указатель на структуру IMAGE_SECTION_HEADER, который задает
# последнюю RVA секцию. Это необязательный параметр.
# Если он задан, то указывает на переменную,
# содержащую значение секции, указанной в качестве
# образа в последней сессии трансляции RVA в VA.


Как видите, все довольно просто. Вы должны только поместить Ваш файл в память и вызвать эту функцию, чтобы получить действительный VA таблицы импорта.
Обратите внимание на то, что далее я буду опускать все различия RVA/VA, но не забывайте конвертировать их один в другой, когда читаете RVA из PE-файла, который Вы восстанавли-ваете, или пишите в него.


3. Полное объяснение
====================

Ниже показан полный образец измененной таблицы импорта (это таблица импорта PE-файла сжатого petite'ом v2.1, и сброшенного в файл непосредственно из памяти):

00 представлены '*'
нестроковые значения представлены '-'

0000C1E8h : 00 00 00 00 00 00 00 00 00 00 00 00 BA C2 00 00 ************----
0000C1F8h : 38 C2 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ----************
0000C208h : C5 C2 00 00 44 C2 00 00 00 00 00 00 00 00 00 00 --------********
0000C218h : 00 00 00 00 D2 C2 00 00 54 C2 00 00 00 00 00 00 ****--------****
0000C228h : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ****************

0000C238h : 7F 89 E7 77 4C BC E8 77 00 00 00 00 E6 9F F1 77 --------****----
0000C248h : 1A 38 F1 77 10 40 F1 77 00 00 00 00 4F 1E D8 77 --------****----
0000C258h : 00 00 00 00
00 00 4D 65 73 73 61 67 65 42 6F 78 ******MessageBox
0000C268h : 41 00 00 00 77 73 70 72 69 6E 74 66 41 00 00 00 A***wsprintfA***
0000C278h : 45 78 69 74 50 72 6F 63 65 73 73 00 00 00 4C 6F ExitProcess***Lo
0000C288h : 61 64 4C 69 62 72 61 72 79 41 00 00 00 00 47 65 adLibraryA****Ge
0000C298h : 74 50 72 6F 63 41 64 64 72 65 73 73 00 00 00 00 tProcAddress****
0000C2A8h : 47 65 74 4F 70 65 6E 46 69 6C 65 4E 61 6D 65 41 GetOpenFileNameA
0000C2B8h : 00 00

0000C2BAh : 55 53 45 52 33 32 2E 64 6C 6C 00 4B 45 52 **USER32.dll*KER
0000C2C8h : 4E 45 4C 33 32 2E 64 6C 6C 00 63 6F 6D 64 6C 67 NEL32.dll*comdlg
0000C2D8h : 33 32 2E 64 6C 6C
00 00 00 00 00 00 00 00 00 00 32.dll**********

Как вы видите эта таблица импорта делится на три основные части :
- от C1E8h->C237h : массив структур IMAGE_IMPORT_DESCRIPTOR,
каждая из которых соответствует импортируемому DLL файлу.
Этот массив заканчивается структурой, заполненной 0.

IMAGE_IMPORT_DESCRIPTOR struct
OriginalFirstThunk dd 0 ;RVA оригинальной несвязанной IAT
TimeDateStamp dd 0 ;здесь не используется
ForwarderChain dd 0 ;здесь не используется
Name dd 0 ;RVA строки с именем DLL
FirstThunk dd 0 ;RVA массива IAT
IMAGE_IMPORT_DESCRIPTOR ends


- от C238h->C25Bh : Массивы двойных слов (dwords), называемые 'IAT' и указываемые
элементами FirstThunk структуры IMAGE_IMPORT_DESCRIPTOR.
Каждое DWORD этого массива соответствует импортируемой функции.


- от C25Ch->C2DDh : Строки имен импортируемых функций и DLL-файлов.
Проблема в том, что они не имеют никакого предопределенного
порядка: иногда имена функций стоят перед DLL, иногда после,
а иногда и перед и после.


Небольшое объяснение таблицы импорта
------------------------------------

OriginalFirstThunk это тот массив IAT, который PE-загрузчик ищет в первую очередь. Если он обнаружен, PE-загрузчик использует его для разрешения возможных проблем в FirstThunk IAT-массива. После загрузки в память каждое двойное слово (dword) FirstThunk-массива,содержащее RVA строки с именем функции, заменяется на реальный адрес функции (адрес той области в памяти, содержимое которой будет выполнено при обращении к функции). По существу нет никакой проблемы с таблицей импорта, если только OriginalFirstThunk остается неизменной.


Здесь мы подходим к нашей проблеме
----------------------------------

Итак, после этого короткого описания мы подходим к проблеме. Если Вы попробуете запус-тить программу, содержащую таблицу импорта, показанную выше, то она не загрузится, а Windows выдаст сообщение об ошибке. Почему? Просто потому, что массив OriginalFirstThunk был удален.
На самом деле, Вы можете заметить, что для каждой IMAGE_IMPORT_DESCRIPTOR-структуры этой таблицы импорта член OriginalFirstThunk равен 00000000h. Хм, тогда мы предполага-ем, что при запуске исполняемого файла PE-загрузчик будет пытаться получить имена им-портируемых функций из FirstThunk-массива. НО, как ВЫ можете заметить, этот массив не содержит больше RVA строк с именами функций, а содержит RVA адреса функции в памяти.


Что мы должны делать
--------------------

Теперь для того, чтобы заставить программу работать, мы должны восстанавливать элементы FirstThunk массива так, чтобы они снова указывали на строки с именами функций, которые мы видим в 3-й части таблицы импорта.
По существу это не очень сложно, но мы должны знать, какой элемент IAT относится к ка-кой функции, по скольку строки функций сортируются иначе, чем элементы FirstThunk.
Итак для каждого элемента IAT мы должны определить имя соответствующей функции. (Фактически мы уже имеем имя DLL из двойного слова IMAGE_IMPORT_DESCRIPTOR.Name, кото-рое, конечно, не было изменено.)


Как идентифицировать каждую функцию
-----------------------------------

Как мы видели выше, каждый поврежденный IAT - это RVA адреса функции в памяти. Эти ад-реса не меняются от сессии к сессии, поэтому мы должны лишь найти функцию, чей адрес указан поврежденной IAT, и заставить IAT указывать на строку с именем этой функции.

Для этого есть очень полезный API в Kernel32.dll - GetProcAddress. Он позволяет Вам по-лучать адрес данной функции. Вот ее описание:

FARPROC GetProcAddress(
HMODULE hModule, // дескриптор DLL модуля
LPCSTR lpProcName // имя функции
);

По сути для каждой поврежденной IAT мы должны только проанализировать все функции, имена которых содержатся в 3-й части таблицы импорта, и дождаться, когда GetProcAddress вернет адрес искомой нами функции.

- Параметр hModule - это десктриптор DLL модуля (то есть базовый адрес образа модуля в памяти), который мы можем получить, используя хорошо известную функцию GetModuleHandleA:

HMODULE GetModuleHandle(
LPCTSTR lpModuleName // адрес имени модуля, для которого нужно получить дескриптор
);
(lpModuleName указает строку с именем файла DLL, которую мы получаем из IMAGE_IMPORT_DESCRIPTOR.Name)

- lpProcName указывает на строку с именем функции.

Обратите внимание на то, что иногда функция импортируется по порядковому номеру. Этот номер содержится в слове (words) по адресу [offset имени_функции - 2]. Поэтому Ваша анализирующая процедура должна будет проверять, как импортируется каждая функция - по имени или по номеру.


Пример использования вышеприведенной таблицы импорта
----------------------------------------------------

Я объясню как исправить первую импортируемую функцию из первой импортируемой dll на вы-шеприведенном образце таблицы импорта.

1. Смотрим первую структуру IMAGE_IMPORT_DESCRIPTOR массива (C1E8h) и получаем имя DLL, указаннываемое членом структуры ".Name" (C1F4h, это указывает на C2BAh). Это USER32.dll.

2. Мы смотрим на член .FirstThunk, указывающий на массив IAT, каждый элемент которой соответствует импортируемой из dll (user32.dll) функции. В данном случае он находится по адресу C1F8h, указывая на C238h. Таким образом, начиная с C238h, мы имеем измененные IAT, которые и предстоит исправить. (Вы можете заметить, что этот массив IAT содержит 2 двойных слова (dwords), следовательно, из данной DLL импортируются 2 функции).

3. Посмотрим на первую измененную IAT. Ее значение - 77E7897Fh. Это адрес функции в па-мяти.

4. Для каждого имени функции из 3-й части таблицы импорта вызываем функцию GetProcAddress. Как только этот API вернет 77E7897Fh - мы достигли правильной функции. Исходя из этого, мы изменяем этот элемент IAT так, чтобы он указывал на имя нужной функции. (В данном случае это 'wsprintfA')

5. Теперь мы должны только заставить IAT указывать на: offset (строка имени функции)-2. Почему -2? Потому что иногда используется вызов функций по порядковому номеру (ordinal). Поэтому здесь мы изменяем содержимое адреса C238h так, чтобы он указывал на C26Ah (а не на 77E7897Fh)

6. Ну вот, эта функция исправлена, теперь нам остается только повторить процесс для всех элементов IAT.


Последние замечания
-------------------

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

___________________________________________________________________________________

Перевод:
Vlad
Sergey R.