Публикация осуществляется с разрешения автора и указанием
ссылки на источник.
В статье рассматривается механизм хранения иконок
в ресурсах 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!
Как видно все правильно: $000A(10) структур IMAGE_RESOURCE_DIRECTORY_ENTRY +
одна пустая.
Уже было сказано, что это уровень типа(Type).
Разберем первую структуру.
Name = $00000002
OffsetToData = $80000060
Видим, что у значения поля Name сброшен старший бит(=0), следовательно никакого
имени здесь нет и это идентификатор(здесь тип) ресурса.
Каждый идентификатор имеет свое значение. И применимо к первому
IMAGE_RESOURCE_DIRECTORY_ENTRY - это 2.
В Delphi типы ресурсов перечислены в Windows.pas(в Си WINUSER.H) и определены
следующим образом:
Пугаться MakeIntResource не следует. Это Си заглушка, которая ничего не делает с
тем параметром, который принимает (вообще такие приколы характерны для Си - чего
стоят только пустые макросы IN и OUT).
Теперь, хорошо видно, что Name соответствует тип RT_BITMAP(директория картинок).
Кстати, если хочешь увидеть имя уже на первом уровне, то загляни HEX-редактором
в ресурсы какой-нибудь системной библиотеки. Например, shell32.dll.
С типом разобрались, на очереди OffsetToData. Снова смотрим на старший бит...
Опа! Установлен(еще бы он был не установлен ;) )! Ок. Сбрасываем его, получаем
OffsetToData = $00000060. Встаем на смещение $10 от начала секции и прыгаем...
Стоп! Стоять на месте! Мы же об иконках говорим! Найдем среди массива
IMAGE_RESOURCE_DIRECTORY_ENTRY тип с идентификатором RT_ICON(подсказка: далеко
идти не придется - дерево же отсортировано).
Проверяем все ли на месте. Тип = 3. Ок. Старший бит OffsetToData установлен! Ок!
Сбрасываем бит($000000A0) и прыгаем вперед на $A0 байтов с вышеозначенного
смещения.
Попали на следующий уровень(Имя). И это все тоже двоичное отсортированное
дерево! Всё та же структура IMAGE_RESOURCE_DIRECTORY_ENTRY. И конец этой "ветви"
можно узнать, сканируя уровень имени в поисках пустой
IMAGE_RESOURCE_DIRECTORY_ENTRY. К сожалению, нам немного не повезло и в место
имени мы опять видим только идентификатор(кстати, для примера я случайно... ну,
правда, случайно :) выбрал exe-файл, в котором не было ни одного имени
ресурса... все на идентификаторах! вот невезуха!). Правда, сейчас уже это
идентификатор самой иконки. А позже мы поймем, почему здесь вообще не может быть
имени.
ЗАМЕТКА: раз уж не удалось увидеть имя ресурса, сообщу, что имя храниться
UNICODE-строкой.
Продвигаемся дальше. Отлично! Следующий уровень! Теперь переходим на offset
$00000530 и видим (вернее не видим)... Ура! Наконец-то мы уже не видим ни одного
старшего бита и готовимся постичь ДАО новой структуры :)
Сразу скажу, что на этом уровне я не разбирался со значением поля Name(кстати,
это все та же IMAGE_RESOURCE_DIRECTORY_ENTRY). Гораздо более интересно поле
OffsetToData. По видимым признакам это офсет на структуру, которая описывает
непосредственно сам ресурс(а мы ее чуть позже опишем).
Еще хорошо бы взять себе за правило в таких случаях:
IMAGE_RESOURCE_DIRECTORY_ENTRY.Name:
старший бит установлен - смещение от ".rsrc" и это имя
старший бит сброшен - это идентификатор
IMAGE_RESOURCE_DIRECTORY_ENTRY.OffsetToData:
старший бит установлен - смещение от ".rsrc"+$10 на следующий уровень
старший бит сброшен - смещение от ".rsrc" на IMAGE_RESOURCE_DATA_ENTRY
Теперь опишем структуру 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).
В итоге "сырой" офсет ресурса от начала файла должен высчитываться так:
Это было отступление.
Поле 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) этот идентификатор. Найдя, переходим на
следующий уровень.
Еще хочется заметить, что кол-во элементов на втором уровне дерева для типа
RT_GROUP_ICON может быть меньше или равно RT_ICON! Эту аксиому объясню ниже.
Ничего нового. Только отмечаем, что здесь идентификатор = $64(100). А могло бы
быть и имя!
А сейчас снова отступление. Графический формат .ICO как и большинство бинарных
форматов данных содержит в себе заголовок! В .ICO файле заголовок прижимается
непосредственно к данным, а в PE он находится в другом ресурсе! В этом и есть
наша проблема! Сейчас нам необходимо восстановить заголовок и ознакомиться с
форматом .ICO, но это уже мелочи :)
Размер заголовка равен $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 говорит нам, сколько иконок будет в конечном файле.
Поля 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, а
всегда меньше или равно!
Здесь наблюдаем 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 соответственно, если нужны
маленькие иконки(например, таблица)).
При разработке CMS S.Builder наша команда
активно использовала AJAX. Теперь вот решили поделиться накопленным
опытом. Начнем с этого хабратопика. Не буду здесь затрагивать различные
фреймворки и библиотеки. Свой код всегда роднее. Для работы с AJAX-ом в
S.Builder написана библиотека
sbAJAX. Можете качать и пользоваться :). В этом файле есть функция
sbEvalJS. Для тех, кто не знает, объясню. При подгрузке через AJAX и вставке
на страницу HTML-кода, содержащего JavaScript, JavaScript выполняться не будет
или полезут баги. Эта функция как раз решает поставленную задачу.
Хотя наш обзор немного запоздал, оригинальный Dojo 1.2 вышел в релизной
версии ещё 6-го октября, но сейчас мы наверстаем упущенное. И так, Dojo Toolkit — это самая мощная и
гибкая ajax-библиотека из всех, что есть на рынке, она активно развивается и
имеет большое комьюнити. Кстати, это самое комьюнити, совместно с компанией
Sitepen, имеет ещё несколько проектов, среди которых и Cometd и некоторые
другие, не менее интересные, о которых мы скоро вам расскажем. Сегодня же все
внимание на флагманский продукт —
Dojo
1.2.
Если вы профессиональный веб-разработчик и постоянно имеете дело с
разработкой и отладкой сложных AJAX приложений, то наверняка знаете и
используете Firebug — плагин для браузера
Firefox, предназначенный для отладки и исследования веб-приложений. Текущая его
версия, 1.2х достаточно стабильная и функциональна, чтобы помочь в 99% проблем,
которые могут возникнуть при разработке. Но и этот инструмент не лишён если не
недостатков, то некоторых фич, которые могли бы облегчить работу. И даже
идеальный инструмент можно сделать ещё более идеальным, как бы это не звучало.