Пятница, 17.05.2024
Королевство Delphi
Главное меню
Статьи
Наш опрос
Нашли свой исходник?
Всего ответов: 94
Статистика
Онлайн всего: 1
Гостей: 1
Пользователей: 0
Форма входа
Главная » Статьи » Разные » Примеры Delphi

Заметки об иконках

В статье рассматривается механизм хранения иконок в ресурсах 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 файл хорошо было описано в книгах "Компьютерные вирусы изнутри и снаружи" и "Техника отладки программ без исходных текстов").
Перечислю только перечень возможных решений:

  1. Попробовать записать иконку в конец секции (в регулярную цепочку нулей)
  2. Расширить секцию ".rsrc", если она последняя в файле (в противном случае большой гемор обеспечен и лучше этим не заниматься)
  3. Добавить свою секцию, увеличить ее размер и скопировать ресурсы из старой в новую(если ресурсов много - размер существенно изменится)

Теперь, надеюсь, у тебя не возникнет проблем, если захочешь заменить иконку у файла вручную или написав программу. К статье приложены исходники на Delphi и скомпилированный код. Программа может выдирать все иконки из EXE,DLL файла и может их либо "расщепить" (опция -a), либо оставить группой (опция -b).

Остается только сказать, как Windows показывает эти иконки на файлах... Ведь, согласись, что просто так не возьмешь любую из 6-ти иконок и не подсунешь пользователю! Все очень просто! Windows выбирает самую качественную(поле ICO_DETAIL_HEADER.BitCount) из соответствующего размера(если выбрано крупное отображение в моем случае - $20x$20 и $10x$10 соответственно, если нужны маленькие иконки(например, таблица)).

Получить ссылку на материал

Категория: Примеры Delphi | Добавил: Барон (12.12.2011)
Просмотров: 1799 | Теги: Иконки | Рейтинг: 0.0/0
[ Пожертвования для сайта ] [ Пожаловаться на материал ]

Если вам помог материал сайта кликните по оплаченной рекламе размещенной в центре

Поиск
Категории раздела
Delphi.NET [3]
Kylix Delphi for Linux [9]
Советы Дельферу [6]
Хитрости в Delphi [2]
Обзор Delphi [45]
Инсталлятор [11]
Пользовательский интерфейс [18]
Примеры Delphi [93]
Функции и процедуры [15]
Разные [31]
Королевство Delphi © 2010-2024
Яндекс цитирования