Низкоуровневое программирование для Дzenствующих

Вид материалаДокументы

Содержание


2.4 Директория ресурсов
DWORD NameOffset:31
Image_resource_data_entry, *pimage_resource_data_entry
MZ поле e_lfanew
2.5 Директория перемещаемых элементов
DWORD-ами, смещаясь на один байт в цикле). 2) Из полученного значения отнимаем OptionalHeader.ImageBase
Подобный материал:
1   ...   4   5   6   7   8   9   10   11   ...   42

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. О сугубо практическом применении можно почитать здесь - ссылка скрыта. Статья - Отрезание секций, перемещение ресурсов, автор Hex.

Восстановление ресурсов, к счастью, не самая сложная процедура. Вкратце, основные принципы таковы:
  1. Получаем из PE заголовка секцию с именем “.rsrc”.
  2. Считывается дерево каталогов. Сами данные ресурсов пока можно и не считывать, а считать их на стадии создания новой секции ресурсов.
  3. Создаём новую секцию ресурсов.
    1. Подсчитываем необходимый размер будущей секции.
    2. Далее записываем все ресурсы (каталоги/имена/данные) в эту секцию.
    3. Затем делается ещё один проход и корректируем все смещения в каталогах.
  4. Корректируем PE заголовок, если это необходимо.

Здесь алгоритма не будет, так как он очень простой, но большой.

Так же стоит помнить, что и в памяти пакерам не следует грохать заголовок PE-файла (во всяком случае, определенную его часть), т.к. ресурсные API, например, LoadResource, просто перестанут работать. Вероятно, ситуацию можно свести к следующему предположению: если хендл чего-то из PE-файла был получен, то далее эту часть PE-заголовка можно убирать, чтобы затруднить нам жизнь.

Словом, пакеры оставляют все сигнатуры, MZ поле e_lfanew, OptionalHeader.DataDirectory[x] и т.п., но часть жизненно важных данных может быть искажена. Поэтому лучше не играть в кошки-мышки, а просто-напросто в опциях дампера (PE Tools, LordPE) активировать режим вставки заголовка файла с диска (Full Dump: Paste header from disk).

2.5 Директория перемещаемых элементов


Теперь, в довесок к изуродованию директории импорта, секции кода, директории ресурсов и т.п., появляется возможность калечить директорию перемещаемых элементов (relocations, fixups). Например, такой фокус производит UPX (но там открыт исходный код, следовательно, нужно ожидать такого подарочка от многих пакеров в скором времени).

Информация из директории перемещаемых элементов применяется тогда, когда адрес загрузки файла отличается от ImageBase. Для ехе-файлов такое встречается достаточно редко (но не так, чтобы и очень – вспомните VTune), а для dll – это норма. Так вот, директория перемещаемых элементов содержит информацию о том, как патчить данные, расположенные по указанному адресу, чтобы было возможно нормальное продолжение работы. В противном случае, операнд инструкции будет указывать в "космос".

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

typedef struct _IMAGE_BASE_RELOCATION {

//с этого адреса лоадер должен начинать падчить адреса

DWORD VirtualAddress;

//размер fixup-блока для данной страницы памяти

DWORD SizeOfBlock;

// WORD TypeOffset[1]; //тип фиксапа

} IMAGE_BASE_RELOCATION;

Давайте разберем на живом примере, ЧТО может быть подвержено перемещению и как с этим работать. Итак, создадим простейший файл:


#include


void main(void)

{

char hello[] = "Name";


printf ("%s", hello);

}


Только попросим при этом линкер создать исполняемый файл с опцией /FIXED:NO, что означает автоматическое создание директории перемещаемых элементов (как правило, это секция .reloc, хотя и не обязательно). Теперь дизассемблируем в IDA, применяем скрипт от Atli Mar Gudmundsson и смотрим:

loc:1002B000 ; DATA XREF: HEADER:100000D8o

; HEADER:10000298o


.reloc:1002B000 relocs_0001_page dd 11000h

.reloc:1002B004 relocs_0001_size dd 54h

.reloc:1002B008 relocs_0001_toff dw 3A4Fh, 3A58h, 3A64h, 3A78h, 3A9Bh,

...

; ограничено VirtualSize секции

Итак, поле relocs_0001_page, очевидно, соответствует полю VirtualAddress (напоминаем, что релоки применяются к виртуальным страницам, отсюда и слово page), relocs_0001_size - SizeOfBlock, relocs_0001_toff - чуть поинтереснее. Как видим, это массив из word, где старшие четыре бита - это тип релока (в даном случае - #define IMAGE_REL_BASED_HIGHLOW 3), а оставшиеся 12 надо сложить с VirtualAddress, чтобы получить RVA данных для обработки. Ну-ка, давайте посмотрим, куда указывает самое первый word из массива - 3А4F. Итак, A4F + 11000:


.text:10011A4E mov eax, dword ptr ds:aName ; "Name"

.text:10011A53 mov [ebp-0Ch], eax

.text:10011A56 mov cl, byte ptr ds:aName+4

.text:10011A5C mov [ebp-8], cl

.text:10011A5F lea eax, [ebp-0Ch]

.text:10011A62 push eax

.text:10011A63 push offset aS_0 ; "%s"

.text:10011A68 call j__printf


Ба! Да прямо в центр инструкции mov, т.е. адрес начала массива &hello[0] подвержен обработке! И так много чего. Релокейшенам подвержены строковые литералы, статические переменные, ссылки на неинициализированные статические данные, абсолютные call/jmp, да мало ли что еще! Словом, кошмар!

Релоки обрабатываются лоадером при загрузке – функция - LdrRelocateImage. Обработка релоков происходит перед обработкой таблицы импорта (не всегда!). Функция LdrpMapDll зовет LdrRelocateImage, которая, в свою очередь, определяет заголовок файла, вычисляет где расположена директория релоков и зовет LdrProcessRelocationBlock.

LdrRelocateImage вызывается когда ZwMapViewOfSection (бывшая NtMapViewOfSection) возвращает STATUS_CONFLICTING_ADDRESSES. В случае загрузки по адресу, отличному от ImageBase модуль загружается полностью (обратите внимание, пакер в любом случае обязан пройтись по всем страницам, поэтому запакованный модуль действительно загружается полностью), релоки накладываются сразу, и потом секция с релоками выгружается из памяти (флаг секции IMAGE_SCN_MEM_DISCARDABLE) в своп и в дальнейшем используются только от туда, т.к. из dll Windows более ничего не читает. Что-то отдаленно похожее алгоритмически можно найти в Wine - функция map_image из файла virtual.c. Реальный код из лоадера выглядит так (Win 2000 SP4):


again:

mov ecx, [eax+IMAGE_BASE_RELOCATION.SizeOfBlock]

lea edx, [eax+_IMAGE_BASE_RELOCATION.TypeOffset]

sub [ebp+HMODULE], ecx

; RtlImageDirectoryEntryToData перетирает это значение.

; Теперь оно содержит размер всех релоков

; эквивалентно вычитанию восьми

add ecx, 0FFFFFFF8h

; -IMAGE_BASE_RELOCATION = sizeof(Type/Offset entries)

push ebx ; Bias

push edx ; Type/Offset entries

shr ecx, 1 ; Each Type/Offset entry is a word

push ecx ; Number of Type/Offset entries

mov ecx, edi ; HMODULE

;т.е. прибавить RVA

add ecx, [eax+IMAGE_BASE_RELOCATION.VirtualAddress]

push ecx ;т.е. имеем VA

call _LdrProcessRelocationBlock@16

test eax, eax ; указатель на следующий блок либо ноль

jz error

cmp [ebp+HMODULE], 0

jnz short again


Как вы видите, будут обработаны все блоки перемещаемых элементов, а уж есть ли релоки во ВСЕХ секциях модуля – это совсем другая песня. Сам алгоритм LdrProcessRelocationBlock достаточно тривиален и очень хорошо расписан во многих источниках (ссылка скрыта, ссылка скрыта и т.п.), где есть исходники и на C/C++ и на Delphi.

Заметим, что применение релоков чревато не только увеличением времени загрузки, а и частыми page fault - вследствие применения релоков к секции получаем ее копию в памяти - механизм copy-on-write - подробнее MSDN - Ruediger R. Asche - Rebasing Win32 DLLs: The Whole Story.

Итак, мы имеем еще один неутешительный вывод - после применения релоков пакер может эту директорию смело убирать, что он и делает.

Восстановление relocation directory – дело непростое. Стандартной техникой со времен DOS считалась загрузка файла по двум разным базовым адресам, и поиск различий в них (например, по этому принципу работает утилита ReloX от mackT). Для загрузки одного и того же файла его достаточно переименовать, иначе лоадер откажется загружать файл повторно (LdrpCheckForLoadedDll). Заметим, что директория перемещаемых элементов все равно сохраняется под пакером, пусть и в его формате, следовательно, нет принципиальных сложностей в загрузке модуля по двум разным базовым адресам.

Однако в PE Tools используется совершенно безумный алгоритм статического анализа файла, изобретенный NEOx'ом после нескольких бутылок пива (водки?). Итак, основной принцип восстановления релоков – это поиск предположительных ссылок, которые в той или иной степени могут быть подвержены релокам. Ссылки находятся следующим образом:

1) Сканируем все секции от начала до конца (читать данные DWORD-ами, смещаясь на один байт в цикле).

2) Из полученного значения отнимаем OptionalHeader.ImageBase (это нужно для того, чтобы вычислить адрес ссылки)

dwOpcode -= pPeh->OptionalHeader.ImageBase;

3) Проверить является ли этот адрес ссылкой, а не кодом. Проверяем – если полученное значение больше OptionalHeader.BaseOfCode, но меньше OptionalHeader.SizeOfImage, значит это ссылка, если нет – читаем следующий DWORD.

if(dwOpcode > pPeh->OptionalHeader.BaseOfCode)

if(dwOpcode < pPeh->OptionalHeader.SizeOfImage)

{

...

//здесь тоже не все так просто!

}

4) На основе этих данных генерируем новые релоки.