Исполняемые файлы имеют фишку, на которую начинающие программисты редко обращают внимание – это директория "VERSION" в секции-ресурсов. Её отсутствие характеризует кодера не с лучшей стороны, если только он не упустил эту инфу умышленно, чтобы замести за собой следы. Все зарекомендовавшие себя легальные разработчики стараются полностью заполнять этот дир, ведь согласитесь, что свойства файла "OllyDbg" в правом окне рисунка ниже вызывают больше доверия, чем подноготная того-же шпиона "Kerberos" на левом..
Здесь, значение полей "тип, размер, дата" система получает своими методами, а не считывает их из секции-ресурсов. Остальные-же поля хранятся в каталоге "RT_VERSION" самого файла, и на выходе из этой статьи мы узнаем, как их туда поместить. Отметим, что заполнять свойства подобного рода можно только в исполняемых файлах типа РЕ-32 (Portable-Executable 32/64 бит), в числе которых: EXE, SYS, DRV, DLL, OCX и CPL:
Оглавление:
1. Секция-ресурсов файла, и типы каталогов в ней;
2. Проблемы языковых стандартов:
3.0. Практика – перечисление всех языков и кодовых страниц системы;
3.1. Практика – чтение ресурса "VERSIONINFO" из файла;
4. Эпилог.
------------------------------------------------------
1. Секция-ресурсов и типы каталогов в ней
Ресурсы в приложении – это связанные с ним статические данные, предназначенные в первую очередь для описания стандартных компонентов софта типа: меню, диалоговых окон, внешних шрифтов, иконок, курсоров, изображений, звуков, текстовых строк, манифестов и прочего хлама. В инклуде фасма "equates\kernel32.inc" прописаны константы возможных вариантов, которые можно найти обычным поиском по префиксу RT – ResourceType. Всего зарегистрировано в нём 24-типа, но мы остановимся только на одном из них – ресурсе "RT_VERSION" под номером 16:
Если сравнивать данные в секции-ресурсов с классическими видами данных по образу внешних DLL-библиотек, то они имеют ряд перечисленных ниже преимуществ:
В большинстве случаях, на практике ресурсы создаются в виде трехуровневого древа, хотя документированный предел вложенных каталогов 32-бита (более 4 млрд). Иерархия секции такова, что имеется корневая директория верхнего уровня, внутри которой перечисляются все используемые данным файлом, типы ресурсов. Теперь, каждому типу нужно присвоить произвольное имя, чтобы в последующем можно было обращаться к нему. Дело в том, что внутри одной секции вполне-себе могут мирно сосуществовать несколько ресурсов одинакового типа, и в этом случае без уникальных имён никак не обойтись. Например чтобы указать версию на двух языках английский/французский и одно меню, можно прописать в корневую директорию три типа по такой схеме:
Следующим в иерархии каталогов идёт описатель конкретного типа. По заданным нами характеристикам его оформляет макрос "resource" компилятора. Он заносит в базу имя ресурса, его идентификатор, язык данных, и указатель на эти данные. В общем случае, зарытую в два этих каталога информацию можно рассматривать как структуру-заголовка "Header" каждого ресурса, которая предваряет собственно полезные данные с версией файла. Их разбором и оформлением в надлежащий вид занимается ещё один макрос "versioninfo", которому нужно передать целых шесть аргументов:
Для ресурса типа "VERSION", инженерами Microsoft предопределены всего 12 полей данных (на рис.ниже выделены коричневым), которые программист может комбинировать в любом порядке. Имена этих полей являются системными константами для некоторых API-функций типа VerQueryValue() для извлечения содержимого поля, поэтому нужно указывать их строго соблюдая правописание, иначе функция будет возвращать ошибку "Неправильный аргумент". Это создаёт определённые неудобства, а потому советую создать заранее подготовленный текстовый шаблон секции, и подключать его к своему проекту посредством вызова include. На рисунке ниже, различными цветами выделена информация, которую нам дозволено менять – остальное всё константы:
Если скомпилировать этот исходник и просмотреть свойства полученного файла, то можно обнаружить, что система отображает не все поля версии, а только те, которые считает нужным. Но это вовсе не означает, что перечисленных полей нет в файле, и чтобы получить полный их лист, в следующей части мы напишем небольшое приложение. Авансом скажу, что в карманах этих штанин можно будет найти достаточно интересную информацию, которую скрывает от нас мастдай – например ф.и.о. создателя в комментах, и прочее:
2. Проблема языковых стандартов
Теперь, отложив скучную теорию в стол, немного попрактикуемся..
Но для начала, нужно рассмотреть весьма мерзкий подарочек от создавших API индусов. Учитывая, что у них там растёт хороший "ганджубас", в этом нет ничего удивительного, ведь только перемещаясь по открытым галлюциногенами порталам можно было написать функцию, ожидающую в аргументе hex-значение в (!)текстовом виде. Хоть это и верх извращения, остаётся только принять эту суровую действительность, поскольку мелкософт ввёл нас в фактический режим апартеида, полностью лишив права голоса.
Если стоит задача чтения из исполняемого файла ресурса "VERSION", то нас ожидают проблемы с указанием языка и кодировки "Language & CodePage". Причём это должны быть строго валидные значения, и никаких нулей там по-умолчанию нет. Дело в том, что функция VerQueryValue() извлекает информацию о версии из указанного ресурса. Однако чтобы подобраться к этой версии, необходимо сначала потянуть за функцию GetFileVersionInfoSize() (возвращает общий размер информации), а затем её выхлоп передать в функцию GetFileVersionInfo(), которая и сбросит необработанные RAW-данные в буфер.
Только теперь можно будет через VerQueryValue() в цикле запрашивать по одной строке каждого из 12-ти текстовых полей версии. До этого момента вроде всё нормально – мелкософт часто практикует альянс из трёх последовательных функций. Но посмотрим на прототип этой функции:
Обратите внимание на формат аргумента для чтения строковых данных '\StringFileInfo\Lang-CP\StrName'. Например если мы хотим прочитать из версии поле "LegalCopyright", которое хранится в кодировке 1033/1252 (Lang/CodePage в hex=0409/04E4), то должны передать в функцию строку следующего вида.. Причём повторюсь, что кодировка должна быть не от фонаря, а валидная, иначе хоть данные и будут присутствовать в файле, функция всё-равно вернёт ошибку:
Как можно было до такого додуматься, и что мешало разделить эту строку на три самостоятельных аргумента? Когда не надо, они пихают в функции по 10-параметров, а здесь решили сэкономить на них. Тогда и кодировку можно было-бы передавать в человеческом hex-виде, и в цикле подставлять название следующего из возможных 12-ти полей версии. В общем одни лайки.. Теперь-же, приходится разгребать эту "кучу-навоза" вручную, динамически в цикле формируя требуемую строку из нескольких составляющих.
Но и это ещё не всё.. Как видно из описания, среди прочего функция может возвращать так-нужную нам строку "Lang/CodePage" (второй вариант в прототипе). Однако на практике делает она это криво, и иногда возвращает совсем не то, что ожидалось. Поэтому и здесь приходится ручками забирать инфу прямо из структуры "StringFileInfo". Как показали личные опыты, VerQueryValue() ищет кодировку в дочерней структуре "VarFileInfo", а она там не фиктивная. Вот и доверяй после этого мелкомягким индусам..
На рисунке ниже скрин буфера с данными о версии из секции-ресурсов, где зелёным выделено поле с валидным языком Lang, и кодовой страницей CP (0409h=1033 , 04B0h=1200):
3.0. Практика – перечисление всех языков и кодовых страниц системы
Ради интереса напишем небольшую программу, которая сбросит на консоль коды, и связанные с ними текстовые строки всех поддерживаемых текущей ОС языков, и их кодовых страниц. Забегая вперёд скажу, что пример можно рассматривать как модуль для следующей программы, где будет представлен вариант чтения всех полей "VERSION" исполняемого файла. Алгоритм данного модуля держится всего на двух функциях – VerLanguageName() для запроса языка по переданному в аргументе коду, и родственной по функционалу GetCPInfoExA(), которая возвращает кодовую страницу СР в текстовом виде. Вот их прототипы (обе прописаны в kernel32.dll):
Программа ниже будет исправно отрабатывать на всей линейки Win любой разрядности. При старте нужно указать тип запроса: 1 = показать информацию о поддерживаемых системой языках, 2 = набор кодовых страниц. Я оформил их в отдельные процедуры, и добавил в исходник секцию-ресурсов, с описанием версии файла.
3.1. Практика – чтение ресурса "VERSIONINFO" из файла
Под занавес напишем ещё одну прожку, для просмотра свойств исполняемых файлов типа: exe,sys,dll,drv,ocx,cpl. Её код вытащит из секции-ресурсов блок с данными "VERSION" и распечатает на консоли его содержимое в форматированном виде. Алгоритм собрал в себе всё выше/изложенное в этой теме, и состоит из следующих частей:
1. Запросить у юзера расширение файлов.
2. Функцией FindFirstFile() начать поиск первого файла в текущем каталоге и если ошибка, то сразу на выход [9]. Имя найденного файла функция сбрасывает в свою структуру "WIN32_FIND_DATA" по смещению(44) – больше нам от неё ничего нужно.
3. Передать это имя функции GetFileVersionInfoSize(), чтобы узнать размер данных "RT_VERSION" в файле. Если функция вернёт нуль, значит в файле нет информации о версии, и сразу прыгаем в конец цикла [8], для поиска следующего файла в каталоге.
4. Если мы здесь, значит файл содержит нужную нам информацию, и мы считываем её в свой буфер посредством GetFileVersionInfo().
5. По смещению 86h от начала приёмного буфера будет лежать Unicode-строка с языком и кодовой страницей "Lang/CP". Теперь нужно перевести её в ANSI (отсеить парные нули), и вставить в глобальную строку, которая будет определять собой аргумент для запроса конкретного поля версии. В дисциплинах такую операцию называют "конкатенация", или склеивание двух и более строк, в одну. Поскольку маркером конца строки всегда является нуль, то я не стал заморачиваться с этим, и оформил строку так:
В результате, если теперь заполнить поля cp и spase, получим единую строку, которую можно будет вскормить в виде аргумента сл.функции чтения. Правда в зависимости от длинны строковой константы, нужно будет вставлять терминальный нуль в её хвост, но это уже дело техники (см.исходник).
6. На данном этапе имеем всё, чтобы считать очередную "строку версии" из файла – зовём для этого функцию VerQueryValue() и получаем указатель на запрошенную ANSI-строку. Поскольку часто разработчик заполняет строки кириллицей (а мы выводим их на консоль), то имеет смысл функцией CharToOem() сразу конвертировать их в кодировку СР-866.
7. Мотаем предыдущий пункт(6) ровно 12-раз, в цикле подставляя следующую текстовую константу в поле space. Это позволит проверить в файле все без исключения поля "VERSION" и если какое-нибудь из них пустое, функция VerQueryValue() будет возвращать ошибку, а мы пропускать вывод его на консоль.
8. По окончании внутреннего цикла из 12 итераций, сбрасываем все переменные в дефолт и функцией FindNextFile() переходим к поиску следующего файла в текущем каталоге. Если файлов с указанным расширением больше нет, функция вернёт ошибку, сигнализируя, что нам пора на выход из программы. Иначе, уходим на начало глобального цикла к этапу [3].
9. Заключительный этап требует всего-лишь освобождения дескриптора поиска функцией FindClose() и ожидания любой клавиши, чтобы осмотреться.
Ну и теперь собственно код, который реализует на практике описанный выше алгоритм:
Рассмотрим бегло этот лог..
Значит в начале я указал искать в текущем каталоги только файлы типа EXE, на что получил версии трёх (не считая своего) файлов. Судя по логу, первый является кейгеном к переводчику "Socrat-Personal", причём разработчик использует шаблон, поскольку константы есть, только они не заполнены. Иначе прожка пропустила-бы их, как в случае с последним файлом "ProcessMM.exe".
Вторым в логе красуется файл-ядра системы Ntoskrnl.exe, в котором заполнены уже почти все возможные поля, кроме коммента и Build. Я его честно скоммуниздил у десятки, о чём свидетельствует поле "FileVersion". Лог закрывает инфа о программе маппинга памяти процесса (Memory-Map), и его разраб не удосужился заполнить даже основные поля, ограничившись для галочки лишь версией. Здесь так-же видно, что у всех файлов язык 1033, а кодировка 1200 или 1252. Расшифровать эти коды можно с помощью предыдущей прожки.
4. Эпилог
В данной статье я попытался освятить только одну часть ресурсов, а ведь к ней присовокупляются ещё 23 типа, и каждый из них заслуживает отдельного внимания, т.к. является вполне самостоятельным компонентом. Чтобы ознакомится со-всеми ресурсами и посмотреть на их формат, можно воспользоваться программой "PE-Explorer". Эта крутая тулза не только просмотрщик, но и редактор любой области исполняемых файлов, начиная от РЕ-заголовка и заканчивая ресурсами. Огромным её плюсом считается возможность при помощи мастера создавать файлы манифестов – советую..
По уже отработанной привычке, в скрепку я положил два рассмотренных файла, которые не привязаны как к версии системы, так и к её разрядности. Всем удачи, пока!
Здесь, значение полей "тип, размер, дата" система получает своими методами, а не считывает их из секции-ресурсов. Остальные-же поля хранятся в каталоге "RT_VERSION" самого файла, и на выходе из этой статьи мы узнаем, как их туда поместить. Отметим, что заполнять свойства подобного рода можно только в исполняемых файлах типа РЕ-32 (Portable-Executable 32/64 бит), в числе которых: EXE, SYS, DRV, DLL, OCX и CPL:
Оглавление:
1. Секция-ресурсов файла, и типы каталогов в ней;
2. Проблемы языковых стандартов:
3.0. Практика – перечисление всех языков и кодовых страниц системы;
3.1. Практика – чтение ресурса "VERSIONINFO" из файла;
4. Эпилог.
------------------------------------------------------
1. Секция-ресурсов и типы каталогов в ней
Ресурсы в приложении – это связанные с ним статические данные, предназначенные в первую очередь для описания стандартных компонентов софта типа: меню, диалоговых окон, внешних шрифтов, иконок, курсоров, изображений, звуков, текстовых строк, манифестов и прочего хлама. В инклуде фасма "equates\kernel32.inc" прописаны константы возможных вариантов, которые можно найти обычным поиском по префиксу RT – ResourceType. Всего зарегистрировано в нём 24-типа, но мы остановимся только на одном из них – ресурсе "RT_VERSION" под номером 16:
Код:
;// Resource types
;//*********************
RT_CURSOR = 1
RT_BITMAP = 2
RT_ICON = 3
RT_MENU = 4
RT_DIALOG = 5
RT_STRING = 6
RT_FONTDIR = 7
RT_FONT = 8
RT_ACCELERATOR = 9
RT_RCDATA = 10
RT_MESSAGETABLE = 11
RT_GROUP_CURSOR = 12
RT_GROUP_ICON = 14
RT_VERSION = 16
RT_DLGINCLUDE = 17
RT_PLUGPLAY = 19
RT_VXD = 20
RT_ANICURSOR = 21
RT_ANIICON = 22
RT_HTML = 23
RT_MANIFEST = 24
Если сравнивать данные в секции-ресурсов с классическими видами данных по образу внешних DLL-библиотек, то они имеют ряд перечисленных ниже преимуществ:
• Ресурс компилируется вместе с приложением в единый исполняемый файл, и становится доступным из его тушки прямым вызовом функций FindResource() и LoadResource() с указанием соответствующих констант (см.лист выше). В результате отпадает необходимость таскать за собой лишний груз в виде библиотек, картинок и прочих внешних файлов.
• Исходя из того, что в описании каждого ресурса имеются поле для установки языка и кодировки (Language/CodePage), в ресурсах можно хранить нескольких языковых наборов, что даёт возможность создавать программы с многоязычным интерфейсом MUI – MultiLanguage User Interface. Для этого, на этапе инициализации приложения запрашивается текущий язык системы, и в соответствии с ним через LoadResource() в память подгружается требуемый набор.
• Уже скомпилированный двоичный ресурс без проблем поддаётся правке, при помощи специальных утилит из категории "Редактор ресурсов". Так, если вы потеряли исходные тексты программ, можно будет поменять иконку на новую, или подправить пару текстовых строк в диалоговом окне.
В большинстве случаях, на практике ресурсы создаются в виде трехуровневого древа, хотя документированный предел вложенных каталогов 32-бита (более 4 млрд). Иерархия секции такова, что имеется корневая директория верхнего уровня, внутри которой перечисляются все используемые данным файлом, типы ресурсов. Теперь, каждому типу нужно присвоить произвольное имя, чтобы в последующем можно было обращаться к нему. Дело в том, что внутри одной секции вполне-себе могут мирно сосуществовать несколько ресурсов одинакового типа, и в этом случае без уникальных имён никак не обойтись. Например чтобы указать версию на двух языках английский/французский и одно меню, можно прописать в корневую директорию три типа по такой схеме:
Код:
directory RT_VERSION, ver1,\
RT_VERSION, ver2,\
RT_MENU, menus
Следующим в иерархии каталогов идёт описатель конкретного типа. По заданным нами характеристикам его оформляет макрос "resource" компилятора. Он заносит в базу имя ресурса, его идентификатор, язык данных, и указатель на эти данные. В общем случае, зарытую в два этих каталога информацию можно рассматривать как структуру-заголовка "Header" каждого ресурса, которая предваряет собственно полезные данные с версией файла. Их разбором и оформлением в надлежащий вид занимается ещё один макрос "versioninfo", которому нужно передать целых шесть аргументов:
• аргумент(1) – имя ресурса, которое мы определили на предыдущем этапе;
• аргумент(2) – операционная система РЕ-файла: как-правило это константа
VOS __ WINDOWS32
;• аргумент(3) – тип нашего файла:
VFT_APP
= программа, VFT_DLL
= библиотека, VFT_DRV
= драйвер;• аргумент(4) – подтип файла, в большинстве случаях не нужен с константой
VFT2_UNKNOWN
;• аргумент(5) – идентификатор языка строковых данных;
• аргумент(6) – кодовая страница языка.
Для ресурса типа "VERSION", инженерами Microsoft предопределены всего 12 полей данных (на рис.ниже выделены коричневым), которые программист может комбинировать в любом порядке. Имена этих полей являются системными константами для некоторых API-функций типа VerQueryValue() для извлечения содержимого поля, поэтому нужно указывать их строго соблюдая правописание, иначе функция будет возвращать ошибку "Неправильный аргумент". Это создаёт определённые неудобства, а потому советую создать заранее подготовленный текстовый шаблон секции, и подключать его к своему проекту посредством вызова include. На рисунке ниже, различными цветами выделена информация, которую нам дозволено менять – остальное всё константы:
Если скомпилировать этот исходник и просмотреть свойства полученного файла, то можно обнаружить, что система отображает не все поля версии, а только те, которые считает нужным. Но это вовсе не означает, что перечисленных полей нет в файле, и чтобы получить полный их лист, в следующей части мы напишем небольшое приложение. Авансом скажу, что в карманах этих штанин можно будет найти достаточно интересную информацию, которую скрывает от нас мастдай – например ф.и.о. создателя в комментах, и прочее:
2. Проблема языковых стандартов
Теперь, отложив скучную теорию в стол, немного попрактикуемся..
Но для начала, нужно рассмотреть весьма мерзкий подарочек от создавших API индусов. Учитывая, что у них там растёт хороший "ганджубас", в этом нет ничего удивительного, ведь только перемещаясь по открытым галлюциногенами порталам можно было написать функцию, ожидающую в аргументе hex-значение в (!)текстовом виде. Хоть это и верх извращения, остаётся только принять эту суровую действительность, поскольку мелкософт ввёл нас в фактический режим апартеида, полностью лишив права голоса.
Если стоит задача чтения из исполняемого файла ресурса "VERSION", то нас ожидают проблемы с указанием языка и кодировки "Language & CodePage". Причём это должны быть строго валидные значения, и никаких нулей там по-умолчанию нет. Дело в том, что функция VerQueryValue() извлекает информацию о версии из указанного ресурса. Однако чтобы подобраться к этой версии, необходимо сначала потянуть за функцию GetFileVersionInfoSize() (возвращает общий размер информации), а затем её выхлоп передать в функцию GetFileVersionInfo(), которая и сбросит необработанные RAW-данные в буфер.
Только теперь можно будет через VerQueryValue() в цикле запрашивать по одной строке каждого из 12-ти текстовых полей версии. До этого момента вроде всё нормально – мелкософт часто практикует альянс из трёх последовательных функций. Но посмотрим на прототип этой функции:
C-подобный:
BOOL VerQueryValueA
pBlock dd 0 ;// указатель на буфер функции GetFileVersionInfo()
lpSubBlock dd 0 ;// указатель на строку с именем требуемой информации
lplpBuffer dd 0 ;// указатель на DWORD, куда вернётся адрес запрощенных данных
puLen dd 0 ;// указатель на DWORD, куда вернётся размер данных
;//**********************************
;// Варианты аргумента "lpSubBlock"
;//**********************************
;// возвращает линк на корневую структуру "VS_FIXEDFILEINFO"
lpSubBlock db '\',0
;// возвращает линк на "Lang-CP".
lpSubBlock db '\VarFileInfo\Translation',0
;// возвращает значение поля, указанного в "StrName"
lpSubBlock db '\StringFileInfo\Lang-CP\StrName',0
Обратите внимание на формат аргумента для чтения строковых данных '\StringFileInfo\Lang-CP\StrName'. Например если мы хотим прочитать из версии поле "LegalCopyright", которое хранится в кодировке 1033/1252 (Lang/CodePage в hex=0409/04E4), то должны передать в функцию строку следующего вида.. Причём повторюсь, что кодировка должна быть не от фонаря, а валидная, иначе хоть данные и будут присутствовать в файле, функция всё-равно вернёт ошибку:
C-подобный:
invoke VerQueryValueA,pBlock,<'\StringFileInfo\040904E4\LegalCopyright',0>,lpBuffer,puLen
Как можно было до такого додуматься, и что мешало разделить эту строку на три самостоятельных аргумента? Когда не надо, они пихают в функции по 10-параметров, а здесь решили сэкономить на них. Тогда и кодировку можно было-бы передавать в человеческом hex-виде, и в цикле подставлять название следующего из возможных 12-ти полей версии. В общем одни лайки.. Теперь-же, приходится разгребать эту "кучу-навоза" вручную, динамически в цикле формируя требуемую строку из нескольких составляющих.
Но и это ещё не всё.. Как видно из описания, среди прочего функция может возвращать так-нужную нам строку "Lang/CodePage" (второй вариант в прототипе). Однако на практике делает она это криво, и иногда возвращает совсем не то, что ожидалось. Поэтому и здесь приходится ручками забирать инфу прямо из структуры "StringFileInfo". Как показали личные опыты, VerQueryValue() ищет кодировку в дочерней структуре "VarFileInfo", а она там не фиктивная. Вот и доверяй после этого мелкомягким индусам..
На рисунке ниже скрин буфера с данными о версии из секции-ресурсов, где зелёным выделено поле с валидным языком Lang, и кодовой страницей CP (0409h=1033 , 04B0h=1200):
3.0. Практика – перечисление всех языков и кодовых страниц системы
Ради интереса напишем небольшую программу, которая сбросит на консоль коды, и связанные с ними текстовые строки всех поддерживаемых текущей ОС языков, и их кодовых страниц. Забегая вперёд скажу, что пример можно рассматривать как модуль для следующей программы, где будет представлен вариант чтения всех полей "VERSION" исполняемого файла. Алгоритм данного модуля держится всего на двух функциях – VerLanguageName() для запроса языка по переданному в аргументе коду, и родственной по функционалу GetCPInfoExA(), которая возвращает кодовую страницу СР в текстовом виде. Вот их прототипы (обе прописаны в kernel32.dll):
C-подобный:
;// Возвращает размер строки, 0 = ошибка
;//**************************************
DWORD VerLanguageNameA
wLang dd 0 ;// код языка
szLang dd 0 ;// получим указатель на строку языка
cchLang dd 0 ;// получим размер строки
;// Возвращает: 1 = ОК, 0 = ошибка
;//**************************************
BOOL GetCPInfoExA
CodePage dd 0 ;// код языковой страницы
dwFlags dd 0 ;// резерв = 0
lpCPInfo dd 0 ;// указатель на структуру "CPIINFOEXA",
;// ...в которой по смещению(24) лежит строка
Программа ниже будет исправно отрабатывать на всей линейки Win любой разрядности. При старте нужно указать тип запроса: 1 = показать информацию о поддерживаемых системой языках, 2 = набор кодовых страниц. Я оформил их в отдельные процедуры, и добавил в исходник секцию-ресурсов, с описанием версии файла.
C-подобный:
format pe console
include 'win32ax.inc'
entry start
;//----------
.data
counter dd 0 ;// порядковый номер
total dd 0 ;// всего найдено в системе
buff db 0 ;// пригодится..
;//----------
.code
proc GetLanguageList ;//<---- Процедура поддержки языков
cinvoke printf,<'1',\
10,' ***************************',10,\
10,' Language list...',10,0>
invoke Sleep,1500 ;// пауза, чтобы осмотреться
xor ecx,ecx ;// EAX=0. Будет счётчиком цикла,
;// ...и сразу константой языка.
@@: push ecx
invoke VerLanguageName,ecx,buff,128 ;// получить язык по константе!
cmp eax,13h ;// остеить ошибки
jz @nul ;//
invoke CharToOem,buff,buff ;// преобразовать в ОЕМ для консоли
cinvoke printf,<10,' %04Xh = %05d = %s',0>,[counter],[counter],buff
inc [total] ;// счётчик найденных +1
@nul: pop ecx ;//
inc [counter] ;// номер языка +1
inc ecx ;// цикл +1
cmp ecx,0xffff ;// проверить на макс.
jnz @b ;// повторить, если не равно
;// иначе: распечать всего найденных
cinvoke printf,<10,10,' Total language found: %d',10,0>,[total]
ret
endp
;//==================================================================
proc GetCodePageList ;//<---- Процедура поддержки кодовых страниц
cinvoke printf,<'2',\
10,' ***************************',10,\
10,' CodePage list...',10,0>
invoke Sleep,1500
xor ecx,ecx ;// EAX=0. Счётчик и константа СР
@@: push ecx ;//
invoke GetCPInfoExA,ecx,0,buff ;// получить строку по константе!
or eax,eax ;// отсеить ошибки
jz @null ;//
invoke CharToOem,buff+24,buff+24 ;// строку в ОЕМ для консоли
cinvoke printf,<10,' %04Xh = %s',0>,[counter],buff+24
inc [total] ;// найденных +1
@null: pop ecx ;//
inc [counter] ;// код СР +1
inc ecx ;//
cmp ecx,0xffff ;// проверить цикл на переполнение
jnz @b ;//
cinvoke printf,<10,10,' Total CodePage found: %d',10,0>,[total]
ret
endp
;//***** ТОЧКА ВХОДА ***********************
start: invoke SetConsoleTitle,<'***Language/CodePage Info v0.1***',0>
cinvoke printf,<10,' ********** MENU ***********',\
10,' 1 = Language, 2 = CodePage',\
10,' Choise number: ',0>
@@: cinvoke getch ;// парсим выбор юзера
cmp al,'1' ;//
je @lang ;// если нажал (1)
cmp al,'2' ;//
jne @b ;// вернуться, если не (2)
call GetCodePageList ;// значит (2),
jmp @exit ;// ..и на выход.
@lang: call GetLanguageList ;// уходим на введённую единицу
@exit: cinvoke getch ;// GAME OVER!
cinvoke exit,0 ;//
;//----- ИМПОРТ ----------
section '.idata' import data readable
library msvcrt,'msvcrt.dll',kernel32,'kernel32.dll',user32,'user32.dll'
import msvcrt, printf,'printf',getch,'_getch',exit,'exit'
include 'api\kernel32.inc'
include 'api\user32.inc'
;//----- РЕСУРСЫ ---------
section '.rsrc' resource data readable
directory RT_VERSION,ver1
resource ver1, 1, LANG_NEUTRAL, vInfo1
versioninfo vInfo1,\
VOS__WINDOWS32, VFT_APP, VFT2_UNKNOWN,\
LANG_ENGLISH + SUBLANG_DEFAULT, 1252,\ ;//<-------- Lang/CodePage
'CompanyName' , 'https://codeby.net',\
'LegalCopyright' , 'Copyright 2020-2021 (c)Marylin',\
'ProductName' , 'Lang/CodePage info',\
'ProductVersion' , '6.1.7601.3821',\
'FileDescription' , 'Win LCID identifier',\
'FileVersion' , '0.0.1',\
'OriginalFilename', 'LangCP.exe',\
'InternalName' , 'LangCP_32/64',\
'Comment' , 'codeby.net/forums/assembler.229/',\
'PrivateBuild' , '0.0.2',\
'SpecialBuild' , '00.01.00'
3.1. Практика – чтение ресурса "VERSIONINFO" из файла
Под занавес напишем ещё одну прожку, для просмотра свойств исполняемых файлов типа: exe,sys,dll,drv,ocx,cpl. Её код вытащит из секции-ресурсов блок с данными "VERSION" и распечатает на консоли его содержимое в форматированном виде. Алгоритм собрал в себе всё выше/изложенное в этой теме, и состоит из следующих частей:
1. Запросить у юзера расширение файлов.
2. Функцией FindFirstFile() начать поиск первого файла в текущем каталоге и если ошибка, то сразу на выход [9]. Имя найденного файла функция сбрасывает в свою структуру "WIN32_FIND_DATA" по смещению(44) – больше нам от неё ничего нужно.
3. Передать это имя функции GetFileVersionInfoSize(), чтобы узнать размер данных "RT_VERSION" в файле. Если функция вернёт нуль, значит в файле нет информации о версии, и сразу прыгаем в конец цикла [8], для поиска следующего файла в каталоге.
4. Если мы здесь, значит файл содержит нужную нам информацию, и мы считываем её в свой буфер посредством GetFileVersionInfo().
5. По смещению 86h от начала приёмного буфера будет лежать Unicode-строка с языком и кодовой страницей "Lang/CP". Теперь нужно перевести её в ANSI (отсеить парные нули), и вставить в глобальную строку, которая будет определять собой аргумент для запроса конкретного поля версии. В дисциплинах такую операцию называют "конкатенация", или склеивание двух и более строк, в одну. Поскольку маркером конца строки всегда является нуль, то я не стал заморачиваться с этим, и оформил строку так:
C-подобный:
;// Константы полей "VERSION".
;// В первом байте хранится длина строки
;//**************************************
n01 db 11,'CompanyName',0
n02 db 14,'LegalCopyright',0
n03 db 15,'LegalTrademarks',0
n04 db 11,'ProductName',0
n05 db 14,'ProductVersion',0
n06 db 15,'FileDescription',0
n07 db 11,'FileVersion',0
n08 db 16,'OriginalFilename',0
n09 db 12,'InternalName',0
n10 db 12,'PrivateBuild',0
n11 db 12,'SpecialBuild',0
n12 db 07,'Comment',0
queryStr db '\StringFileInfo\' ;// начало строки (всегда постоянна)
cp db '........\' ;// резерв под 8 символьную hex-строку Lang/CP (будь она не ладна)
space db '.................: ',0 ;// резерв под макс.16 символов константы 'OriginalFilename',
;// .. +2 для форматированного вывода на консоль.
В результате, если теперь заполнить поля cp и spase, получим единую строку, которую можно будет вскормить в виде аргумента сл.функции чтения. Правда в зависимости от длинны строковой константы, нужно будет вставлять терминальный нуль в её хвост, но это уже дело техники (см.исходник).
6. На данном этапе имеем всё, чтобы считать очередную "строку версии" из файла – зовём для этого функцию VerQueryValue() и получаем указатель на запрошенную ANSI-строку. Поскольку часто разработчик заполняет строки кириллицей (а мы выводим их на консоль), то имеет смысл функцией CharToOem() сразу конвертировать их в кодировку СР-866.
7. Мотаем предыдущий пункт(6) ровно 12-раз, в цикле подставляя следующую текстовую константу в поле space. Это позволит проверить в файле все без исключения поля "VERSION" и если какое-нибудь из них пустое, функция VerQueryValue() будет возвращать ошибку, а мы пропускать вывод его на консоль.
8. По окончании внутреннего цикла из 12 итераций, сбрасываем все переменные в дефолт и функцией FindNextFile() переходим к поиску следующего файла в текущем каталоге. Если файлов с указанным расширением больше нет, функция вернёт ошибку, сигнализируя, что нам пора на выход из программы. Иначе, уходим на начало глобального цикла к этапу [3].
9. Заключительный этап требует всего-лишь освобождения дескриптора поиска функцией FindClose() и ожидания любой клавиши, чтобы осмотреться.
Ну и теперь собственно код, который реализует на практике описанный выше алгоритм:
C-подобный:
format pe console
include 'win32ax.inc'
entry start
;//----------
.data
nameTable dd n01,n02,n03,n04,n05,n06,n07,n08,n09,n10,n11,n12
nextOffs dd 0
n01 db 11,'CompanyName',0
n02 db 14,'LegalCopyright',0
n03 db 15,'LegalTrademarks',0
n04 db 11,'ProductName',0
n05 db 14,'ProductVersion',0
n06 db 15,'FileDescription',0
n07 db 11,'FileVersion',0
n08 db 16,'OriginalFilename',0
n09 db 12,'InternalName',0
n10 db 12,'PrivateBuild',0
n11 db 12,'SpecialBuild',0
n12 db 07,'Comment',0
queryStr db '\StringFileInfo\'
cp db '00000000\'
space db '.................: ',0
fMask db '*.'
extens db 32 dup(0) ;// под маску файлов EXE,DLL и прочии.
align 16
fData db 128 dup(0) ;// структура "FIND_DATA"
fName = fData+44 ;// смещение имени файла в ней
fHndl dd 0 ;// дескриптор поиска
lpBuff dd 0 ;// указатель на строки
lpLen dd 0 ;// под размер этих строк
counter dd 12 ;// всего строковых констант
codePage db 64 dup(0) ;// под текст кодовой страницы
endPtr dd 0 ;// для fn. strtol()
align 16
verSize dd 0 ;// размер данных VERSION
verData rb 4096 ;// 4К буфер под эти данные
;//----------
.code
start: invoke SetConsoleTitle,<'***File Version Info v0.1***',0>
cinvoke printf,<10,' Support file..: exe,sys,dll,drv,ocx,cpl',\
10,' Type extension: ',0>
cinvoke scanf,<'%s'>,extens ;// запрашиваем расширение файла
;//===== Начинаем поиск первого файла в текущей папке
invoke FindFirstFile, fMask, fData
mov [fHndl],eax ;// запомнить хэндл поиска
cmp eax,-1 ;// проверить на ошибку
jnz @findAnyFile ;// ОК!
cinvoke printf,<' ERROR! File not found.',0>
jmp @exit ;// иначе: Error
;// покажем имя найденного файла
@findAnyFile:
cinvoke printf,<10,10,' File: %s',\
10,' ********************',0>,fName
;//===== Вывод VersionInfo из секции-ресурсов
;// для начала нужно узнать размер данных
invoke GetFileVersionInfoSize,fName,verSize
push eax
cinvoke printf,<10,' Version data size..: %u byte',0>,eax
pop ecx
or ecx,ecx ;// проверить наличие VersionInfo в ресурсах
je @fuck ;// если прокол..
;//===== Копируем Version из файла в свой буфер "verData"
;// в регистре ECX лежит размер текущих данных
invoke GetFileVersionInfo,fName,0,ecx,verData
;// Не рабочий метод получения Lang/CodePage
;// лучше гарантированно вытащить их семью строками ниже
;// invoke VerQueryValue,verData,<'\VarFileInfo\Translation',0>,lpBuff,lpLen
;// Здесь нужно взять из буфера "язык и кодировку" (unicode),
;// чтобы передать их в функцию VerQueryValue() как ANSI.
mov esi,verData ;// начало данных
add esi,0x86 ;// +86h = адрес Lang/CP (источник)
mov edi,cp ;// приёмник для конкатенации строк
mov ecx,8 ;// длина Unicode-строки
@@: lodsw ;// взять 2-байта
stosb ;// сохранить 1 младший
loop @b ;// повторить ECX-раз..
;// За одно покажем язык/кодировку пользователю
mov esi,cp ;// источник
mov edi,codePage ;// приёмник
lodsd ;//
stosd ;// скопировать из ESI в EDI 4-байта
sub edi,4 ;// ...(edi на родину)
cinvoke strtol,codePage,endPtr,16 ;// перевести из строки в hex-число!
push eax
cinvoke printf,<10,' Lang/CodePage......: %04d/',0>,eax ;//<----- язык
lodsd ;// вторые 4-байта
stosd ;//
cinvoke strtol,codePage,endPtr,16
cinvoke printf,<'%04d',0>,eax ;//<----- кодировка
;// покажем строкой изъятый язык (код лежит в EAX)
pop eax
invoke VerLanguageName,eax,codePage,64
invoke CharToOem,codePage,codePage
cinvoke printf,<' = %s',0>,codePage
;//***** ВНУТРЕННИЙ ЦИКЛ ИЗ 12 ИТЕРАЦИЙ *******************
@prnVersion:
mov edi,space ;// адрес поля с текстовой константой
mov ecx,17 ;// его длина
mov al,'.' ;// чем заполнить
rep stosb ;// очищаем поле от мусора!
mov ebx,nameTable ;// указатель на таблицу имён
add ebx,[nextOffs] ;// + сл.смещение
mov esi,[ebx] ;// ESI = адрес строковой константы
mov cl,byte[esi] ;// ECX = длина этой константы (первый байт)
inc esi ;// пропустить этот байт
push esi ;// указатель на начало в стек
mov edi,space ;// адрес конкатенации
rep movsb ;// склеить строки!
mov byte[edi],0 ;// вставить в хвост нуль, для передачи в fn.
push edi ;//
invoke VerQueryValue,verData,queryStr,lpBuff,lpLen ;// получить данные!
pop edi esi ;//
mov byte[edi],'.' ;// убрать терминальный нуль (там есть ещё один)
or eax,eax ;// проверить наличие инфы данного типа
jz @f ;// пропустить печать, если нет..
;// иначе:
;// В некоторых файлах встречается кирилица 1251,
;// поправим её кодировку для вывода на консоль 866.
invoke CharToOem,[lpBuff],[lpBuff]
cinvoke printf,<10,' %s %s',0>,space,[lpBuff]
@@: add [nextOffs],4 ;// сл.смещение в таблице
dec [counter] ;// уменьшить счётчик 12-ти
jnz @prnVersion ;// повторить вн.цикл, если он не нуль
;//===== Продолжить поиск файлов..
@fuck: mov [nextOffs],0 ;// очистить все переменные от мусора
mov [counter],12 ;// ^^^^
invoke FindNextFile,[fHndl],fData ;// продолжить поиск файлов в каталоге
or eax,eax ;// 0 = ошибка
jnz @findAnyFile ;// иначе: мотаем внешний цикл дальше
;//===== КОНЕЦ ПРОГРАММЫ ===================================
@exit: invoke FindClose,[fHndl] ;// хэндл поиска в топку
cinvoke getch,verData ;// ждём клаву...
cinvoke exit,0
;//----- ИМПОРТ ----------
section '.idata' import data readable
library msvcrt, 'msvcrt.dll',version,'version.dll',\
kernel32,'kernel32.dll',user32,'user32.dll'
import msvcrt, printf,'printf',scanf,'scanf',\
getch,'_getch',exit,'exit',strtol,'strtol'
import version, GetFileVersionInfoSize,'GetFileVersionInfoSizeA',\
GetFileVersionInfo,'GetFileVersionInfoA',\
VerQueryValue,'VerQueryValueA'
include 'api\kernel32.inc'
include 'api\user32.inc'
;//----- РЕСУРСЫ ----------
section '.rsrc' resource data readable
directory RT_VERSION,ver1
resource ver1,1,LANG_NEUTRAL,vInfo
versioninfo vInfo, VOS__WINDOWS32, VFT_APP, VFT2_UNKNOWN,\
LANG_ENGLISH + SUBLANG_DEFAULT, 1252,\
'CompanyName' , 'https://codeby.net',\
'LegalCopyright' , 'Copyright 2020-2021 (c)Marylin',\
'ProductName' , 'VERSION-INFO',\
'ProductVersion' , '6.1.7601.3821',\
'FileDescription' , 'PE-file version Identifier',\
'FileVersion' , '0.0.1.0',\
'OriginalFilename', 'VerInfo.exe',\
'InternalName' , 'VerInfo32/64',\
'Comment' , 'codeby.net/forums/assembler.229/',\
'LegalTrademarks' , 'FreeWare',\
'PrivateBuild' , '0.00.02.00',\
'SpecialBuild' , '0.01.00.01'
Рассмотрим бегло этот лог..
Значит в начале я указал искать в текущем каталоги только файлы типа EXE, на что получил версии трёх (не считая своего) файлов. Судя по логу, первый является кейгеном к переводчику "Socrat-Personal", причём разработчик использует шаблон, поскольку константы есть, только они не заполнены. Иначе прожка пропустила-бы их, как в случае с последним файлом "ProcessMM.exe".
Вторым в логе красуется файл-ядра системы Ntoskrnl.exe, в котором заполнены уже почти все возможные поля, кроме коммента и Build. Я его честно скоммуниздил у десятки, о чём свидетельствует поле "FileVersion". Лог закрывает инфа о программе маппинга памяти процесса (Memory-Map), и его разраб не удосужился заполнить даже основные поля, ограничившись для галочки лишь версией. Здесь так-же видно, что у всех файлов язык 1033, а кодировка 1200 или 1252. Расшифровать эти коды можно с помощью предыдущей прожки.
4. Эпилог
В данной статье я попытался освятить только одну часть ресурсов, а ведь к ней присовокупляются ещё 23 типа, и каждый из них заслуживает отдельного внимания, т.к. является вполне самостоятельным компонентом. Чтобы ознакомится со-всеми ресурсами и посмотреть на их формат, можно воспользоваться программой "PE-Explorer". Эта крутая тулза не только просмотрщик, но и редактор любой области исполняемых файлов, начиная от РЕ-заголовка и заканчивая ресурсами. Огромным её плюсом считается возможность при помощи мастера создавать файлы манифестов – советую..
По уже отработанной привычке, в скрепку я положил два рассмотренных файла, которые не привязаны как к версии системы, так и к её разрядности. Всем удачи, пока!
Вложения
Последнее редактирование: