Об упаковщиках в последний раз: Часть первая - теоретическая

Обсудить статью на форуме


Массу свежих 2020 года крэкерских инструментов, видеоуроков и статей вы сможете найти на видеокурсе от нашего сайта. Подробнее здесь.
Сложное – то, что делается мгновенно,
невозможное – то, что требует лишь
немногим большего времени
Рецензент:
Dr.Golova/UINC.RU
Главный корректор:
Edmond/HI-TECH

 

1. Лирическое отступление
2. Основные концепции
2.1 RVA/VA и иже с ними
2.2 О секциях PE-файла
2.3 Директория импорта
2.4 Директория ресурсов
2.5 Директория перемещаемых элементов
2.6 NTDLL.DLL – Windows Loader
3. 64-bit PE-files
4. Страшное слово XML
5. Благодарности

1. Лирическое отступление

Решено было разбить эту статью на две-три части. Первая часть собственно теоретическая. Она дает абсолютно необходимый минимум теории, который позволит успешно распаковывать файлы. В статье также описывается формат XML, который затем переводится в CSS-HTML при помощи XSLT-процессора. Таким образом, наряду со знанием структуры PE-файла вы, по прочтении статьи, будете иметь самые базовые представления о XML/CSS/XSLT, Perl/Java и других страшных технологиях. Мы достаточно неплохо осознем, что тема уже достаточно замусолена, поэтому нудного пересказа PE-документации здесь не будет. Скорее, вам стоит воспринимать эту статью как некий коллектор, попытку обобщения и анализа всей доступной информации о пакерах, PE-файлах, антиотладке и т.п. А поскольку есть возможность перевода статей с испанского и французского языков (помимо стандартного английского), то должно получится что-то неплохое. Вторая часть будет чисто практической. Мы рассмотрим UPX, Aspack, Asprotect, Crunch, Armadillo и вскользь упомянем о других, менее популярных пакерах. Все опыты, приведенные в первой части проводились на calc.exe – стандартном калькуляторе из поставки Windows 2000. Вторая часть тоже будет проходить вместе с calc.exe. Реальные случаи будете рассматривать сами. Последнее, но немаловажное замечание – ВСЕ, что здесь обсуждается, валидно ТОЛЬКО для Windows 2k/XP/2003 (последняя – с натяжкой!). 9x НЕ рассматривается как анахронизм. Замечание важно, т.к. 9x и NT по-разному трактуют оперативную память, загружают библиотеки, работают с процессами, обрабатывают исключения и т.п.

Для того чтобы понимать, что здесь написано, введем пару допущений. Допущение номер раз: вы знаете ассемблер; №2: вы знаете, что такое исполняемый файл, т.е. файл загружается с диска в оперативную память с помощью механизмов ОС, при этом ОС определенным образом может при загрузке корректировать информацию внутри файла, если это необходимо; №3: вы слышали о языке программирования Perl; №4 – вы имеете очень приличное представление о базовых типах языка С – структурах, массивах и т.п., последнее – пятое – вы имеете хоть и очень смутное, но все же какое-то представление о том, что такое PE-формат исполняемого файла.

2. Основные концепции

Теперь, наконец, давайте рассмотрим формат PE-файла. Скажем сразу, что в наши намерения не входило давать здесь пересказ технической документации MS. Файл со спецификациями PE/COFF при-ложен, на wheaty.net есть статьи Питрека на эту тему. Полагаем, в русской части Интернета сравнительно просто найти саму книгу Питрека. Из русской литературы можно предложить Румянцева, где неплохо описаны поля, и даже есть исходники, хотя их качество нам не нравится, и статью о загрузчике PE-файлов на RSDN.ru – там все расписано очень неплохо, есть и исходники, но они на Delphi, а этот язык мы не знаем и не любим. Однако алгоритм, по справедливому замечанию автора, везде одинаков.

Да, чуть не забыли. Для хорошо знающих английский, есть абсолютно великолепная вещь, совершенно свободно распространяемая - http://www.iecc.com/linker/ - слейте ее целиком, и обязательно прочтите, просто изумительно! Книга будет продублирована и на WASM.RU, ибо вполне стоит того! Практическим примером к этой книге может служить техническая документация с сайта http://www.cs.virginia.edu/~lcc-win32/ - Якоб Навия очень неплохо расписывает многие тонкости по созданию компилятора и линкера. Еще дополнительно прилагается англоязычное описание PE-формата (версия 1.7) от Lumnificer, где подробно расписываются все поля, и даже объясняется, как создавать PE-файл ручками, т.е. расписывается работа примитивного линкера!

Так же, посоветуем изучить исходный код программы Wine – эмулятора Windows для Linux. Wine умеет загружать pe-файлы под Linux, причем код pe_image.c написан очень развернуто. А там еще мно-о-го примеров есть… Сайт - http://www.winehq.com.

Теперь очень быстренько пробежимся по списку литературы, описывающего работу NTDLL.dllWindows loader'а.

Нашими маяками в этом бушующем море станут статьи Питрека.
А также, основанная на этом статья Russell Osterlund с сайта http://www.smidgeonsoft.com, название статьи – "Windows 2000 Loader: What Happens Inside Windows 2000: Solving the Mysteries of the Loader" – лежит на MSDN. Лучше пока еще ничего не написано. Примеры для статьи скачивайте только с MS, родной сайт лучше не трогать. Причем после слива можно смело удалить все dsp/dsw/ncb/suo/sln файлы, оставив только vcproj. Причина в том, что все файлы проектов повреждены, во всяком случае, VS.NET 2003 их открыть не смогла. Другое дело vcproj, который, по сути, является простым xml-файлом. Подредактируйте их немного (неправильно прописаны пути Output Directory и т.п.), и держите в уме, что проект Forwarder требует lib-файл проекта Forwarded, поэтому второй должен собираться раньше.

Во всей остальной части статьи мы будем жестко придерживаться терминов, принятых Microsoft. Все определения структур взяты из winnt.h. Эта мера кажется очень разумной, т.к. на данный момент существует дикая неразбериха в терминах - существует целая куча слов, для которых нет четких определений, например, секция импорта, таблица импорта, IAT и т.п. Каждый писатель под этими словами понимает что-то свое, в результате чего имеем полный беспредел.

Теперь взгляните на рисунок ниже:

Структура PE файла
Рис. 1.

Мы предполагаем, что вы оторветесь от чтения этой статьи и проведете несколько минут с этим рисунком и официальной документацией от MS, держа в уме, что PE файл, по сути, просто совокупность секций, которые описываются в заголовке.
Единственное что, стоит объяснить несколько нюансов.

2.1 RVA/VA и иже с ними

RVA переводится как Relative Virtual Address - относительный виртуальный адрес. Его относительность заключается в том, что он отсчитывается от адреса загрузки, который может быть, а может и не быть, равен ImageBase.
Обязательно учтите, что RVA имеет РАЗНЫЙ смысл для бинарных файлов (exe, dll, sys) и объектных файлов (obj, lib). Вторые мы здесь не рассматриваем, а для первых – файл загружается в память и RVA какого-либо элемента вычисляется так:

RVA = VA – адрес загрузки,

где VA (Virtual Address) – виртуальный адрес элемента в памяти, а адрес загрузки берется из поля Op-tionalHeader.ImageBase, в том случае, если он равен ImageBase, либо вычисляется лоадером.
Обратите внимание на то, что элементарные просмотрщики файла, например, QVIEW/WinHEX и т.п. отображают абсолютные offset-ы, HIEW способен переключаться (Alt+F1) между показом VA и внутрифайловых смещений (local/global mode), а IDA вообще частично эмулирует работу ntdll.dll – Windows-loader'a, поэтому показывает только VA (если вы не укажете иное, например, загрузив файл как "binary").
Давайте рассмотрим пример для практического усвоения материала. Точка входа calc.exe (также часто называемая EP – entry point) выглядит так:

.01012420: 55 8B	;VA

А в виде абсолютных внутрифайловых смещений – вот так:

00011A20: 55 8B
 ;обратите внимание на отсутствие точки!
 ;HIEW требует точку, если хотите VA!

Давайте попытаемся разобраться, какой же механизм пересчета VA во внутрифайловые смещения.

Алгоритм примерно выглядит так:

1) По VA рассчитать RVA

VA = RVA + ImageBase

2) Для RVA найти секцию, в которой мы находимся (о секциях мы поговорим чуть ниже).

3) Пересчитать RVA во внутрифайловый offset по формуле:

offset = RVA - IMAGE_SECTION_HEADER.VirtualAddress +
    + IMAGE_SECTION_HEADER.PointerToRawData

Теперь давайте посчитаем ручками. В качестве примера возьмем адрес точки входа calc.exe - 0x01012420.

Сразу смотрим в HIEW на значение ImageBase -

  ¦ Image base 01000000 ¦ Subsystem GUI ¦
     VA = RVA + ImageBase;
    01012420 = RVA + 1000000;
    RVA = 12420;
    //что можно сразу увидеть из OptionalHeader.EntryPoint

Теперь определяемся, где же у нас лежит этот RVA. Ага, RVA лежит в секции .text. Обратите вни-мание на то, что лежит он ближе к концу секции:

Number Name VirtSize RVA PhysSize Offset Flag
1 .text 000124EE 00001000 00012600 00000600 60000020

Теперь VA во внутрифайловое смещение:

offset = 12420 - 1000 + 600 = 11A20;

Заметим, что процесс этот уже давным-давно автоматизирован. Есть такие утилиты как LordPE и PE Tools, а в них есть такая вещь как FLC (File Location Calculator), который по данному VA отобразит RVA и внутрифайловый offset.
Имея на руках некоторые API-функции - GetModuleHandle, ImageRvaToVa и ImageRvaToSection, несложно написать программу, которая быстренько переведет любой VA во внутрифайловый оффсет. Например, аналог функции от MS ImageRvaToSection может выглядеть где-то так:

  /***********************
  * Return value: Возвращается структура IMAGE_SECTION_HEADER, для данного RVA
  * Parameters: Первый – это структура IMAGE_FILE_HEADER
  * Parameters: Второй – это RVA для которого нужно найти секцию
  ************************/
 
 PIMAGE_SECTION_HEADER RvaToSection(PIMAGE_FILE_HEADER pFH, DWORD dwRVA)
 {
 		UINT                  i;
 		IMAGE_SECTION_HEADER  *pSH;
 
 		pSH = (PIMAGE_SECTION_HEADER)((DWORD)(pFH + 1)
 				 		+ pFH->SizeOfOptionalHeader);
 		for (i = 0; i < pFH->NumberOfSections; i++)
 		{
 			if (dwRVA >= pSH->VirtualAddress
 				&& dwRVA < pSH->VirtualAddress
 						+ pSH->Misc.VirtualSize)
 				return pSH;
 			++pSH;
 		}
 		return NULL;
 }
   

Примеры функций RtlImageRvaToVa /RtlImageRvaToSection /RtlImageDirectoryEntryToData есть в файле loader.c из wine.

Например, есть у вас есть dll, загруженная по адресу, отличному от ImageBase. Вы нашли защиту в Soft-Ice, и хотите пропатчить это место HIEW'вом - тут-то и пригодится умение.

2.2 О секциях PE-файла

PE-файлы были спроектированы для работы в ОС со страничной адресацией памяти. Известно, что Windows делит память на страницы различного размера, поэтому и PE-файл разбит на секции. Это очень важно понимать, поскольку секции PE-файла в памяти и на диске - это вовсе не одно и то же! В том случае, если сами данные в секции имеют общий объем менее размера кластера - секция на диске выравнивается (т.е., дополняется нулями, int3 - CCh или nop – 90h) под размер кластера (не путать с сек-тором – кластеры состоят из многих секторов диска!), который обычно равен 512 байтам (или другое значение – поле OptionalHeader.FileAlignment).

Когда файл загружается, то секция попадает в страницу памяти большего размера – 4 кб (или дру-гое значение - OptionalHeader.SectionAlignment), и выравнивается вновь, если это необходимо, уже только нулями. Обратите вниманиме, мы говорим "секция" – т.е., секция .text, CODE, .data, DATA, .aspack или что-либо еще. Каждая секция будет проецироваться на одну или несколько страниц памяти.

Не путайте понятие "Секция" (section) и "Директория" (directory). Директории (импорта, экспорта, ресурсов, исключений или чего-то еще) находятся ВНУТРИ секций. Заметьте, секция определяет разбиение PE-файла на части, но секции глубоко все равно, какой тип данных будет находится внутри нее. Для того, чтобы охарактеризовать тип, у нас есть директории! Директории характеризуют данные, содержащиеся в PE, по их типу и функциональности. Таким образом, можно говорить, что секция является физическим разбиением PE файла на состовляющие, а директория - логическим.

Иногда размер секции может занимать много кластеров, и отображаться Windows на несколько страниц памяти. Общее количество секций должно в точности соответствовать полю FileHeader.NumberOfSections. Теоретически, с этим шутить не стоит, так как шаг в сторону – стреляю. Такой файл не будет опознан ntdll.dll, поэтому на поле NumberOfSections можно смело(?) опираться. Однако и здесь нам уже радость испортили! Некоторые крипторы, например, telock, заменяют это значение в ПАМЯТИ, например, на 0xFFA4. Угадайте, что случится, когда вы попытаетесь запустить дамп с такого файла? А что будет, если утилита, просматривающая содержимое полей секций в цикле, будет ориентироваться на такую переменную как на счетчик цикла, нечто вроде такого:

for (i=0; i<= FileHeader.NumberOfSections; i++)
 //где FileHeader.NumberOfSections == 0xFFA4
 {
 //Здесь обрабатываем содержимое секции
 //А теперь представим, что секции закончились. О-о-о-о-й!
 //да, попробуйте такой файл посмотреть HIEW. Забавная реакция!
 }

Как вариант, можно использовать значение NumberOfSections с диска. Другую альтернативу мы рассмотрим, после того как ознакомимся со структурой секции изнутри.

LordPE, HIEW на этот трюк покупаются! Все версии IDA - тоже! PE Tools – нет! Прекрасная защита от сброса дампа на диск (имеется в виду, дамп сбросить можно, а вот заставить его заработать потом.... тоже можно, но чуть посложнее)! Хотя, с другой стороны, такой файл не будет загружен самой Windows. Цитирую Евгения Сусликова: "не должно быть хиеву умнее виндов". Что ж, вполне справедливо.
Для каждой секции в заголовке PE-файла указан ее размер на диске и в памяти. Структура выглядит так:

 
 #define IMAGE_SIZEOF_SHORT_NAME 8
 typedef struct _IMAGE_SECTION_HEADER {
 //имя не более 8 символов
     BYTE    Name[IMAGE_SIZEOF_SHORT_NAME];
     union
 	{
             DWORD   PhysicalAddress;
             DWORD   VirtualSize;	
 //помеченные красным значения характеризуют секцию в памяти
 //VirtualSize – общий размер секции в памяти.
 //если это число БОЛЬШЕ размера секции на диске
 // – секция дополняется нулями
 	} Misc;
     DWORD   VirtualAddress;
 //RVA первого байта секции в памяти
 //синий шрифт – данные о секции на диске
     DWORD   SizeOfRawData;
 //Размер ИНИЦИАЛИЗИРОВАННЫХ данных на диске
 //Ой, какое интересное поле! Поговорим о нем чуть ниже.
 //Вкупе с VirtualSize им часто пользуются протекторы
     DWORD   PointerToRawData;
 //очень полезное поле – offset от начала файла до первого байта секции
     DWORD   Characteristics;
 //характеристики секции – это поле тоже очень любят менять,
 // поговорим ниже и о нем 
 } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
 
 

Особый интерес в этой структуре вызывают два поля – VirtualAddress и SizeOfRawData. Все остальные тоже очень интересны, но эти два широко используются протекторами для сокрытия от наших глаз того, что должно быть скрыто. К примеру, вам, вероятно, часто доводилось видеть нечто вроде такого в IDA:

.data1:005B6FFD                 db    0 ;
 .data1:005B6FFE                 db    0 ;
 .data1:005B6FFF                 db    0 ;
 .data1:005B7000                 db    ? ;
 .data1:005B7001                 db    ? ;
 .data1:005B7002                 db    ? ;
 .data1:005B7003                 db    ? ;

в то время как самый обычный hex-редактор выдавал вполне осмысленные цифры. Проблема и ее решение достаточно просты. Обратите пристальное внимание на поля VirtualSize и SizeOfRawData той секции, где есть такой подарочек. Значение SizeOfRawData должно быть меньше значения поля VirtualSize:

Number Name VirtSize RVA PhysSize Offset Flag
1 .text 00195C5C 00001000 00000000 00000000 60000020
2 .data 00008DD4 00197000 00000000 00000000 C0000040
3 .text1 00010000 001A0000 0000F000 00001000 60000020
4 .data1 00020000 001B0000 00007000 00010000 C0000040
5 .pdata 000B0000 001D0000 000B0000 00017000 C0000040
6 .rsrc 00001000 00280000 00001000 000C7000 40000040

Именно поэтому и появляются знаки вопроса – IDA ориентируется только на raw-байты (хотя, в случае секции кода с этим можно поспорить). Чтобы исправить положение, надо просто приравнять значение SizeOfRawData значению VirtualSize. И, вуаля:

.data1:005B7000 aPdata000_0     db 'PDATA000'
 .data1:005B7008                 db    2 ;
 .data1:005B7009                 db    1 ;

Заметим также, что ни VirtualSize, ни SizeOfRawData различных секций перекрываться не могут. Таким образом подправленный файл просто не будет загружен.
Возможен и еще, например, такого рода трюк. Положим, есть файл, у которого ручками создана несуществующая директория отладки (Debug Directory):

#define IMAGE_DIRECTORY_ENTRY_DEBUG	6

Структура директории выглядит так:

typedef struct _IMAGE_DEBUG_DIRECTORY {
     DWORD   Characteristics;
     DWORD   TimeDateStamp;
     WORD    MajorVersion;
     WORD    MinorVersion;
     DWORD   Type;
     DWORD   SizeOfData;
     DWORD   AddressOfRawData;
     DWORD   PointerToRawData;
 } IMAGE_DEBUG_DIRECTORY, *PIMAGE_DEBUG_DIRECTORY;

Положим, мы задали заведомо огромные значения полей SizeOfData, AddressOfRawData и PointerToRawData. Лоадера эти поля абсолютно не интересуют (ему важна лишь валидность параметров в IMAGE_DATA_DIRECTORY). Прямо сказано, что эти поля касаются только отладчика (или дизассемблера). Так вот, IDA на таком файле (до версии 4.3 включительно) слетит наглухо! Такого рода трюк (© Dr. Golova) был применен в программе, которую рассматривал Sten в своей статье – "Исследование подаруночка" на reversing.net, если кто помнит.

Популярны также трюки с директориями импорта-экспорта. Некоторые поля (OriginalFirstThunk, например) забивают 0xFFFFFFFF или чем-нибудь не менее гадким - RVA OriginalFirstThunk должно, согласно действиям лоадера (Win2k SP4), укладываться в диапазон

cmp     ecx, [eax+IMAGE_NT_HEADERS.OptionalHeader.SizeOfHeaders]
 jb      loc_77F9373F	;если меньше SizeOfHeaders
 cmp     ecx, [eax+IMAGE_NT_HEADERS.OptionalHeader.SizeOfImage]
 jnb     loc_77F9373F	;и больше либо равно SizeOfImage


если это не так – файл загружен не будет. В этих случаях IDA говорит "Can't find a translation for virtual address…", и … раньше слетала, а теперь продолжает работу. HIEW в своей работе на winnt.h не опирается вообще, он был создан раньше. Валидность адресов директории импорта/экспорта проверяется валидностью VA. Мы поговорим о подобных приемах немного позднее.

Флаги секции – тоже вещь небезынтересная! Здесь мы не будем углубляться в детали. Скажем лишь, что флаги секции могут преобразовываться лоадером в атрибуты страниц и сегментов, биты CR-регистров и т.д. Это достаточно сложная тема, требующая неплохого понимания принципов работы защищенного режима. Мы бы порекомендовали Рендалла Хайда "Art of Assembly" и Михаила Гука. Заодно можно почитать Фроловых, а также ознакомится со статьями Broken Sword на WASM.RU. После этого можно смело утверждать, что вы будете является одним из очень твердых специалистов по защищенному режиму процессоров х86 :). Такое знание не будет бесполезным. Например, понимание принципов защиты страниц виртуальной памяти помогает практически однозначно идентифицировать Aspack. Сбросьте флаг разрешения записи у секции кода в программе, предположительно запакованной Aspack, и взгляните на результаты. Почему такое происходит – мы поясним во второй части. Обязательно обратите внимание - это поле НИКОГДА не должно быть равным нулю. Как это можно использовать - см. ниже.

Сейчас уместным было бы посоветовать на какое-то время отвлечься от статьи, и попробовать поэкспериментировать с секциями файла. Да, мы сделаем это немного позднее, во второй и третьей частях, когда будем чистить файл от мусора, оставленного крипторами, однако, то, что сделано собственными руками, едва ли когда-нибудь забудется. Например, попробуйте поставить поле VirtualAddress какой-нибудь секции в ноль (как, например, это делает линкер от Watcom). Также учтите, что количество страниц в памяти не обязательно будет соответствовать количеству секций в PE-файле. Во-первых, размер, во-вторых – сама Windows просто не загрузит страницу памяти до тех пор, пока она не нужна. Только если страница нужна, тогда она будет подгружена.

Еще нюанс. Все упаковщики, пакеры и крипторы очень нежно относятся к секции .rsrc. Секция кода, директрия импорта – все это безжалостно калечится, однако секцию ресурсов трогать боятся. При-чина проста. В исходном коде UPX можно найти следующие комментарии:

// after some windoze debugging I found that the name of the sections
 // DOES matter :( .rsrc is used by oleaut32.dll (TYPELIBS)
 // and because of this lame dll, the resource stuff must be the
 // first in the 3rd section - the author of this dll seems to be
 // too idiot to use the data directories... M$ suxx 4 ever!
 // ... even worse: exploder.exe in NiceTry also depends on this to
 // locate version info

И действительно. В файле oleaut32.dll можно найти следующие строки, сохранившиеся там со времен 95-го по 2k и выше:

 
 .77A078C7: 7825               js         .077A078EE  -----v (2)
 .77A078C9: 8D4580             lea         eax,[ebp][-80]
 .77A078CC: 687CB7A377      push        077A3B77C  -----v (3)
 .77A078D1: 50                 push        eax
 .77A078D2: FF15F8229B77       call        lstrcmpiA ;KERNEL32.dll
 
 .77A3B770:  44 00 49 00-52 00 00 00-2A 00 00 00-2E 72 73 72  D I R   *   .rsr
 .77A3B780:  63 00 00 00-00 00 00 00-74 79 70 65-6C 69 62 00  c       typelib
   

И из-за откровенно неумного, недальновидного, …, не будем перечислять, поведения программиста, кракеры получили большой подарок, а авторы пакеров – подарок поменьше. Спасибо, Microsoft! И абсолютно без всякой иронии! Так что, если автор хочет, чтобы его файл имел красивую иконку, то он должен предоставить ресурсы файла в наглядном виде. Конечно, не все так просто, секция ресурсов то-же безжалостно калечится, однако, вероятность того, что название секции будет сохранено наряду с иконкой, очень велика. Что до остального.… Разберемся с этим чуточку попозже.

Сказанное выше справедливо для всех приложений, имеющих ресурсы (очевидно, часто, но не всегда, консольные приложения к таковым не относятся).

В заключение, рассмотрим алгоритм подсчета количества секций PE-файла. Как уже упоминалось, поле FileHeader.NumberOfSections не может служить достаточно надежным проводником. Тогда что? Пока мы используем этот алгоритм (к слову, тоже ненадежный), но, возможно, после выхода статьи, он уже не будет работать в силу вполне определенных причин. Тогда нам с вами останется последний верный и надежный способ – считать значение из файла диске. Многие утилиты это позволяют. Итак, производим подсчет секций, и смотрим на поля PointerToRelocations, PointerToLinenumbers, NumberOfRelocations, NumberOfLinenumbers. Если все эти поля равны нулю, а поле Characteristics - НЕТ, значит - это секция.

Алгоритм прост:

 
 /***********************
  * Return value: настоящее значение NumberOfSections
  * Parameters: pMem - возвращаемое значение
  * от CreateFileMapping, либо от GetModuleHandle
  ************************/
 
 WORD GetRealNumberOfSections(PVOID pMem)
 {
 	PIMAGE_DOS_HEADER       pDosh;
    	PIMAGE_NT_HEADERS       pNTh;
 PIMAGE_SECTION_HEADER   pSh;
 
    	WORD  iRealNumOfSect = 0;
 
 	// Устанавливаем SEH
       __try
 {
 	// Считывает IMAGE_DOS_HEADER
 		pDosh = (PIMAGE_DOS_HEADER)pMem;
 		// Считываем IMAGE_NT_HEADERS 
 		pNTh = (PIMAGE_NT_HEADERS)((DWORD)pDosh + pDosh->e_lfanew);
 		// Получаем указатель на первую секцию
 		pSh = IMAGE_FIRST_SECTION32(pNTh);
 
 		// Считываем секции по очереди
 		for(word i = 0; i < (pNTh->FileHeader.NumberOfSections); i++)
 		{
 			if(!pSh->PointerToRelocations &&
 				!pSh->PointerToLinenumbers &&
 				!pSh->NumberOfRelocations &&
 				!pSh->NumberOfLinenumbers &&
 				pSh->Characteristics)
 				// Увеличиваем счётчик секций
 				iRealNumOfSect++;
 			else
 				return iRealNumOfSect;
 			++pSh; // Переходим к следующей секции
 		}
 		return iRealNumOfSect;
 	}
 	__except(EXCEPTION_EXECUTE_HANDLER)
 	{
 		// Ошибка “Access Violation!”
 		return 0;
 	}
 }
   

Уже упоминалось, что линкер Watcom устанавливает значение поля VirualSize в нуль. Тогда возникает вопрос - откуда брать VirualSize? Рекомендуется использовать значение поля SizeOfRawData, выровненное по SectionAlignment. Ниже указан макрос на C++ для выравнивания секции по SectionAlignment:

#define RALIGN(dwToAlign, dwAlignOn) ((dwToAlign % dwAlignOn == 0) ?
  dwToAlign : dwToAlign - (dwToAlign % dwAlignOn) + dwAlignOn)

Используется так: VirtualSize = RALIGN(VirtualSize, SectionAlignment);

Eще немного об ImageBase. Понятия «директория кода» не существует. Есть только секция кода, которая может называться, как душе угодно. Начало секции вычисляется из IMAGE_OPTIONAL_HEADER.BaseOfCode. В случае, если значение поля AddressOfEntryPoint не укладывается в диапазон,

BaseOfCode<=AddressOfEntryPoint<=SizeOfCode


то это внимательному человеку может говорить о многом. Во-первых, файл, вероятно, запакован (почему так – см. часть вторую). Во-вторых, теперь вам придется сушить мозги с его распаковкой. Некоторые утилиты, например, OllyDbg, это дело подмечают, о чем вежливо предупреждают. Мало ли что?

Теперь перейдем к директории импорта.

2.3 Директория импорта

Директория Импорта
Рис. 2.

Для закрепления увиденного материала, опять таки, рекомендуется немного оторваться от чтения, и что-то поклацать. Все пояснения полей есть и в англо- и в русскоязычной документации.

Однако перед тем как перейти к прикладным аспектам, давайте немного разберемся с путаницей в терминологии. Прежде всего, массив из структур IMAGE_IMPORT_DESCRIPTOR иначе как директорией импорта называть некорректно. Структура эта содержит ряд полей, среди которых есть OriginalFirstThunk и FirstThunk. Очень важно понимать состояние структур, на которые эти поля показывают. Структуры называются IMAGE_THUNK_DATA – их две!

 OriginalFirstThunk ? IMAGE_THUNK_DATA №1
 /*обратите внимение, этой структуры может не быть,
  точнее, адреса будут заполнены нулями – это можно считать багом
  борландовского линкера.
  MS утилита bind.exe отказывается обрабатывать такой файл,
  почему так – см. ниже*/
 
 FirstThunk ? IMAGE_THUNK_DATA №2

До загрузки образа в память OriginalFirstThunk и FirstThunk содержат RVA (которые, естественно, тоже различаются) на эти структуры, являющиеся ни чем иным, как таблицами адресов импорта (IATimport address table). Но и здесь уже тоже наизобретали велосипедов! IMAGE_THUNK_DATA на которую указывает OriginalFirstThunk ntdll.dll при загрузке в нормальных условиях не обрабатывается (хотя внутренняя функция ntdll.dll LdrpSnapIAT может читать и оттуда, см. ниже), а адреса в структуре, на которую указывает FirstThunk действительно меняются (патчатся лоадером), поэтому OriginalFirstThunk называют еще import lookup table (import name table), а FirstThunk – вот это уже настоящая IAT, для которой есть свой #define:

#define IMAGE_DIRECTORY_ENTRY_IAT            12

Была когда-то и СЕКЦИЯ импорта, звали .idata, но сейчас она, пожалуй, издохла. Во всяком случае, сейчас этот зверь – редкий. Разве что линкер специально попросите...

Забавно, что даже LordPE, написанный действительно талантливым программистом, и тот не избежал этого бага. Поглядите потом, что вытворяет! Кнопочка – Directories, а директорию импорта называет IAT, а offset вообще обалденный выдает! PETools зато отображает корректно и термины корректные.

Теперь рассмотрим саму структуру и манипуляции с полями. Итак:

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
     union {
 // 0 for terminating null import descriptor
         DWORD   Characteristics;
 // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
         DWORD   OriginalFirstThunk;
     };
     DWORD   TimeDateStamp; // 0 if not bound,
                            // -1 if bound, and real date\time stamp
                            // in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
                            // O.W. date/time stamp of DLL bound to (Old BIND)
     DWORD   ForwarderChain;// -1 if no forwarders, 
                            //положительное число в противном случае
 // RVA на имя dll
     DWORD   Name;
 // RVA to IAT (if bound this IAT has actual addresses)
     DWORD   FirstThunk;
 } IMAGE_IMPORT_DESCRIPTOR;

Перед тем, как начинать разбирать структуру, очень важно четко представлять, что в последнее время развелось много всяких наворотов на бедную IAT.

NTDLL.dll (функция LdrpWalkImportDescriptor) может читать IAT из:

 
 1)	OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT] -
 из IMAGE_IMPORT_DESCRIPTOR.OriginalFirstThunk,
 если удовлетворяются определенные условия.
 
 2)	OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IAT] –
 т.е., из IMAGE_IMPORT_DESCRIPTOR.FirstThunk - это стандарт
 
 3)	OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT]
  - все файлы Windows имеют такую привязку

Есть и 4-й #define:

#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT   13
 // Несмотря на #define в winnt.h определений полей структуры вы там не найдете.
 // Структура определена в delayimp.h в директории VC.
 // Малоизвестен тот факт, что линкер
 // от MS умеет также ассемблировать код!
 // Так вот, именно это здесь и используется.
 // В исполняемый файл просто-напросто запихиваются
 // дополнительные клочки кода. 

NTDLL.dll это не патчит, т.к. здесь уже используется совсем другой механизм. Фактически, это аналог LoadLibrary/GetProcAddress в одной упаковке. Более подробно - как всегда, Мэтт Питрек.

Здесь тоже понаворочено более, чем достаточно. Внутри Delay Load Directory Table есть стандартная таблица импорта, может выполняться привязка, т.е. возникать Bound Delay Import Table, есть Unload Delay Import Table и т.п. Мало не покажется!

LordPE понимает, что такое директория отложенного импорта. PE Tools пока выдает только VirtualAddress и Size (т.е. стандартные описания директории), HIEW (включая 6.85) вообще не умеет работать с директориями, IDA, хм.., IDA знает все.

Теперь у лоадера есть выбор – из какого же поля, собственно читать? Сначала обрабатывается цепочка директорий. Функция LdrpWalkImportDescriptor обрабатывает ТОЛЬКО директорию привязанного импорта, если VirtualAddress директории IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT не равен нулю, и временные пометки (TimeDateStamp), зашитые внутри модуля при его создании, соответствуют таковым в библиотеках, к которым этот модуль привязан (системные библиотеки Windows, dll приложения и т.п.).

В случае отсутствия IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (или невыполнения некоторых условий, например, загрузки экспортируемого модуля не по адресу ImageBase) читается уже стандартный IMAGE_IMPORT_DESCRIPTOR.
Так же уже не вполне ясна роль поля IMAGE_IMPORT_DESCRIPTOR.TimeDateStamp, которое может иметь три значения:

  1. 0 – стандартный IMAGE_DIRECTORY_ENTRY_IMPORT.
  2. ххххххххh – из IMAGE_IMPORT_DESCRIPTOR.FirstThunk, но с некоторыми оговорками, см. ниже.
  3. -1 – лоадер читает из IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT. Как уже говорилось, в этом случае лоадеру совершенно безразлично (при соблюдении определенных условий), есть ли IMAGE_DIRECTORY_ENTRY_IMPORT вообще или нет! Если попытка чтения BOUND_IMPORT проваливается в силу, например, несовпадения TimeDateStamp у exe файла и dll, к которым он привязан, то тогда читается стандартная IMAGE_DIRECTORY_ENTRY_IMPORT, но как при этом обрабатывается –1 – неясно. Во всяком случае, в коде LdrpWalkImportDescriptor НЕТ сравнения TimeDateStamp с -1, поэтому можно предположить, что достаточно существования IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT.

Теперь продолжим исследование. Начнем с поля Name. Это RVA на ASCIIZ-строку нужной библиотеки. Совершенно неоправданно на нее опираются во многих алгоритмах, где массив из структур IMAGE_IMPORT_DESCRIPTOR обрабатывается в цикле. С одной стороны да, если имя библиотеки не указано, то откуда изволите функцию импортировать, а с другой стороны – есть поле TimeDateStamp, которое может быть равно –1, точнее, есть директория BIAT (есть и такое название у директории привязанного импорта), и, если она присутствует, о старой директории импорта можно забыть! И можно вытворять, что душе угодно с массивом из IMAGE_IMPORT_DESCRIPTOR, лоадера это нисколько не волнует! Т.е. утилита, обрабатывающая такой файл, это дело должна учитывать, т.е. частично эмулировать работу лоадера. PE Tools это умеет. LordPE - тоже.

При TimeDateStamp == 0 мы имеем дело со старой доброй стандартной IAT. До загрузки OriginalFirstThunk и FirstThunk имеют одинаковую структуру (IMAGE_THUNK_DATA), которая физически продублирована в разных местах РЕ-файла. После загрузки FirstThunk содержит реальные адреса функций, OriginalFirstThunk остается такой же. Лоадер патчит адреса в FirstThunk, меняя атрибуты страницы на время выполнения этой операции.

При TimeDateStamp равном ххххххххh имеем интересный случай со «старой привязкой». Важно понимать, что в этом случае лоадер не делает ничего! Пара проверок на валидность и образ можно запускать! Добиться эффекта можно, запустив утилиту bind с ключом –o.

bind –o –u filename
 // без ключа -u изменения в образ записаны не будут,
 // если интересно, что
 // происходит - используйте ключ -v. В этом случае утилита просто
 // озвучит свои действия, но файл не изменит
 

Поле OriginalFirstThunk не затрагивается, а поле FirstThunk содержит настоящие адреса API-функций. Т.е. IMAGE_IMPORT_BY_NAME смысла более не имеет, т.к. DWORD в IMAGE_THUNK_DATA теперь, по сути, представляет адрес API-функции в памяти, а не ссылку на IMAGE_IMPORT_BY_NAME.

Импорт может просто привязываться, а может и происходить так называемый “API forwarding”, что можно перевести как "перенаправление API". Это очень изящная штука, которая помогает MS скрывать разницу между OS. Классический пример с HeapAlloc. Определена в kernel32.dll, но в действительности КОДА ее там и близко нет! Вместо этого вызов сразу перенаправляется на ntdll.dll, где упирается в функцию RtlAllocateHeap! Т.е. где-то так:

calc.exe -> kernel32.dll -> ntdll.dll
(call HeapAlloc) (немедленный форвард) (RtlAllocateHeap)
  (NTDLL.RtlAllocateHeap)  

Этого эффекта можно добиться, употребляя следующее выражение при компиляции dll:

#pragma comment(linker, "/export:HeapAlloc=NTDLL.RtlAllocateHeap")

Аналогичного эффекта можно добиться и с помощью def-файла:

EXPORTS
         HeapAlloc=NTDLL.RtlAllocateHeap

Exe-файл, использующий dll c таким редиректом, должен загружать ее динамически – через LoadLibrary/GetProcAddress(“HeapAlloc”) - если импорт по имени, либо по ординалу с помощью макроса MAKEINTRESOURCE. Если хотите линковать статически - тоже нет проблем. Код dll:

 
 #include <windows.h>
 
 BOOL APIENTRY DllMain(
 		HANDLE hModule,
 		DWORD  ul_reason_for_call,
 		LPVOID lpReserved)
 {
     return TRUE;
 }
 
 void F1(void)
 {
 	MessageBeep(0xFFFFFFFF);
 }
 
 

def-файл для dll (можно обойтись и без него, если использовать директиву pragma):

LIBRARY Forward
 EXPORTS F1 @1
 EXPORTS Hi = User32.MessageBoxA @2 NONAME

И, наконец, код exe, который будет использовать такую хитрую dll:

 
 #include <windows.h>
 
 extern "C" int __stdcall Hi(int, char*, char*, int);
 
 int main(void)
 {
 	Hi(0, "Lya-lya-lya", "Test", 0);
 	return 0;
 }
   

Обратите внимание, в самой dll нет и следа кода форварда, это не является необходимым. Директива Hi = User32.MessageBoxA @2 NONAME предписывает компилятору перенаправить вызов Hi, присвоить этой функции ординал номер два, и сделать так, чтобы в результирующей dll имени функции НЕ было (заметьте, имя функции сохранится в lib, но не будет существовать в dll). В exe файле две директивы extern "C" и __stdcall помогут нам избежать проблем с манглингом функций.

Файл с API-forwarding будет иметь таблицу экспорта с функцией, имеющей нестандартный RVA, значение которого не будет укладываться в лимит размера директории. Алгоритм обработки таких форвардов заложен в функцию LdrpSnapIAT и, как нам кажется, Russell в своей статье здесь допустил ошибку. Смотрите:

   NTSTATUS
 	LdrpSnapIAT
 ...
 {
 …
 		// there are forwarders
 		if (-1 != pImageImportDescriptor->ForwarderChain)
 		{
 			IATFirstThunkEntry =
 				pLoadingItem->ImageBase +
 				pImageImportDescriptor->FirstThunk +
 				(pImageImportDescriptor->ForwarderChain / 4);
 …
 
 }
   

Реальный кусочек кода из лоадера (Windows 2000, SP4):

.text:77F99897       mov     eax, [ecx+IMAGE_IMPORT_DESCRIPTOR.ForwarderChain]
 .text:77F9989A       cmp     eax, 0FFFFFFFFh ; -1
 .text:77F9989D       jz      no_forwarder
 .text:77F998A3       mov     ebx, [edi]
 .text:77F998A5       lea     edx, [ebx+eax*4]
 .text:77F998A8       mov     eax, [ecx]
 .text:77F998AA       add     eax, edx
 .text:77F998AC       mov     esi, [ecx+IMAGE_IMPORT_DESCRIPTOR.FirstThunk]
 .text:77F998AF       add     esi, edx

Т.е. не деление, а умножение! Для каждого IMAGE_IMPORT_DESCRIPTOR такой код вызывается единожды. ForwarderChain здесь является индексом первого форварда.

Продолжаем. Если файл привязан к dll, которая загружена по адресу, отличному от ImageBase, привязка перестает быть валидной. В этом случае лоадер будет читать IAT, если не валидна и она – образ загружен не будет.

Время загрузки файла немного уменьшается – это происходит за счет того, что NTDLL.dll уже не патчит адреса внутри модуля, все уже сделано.
При TimeDateStamp равном –1 (т.е. 0xFFFFFFFF) имеем новый стиль привязки.

Лоадер сначала проверяет IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT, а только уж потом обращается к IMAGE_IMPORT_DESCRIPTOR (если обращается!). Проведите простой эксперимент: привяжите файл – утилита bind –u filename, потом удалите ВСЕ содержимое IMAGE_IMPORT_DESCRIPTOR, или наполните его мусором. Запустите файл. Ну как? Еще раз повторим. Если не выполняется строгое равенство TimeDateStamp, если файл загружен не на ImageBase, если API адрес не найден и т.п, то мы опять читаем IAT. Поэтому bind отказывается производить привязку образов, не имеющих OriginalFirstThunk – если что-то пойдет не так, мы ведь не сможем восстановиться!

IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT имеет свою структуру:

typedef struct _IMAGE_BOUND_IMPORT_DESCRIPTOR {
     DWORD   TimeDateStamp;	//очень разумная мера!
 /*Откуда, например, лоадер может знать,
  а не изменилась ли версия dll, к которой привязаны адреса файла?
  Только сравнивая «временные штампы» библиотек.
  Если данный TimeStamp совпадает с таковым у библиотеки – все ОК,
  нет – тогда плохо - выставляется флажок ошибки,
  и лоадер начинает работу по чтению IID*/
     WORD    OffsetModuleName;
 //offset (а НЕ RVA!!!) на имя библиотеки от начала директории
     WORD    NumberOfModuleForwarderRefs;
 /*
  Заметьте, это поле – счетчик,
  указатель количества структур типа IMAGE_BOUND_FORWARDER_REF,
  которые следуют ПОСЛЕ данной структуры.
  Строение их абсолютно идентично таковому у IMAGE_BOUND_IMPORT_DESCRIPTOR
  за исключением поля NumberOfModuleForwarderRefs, которое зарезервировано
 */
 // Array of zero or more IMAGE_BOUND_FORWARDER_REF follows
 } IMAGE_BOUND_IMPORT_DESCRIPTOR,  *PIMAGE_BOUND_IMPORT_DESCRIPTOR;

Обратите внимание, что структуры IMAGE_BOUND_IMPORT_DESCRIPTOR и IMAGE_BOUND_FORWARDER_REF в файле перемешаны. Форвард означает, что API как бы «перенаправляется» в другую dll. В ту самую, где, собственно и определен. В этом случае первая dll служит как бы переходником, не более того.

«Живой» пример на нашем неизменном calc.exe выглядит так:

 
 ; 1-dec-1999 7:37:27
 HEADER:01000238 bound_import_directory dd 3844D037h
 HEADER:0100023C            dw offset на "SHELL32.dll"
 ;поле содержит 0х38, что прямиком отсылает на к 01000270 (0х38+0х38 == 0х70)
 ;в IDA жмем G, потом +38 и усе ?
 HEADER:0100023E            dw 0
 HEADER:01000240            dd 37F2C227h	; 30-sep-1999 1:51:35
 HEADER:01000244            dw offset на "MSVCRT.dll"
 HEADER:01000246            dw 0
 ...
 HEADER:01000266            dw 0
 HEADER:01000268 bound_import_directory_terminator dd 0
 HEADER:0100026C            dw 0
 HEADER:0100026E            dw 0
 HEADER:01000270 bound_SHELL32_dll   db 'SHELL32.dll',0
 HEADER:0100027C bound_MSVCRT_dll    db 'MSVCRT.dll',0
 HEADER:01000287 bound_ADVAPI32_dll  db 'ADVAPI32.dll',0
 HEADER:01000294 bound_KERNEL32_dll  db 'KERNEL32.dll',0
 HEADER:010002A1 bound_GDI32_dll     db 'GDI32.dll',0
 HEADER:010002AB bound_USER32_dll    db 'USER32.dll',0
   

Также обязательно необходимо учитывать, что перебазирование dll, к которой привязан данный исполняемый файл (приложение можно "привязать" и к своим библиотекам, bind это позволяет), также повлечет за собой чтение из "старой" директории импорта.

Теперь остановимся на одной грустной-грустной для нас с вами вещи. Положим, мы сдампили программу каким-нибудь дампером, например, LordPE. Положим, также, что некоторые опции утилиты были отключены. Что в этом случае у нас будет с директорией импорта? Предположим, имен и ординалов у нас нет!

Очевидно, файл будет рабочим только при соблюдении одного-единственного непреложного условия – все API-адреса в IAT (FirstThunk) должны быть строго валидными. Шаг в сторону – и такой файл можно просто выбросить. Или нет? Здесь мы подходим к понятию "восстановление таблицы импорта".

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


 /************************
  * Функция ищет старую таблицу импорта, и возвращает указатель
  * на первый IMAGE_IMPORT_DESCRIPTOR
  * Первый параметр это - указатель на память (Memory Mapped Files)
  * Второй параметр это - размер памяти
  *************************/
 
 // Процесс этот ресурсоемкий
 LPVOID FindOldImportTable(LPVOID pMem, DWORD dwSize)
 {
     CHAR *szStr; //Тут хранится имя модуля
     HMODULE hModule;
     PIMAGE_IMPORT_DESCRIPTOR pIID = {0};
 
     for(DWORD dwRVA = 0; dwRVA < dwSize; dwRVA++)
     {
         // Устанавливаем SEH, так как указатель pIID->Name,
         // иногда ведёт за пределы файла
     	  __try
         {
             // Получаем структуру IMAGE_IMPORT_DESCRIPTOR по адресу dwRVA
             pIID = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)pMem + dwRVA);
             // Если pIID->Name равно хоть чему нибудь, идём дальше
             if(pIID->Name)
             {
                 // Получаем имя модуля по этому адресу
                 szStr = (CHAR*)ImageRvaToVa(
 					        ImageNtHeader(pMem),
 					        pMem, pIID->Name,
 					        NULL);
                 if(!IsBadStringPtr(szStr, 40))
                 {
                     // Если строка больше 4-х символов, идём дальше.
                     // Почему 4-м? Потому, что ".dll"
                     if(lstrlen(szStr) > 4)
                     {
                         // Загружаем Dll, если загрузилась идём дальше
                         hModule = LoadLibrary(szStr);
                         if(hModule != NULL)
                         {
                             FreeLibrary(hModule); // Выгружаем Dll
 			    // Небольшая проверка на валидность структуры
 			    if(pIID->TimeDateStamp == 0xFFFFFFFF ||
 			    pIID->TimeDateStamp == 0x00000000)
 	                    // Возвращаем указатель
 	                    // на первый IMAGE_IMPORT_DESCRIPTOR
                             return pIID;
                         }
                     }
                 }
             }
         }
         __except(EXCEPTION_EXECUTE_HANDLER)
         {
             // "Access violation" однако. Все равно, продолжаем...
             continue;
         }
     }
 
     return NULL;
 }
 
 /************************
  * Функция делает простой rebuild, и возвращает размер новой таблицы импорта
  * Первый параметр это – указатель на память (Memory Mapped Files)
  * Второй параметр это - адрес возвращённый функцией FindOldImportTable
  *************************/
 
 DWORD RebuildIT(LPVOID pMem, LPVOID pStart)
 {
 	PIMAGE_DOS_HEADER         pDos = {0};
 	PIMAGE_NT_HEADERS         pNT = {0};
 	PIMAGE_IMPORT_DESCRIPTOR  pIID = {0};
 	DWORD                     dwITSize = sizeof(IMAGE_IMPORT_DESCRIPTOR);
 
 	pDos = (PIMAGE_DOS_HEADER)pMem;
 	pNT = (PIMAGE_NT_HEADERS)((DWORD)pMem + pDos->e_lfanew);
 	pIID = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)pStart);
 
 	if(!pIID)
 		return 0;
 
 	while (pIID->Name)
 	{
 		dwITSize += sizeof(IMAGE_IMPORT_DESCRIPTOR);
 		pIID->TimeDateStamp  = 0x00000000;
 		pIID->ForwarderChain = 0x00000000;
 		++pIID;
 	}
 
 	return dwITSize;
 }
 
 //Пример использования:
 
 DWORD  dwMapSize; // Размер памяти
 LPVOID m_pMap;    // Указатель на память (Memory Mapped Files)
 PIMAGE_DOS_HEADER pDos = {0};
 PIMAGE_NT_HEADERS pNT = {0};
 
 ...
 
 // Ищем старую таблицу импорта
 IMAGE_IMPORT_DESCRIPTOR* pIID=
 (PIMAGE_IMPORT_DESCRIPTOR)FindOldImportTable(m_pMap, dwMapSize);
 if(pIID)
 {
       // Если найдена, делаем rebuild
 	DWORD dwSize = RebuildIT(m_pMap, pIID);
 	if(dwSize > 0)
 	{
 		pDos = (PIMAGE_DOS_HEADER)m_pMap;
 		pNT = (PIMAGE_NT_HEADERS)((DWORD) m_pMap + pDos->e_lfanew);
             	// Устанавливаем RVA таблицы
    pNT->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress
                   = (DWORD)pIID - (DWORD)m_pMap;
 	        // Устанавливаем размер таблицы
    pNT->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size
                   = dwSize;
 	}
 }
 
 ...
   

Заметим, что данный алгоритм будет работать только на самых простых пакерах (например PECompact), с более продвинутыми, работать уже НЕ будет. Пошла сейчас мода коверкать директорию импорта. И здесь пакеры потихоньку выигрывают, т.к. искалечить всегда легче, чем восстановить. Никаких новых алгоритмов и идей здесь более обсуждаться не будет, т.к. это знание пойдет только во вред ибо может быть использовано авторами пакеров. К нашему глубокому сожалению в сети существуют некоторые проекты по восстановлению директории импорта с открытым исходным кодом, однако это большой недостаток. Уж лучше иметь работающий ImpRec, который худо-бедно но обновляется, чем открытый исходный код с устаревшей, бесполезной идеей.

Теперь рассмотрим, как работать со структурами PE-файла в IDA.

Простите за банальность – IDA – это очень мощный дизассемблер. Практически все операции идут под «капотом». Дизассемблировав PE-файл, вы даже не увидите его заголовков, IDA автоматически прогуляется по всей таблице импорта, все сделано за вас. Это и хорошо, и плохо. Хорошо это тогда, когда нет ни времени, ни желания ковыряться в секциях, просто быстро поломать. Плохо это тогда, когда попадается хитро подправленный файл, который IDA в автоматическом режиме взять не может.

Для примера давайте быстро вручную пробежимся по структурам секции импорта calc.exe. Для начала нам надо найти начало секции таблицы импорта. Итак, быренько запускаем какой-нибудь PE-редактор, например, PE Tools. В PE Editor давим кнопочку Directories. Как вы помните, это массив из #define, где секция импорта идет второй

#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 

Ее RVA - 12A40. ImageBase файла – OptionalHeader.ImageBase01000000, следовательно, в IDA давим G, потом вводим 1012A40 (01000000 + 12A40). Вот прямо с этого места у нас и начинается массив из структур типа IMAGE_IMPORT_DESCRIPTOR.

Выглядит так:

.text:01012A40                 db 0C0h ; L
 .text:01012A41                 db  2Bh ; +
 .text:01012A42                 db    1 ;
 .text:01012A43                 db    0 ;
 .text:01012A44                 db 0FFh ;
 .text:01012A45                 db 0FFh ;
 .text:01012A46                 db 0FFh ;
 ...

Обратите внимание на название директории еще раз. Директория будет располагаться внутри секции. Более того, ничто не мешает слить большинство секций файла в одну. Однако мы отвлеклись.

Для того чтобы указать IDA, что тут у нас структура и надо бы преобразовать поля, лезем в Structures. Там создаем Ins структуру тютелька в тютельку по документации из winnt.h:

0000 IMAGE_IMPORT_DESCRIPTOR struc
 ; (sizeof=0x14, standard type)
 0000 OriginalFirstThunk dd ?
 0004 TimeDateStamp   dd ?
 0008 ForwarderChain  dd ?
 000C Name            dd ?
 0010 FirstThunk      dd ?
 0014 IMAGE_IMPORT_DESCRIPTOR ends

Далее остается применить эту структуру к смещению 1012A40. Примерно так:

.text:01012A40             dd 12BC0h               ; OriginalFirstThunk
 .text:01012A40             dd 0FFFFFFFFh           ; TimeDateStamp
 .text:01012A40             dd 0FFFFFFFFh           ; ForwarderChain
 .text:01012A40             dd 12CE6h               ; Name
 .text:01012A40             dd 10F4h                ; FirstThunk

И так до тех пор пока не попадется IMAGE_IMPORT_DESCRIPTOR, ВСЕ поля которого содержат нули. Аналогичную песню с определением новых структур придется спеть и для IMAGE_THUNK_DATA (а, в случае TimeDateStamp == 0 и для IMAGE_IMPORT_BY_NAME) и т.д., и т.п. А может и не придется! Ведь IDA хранит список стандартных структур! Поэтому, Add struct type > Add standard structure > Search. Но неплохо бы это все дело автоматизировать еще больше! Что ж, не мы с вами такие ленивые. Топаем на datarescue.com/idabаse/idadown.htm и сливаем оттуда PE utilities от Atli Mar Gudmundsson. Необходимо поместить их в %IDADIR%\idc, а потом F2 и файл pe_sections.idc. Если до этого вы не просто читали, а и пытались что-то делать ручками, то будете приятно удивлены.

2.4 Директория ресурсов

Очевидно, что в этой директории программисты хранят иконки, кнопочки, и прочие маленькие радости пользователя. Уже упоминалось, что директория ресурсов обязательно должна размещаться в секции .rsrc, причем имя секции должно быть в точности таким – ".rsrc". Причины уже объяснялись.

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

Взгляните:

 
 typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {
     union {
         struct {
             DWORD NameOffset:31;		//сюда
 //Обратите внимание - offset от начала секции.
             DWORD NameIsString:1;
         };
 ...
 } IMAGE_RESOURCE_DIRECTORY_ENTRY, *PIMAGE_RESOURCE_DIRECTORY_ENTRY;
 
 typedef struct _IMAGE_RESOURCE_DATA_ENTRY {
     DWORD   OffsetToData;			//и вот сюда
 // Несмотря на название - это самый типичный RVA 
     DWORD   Size;
     DWORD   CodePage;
     DWORD   Reserved;
 } IMAGE_RESOURCE_DATA_ENTRY, *PIMAGE_RESOURCE_DATA_ENTRY;
 
   

Здесь хотелось бы процитировать Randy Kath с его статьей "The Portable Executable File Format from Top to Bottom": "The two fields OffsetToData and Size indicate the location and size of the actual resource data. Since this information is used primarily by functions once the application has been loaded, it makes more sense to make the OffsetToData field a relative virtual address. This is precisely the case. Interestingly enough, all other offsets, such as pointers from directory entries to other directories, are offsets relative to the location of the root node."

Да, справедливо то, что полностью уничтожить директорию нельзя и теперь мы знаем почему. Однако, положим, вам пришлось проделать некие хитрые манипуляции с вашим конкретным файлом, например, после распаковки убрать более ненужные секции упаковщика. ОК, вы их убрали и лишились ресурсов, т.к. RVA перестал быть валидным! Значит, надо ресурсы восстановить, т.е. пересчитать смещения. Для этой цели у нас популярным средством является Resource Rebuilder v1.0 by Dr.Golova, а у зарубежных коллег, пожалуй, PE Rebuilder 0.96. О сугубо практическом применении можно почитать здесь - http://xtin.org. Статья - Отрезание секций, перемещение ресурсов, автор Hex.

Восстановление ресурсов, к счастью, не самая сложная процедура. Вкратце, основные принципы таковы:



Обсуждение статьи: Об упаковщиках в последний раз: Часть первая - теоретическая >>>


При перепечатке ссылка на https://exelab.ru обязательна.



Видеокурс ВЗЛОМ