Всем привет! Когда-то давно я исследовал PE-формат, в особенности EXE. Я решил создать простой загрузчик исполняемых файлов специально для VB6-скомпилированных приложений. Этот загрузчик, по моим задумкам, должен загружать любое VB6-скомпилированное приложение из памяти, миную запись в файл. ВСЕ ЭТО БЫЛО СДЕЛАНО ДЛЯ ЭКСПЕРИМЕНТАЛЬНЫХ ЦЕЛЕЙ ДЛЯ ТОГО ЧТОБЫ ПРОВЕРИТЬ ТАКУЮ ВОЗМОЖНОСТЬ НА VB6. Из-за того что VB6-скомпилированные приложения не используют большинство PE-фичей это было довольно легкой задачей. Также большинство программистов говорят что любая VB6-скомпилированная программа неукоснительно связана с VB6-рантаймом (msvbvm60) и что такая программа не будет работать без рантайма и рантайм является довольно медленным. Сегодня я докажу что можно написать приложение абсолютно не использующее рантайм (хотя я такое уже делал в драйвере). Я думаю что это могло бы быть интересным для тех кто хочет изучить базовые принципы работы с PE файлами.
Прежде чем мы начнем я бы хотел сказать пару слов о проектах. Эти проекты не тестировались достаточно хорошо, поэтому они могут содержать различные проблемы. Также загрузчик не поддерживает множество возможностей PE-файлов следовательно некоторые приложения могут не работать.
Итак...
Этот обзор включает три проекта:
- Compiler - самый большой проект из всех. Он позволяет создавать лаунчер базируемый на загрузчике, пользовательских файлах, командах и манифесте;
- Loader - простейший загрузчик который выполняет команды, распаковывает файлы и запускает EXE из памяти;
- Patcher - маленькая утилита которая удаляет рантайм из VB6-скомпилированного приложения.
Перое хранилище хранит все файлы которые будут распакованы и главный EXE который будет запускаться из памяти. Второе хранилище хранит команды которые будут переданы в функцию ShellExecuteEx после процесса того как процесс распаковки будет окончен.
Загрузчик поддерживает следующие подставляемые символы (для путей):
- <app> - путь, откуда запущен EXE;
- <win> - системная директория;
- <sys> - System32;
- <drv> - системный диск;
- <tmp> - временная директория;
- <dtp> - рабочий стол.
Компилятор.
Это приложение формирующее информацию для инсталляции и размещающее ее в ресурсах загрузчика. Вся информация хранится в файлах проекта. Вы можете сохранять и загружать проекты из файлов. Класс clsProject описывает такой проект. Компилятор содержит 3 секции: storage, execute, mainfest.
Секция 'storage' позволяет добавлять файлы которые будут скопированы в момент запуска приложения. Каждая запись в списке имеет флаги: 'replace if exists', 'main executable', 'ignore error'. Если выбрана 'replace if exists' то файл будет скопирован из ресурсов даже если он есть на диске. Флаг 'main executable' может быть установлен только единственного исполняемого файла который будет запущен когда все операции будут исполнены. И наконец 'ignore error' просто заставляет игнорировать все ошибки и не выводить сообщения. Порядок расположения записей в списке соответствует порядку распаковки файлов, исключая главный исполняемый файл. Главный исполняемый файл не извлекается и запускается после всех операций. Класс clsStorage описывает данную секцию. Этот класс содержит коллекцию объектов класса
clsStorageItem и дополнительные методы. Свойство MainExecutable определяет индекс главного исполняемого файла в хранилище. Когда этот параметр равен -1 значит главный исполняемый файл не задан. Класс clsStoragaItem описывает одну запись из списка хранилища, который содержит свойства определяющие поведение итема. Секция 'storage' полезна если вы хотите скопировать файлы на диск перед выполнением главного приложения (различные ресурсы/OCX/DLL и т.п.).
Следующая секция называется 'execute'. Она содержит список выполняемых команд. Эти команды просто передаются в функцию ShellExecuteEx. Таким образом можно к примеру зарегистрировать библиотеки или сделать что-то еще. Каждый элемент этого списка имеет два свойства: путь и параметры. Стоит отметить что все команды выполняються синхронно в порядке заданным в списке. Также каждый элемент списка может иметь флаг 'ignore error' который предотвращает вывод каких-либо сообщений об ошибках. Секция 'execute' представлена двумя классами clsExecute and clsExecuteItem которые очень похожи на классы хранилища.
Последняя секция - 'manifest'. Это просто текстовый файл который добавляеться в финальный файл в качестве манифеста. Для того чтобы включить манифест в EXE нужно просто выбрать флажок 'include manifest' во вкладке 'mainfest'. Это может быть полезно для использования библиотек без регистрации, визуальных стилей и т.п.
Все классы ссылаються на объект проекта (clsProject) который управляет ими. Каждый класс который ссылается на проект может быть сохранен или заружен используя PropertyBag в качестве контейнера. Все ссылки сохраняються с относительными путями (как в .vbp файле) поэтому можно перемещать папку с проектом без проблем с путями. Для того чтобы транслировать из/то относительного/абсолютного пути я использовал функции PathRelativePathTo и PathCanonicalize.
Итак, это была базовая информация о проекте Compiler. Сейчас я расскажу о процедуре компиляции. Как я уже сказал вся информация об инсталляции сохраняется в ресурсы загрузчика. Вначале на нужно определить формат данных:
- Код: Выделить всё
' // Storage list item
Private Type BinStorageListItem
ofstFileName As Long ' // Offset of file name
ofstDestPath As Long ' // Offset of file path
dwSizeOfFile As Long ' // Size of file
ofstBeginOfData As Long ' // Offset of beginning data
dwFlags As FileFlags ' // Flags
End Type
' // Execute list item
Private Type BinExecListItem
ofstFileName As Long ' // Offset of file name
ofstParameters As Long ' // Offset of parameters
dwFlags As ExeFlags ' // Flags
End Type
' // Storage descriptor
Private Type BinStorageList
dwSizeOfStructure As Long ' // Size of structure
iExecutableIndex As Long ' // Index of main executable
dwSizeOfItem As Long ' // Size of BinaryStorageItem structure
dwNumberOfItems As Long ' // Number of files in storage
End Type
' // Execute list descriptor
Private Type BinExecList
dwSizeOfStructure As Long ' // Size of structure
dwSizeOfItem As Long ' // Size of BinaryExecuteItem structure
dwNumberOfItems As Long ' // Number of items
End Type
' // Base information about project
Private Type BinProject
dwSizeOfStructure As Long ' // Size of structure
storageDescriptor As BinStorageList ' // Storage descriptor
execListDescriptor As BinExecList ' // Command descriptor
dwStringsTableLen As Long ' // Size of strings table
dwFileTableLen As Long ' // Size of data table
End Type
Структура BinProject размещается в начале ресурсов. Заметьте что проект сохраняется как RT_RCDATA с именем PROJECT. Поле dwSizeOfStructure определяет размер структуры BinProject. storageDescriptor и execListDescriptor определяют описатели хранилища и команд соответственно. Поле dwStringsTableLen показывает размер строковой таблицы. Строковая таблица содержит все имена и команды в формате UNICODE. Поле dwFileTableLen определяет размер всех данных в хранилище. И хранилище BinStorageList и списки команд BinExecList также имеют поля dwSizeOfItem и dwSizeOfStructure которые определяют размер структуры описателя и размер одного элемента в списке. Эти структуры также содержат поле dwNumberOfItems которое показывает количество элементов в списке. Поле iExecutableIndex содержит индекс исполняемого файла в хранилище. Общая структура показана на рисунке:
Любой элемент может ссылаться на таблицу строк и таблицу файлов. Для этой цели используется смещение относительно начала таблицы. Все итемы расположены одна за другой. Теперь мы знаем внутренний формат проекта и можем поговорить о том как постороить загрузчик который будет содержать эти данные. Как я уже сказал мы сохраняем данные в ресурсы загрузчика. О самом загрузчике я расскажу позднее, а сейчас я хотел бы заметить одну важную особенность. Когда мы ложим данные проекта в EXE файл загрузчика то это не затрагивает другие данные в ресурсах. Для примера, если запустить такой EXE то информация хранящаяся в ресурсах внутреннего EXE не будет загружена. Тоже самое относится к иконкам и версии приложения. Для избежания данных проблем нужно скопировать все ресурсы из внутреннего EXE в загрузчик. WinAPI предоставляет набор функций для замены ресурсов. Для того чтобы получить список ресурсов нам нужно распарсить EXE файл и извлечь данные. Я написал функцию LoadResources которая извлекает все ресурсы EXE файла в массив.
PE формат.
Для того чтобы получить ресурсы из EXE файла, запустить EXE из памяти и хорошо разбираться в структуре EXE фала мы должны изучить PE (portable executable) формат. PE формат имеет довольно сложную структуру. Когда загрузчик запускает PE file (exe или dll) он делает довольно много работы. Каждый PE файл начинается со специальной структуры IMAGE_DOS_HEADER aka. DOS-заглушка. Поскольку и DOS и Windows приложения имеют расширение exe существует возможность запуска exe файла в DOS, но если попытаться сделать это в DOS то он выполнит это заглушку. Обычно в этом случае показываетсясообщение: "This program cannot be run in DOS mode", но мы можем написать там любую программу:
- Код: Выделить всё
Type IMAGE_DOS_HEADER
e_magic As Integer
e_cblp As Integer
e_cp As Integer
e_crlc As Integer
e_cparhdr As Integer
e_minalloc As Integer
e_maxalloc As Integer
e_ss As Integer
e_sp As Integer
e_csum As Integer
e_ip As Integer
e_cs As Integer
e_lfarlc As Integer
e_ovno As Integer
e_res(0 To 3) As Integer
e_oemid As Integer
e_oeminfo As Integer
e_res2(0 To 9) As Integer
e_lfanew As Long
End Type
Но поскольку мы не пишем DOS программы для нас эта структура не важна. Нам интересно только поля e_magic и e_lfanew. Первое поле должно содержать сигнатуру 'MZ' aka. IMAGE_DOS_SIGNATURE а второе смещение до очень важной структуры IMAGE_NT_HEADERS:
- Код: Выделить всё
Type IMAGE_NT_HEADERS
Signature As Long
FileHeader As IMAGE_FILE_HEADER
OptionalHeader As IMAGE_OPTIONAL_HEADER
End Type
Первое поле этой структуры содержит сигнатуру 'PE\0\0' (aka. IMAGE_NT_SIGNATURE). Следующее поле описывает исполняемый файл и имеет следующий формат:
- Код: Выделить всё
Type IMAGE_FILE_HEADER
Machine As Integer
NumberOfSections As Integer
TimeDateStamp As Long
PointerToSymbolTable As Long
NumberOfSymbols As Long
SizeOfOptionalHeader As Integer
Characteristics As Integer
End Type
Поле Machine определяет архитектуру процессора и должно иметь значение IMAGE_FILE_MACHINE_I386 в нашем случае. Поле NumberOfSections определяет количество секций в PE файле.
- Любой EXE файл содержит секции. Каждая секция занимает место в адресном пространстве процесса и опционально в файле. Секция может содержать как код так и данные (инизиализированные или не), а также имеет имя. Наиболее распространенные имена: .text, .data, .rsrc. Обычно секция .text содержит код, .data инициализированные данные, а .rsrc - ресурсы. Можно изменять это поведение используя дериктивы линкера. Каждая секция имеет адрес называемый виртуальным адресом. В общем в PE формате существует несколько типов адресации. Первый - относительный виртуальный адрес (RVA). Из-за того что PE фал может быть загружен по любому адресу все ссылки внутри PE файла имеют относительную адресацию. RVA - это смещение относительно базового адреса (адреса первого байта PE-образа в памяти). Сумма RVA и базового адреса называется виртуальным адресом (VA). Также существует RAW-смещение которое показывает смещение относительно начала файла относительно RVA. Заметьте что RVA <> RAW. Когда модуль загружается каждая секция размещается по виртуальному адресу. Для примера модуль может иметь секцию что не имеет инициализированных данных. Такая секция не будет занимать место в PE-файле, но будет в памяти. Это очень важный момент поскольку мы будем работать с сырым EXE файлом.
Поле TimeDateStamp содержит дату создания PE модуля в формате UTC. Поля PointerToSymbolTable and NumberOfSymbols содержат информацию о символах в PE файлах. В общем эти поля содержат нули, но эти поля всегда используються в объектных файлах (*.OBJ, *.LIB) для разрешения ссылок во время линковки а также содержат отладочную информацию для PE модуля. Следующее поле SizeOfOptionalHeader содержит размер структуры расположенной после IMAGE_FILE_HEADER так называемой IMAGE_OPTIONAL_HEADER которая всегда присутствует в PE файлах (хотя может отсутствовать в OBJ файлах). Эта структура являеться очень важной для загрузки PE модуля в память. Заметьте что эта структура различается в 32 битных и 64 битных PE-модулях. И наконец поле Characteristics содержит PE-аттрибуты.
Структура IMAGE_OPTIONAL_HEADER имеет следующий формат:
- Код: Выделить всё
Type IMAGE_OPTIONAL_HEADER
Magic As Integer
MajorLinkerVersion As Byte
MinorLinkerVersion As Byte
SizeOfCode As Long
SizeOfInitializedData As Long
SizeOfUnitializedData As Long
AddressOfEntryPoint As Long
BaseOfCode As Long
BaseOfData As Long
ImageBase As Long
SectionAlignment As Long
FileAlignment As Long
MajorOperatingSystemVersion As Integer
MinorOperatingSystemVersion As Integer
MajorImageVersion As Integer
MinorImageVersion As Integer
MajorSubsystemVersion As Integer
MinorSubsystemVersion As Integer
W32VersionValue As Long
SizeOfImage As Long
SizeOfHeaders As Long
CheckSum As Long
SubSystem As Integer
DllCharacteristics As Integer
SizeOfStackReserve As Long
SizeOfStackCommit As Long
SizeOfHeapReserve As Long
SizeOfHeapCommit As Long
LoaderFlags As Long
NumberOfRvaAndSizes As Long
DataDirectory(15) As IMAGE_DATA_DIRECTORY
End Type
Первое поле содержит тип образа (x86, x64 или ROM образ). Нас интересует только IMAGE_NT_OPTIONAL_HDR32_MAGIC который представляет собой 32 битное приложение. Следующие 2 поля не являются важными (они использовались на старых системах) и содержат 4. Следующая группа полей содержит размер всех секций с кодом, инициализированными данными и неинициализированными данными. Эти значения должны быть кратными значению SectionAlignment этой структуры (см. далее). Поле AddressOfEntryPoint является очень важным RVA значением которое определяет точку входа в программу. Мы будем использовать это поле когда загрузим PE образ в память для запуска кода. Следующим важным полем является ImageBase которое задает предпочитаемый виртуальный адрес загрузки модуля. Когда загрузчик начинает загружать модуль, то он старается сделать это по предпочитаемому виртуальному адресу (находящимся в ImageBase). Если этот адрес занят, то загрузчик проверяет поле Characteristics структуры IMAGE_FILE_HEADER. Если это поле содержит флаг IMAGE_FILE_RELOCS_STRIPPED то модуль не сможет быть загружен. Для того чтобы загрузить такие модули нам нужно добавить информацию о релокации которая позволит загрузчику настроить адреса внутри PE-образа если модуль не может загрузится по предпочитаемому базовому адресу. Мы будем использоват это поле вместе с SizeOfImage для того чтобы зарезервировать память под распакованный EXE. Поля SectionAlignment and FileAlignment содержат выравнивание секций в памяти и в файле соответственно. Изменяя файловое выравнивание можно уменьшить размер PE файла, но система может не загрузить данный PE файл. Выравнивание секций обычно равно размеру страницы в памяти. Поле SizeOfHeaders задает размер всех заголовков (DOS Заголовок, NT заголовок, заголовки секций) выровненное на FileAlignment. Значения SizeOfStackReserve и SizeOfStackCommit определяют общий размер стека и начальный размер стека. Тоже самое и для полей SizeOfHeapReserve и SizeOfHeapCommit, но для кучи. Поле NumberOfRvaAndSizes содержит количество элементов в массиве DataDirectory. Это поле всегда равно 16. Массив DataDirectory является также очень важным поскольку в нем содержатся каталоги данных которые содержат нужную информацию об импорте, экспорте, ресурсах, релокациях и т.д. Мы будем использовать только несколько элементов из этого каталога которые используются VB6 компилятором. Я расскажу о каталогах немного позже, давайте посмотрим что находится за каталогами. За каталогами содержаться описатели секций. Количество этих описателей, если вспомнить, мы получили из структуры IMAGE_FILE_HEADER. Рассмотрим формат заголовка секции:
- Код: Выделить всё
Type IMAGE_SECTION_HEADER
SectionName(7) As Byte
VirtualSize As Long
VirtualAddress As Long
SizeOfRawData As Long
PointerToRawData As Long
PointerToRelocations As Long
PointerToLinenumbers As Long
NumberOfRelocations As Integer
NumberOfLinenumbers As Integer
Characteristics As Long
End Type
Первое поле содержит имя секции в формате UTF-8 c завершающим нуль-терминалом. Это имя ограничено 8-ю символами (если имя секции имеет размер 8 символов то нуль-терминатор игнорируется). COFF файл может иметь имя больше чем 8 символов в этом случае имя начинается с символа '/' за которым следует ASCII строка с десятичным значением смещения в строковой таблице (поле IMAGE_FILE_HEADER). PE файл не поддерживает длинные имена секций. Поля VirtualSize и VirtualAddress содержат размер секции в памяти и адрес (RVA). Поля SizeOfRawData и PointerToRawData содержат RAW адрес данных в файле (если секция содержит инициализированные данные). Это ключевой момент потому что мы можем вычислить RAW адрес с помощью относительного виртуального адреса используя информацию из заголовка секций. Я написал функцию для перевода RVA адресации в RAW смещение в файле:
- Код: Выделить всё
' // RVA to RAW
Function RVA2RAW( _
ByVal rva As Long, _
ByRef sec() As IMAGE_SECTION_HEADER) As Long
Dim index As Long
For index = 0 To UBound(sec)
If rva >= sec(index).VirtualAddress And _
rva < sec(index).VirtualAddress + sec(index).VirtualSize Then
RVA2RAW = sec(index).PointerToRawData + (rva - sec(index).VirtualAddress)
Exit Function
End If
Next
RVA2RAW = rva
End Function
Эта функция перечисляет все секции и проверяет если переданный адрес находится в пределах секции. Следующие 5 полей используються только в COFF файлах и не важны в PE файлах. Поле Characteristics содержит атрибуты секции такие как права доступа к памяти и управление. Мы будем использовать это поле для защиты памяти exe файла в загрузчике.
Давайте теперь вернемся к каталогам данных. Как мы видели существует 16 элементов в данном каталоге. Обычно PE файл не использует их все. Давайте рассмотрим структуру элемента каталога:
- Код: Выделить всё
Private Type IMAGE_DATA_DIRECTORY
VirtualAddress As Long
Size As Long
End Type
Эта структура содержит два поля. Первое поле содержит RVA адрес данных каталога, воторое - размер. Когда элемент каталога не представлен в PE файле то оба поля содержат нули. Вообще большинство VB6-компилируемых приложений имеют только 4 каталога: таблица импорта, таблица ресурсов, таблица связанного импорта и таблица адресов импорта (IAT). Сейчас мы рассмотрим таблицу ресурсов которая имеет индекс IMAGE_DIRECTORY_ENTRY_RESOURCE потому что мы работаем с этой информацией в проекте Compiler.
Все ресурсы в EXE файле представлены в виде трехуровнего дерева. Первый уровень определяет тип ресурса (RT_BITMAP, RT_MANIFEST, RT_RCDATA, и т.д.), следующий - идентификатор ресурса и наконец третий - язык. В стандартном редакторе ресурсов VB Resource Editor можно изменять только первые 2 уровня. Все ресурсы размещаются таблице ресурсов расположенной в секции .rsrc EXE файла. Благодаря такой структуре мы можем изменять ресурсы даже в готовом EXE файле. Для того чтобы добраться до самих данных в секции ресурсов нам сначала нужно прочитать IMAGE_DIRECTORY_ENTRY_RESOURCE из опционального хидера. Поле VirtualAddress содержит RVA таблицы ресурсов которая имеет следующий формат:
- Код: Выделить всё
Type IMAGE_RESOURCE_DIRECTORY
Characteristics As Long
TimeDateStamp As Long
MajorVersion As Integer
MinorVersion As Integer
NumberOfNamedEntries As Integer
NumberOfIdEntries As Integer
End Type
Эта структура описывает все ресурсы в PE файле. Первые 4 поля не важны для нас; поле NumberOfNamedEntries и NumberOfIdEntries содержат количество именованных записей и записей с числовыми идентификаторами соответственно. Для примера, когда мы добавляем картинку в стандартном редакторе это добавит запись с числовым идентификатором равным 2 (RT_BITMAP). Сами записи расположены сразу после IMAGE_RESOURCE_DIRECTORY и имеют следующую структуру:
- Код: Выделить всё
Type IMAGE_RESOURCE_DIRECTORY_ENTRY
NameId As Long
OffsetToData As Long
End Type
Первое поле этой структуры определяет является ли это именованной запись либо это запись с числовым идентификатором в зависимости от старшего бита. Если этот бит установлен то остальные биты определяют смещение от начала ресурсов к структуре IMAGE_RESOURCE_DIR_STRING_U которая имет следующий формат:
- Код: Выделить всё
Type IMAGE_RESOURCE_DIR_STRING_U
Length As Integer
NameString As String
End Type
Заметьте что это не правильная VB-структура и показана для наглядности. Первые два байта являются беззнаковым целым которые показывают длину строки в формате UNICODE (в символах) которая следует за ними. Таким образом для того чтобы получить строку нам нужно прочитать первые два байта с размером, выделить память для строки согласно этого размера и прочитать данные в строковую переменную. Напротив, если старший бит поля NameId сброшен то оно содержит числовой идентификатор ресурса (RT_BITMAP в примере). Поле OffsetToData имеет также двойную интерпретацию. Если старший бит установлен то это смещение (от начала ресурсов) до следующего уровня дерева ресурсов, т.е. до структуры IMAGE_RESOURCE_DIRECTORY. Иначе - это смещение до структуры IMAGE_RESOURCE_DATA_ENTRY:
- Код: Выделить всё
Type IMAGE_RESOURCE_DATA_ENTRY
OffsetToData As Long
Size As Long
CodePage As Long
Reserved As Long
End Type
Наиболее важными для нас являются поля OffsetToData and Size которые содержат RVA и размер сырых данных ресурса. Теперь мы можем извлечь все данные из ресурсов любого PE файла.
Компиляция.
Итак, когда мы начинаем компиляцию проекта то вызывается метод Compile объекта класса clsProject. Вначале упаковываются все элементы хранилища и команд в бинарный формат (BinProject, BinStorageListItem, и т.д.) и формируются таблица строк и файловая таблица. Строковая таблица сохраняется как набор строк разделенных нуль-терминалом. Я использую специальный класс clsStream для безопасной работы с бинарными данными. Этот класс позволяет читать и писать любые данные или потоки в двоичный буфер, сжимать буфер. Я использую функцию RtlCompressBuffer для сжатия потока которая использует LZ-сжатие. После упаковки и сжатия проверяется выходной формат файла. Поддерживаются 2 типа файлов: бинарный (сырые данные проекта) и исполняемый (загрузчик). Двоичный формат не интересен поэтому мы будем рассматривать исполняемый формат. Вначале извлекаются все ресурсы из главного исполняемого файла в трехуровневый каталог. Эта операция выполняется с помощью функции ExtractResorces. Имена-идентификаторы сохраняются в строковом виде с префиксом '#'. Потом клонируется шаблон загрузчика в результирующий файл, начинается процесс модификации ресурсов в EXE файле используя функцию BeginUpdateResource. После этого последовательно копируются все извлеченные ресурсы (UpdateResource), двоичный проект и манифест (если нужно) в результирующий файл и применяются изменения функцией EndUpdateResource. Опять повторюсь, бинарный проект сохраняется с именем PROJECT и имеет тип RT_DATA. В общем все.
Загрузчик.
Итак. я думаю это наиболее интересная часть. Итак, нам нужно избегать использование рантайма. Как этого добится? Я дам некоторые правила:
- Установить в качестве стартовой функции пользовательскую функцию;
- Избегать любых объектов и классов в проекте;
- Избегать непосредственных массивов. Массивы фиксированного размера в пользовательских типах не запрещены;
- Избегать строковых переменных а также Variant/Object переменных. В некоторых случаях Currency/Date;
- Избегать API функции задекларированые с помощью ключевого слова Declare;
- Избегать VarPtr/StrPtr/ObjPtr и некоторые стандартные функции;
- ...
- ...
Итак, начнем. Для того чтобы избежать использования строковых переменных я храню все строковые переменные как Long указатели на строки. Существует проблема с загрузкой строк поскольку мы не можем обращаться к любой строке чтобы загрузить ее. Я решил использовать ресурсы в качестве хранилища строк и загружать их по числовому идентификатору. Таким образом мы можем хранить указатель в переменной Long без обращения к рантайму. Я использовал TLB (библиотеку типов) для всех API функций без атрибута usesgetlasterror чтобы избежать объявление через Declare. Для установки стартовой функции я использую опции линкера. Стартовая функция в загрузчике - Main. Обратите внимание, если в IDE выбрать стартовую функцию Main на самом деле это не будет стартовой функцией приложения потому что VB6-скомпилированное приложение начинается с функции __vbaS которая вызывает функцию ThunRTMain из рантайма, которая инициализирует рантайм и поток.
Загрузчик содержит три модуля:
- modMain - стартовая функция и работа с хранилищем;
- modConstants - работа со строковыми константами;
- modLoader - загрузчик EXE файла.
- Код: Выделить всё
' // Startup subroutine
Sub Main()
' // Load constants
If Not LoadConstants Then
MessageBox 0, GetString(MID_ERRORLOADINGCONST), 0, MB_ICONERROR Or MB_SYSTEMMODAL
GoTo EndOfProcess
End If
' // Load project
If Not ReadProject Then
MessageBox 0, GetString(MID_ERRORREADINGPROJECT), 0, MB_ICONERROR Or MB_SYSTEMMODAL
GoTo EndOfProcess
End If
' // Copying from storage
If Not CopyProcess Then GoTo EndOfProcess
' // Execution process
If Not ExecuteProcess Then GoTo EndOfProcess
' // If main executable is not presented exit
If ProjectDesc.storageDescriptor.iExecutableIndex = -1 Then GoTo EndOfProcess
' // Run exe from memory
If Not RunProcess Then
' // Error occrurs
MessageBox 0, GetString(MID_ERRORSTARTUPEXE), 0, MB_ICONERROR Or MB_SYSTEMMODAL
End If
EndOfProcess:
If pProjectData Then
HeapFree GetProcessHeap(), HEAP_NO_SERIALIZE, pProjectData
End If
ExitProcess 0
End Sub
Вначале вызывается функция LoadConstants для того чтобы загрузить все необходимые константы из ресурсов:
- Код: Выделить всё
' // modConstants.bas - main module for loading constants
' // © Krivous Anatoly Anatolevich (The trick), 2016
Option Explicit
Public Enum MessagesID
MID_ERRORLOADINGCONST = 100 ' // Errors
MID_ERRORREADINGPROJECT = 101 '
MID_ERRORCOPYINGFILE = 102 '
MID_ERRORWIN32 = 103 '
MID_ERROREXECUTELINE = 104 '
MID_ERRORSTARTUPEXE = 105 '
PROJECT = 200 ' // Project resource ID
API_LIB_KERNEL32 = 300 ' // Library names
API_LIB_NTDLL = 350 '
API_LIB_USER32 = 400 '
MSG_LOADER_ERROR = 500
End Enum
' // Paths
Public pAppPath As Long ' // Path to application
Public pSysPath As Long ' // Path to System32
Public pTmpPath As Long ' // Path to Temp
Public pWinPath As Long ' // Path to Windows
Public pDrvPath As Long ' // Path to system drive
Public pDtpPath As Long ' // Path to desktop
' // Substitution constants
Public pAppRepl As Long
Public pSysRepl As Long
Public pTmpRepl As Long
Public pWinRepl As Long
Public pDrvRepl As Long
Public pDtpRepl As Long
Public pStrNull As Long ' // \0
Public hInstance As Long ' // Base address
Public lpCmdLine As Long ' // Command line
Public SI As STARTUPINFO ' // Startup parameters
Public LCID As Long ' // LCID
' // Load constants
Function LoadConstants() As Boolean
Dim lSize As Long
Dim pBuf As Long
Dim index As Long
Dim ctl As tagINITCOMMONCONTROLSEX
' // Load windows classes
ctl.dwSize = Len(ctl)
ctl.dwICC = &H3FFF&
InitCommonControlsEx ctl
' // Get startup parameters
GetStartupInfo SI
' // Get command line
lpCmdLine = GetCommandLine()
' // Get base address
hInstance = GetModuleHandle(ByVal 0&)
' // Get LCID
LCID = GetUserDefaultLCID()
' // Alloc memory for strings
pBuf = SysAllocStringLen(0, MAX_PATH)
If pBuf = 0 Then Exit Function
' // Get path to process file name
If GetModuleFileName(hInstance, pBuf, MAX_PATH) = 0 Then GoTo CleanUp
' // Leave only directory
PathRemoveFileSpec pBuf
' // Save path
pAppPath = SysAllocString(pBuf)
' // Get Windows folder
If GetWindowsDirectory(pBuf, MAX_PATH) = 0 Then GoTo CleanUp
pWinPath = SysAllocString(pBuf)
' // Get System32 folder
If GetSystemDirectory(pBuf, MAX_PATH) = 0 Then GoTo CleanUp
pSysPath = SysAllocString(pBuf)
' // Get Temp directory
If GetTempPath(MAX_PATH, pBuf) = 0 Then GoTo CleanUp
pTmpPath = SysAllocString(pBuf)
' // Get system drive
PathStripToRoot pBuf
pDrvPath = SysAllocString(pBuf)
' // Get desktop path
If SHGetFolderPath(0, CSIDL_DESKTOPDIRECTORY, 0, SHGFP_TYPE_CURRENT, pBuf) Then GoTo CleanUp
pDtpPath = SysAllocString(pBuf)
' // Load wildcards
For index = 1 To 6
If LoadString(hInstance, index, pBuf, MAX_PATH) = 0 Then GoTo CleanUp
Select Case index
Case 1: pAppRepl = SysAllocString(pBuf)
Case 2: pSysRepl = SysAllocString(pBuf)
Case 3: pTmpRepl = SysAllocString(pBuf)
Case 4: pWinRepl = SysAllocString(pBuf)
Case 5: pDrvRepl = SysAllocString(pBuf)
Case 6: pDtpRepl = SysAllocString(pBuf)
End Select
Next
' // vbNullChar
pStrNull = SysAllocStringLen(0, 0)
' // Success
LoadConstants = True
CleanUp:
If pBuf Then SysFreeString pBuf
End Function
' // Obtain string from resource (it should be less or equal MAX_PATH)
Public Function GetString( _
ByVal ID As MessagesID) As Long
GetString = SysAllocStringLen(0, MAX_PATH)
If GetString Then
If LoadString(hInstance, ID, GetString, MAX_PATH) = 0 Then SysFreeString GetString: GetString = 0: Exit Function
If SysReAllocString(GetString, GetString) = 0 Then SysFreeString GetString: GetString = 0: Exit Function
End If
End Function
Функция LoadConstants загружает все необходимые переменные и строки (hInstance, LCID, командная строка, подстановочные символы, пути по умолчанию, и т.д.). Все строки сохраняются в формате UNICODE-BSTR. Функция GetString загружает строку из ресурсов по ее идентификатору. Перечисление MessagesID содержит некоторые строковые идентификаторы нужные для работы программы (сообщения об ошибках, имена библиотек, и.т.д.). Когда все константы загрузятся вызывается функция ReadProject которая загружает проект:
- Код: Выделить всё
' // Load project
Function ReadProject() As Boolean
Dim hResource As Long: Dim hMememory As Long
Dim lResSize As Long: Dim pRawData As Long
Dim status As Long: Dim pUncompressed As Long
Dim lUncompressSize As Long: Dim lResultSize As Long
Dim tmpStorageItem As BinStorageListItem: Dim tmpExecuteItem As BinExecListItem
Dim pLocalBuffer As Long
' // Load resource
hResource = FindResource(hInstance, GetString(PROJECT), RT_RCDATA)
If hResource = 0 Then GoTo CleanUp
hMememory = LoadResource(hInstance, hResource)
If hMememory = 0 Then GoTo CleanUp
lResSize = SizeofResource(hInstance, hResource)
If lResSize = 0 Then GoTo CleanUp
pRawData = LockResource(hMememory)
If pRawData = 0 Then GoTo CleanUp
pLocalBuffer = HeapAlloc(GetProcessHeap(), HEAP_NO_SERIALIZE, lResSize)
If pLocalBuffer = 0 Then GoTo CleanUp
' // Copy to local buffer
CopyMemory ByVal pLocalBuffer, ByVal pRawData, lResSize
' // Set default size
lUncompressSize = lResSize * 2
' // Do decompress...
Do
If pUncompressed Then
pUncompressed = HeapReAlloc(GetProcessHeap(), HEAP_NO_SERIALIZE, ByVal pUncompressed, lUncompressSize)
Else
pUncompressed = HeapAlloc(GetProcessHeap(), HEAP_NO_SERIALIZE, lUncompressSize)
End If
status = RtlDecompressBuffer(COMPRESSION_FORMAT_LZNT1, _
ByVal pUncompressed, lUncompressSize, _
ByVal pLocalBuffer, lResSize, lResultSize)
lUncompressSize = lUncompressSize * 2
Loop While status = STATUS_BAD_COMPRESSION_BUFFER
pProjectData = pUncompressed
If status Then GoTo CleanUp
' // Validation check
If lResultSize < LenB(ProjectDesc) Then GoTo CleanUp
' // Copy descriptor
CopyMemory ProjectDesc, ByVal pProjectData, LenB(ProjectDesc)
' // Check all members
If ProjectDesc.dwSizeOfStructure <> Len(ProjectDesc) Then GoTo CleanUp
If ProjectDesc.storageDescriptor.dwSizeOfStructure <> Len(ProjectDesc.storageDescriptor) Then GoTo CleanUp
If ProjectDesc.storageDescriptor.dwSizeOfItem <> Len(tmpStorageItem) Then GoTo CleanUp
If ProjectDesc.execListDescriptor.dwSizeOfStructure <> Len(ProjectDesc.execListDescriptor) Then GoTo CleanUp
If ProjectDesc.execListDescriptor.dwSizeOfItem <> Len(tmpExecuteItem) Then GoTo CleanUp
' // Initialize pointers
pStoragesTable = pProjectData + ProjectDesc.dwSizeOfStructure
pExecutesTable = pStoragesTable + ProjectDesc.storageDescriptor.dwSizeOfItem * ProjectDesc.storageDescriptor.dwNumberOfItems
pFilesTable = pExecutesTable + ProjectDesc.execListDescriptor.dwSizeOfItem * ProjectDesc.execListDescriptor.dwNumberOfItems
pStringsTable = pFilesTable + ProjectDesc.dwFileTableLen
' // Check size
If (pStringsTable + ProjectDesc.dwStringsTableLen - pProjectData) <> lResultSize Then GoTo CleanUp
' // Success
ReadProject = True
CleanUp:
If pLocalBuffer Then HeapFree GetProcessHeap(), HEAP_NO_SERIALIZE, pLocalBuffer
If Not ReadProject And pProjectData Then
HeapFree GetProcessHeap(), HEAP_NO_SERIALIZE, pProjectData
End If
End Function
Как можно увидеть я использую кучу процесса вместо массивов. Вначале загружается ресурс с проектом - PROJECT и копируется в кучу, затем производится декомпрессия используя функцию RtlDecompressBuffer. Эта функция не возвращает необходимый размер буфера поэтому мы пытаемся распаковать буфер увеличивая выходной размер буфера пока декомпрессия не будет успешно выполнена. После декомпрессии проверяются все параметры и инициализируются глобальные указатели проекта.
Если проект успешно загружен то вызывается функция CopyProcess которая распаковывает все файлы из хранилища, согласно данным проекта:
- Код: Выделить всё
' // Copying process
Function CopyProcess() As Boolean
Dim bItem As BinStorageListItem: Dim index As Long
Dim pPath As Long: Dim dwWritten As Long
Dim msg As Long: Dim lStep As Long
Dim isError As Boolean: Dim pItem As Long
Dim pErrMsg As Long: Dim pTempString As Long
' // Set pointer
pItem = pStoragesTable
' // Go thru file list
For index = 0 To ProjectDesc.storageDescriptor.dwNumberOfItems - 1
' // Copy file descriptor
CopyMemory bItem, ByVal pItem, Len(bItem)
' // Next item
pItem = pItem + ProjectDesc.storageDescriptor.dwSizeOfItem
' // If it is not main executable
If index <> ProjectDesc.storageDescriptor.iExecutableIndex Then
' // Normalize path
pPath = NormalizePath(pStringsTable + bItem.ofstDestPath, pStringsTable + bItem.ofstFileName)
' // Error occurs
If pPath = 0 Then
pErrMsg = GetString(MID_ERRORWIN32)
MessageBox 0, pErrMsg, 0, MB_ICONERROR Or MB_SYSTEMMODAL
GoTo CleanUp
Else
Dim hFile As Long
Dim disp As CREATIONDISPOSITION
' // Set overwrite flags
If bItem.dwFlags And FF_REPLACEONEXIST Then disp = CREATE_ALWAYS Else disp = CREATE_NEW
' // Set number of subroutine
lStep = 0
' // Run subroutines
Do
' // Disable error flag
isError = False
' // Free string
If pErrMsg Then SysFreeString pErrMsg: pErrMsg = 0
' // Choose subroutine
Select Case lStep
Case 0 ' // 0. Create folder
If Not CreateSubdirectories(pPath) Then isError = True
Case 1 ' // 1. Create file
hFile = CreateFile(pPath, FILE_GENERIC_WRITE, 0, ByVal 0&, disp, FILE_ATTRIBUTE_NORMAL, 0)
If hFile = INVALID_HANDLE_VALUE Then
If GetLastError = ERROR_FILE_EXISTS Then Exit Do
isError = True
End If
Case 2 ' // 2. Copy data to file
If WriteFile(hFile, ByVal pFilesTable + bItem.ofstBeginOfData, _
bItem.dwSizeOfFile, dwWritten, ByVal 0&) = 0 Then isError = True
If dwWritten <> bItem.dwSizeOfFile Then
isError = True
Else
CloseHandle hFile: hFile = INVALID_HANDLE_VALUE
End If
End Select
' // If error occurs show notification (retry, abort, ignore)
If isError Then
' // Ignore error
If bItem.dwFlags And FF_IGNOREERROR Then Exit Do
pTempString = GetString(MID_ERRORCOPYINGFILE)
pErrMsg = StrCat(pTempString, pPath)
' // Cleaning
SysFreeString pTempString: pTempString = 0
Select Case MessageBox(0, pErrMsg, 0, MB_ICONERROR Or MB_SYSTEMMODAL Or MB_CANCELTRYCONTINUE)
Case MESSAGEBOXRETURN.IDCONTINUE: Exit Do
Case MESSAGEBOXRETURN.IDTRYAGAIN
Case Else: GoTo CleanUp
End Select
Else: lStep = lStep + 1
End If
Loop While lStep <= 2
If hFile <> INVALID_HANDLE_VALUE Then
CloseHandle hFile: hFile = INVALID_HANDLE_VALUE
End If
' // Cleaning
SysFreeString pPath: pPath = 0
End If
End If
Next
' // Success
CopyProcess = True
CleanUp:
If pTempString Then SysFreeString pTempString
If pErrMsg Then SysFreeString pErrMsg
If pPath Then SysFreeString pPath
If hFile <> INVALID_HANDLE_VALUE Then
CloseHandle hFile
hFile = INVALID_HANDLE_VALUE
End If
End Function
Эта процедура проходит по всем элементам хранилища и распаковывает их одна за одной исключая главный исполняемый файл. Функция NormalizePath заменяет подстановочные знаки на реальные пути. Также существует функция CreateSubdirectories которая создает промежуточные директории (если необходимо) по переданному в качестве параметра пути. Затем вызывается функция CreateFile для создания файла затем через WriteFile данные пишутся в файл. Если происходит ошибка то выводится стандартное сообщение с предложением повторить, отменить или игнорировать.
- Код: Выделить всё
' // Create all subdirectories by path
Function CreateSubdirectories( _
ByVal pPath As Long) As Boolean
Dim pComponent As Long
Dim tChar As Integer
' // Pointer to first char
pComponent = pPath
' // Go thru path components
Do
' // Get next component
pComponent = PathFindNextComponent(pComponent)
' // Check if end of line
CopyMemory tChar, ByVal pComponent, 2
If tChar = 0 Then Exit Do
' // Write null-terminator
CopyMemory ByVal pComponent - 2, 0, 2
' // Check if path exists
If PathIsDirectory(pPath) = 0 Then
' // Create folder
If CreateDirectory(pPath, ByVal 0&) = 0 Then
' // Error
CopyMemory ByVal pComponent - 2, &H5C, 2
Exit Function
End If
End If
' // Restore path delimiter
CopyMemory ByVal pComponent - 2, &H5C, 2
Loop
' // Success
CreateSubdirectories = True
End Function
' // Get normalize path (replace wildcards, append file name)
Function NormalizePath( _
ByVal pPath As Long, _
ByVal pTitle As Long) As Long
Dim lPathLen As Long: Dim lRelacerLen As Long
Dim lTitleLen As Long: Dim pRelacer As Long
Dim lTotalLen As Long: Dim lPtr As Long
Dim pTempString As Long: Dim pRetString As Long
' // Determine wildcard
Select Case True
Case IntlStrEqWorker(0, pPath, pAppRepl, 5): pRelacer = pAppPath
Case IntlStrEqWorker(0, pPath, pSysRepl, 5): pRelacer = pSysPath
Case IntlStrEqWorker(0, pPath, pTmpRepl, 5): pRelacer = pTmpPath
Case IntlStrEqWorker(0, pPath, pWinRepl, 5): pRelacer = pWinPath
Case IntlStrEqWorker(0, pPath, pDrvRepl, 5): pRelacer = pDrvPath
Case IntlStrEqWorker(0, pPath, pDtpRepl, 5): pRelacer = pDtpPath
Case Else: pRelacer = pStrNull
End Select
' // Get string size
lPathLen = lstrlen(ByVal pPath)
lRelacerLen = lstrlen(ByVal pRelacer)
' // Skip wildcard
If lRelacerLen Then
pPath = pPath + 5 * 2
lPathLen = lPathLen - 5
End If
If pTitle Then lTitleLen = lstrlen(ByVal pTitle)
' // Get length all strings
lTotalLen = lPathLen + lRelacerLen + lTitleLen
' // Check overflow (it should be les or equal MAX_PATH)
If lTotalLen > MAX_PATH Then Exit Function
' // Create string
pTempString = SysAllocStringLen(0, MAX_PATH)
If pTempString = 0 Then Exit Function
' // Copy
lstrcpyn ByVal pTempString, ByVal pRelacer, lRelacerLen + 1
lstrcat ByVal pTempString, ByVal pPath
' // If title is presented append
If pTitle Then
' // Error
If PathAddBackslash(pTempString) = 0 Then GoTo CleanUp
' // Copy file name
lstrcat ByVal pTempString, ByVal pTitle
End If
' // Alloc memory for translation relative path to absolute
pRetString = SysAllocStringLen(0, MAX_PATH)
If pRetString = 0 Then GoTo CleanUp
' // Normalize
If PathCanonicalize(pRetString, pTempString) = 0 Then GoTo CleanUp
NormalizePath = pRetString
CleanUp:
If pTempString Then SysFreeString pTempString
If pRetString <> 0 And NormalizePath = 0 Then SysFreeString pRetString
End Function
' // Concatenation strings
Function StrCat( _
ByVal pStringDest As Long, _
ByVal pStringAppended As Long) As Long
Dim l1 As Long, l2 As Long
l1 = lstrlen(ByVal pStringDest): l2 = lstrlen(ByVal pStringAppended)
StrCat = SysAllocStringLen(0, l1 + l2)
If StrCat = 0 Then Exit Function
lstrcpyn ByVal StrCat, ByVal pStringDest, l1 + 1
lstrcat ByVal StrCat, ByVal pStringAppended
End Function
После извлечения файлов вызывается функция ExecuteProcess которая запускает выполнение команд используя функцию ShellExecuteEx:
- Код: Выделить всё
' // Execution command process
Function ExecuteProcess() As Boolean
Dim index As Long: Dim bItem As BinExecListItem
Dim pPath As Long: Dim pErrMsg As Long
Dim shInfo As SHELLEXECUTEINFO: Dim pTempString As Long
Dim pItem As Long: Dim status As Long
' // Set pointer and size
shInfo.cbSize = Len(shInfo)
pItem = pExecutesTable
' // Go thru all items
For index = 0 To ProjectDesc.execListDescriptor.dwNumberOfItems - 1
' // Copy item
CopyMemory bItem, ByVal pItem, ProjectDesc.execListDescriptor.dwSizeOfItem
' // Set pointer to next item
pItem = pItem + ProjectDesc.execListDescriptor.dwSizeOfItem
' // Normalize path
pPath = NormalizePath(pStringsTable + bItem.ofstFileName, 0)
' // Fill SHELLEXECUTEINFO
shInfo.lpFile = pPath
shInfo.lpParameters = pStringsTable + bItem.ofstParameters
shInfo.fMask = SEE_MASK_NOCLOSEPROCESS Or SEE_MASK_FLAG_NO_UI
shInfo.nShow = SW_SHOWDEFAULT
' // Performing...
status = ShellExecuteEx(shInfo)
' // If error occurs show notification (retry, abort, ignore)
Do Until status
If pErrMsg Then SysFreeString pErrMsg: pErrMsg = 0
' // Ignore error
If bItem.dwFlags And EF_IGNOREERROR Then
Exit Do
End If
pTempString = GetString(MID_ERROREXECUTELINE)
pErrMsg = StrCat(pTempString, pPath)
SysFreeString pTempString: pTempString = 0
Select Case MessageBox(0, pErrMsg, 0, MB_ICONERROR Or MB_SYSTEMMODAL Or MB_CANCELTRYCONTINUE)
Case MESSAGEBOXRETURN.IDCONTINUE: Exit Do
Case MESSAGEBOXRETURN.IDTRYAGAIN
Case Else: GoTo CleanUp
End Select
status = ShellExecuteEx(shInfo)
Loop
' // Wait for process terminaton
WaitForSingleObject shInfo.hProcess, INFINITE
CloseHandle shInfo.hProcess
Next
' // Success
ExecuteProcess = True
CleanUp:
If pTempString Then SysFreeString pTempString
If pErrMsg Then SysFreeString pErrMsg
If pPath Then SysFreeString pPath
End Function