В статье рассматривается механизм хранения иконок
в ресурсах EXE и DLL-файлов
Многих интересует вопрос: как хранятся иконки в exe-файлах... меня тоже
заинтересовал, а результат моего интереса ты сейчас читаешь. Корень всего - это
секция ресурсов.
Поскольку с английским у меня не очень, то переводить "The Portable
Executable File Format from Top to Bottom" не очень и хотелось. А в рунете
информацию приходилось доставать "по каплям". Начнем с каталога данных (лежит
сразу за концом опционального заголовка). Третий элемент содержит RVA и размер
директории ресурсов. Но... Windows игнорирует запись в IMAGE_DATA_DIRECTORY и
ждет, что иконки и остальные ресурсы хранятся в секции ".rsrc"(так утверждает
Randy Kath в вышеупомянутом английском мануале, но я проблем не заметил при
работе с распакованным файлом, в котором были затерты все имена секций)! При
этом RVA этой секции и RVA третьего элемента каталога данных "волшебным" образом
совпадают.
Директория ресурсов начинается со следующей структуры
IMAGE_RESOURCE_DIRECTORY:
[Смещение Тип Имя_Поля ]
________________________________________________
[$00 dword Characteristics ]
[$04 dword TimeDateStamp ]
[$08 word MajorVersion ]
[$0A word MinorVersion ]
[$0C word NumberOfNamedEntries ]
[$0E word NumberOfIdEntries ]
------------------------------------------------------------------------
Поле Characteristics зарезервировано и не используется(хоть и получило имя).
Поле TimeDateStamp - дата и время в формате UNIX подключения ресурсов ресурсным
компилятором.
Поле MajorVersion - старшая версия компилятора ресурсов.
Поле MinorVersion - младшая версия компилятора ресурсов.
Поле NumberOfNamedEntries - содержит кол-во имен в массиве структур
IMAGE_RESOURCE_DIRECTORY_ENTRY.
Поле NumberOfIdEntries - содержит кол-во идентификаторов ресурсов.
Из всех этих полей обязательными является только 2 (NumberOfNamedEntries и
NumberOfIdEntries), а остальные могут быть забиты нулями.
Вот реальный пример:
[смещение: данные]//смещение относительно секции ".rsrc"
|00: 00 00 00 00|00 00 00 00|00 00|00 00|00 00|0A 00|
|10: ... |
Здесь все поля, кроме NumberOfIdEntries забиты нулями. NumberOfIdEntries = $000A
Сразу же за этой структурой идет массив структур
IMAGE_RESOURCE_DIRECTORY_ENTRY:
[Смещение Тип Имя_Поля ]
________________________________________________
[$00 dword Name ]
[$04 dword OffsetToData ]
------------------------------------------------------------------------
Размер массива - сумма значений в полях
IMAGE_RESOURCE_DIRECTORY.NumberOfNamedEntries и
IMAGE_RESOURCE_DIRECTORY.NumberOfIdEntries. В нашем примере структур всего
10($0A+$00).
Тут следует сделать отступление. Вообще, ресурсы - это двоичное отсортированное
дерево и их структура позволяет содержать 2^31 уровней. Но используются только
первые 3:
-Type
-Name
-Language
(тип, имя, язык)
Итак, сразу же за структурой IMAGE_RESOURCE_DIRECTORY идет первый уровень этого
дерева, т.е. Type.
Кол-во "ветвей" на этом уровне мы уже знаем. В конце массив дополняется пустой
структурой IMAGE_RESOURCE_DIRECTORY_ENTRY (кто знаком с импортом сразу узнает
этот механизм).
IMAGE_RESOURCE_DIRECTORY_ENTRY имеет следующее назначение полей:
Поле Name может принимать 2 значения в зависимости от старшего бита. Если
старший бит установлен в 1, то это поле является смещением имени ресурса
относительно начала секции ".rsrc"(ВАЖНО!!!), если сброшено в 0, то это
идентификатор ресурса.
Поле OffsetToData так же зависит от старшего бита и является смещением от
первого IMAGE_RESOURCE_DIRECTORY_ENTRY на следующий уровень двоичного дерева
ресурсов, если старший бит установлен в 1, иначе является смещением относительно
НАЧАЛА СЕКЦИИ ".rsrc"(ВАЖНО!!!) на структуру IMAGE_RESOURCE_DATA_ENTRY(будет
описана ниже).
ЗАМЕТКА: кстати, естественно, при отсчете смещений необходимо сбрасывать
идентифицирующий старший бит в 0!
Реальный пример:
[смещение:данные]//смещение относительно секции ".rsrc"
|00: 00 00 00 00|00 00 00 00|00 00|00 00|00 00|0A 00|//IMAGE_RESOURCE_DIRECTORY
|10: 02 00 00 00|60 00 00 80|03 00 00 00|A0 00 00 80|//начало
IMAGE_RESOURCE_DIRECTORY_ENTRY
|20: 04 00 00 00|70 02 00 80|05 00 00 00|A8 02 00 80|
|30: 06 00 00 00|48 03 00 80|0E 00 00 00|88 03 00 80|
|40: 10 00 00 00|40 04 00 80|18 00 00 00|58 04 00 80 |
|50: F0 00 00 00|70 04 00 80|F1 00 00 00|88 04 00 80|
|60: 00 00 00 00|00 00 00 00|.. .. .. .. |.. .. .. .. |//пустая структура
IMAGE_RESOURCE_DIRECTORY_ENTRY
Как видно все правильно: $000A(10) структур IMAGE_RESOURCE_DIRECTORY_ENTRY +
одна пустая.
Уже было сказано, что это уровень типа(Type).
Разберем первую структуру.
Name = $00000002
OffsetToData = $80000060
Видим, что у значения поля Name сброшен старший бит(=0), следовательно никакого
имени здесь нет и это идентификатор(здесь тип) ресурса.
Каждый идентификатор имеет свое значение. И применимо к первому
IMAGE_RESOURCE_DIRECTORY_ENTRY - это 2.
В Delphi типы ресурсов перечислены в Windows.pas(в Си WINUSER.H) и определены
следующим образом:
const
{ Predefined Resource Types }
{$EXTERNALSYM RT_CURSOR}
RT_CURSOR = MakeIntResource(1);
{$EXTERNALSYM RT_BITMAP}
RT_BITMAP = MakeIntResource(2);
{$EXTERNALSYM RT_ICON}
RT_ICON = MakeIntResource(3);
{$EXTERNALSYM RT_MENU}
RT_MENU = MakeIntResource(4);
{$EXTERNALSYM RT_DIALOG}
RT_DIALOG = MakeIntResource(5);
{$EXTERNALSYM RT_STRING}
RT_STRING = MakeIntResource(6);
{$EXTERNALSYM RT_FONTDIR}
RT_FONTDIR = MakeIntResource(7);
{$EXTERNALSYM RT_FONT}
RT_FONT = MakeIntResource(8);
{$EXTERNALSYM RT_ACCELERATOR}
RT_ACCELERATOR = MakeIntResource(9);
{$EXTERNALSYM RT_RCDATA}
RT_RCDATA = Types.RT_RCDATA; //MakeIntResource(10);
{$EXTERNALSYM RT_MESSAGETABLE}
RT_MESSAGETABLE = MakeIntResource(11);
{$EXTERNALSYM DIFFERENCE}
DIFFERENCE = 11;
{$EXTERNALSYM RT_GROUP_CURSOR}
RT_GROUP_CURSOR = MakeIntResource(DWORD(RT_CURSOR + DIFFERENCE));
{$EXTERNALSYM RT_GROUP_ICON}
RT_GROUP_ICON = MakeIntResource(DWORD(RT_ICON + DIFFERENCE));
{$EXTERNALSYM RT_VERSION}
RT_VERSION = MakeIntResource(16);
{$EXTERNALSYM RT_DLGINCLUDE}
RT_DLGINCLUDE = MakeIntResource(17);
{$EXTERNALSYM RT_PLUGPLAY}
RT_PLUGPLAY = MakeIntResource(19);
{$EXTERNALSYM RT_VXD}
RT_VXD = MakeIntResource(20);
{$EXTERNALSYM RT_ANICURSOR}
RT_ANICURSOR = MakeIntResource(21);
{$EXTERNALSYM RT_ANIICON}
RT_ANIICON = MakeIntResource(22);
Пугаться MakeIntResource не следует. Это Си заглушка, которая ничего не
делает с тем параметром, который принимает (вообще такие приколы характерны для
Си - чего стоят только пустые макросы IN и OUT).
Теперь, хорошо видно, что Name соответствует тип RT_BITMAP(директория
картинок).
Кстати, если хочешь увидеть имя уже на первом уровне, то загляни HEX-редактором
в ресурсы какой-нибудь системной библиотеки. Например, shell32.dll.
С типом разобрались, на очереди OffsetToData. Снова смотрим на старший бит...
Опа! Установлен(еще бы он был не установлен ;) )! Ок. Сбрасываем его, получаем
OffsetToData = $00000060. Встаем на смещение $10 от начала секции и прыгаем...
Стоп! Стоять на месте! Мы же об иконках говорим! Найдем среди массива
IMAGE_RESOURCE_DIRECTORY_ENTRY тип с идентификатором RT_ICON(подсказка: далеко
идти не придется - дерево же отсортировано).
Проверяем все ли на месте. Тип = 3. Ок. Старший бит OffsetToData установлен! Ок!
Сбрасываем бит($000000A0) и прыгаем вперед на $A0 байтов с вышеозначенного
смещения.
[смещение: данные]//смещение относительно секции ".rsrc"
|00: 00 00 00 00|00 00 00 00|00 00|00 00|00 00|0A 00|//IMAGE_RESOURCE_DIRECTORY
|10: ... |//пропущено
|B0: 01 00 00 00|30 05 00 80|.. .. .. .. |.. .. .. .. |//сюда прыгнули
Попали на следующий уровень(Имя). И это все тоже двоичное отсортированное
дерево! Всё та же структура IMAGE_RESOURCE_DIRECTORY_ENTRY. И конец этой "ветви"
можно узнать, сканируя уровень имени в поисках пустой
IMAGE_RESOURCE_DIRECTORY_ENTRY. К сожалению, нам немного не повезло и в место
имени мы опять видим только идентификатор(кстати, для примера я случайно... ну,
правда, случайно :) выбрал exe-файл, в котором не было ни одного имени
ресурса... все на идентификаторах! вот невезуха!). Правда, сейчас уже это
идентификатор самой иконки. А позже мы поймем, почему здесь вообще не может быть
имени.
ЗАМЕТКА: раз уж не удалось увидеть имя ресурса, сообщу, что имя храниться
UNICODE-строкой.
Продвигаемся дальше. Отлично! Следующий уровень! Теперь переходим на offset
$00000530 и видим (вернее не видим)... Ура! Наконец-то мы уже не видим ни одного
старшего бита и готовимся постичь ДАО новой структуры :)
[смещение: данные]//смещение относительно секции ".rsrc"
|0000: 00 00 00 00|00 00 00 00|00 00|00 00|00 00|0A
00|//IMAGE_RESOURCE_DIRECTORY
|0010: ... |//пропущено
|0540: 09 04 00 00|E0 0F 00 00|.. .. .. .. |.. .. .. .. |//сюда прыгнули
Сразу скажу, что на этом уровне я не разбирался со значением поля
Name(кстати, это все та же IMAGE_RESOURCE_DIRECTORY_ENTRY). Гораздо более
интересно поле OffsetToData. По видимым признакам это офсет на структуру,
которая описывает непосредственно сам ресурс(а мы ее чуть позже опишем).
Еще хорошо бы взять себе за правило в таких случаях:
IMAGE_RESOURCE_DIRECTORY_ENTRY.Name:
старший бит установлен - смещение от ".rsrc" и это имя
старший бит сброшен - это идентификатор
IMAGE_RESOURCE_DIRECTORY_ENTRY.OffsetToData:
старший бит установлен - смещение от ".rsrc"+$10 на следующий уровень
старший бит сброшен - смещение от ".rsrc" на IMAGE_RESOURCE_DATA_ENTRY
[смещение: данные]//смещение относительно секции ".rsrc"
|0000: 00 00 00 00|00 00 00 00|00 00|00 00|00 00|0A
00|//IMAGE_RESOURCE_DIRECTORY
|0010: ... |//пропущено
|0FE0: C0 66 02 00|28 01 00 00|00 00 00 00|00 00 00 00|//сюда прыгнули
Теперь опишем структуру IMAGE_RESOURCE_DATA_ENTRY:
[Смещение Тип Имя_Поля ]
__________________________________________
[$00 dword OffsetToData ]
[$04 dword Size ]
[$08 dword CodePage ]
[$0C dword Reserved ]
---------------------------------------------------------------
Поле OffsetToData - это RVA(Relative Virtual Address) ресурса. Сложите этот
адрес с ImageBase и получите VA(Virtual Address) ресурса в памяти (при загрузке
модуля - приложения, DLL, etc).
Кто забыл - напомню, как получить смещение относительно начала файла в "сыром"
виде. Сначала определите, в какой секции PE-файла находится ресурс(section.VirtualAddress<IMAGE_RESOURCE_DATA_ENTRY.OffsetToData<section.VirtualAddress+section.VirtualSize).Возьмите
"сырое" смещение этой секции в файле(section.PointerToRawData).Сложите с этим
"сырым" смещением разницу между IMAGE_RESOURCE_DATA_ENTRY.OffsetToData и
section.VirtualAddress(IMAGE_RESOURCE_DATA_ENTRY.OffsetToData -
section.VirtualAddress).
В итоге "сырой" офсет ресурса от начала файла должен высчитываться так:
raw_resource_offset:=section.PointerToRawData+(IMAGE_RESOURCE_DATA_ENTRY.OffsetToData
- section.VirtualAddress);
Реальный пример:
raw_resource_offset:=$24000+($266C0-$25000)=$256C0
Это было отступление.
Поле Size, конечно, определяет размер ресурса. У нас это должен быть размер
иконки(почему "должен" объясню позже).
Поле CodePage я детально не исследовал, но тут название говорит само за себе.
Более того к иконкам оно вообще не применимо и сбрасывается в ноль.
Поле Reserved всегда ноль.
Теперь берем любой HEX-редактор (я предпочитаю WinHex), открываем исследуемый
файл, переходим на вычисленный raw_resource_offset, копируем
IMAGE_RESOURCE_DATA_ENTRY.Size байт, создаем новый файл, записываем туда
скопированные байты, сохраняем с расширением .ico и пытаемся открыть любым
графическим просмотрщиком... А вот на этом месте слышим только ту сокровенную
фразу, которая пронеслась в кинотеатре в конце первой части "Параграф 78"("Нае*али!!!").
Спокуха! Сейчас все будет!
Если внимательно посмотреть на список типов, которые приводились выше, можно
заметить еще одно упоминание об иконках -
RT_GROUP_ICON=RT_ICON(3)+DIFFERENCE(11). И того $0E!
Ищем на первом уровне дерева(Type) этот идентификатор. Найдя, переходим на
следующий уровень.
[смещение: данные]//смещение относительно секции ".rsrc"
|0000: 00 00 00 00|00 00 00 00|00 00|00 00|00 00|0A
00|//IMAGE_RESOURCE_DIRECTORY
|0010: ... |//пропущено
|0398: 64 00 00 00|28 0D 00 80|.. .. .. .. |.. .. .. .. |//сюда прыгнули
Еще хочется заметить, что кол-во элементов на втором уровне дерева для типа
RT_GROUP_ICON может быть меньше или равно RT_ICON! Эту аксиому объясню ниже.
Ничего нового. Только отмечаем, что здесь идентификатор = $64(100). А могло
бы быть и имя!
А сейчас снова отступление. Графический формат .ICO как и большинство бинарных
форматов данных содержит в себе заголовок! В .ICO файле заголовок прижимается
непосредственно к данным, а в PE он находится в другом ресурсе! В этом и есть
наша проблема! Сейчас нам необходимо восстановить заголовок и ознакомиться с
форматом .ICO, но это уже мелочи :)
[смещение: данные]//смещение относительно секции ".rsrc"
|0000: 00 00 00 00|00 00 00 00|00 00|00 00|00 00|0A
00|//IMAGE_RESOURCE_DIRECTORY
|0010: ... |//пропущено
|0В38: 09 04 00 00|30 15 00 00|.. .. .. .. |.. .. .. .. |//сюда прыгнули
Сейчас узнаем RVA заголовка иконки и размер.
[смещение: данные]//смещение относительно секции ".rsrc"
|0000: 00 00 00 00|00 00 00 00|00 00|00 00|00 00|0A
00|//IMAGE_RESOURCE_DIRECTORY
|0010: ... |//пропущено
|1530: F0 8D 02 00|5A 00 00 00|00 00 00 00|00 00 00 00|//сюда прыгнули
Размер заголовка равен $5A, а по RVA мы уже умеем находить файловое смещение.
Знакомимся с форматом .ICO
Я опишу только заголовок. В сети можно легко найти описание самого формата.
ICO header можно условно разбить на две части. Первую с фиксированной
длиной(назовем ICO_HEADER).
ICO_HEADER:
[Смещение Тип Имя_Поля ]
__________________________________________
[$00 word Reserved ]
[$02 word Type ]
[$04 word Count ]
---------------------------------------------------------------
Здесь:
Поле Reserved должно быть 0.
Поле Type должно быть 1.
Поле Count содержит фиксированное кол-во записей(ICO_DETAIL_HEADER), которые
идут сразу за ICO_HEADER заголовком. Это обстоятельство связано с тем, что в
одном .ICO файле может находиться различное число иконок (они могут отличаться
размером, глубиной цвета и вообще иметь различное графическое представление).
Поле Count говорит нам, сколько иконок будет в конечном файле.
ICO_DETAIL_HEADER:
[Смещение Тип Имя_Поля ]
_________________________________________
[$00 byte Width ]
[$01 byte Height ]
[$02 byte ColorCount ]
[$03 byte Reserved ]
[$04 word Planes ]
[$06 word BitCount ]
[$08 dword SizeInBytes ]
[$0C dword FileOffset ]
---------------------------------------------------------------
Поля Width и Height соответственно определяют ширину и высоту
иконки.
ColorCount - количество цветов (2,16,0=256)
Reserved = 0
Planes = 1
BitCount - глубина цвета.
SizeInBytes - размер самой иконки без заголовков!!!
FileOffset - смещение относительно начала файла!!!
Это все хорошо, только Microsoft немного изменила
ICO_DETAIL_HEADER, но не настолько, чтобы можно было с ним не разобраться
вручную.
ICON_GROUP:
[Смещение Тип Имя_Поля ]
_________________________________________
[$00 byte Width ]
[$01 byte Height ]
[$02 byte ColorCount ]
[$03 byte Reserved ]
[$04 word Planes ]
[$06 word BitCount ]
[$08 dword SizeInBytes ]
[$0C word Number ]
---------------------------------------------------------------
Изменилось только последнее поле.
Number указывает на порядковый номер элемента в каталоге RT_ICON(теперь стало
понятно, почему в RT_ICON не может быть имени - оно может храниться только в
RT_GROUP_ICON) и при каждом повторении ICON_GROUP поле Number увеличивается на
1(и указывает соответственно на следующую запись в RT_ICON). Итак, у одной
записи в RT_GROUP_ICON может быть несколько иконок! Теперь ясно, почему кол-во
структур в RT_GROUP_ICON не может быть больше кол-ва структур в RT_ICON, а
всегда меньше или равно!
[смещение: данные]//смещение относительно секции ".rsrc"
|0000: 00 00 00 00|00 00 00 00|00 00|00 00|00 00|0A 00
|//IMAGE_RESOURCE_DIRECTORY
|0010: ... |//пропущено
|3DF0: 00 00 01 00¦06 00 10 10¦10 00 01 00|04 00 28 01|
|3E00: 00 00 01 00¦10 10 00 00¦01 00 08 00¦68 05 00 00 |
|3E10: 02 00 10 10¦00 00 01 00¦20 00 68 04¦00 00 03 00 |
|3E20: 20 20 10 00¦01 00 04 00¦E8 02 00 00¦04 00 20 20 |
|3E30: 00 00 01 00¦08 00 A8 08¦00 00 05 00¦20 20 00 00|
|3E40: 01 00 20 00¦A8 10 00 00¦06 00 .. .. |.. .. .. .. |
Здесь наблюдаем ICO_HEADER, а за ним $0006 ICON_GROUP.
Добавляем в HEX-редакторе в начало файла иконок свободного
места столько, чтобы хватило всем заголовкам иконок(у меня их 6):
CountHeaderBytes := sizeof(ICO_HEADER)+(sizeof(ICO_DETAIL_HEADER)*ICO_HEADER.Count);
CountHeaderBytes = $66;//у меня
Копируем сначала ICO_HEADER, за ним по порядку ICO_DETAIL_HEADER столько сколько
нужно раз. При этом, не забывая корректировать смещения иконок (последнее поле
ICO_DETAIL_HEADER). Так у меня смещение первой иконки = $66(совпадение с
размером заголовков не случайно ;) ), смещение в следующей записи =
$66+ICO_DETAIL_HEADER[0].SizeInBytes(размер в байтах предыдущей иконки) и т.д.
После такой корректировки остается скопировать в новый .ico-файл из старого PE
сумму всех ICO_DETAIL_HEADER.SizeInBytes со смещения иконки(значки следуют один
за другим), которую мы нашли в самом начале.
А что, если ты захотел не вытащить иконку из файла, а наоборот
записать ее туда? Самый простой способ это сделать - записать поверх старой. Но
здесь вырисовывается одна проблема - размер старой иконки может быть меньше той,
которую тебе необходимо записать. В этом случае не буду повторяться и просто
отошлю за решением к Крису Касперски(внедрение в EXE,DLL файл хорошо было
описано в книгах "Компьютерные вирусы изнутри и снаружи" и "Техника отладки
программ без исходных текстов").
Перечислю только перечень возможных решений:
- Попробовать записать иконку в конец секции (в регулярную цепочку нулей)
- Расширить секцию ".rsrc", если она последняя в файле (в противном случае
большой гемор обеспечен и лучше этим не заниматься)
- Добавить свою секцию, увеличить ее размер и скопировать ресурсы из старой в
новую(если ресурсов много - размер существенно изменится)
Теперь, надеюсь, у тебя не возникнет проблем, если захочешь
заменить иконку у файла вручную или написав программу. К статье приложены
исходники на Delphi и скомпилированный код. Программа может выдирать все иконки
из EXE,DLL файла и может их либо "расщепить" (опция -a), либо оставить группой
(опция -b).
Остается только сказать, как Windows показывает эти иконки на
файлах... Ведь, согласись, что просто так не возьмешь любую из 6-ти иконок и не
подсунешь пользователю! Все очень просто! Windows выбирает самую
качественную(поле ICO_DETAIL_HEADER.BitCount) из соответствующего размера(если
выбрано крупное отображение в моем случае - $20x$20 и $10x$10 соответственно,
если нужны маленькие иконки(например, таблица)).
|