Запись от The trick размещена 14.09.2016 в 19:20 Обновил(-а) The trick 14.09.2016 в 19:29
Всем привет! Когда-то давно я исследовал PE-формат, в особенности EXE. Я решил создать простой загрузчик исполняемых файлов специально для VB6-скомпилированных приложений. Этот загрузчик, по моим задумкам, должен загружать любое VB6-скомпилированное приложение из памяти, миную запись в файл. ВСЕ ЭТО БЫЛО СДЕЛАНО ДЛЯ ЭКСПЕРИМЕНТАЛЬНЫХ ЦЕЛЕЙ ДЛЯ ТОГО ЧТОБЫ ПРОВЕРИТЬ ТАКУЮ ВОЗМОЖНОСТЬ НА VB6. Из-за того что VB6-скомпилированные приложения не используют большинство PE-фичей это было довольно легкой задачей. Также большинство программистов говорят что любая VB6-скомпилированная программа неукоснительно связана с VB6-рантаймом (msvbvm60) и что такая программа не будет работать без рантайма и рантайм является довольно медленным. Сегодня я докажу что можно написать приложение абсолютно не использующее рантайм (хотя я такое уже делал в драйвере). Я думаю что это могло бы быть интересным для тех кто хочет изучить базовые принципы работы с PE файлами.
Прежде чем мы начнем я бы хотел сказать пару слов о проектах. Эти проекты не тестировались достаточно хорошо, поэтому они могут содержать различные проблемы. Также загрузчик не поддерживает множество возможностей PE-файлов следовательно некоторые приложения могут не работать.
Итак...
Этот обзор включает три проекта:
Compiler - самый большой проект из всех. Он позволяет создавать лаунчер базируемый на загрузчике, пользовательских файлах, командах и манифесте;
Loader - простейший загрузчик который выполняет команды, распаковывает файлы и запускает EXE из памяти;
Patcher - маленькая утилита которая удаляет рантайм из VB6-скомпилированного приложения.
Я буду называть EXE что содержит команды, файлы и исполнительный файл - инсталляцией. Главная идея этой задумки - это положить информацию об инсталляции в ресурсы загрузчика. Когда загрузчик загружается он считывает эту информацию и выполняет команды из ресурсов. Я решил использовать специальное хранилище для хранения файлов и EXE и отдельное хранилище для команд.
Перое хранилище хранит все файлы которые будут распакованы и главный 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 itemPrivateType BinStorageListItem
ofstFileName AsLong' // Offset of file name
ofstDestPath AsLong' // Offset of file path
dwSizeOfFile AsLong' // Size of file
ofstBeginOfData AsLong' // Offset of beginning data
dwFlags As FileFlags ' // FlagsEndType' // Execute list itemPrivateType BinExecListItem
ofstFileName AsLong' // Offset of file name
ofstParameters AsLong' // Offset of parameters
dwFlags As ExeFlags ' // FlagsEndType' // Storage descriptorPrivateType BinStorageList
dwSizeOfStructure AsLong' // Size of structure
iExecutableIndex AsLong' // Index of main executable
dwSizeOfItem AsLong' // Size of BinaryStorageItem structure
dwNumberOfItems AsLong' // Number of files in storageEndType' // Execute list descriptorPrivateType BinExecList
dwSizeOfStructure AsLong' // Size of structure
dwSizeOfItem AsLong' // Size of BinaryExecuteItem structure
dwNumberOfItems AsLong' // Number of itemsEndType' // Base information about projectPrivateType BinProject
dwSizeOfStructure AsLong' // Size of structure
storageDescriptor As BinStorageList ' // Storage descriptor
execListDescriptor As BinExecList ' // Command descriptor
dwStringsTableLen AsLong' // Size of strings table
dwFileTableLen AsLong' // Size of data tableEndType
Структура 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", но мы можем написать там любую программу:
Но поскольку мы не пишем DOS программы для нас эта структура не важна. Нам интересно только поля e_magic и e_lfanew. Первое поле должно содержать сигнатуру 'MZ' aka. IMAGE_DOS_SIGNATURE а второе смещение до очень важной структуры IMAGE_NT_HEADERS:
Visual Basic
1
2
3
4
5
Type IMAGE_NT_HEADERS
Signature AsLong
FileHeader As IMAGE_FILE_HEADER
OptionalHeader As IMAGE_OPTIONAL_HEADER
EndType
Первое поле этой структуры содержит сигнатуру 'PE\0\0' (aka. IMAGE_NT_SIGNATURE). Следующее поле описывает исполняемый файл и имеет следующий формат:
Поле 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 имеет следующий формат:
Первое поле содержит тип образа (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. Рассмотрим формат заголовка секции:
Первое поле содержит имя секции в формате UTF-8 c завершающим нуль-терминалом. Это имя ограничено 8-ю символами (если имя секции имеет размер 8 символов то нуль-терминатор игнорируется). COFF файл может иметь имя больше чем 8 символов в этом случае имя начинается с символа '/' за которым следует ASCII строка с десятичным значением смещения в строковой таблице (поле IMAGE_FILE_HEADER). PE файл не поддерживает длинные имена секций. Поля VirtualSize и VirtualAddress содержат размер секции в памяти и адрес (RVA). Поля SizeOfRawData и PointerToRawData содержат RAW адрес данных в файле (если секция содержит инициализированные данные). Это ключевой момент потому что мы можем вычислить RAW адрес с помощью относительного виртуального адреса используя информацию из заголовка секций. Я написал функцию для перевода RVA адресации в RAW смещение в файле:
Visual Basic
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
' // RVA to RAWFunction RVA2RAW( _
ByVal rva AsLong, _
ByRef sec() As IMAGE_SECTION_HEADER) AsLongDim index AsLongFor index = 0 ToUBound(sec)
If rva >= sec(index).VirtualAddress And _
rva < sec(index).VirtualAddress + sec(index).VirtualSize Then
RVA2RAW = sec(index).PointerToRawData + (rva - sec(index).VirtualAddress)
ExitFunctionEndIfNext
RVA2RAW = rva
EndFunction
Эта функция перечисляет все секции и проверяет если переданный адрес находится в пределах секции. Следующие 5 полей используються только в COFF файлах и не важны в PE файлах. Поле Characteristics содержит атрибуты секции такие как права доступа к памяти и управление. Мы будем использовать это поле для защиты памяти exe файла в загрузчике.
Давайте теперь вернемся к каталогам данных. Как мы видели существует 16 элементов в данном каталоге. Обычно PE файл не использует их все. Давайте рассмотрим структуру элемента каталога:
Эта структура содержит два поля. Первое поле содержит 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 таблицы ресурсов которая имеет следующий формат:
Эта структура описывает все ресурсы в PE файле. Первые 4 поля не важны для нас; поле NumberOfNamedEntries и NumberOfIdEntries содержат количество именованных записей и записей с числовыми идентификаторами соответственно. Для примера, когда мы добавляем картинку в стандартном редакторе это добавит запись с числовым идентификатором равным 2 (RT_BITMAP). Сами записи расположены сразу после IMAGE_RESOURCE_DIRECTORY и имеют следующую структуру:
Visual Basic
1
2
3
4
Type IMAGE_RESOURCE_DIRECTORY_ENTRY
NameId AsLong
OffsetToData AsLongEndType
Первое поле этой структуры определяет является ли это именованной запись либо это запись с числовым идентификатором в зависимости от старшего бита. Если этот бит установлен то остальные биты определяют смещение от начала ресурсов к структуре IMAGE_RESOURCE_DIR_STRING_U которая имет следующий формат:
Visual Basic
1
2
3
4
Type IMAGE_RESOURCE_DIR_STRING_U
Length AsInteger
NameString AsStringEndType
Заметьте что это не правильная VB-структура и показана для наглядности. Первые два байта являются беззнаковым целым которые показывают длину строки в формате UNICODE (в символах) которая следует за ними. Таким образом для того чтобы получить строку нам нужно прочитать первые два байта с размером, выделить память для строки согласно этого размера и прочитать данные в строковую переменную. Напротив, если старший бит поля NameId сброшен то оно содержит числовой идентификатор ресурса (RT_BITMAP в примере). Поле OffsetToData имеет также двойную интерпретацию. Если старший бит установлен то это смещение (от начала ресурсов) до следующего уровня дерева ресурсов, т.е. до структуры IMAGE_RESOURCE_DIRECTORY. Иначе - это смещение до структуры IMAGE_RESOURCE_DATA_ENTRY:
Visual Basic
1
2
3
4
5
6
Type IMAGE_RESOURCE_DATA_ENTRY
OffsetToData AsLong
Size AsLong
CodePage AsLong
Reserved AsLongEndType
Наиболее важными для нас являются поля 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 файла.
Когда загрузчик запустился выполняется функция Main:
Функция LoadConstants загружает все необходимые переменные и строки (hInstance, LCID, командная строка, подстановочные символы, пути по умолчанию, и т.д.). Все строки сохраняются в формате UNICODE-BSTR. Функция GetString загружает строку из ресурсов по ее идентификатору. Перечисление MessagesID содержит некоторые строковые идентификаторы нужные для работы программы (сообщения об ошибках, имена библиотек, и.т.д.). Когда все константы загрузятся вызывается функция ReadProject которая загружает проект:
' // Load projectFunction ReadProject() AsBooleanDim hResource AsLong: Dim hMememory AsLongDim lResSize AsLong: Dim pRawData AsLongDim status AsLong: Dim pUncompressed AsLongDim lUncompressSize AsLong: Dim lResultSize AsLongDim tmpStorageItem As BinStorageListItem: Dim tmpExecuteItem As BinExecListItem
Dim pLocalBuffer AsLong' // Load resource
hResource = FindResource(hInstance, GetString(PROJECT), RT_RCDATA)
If hResource = 0 ThenGoTo CleanUp
hMememory = LoadResource(hInstance, hResource)
If hMememory = 0 ThenGoTo CleanUp
lResSize = SizeofResource(hInstance, hResource)
If lResSize = 0 ThenGoTo CleanUp
pRawData = LockResource(hMememory)
If pRawData = 0 ThenGoTo CleanUp
pLocalBuffer = HeapAlloc(GetProcessHeap(), HEAP_NO_SERIALIZE, lResSize)
If pLocalBuffer = 0 ThenGoTo CleanUp
' // Copy to local buffer
CopyMemory ByVal pLocalBuffer, ByVal pRawData, lResSize
' // Set default size
lUncompressSize = lResSize * 2
' // Do decompress...DoIf pUncompressed Then
pUncompressed = HeapReAlloc(GetProcessHeap(), HEAP_NO_SERIALIZE, ByVal pUncompressed, lUncompressSize)
Else
pUncompressed = HeapAlloc(GetProcessHeap(), HEAP_NO_SERIALIZE, lUncompressSize)
EndIf
status = RtlDecompressBuffer(COMPRESSION_FORMAT_LZNT1, _
ByVal pUncompressed, lUncompressSize, _
ByVal pLocalBuffer, lResSize, lResultSize)
lUncompressSize = lUncompressSize * 2
LoopWhile status = STATUS_BAD_COMPRESSION_BUFFER
pProjectData = pUncompressed
If status ThenGoTo CleanUp
' // Validation checkIf lResultSize < LenB(ProjectDesc) ThenGoTo CleanUp
' // Copy descriptor
CopyMemory ProjectDesc, ByVal pProjectData, LenB(ProjectDesc)
' // Check all membersIf ProjectDesc.dwSizeOfStructure <> Len(ProjectDesc) ThenGoTo CleanUp
If ProjectDesc.storageDescriptor.dwSizeOfStructure <> Len(ProjectDesc.storageDescriptor) ThenGoTo CleanUp
If ProjectDesc.storageDescriptor.dwSizeOfItem <> Len(tmpStorageItem) ThenGoTo CleanUp
If ProjectDesc.execListDescriptor.dwSizeOfStructure <> Len(ProjectDesc.execListDescriptor) ThenGoTo CleanUp
If ProjectDesc.execListDescriptor.dwSizeOfItem <> Len(tmpExecuteItem) ThenGoTo 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 sizeIf (pStringsTable + ProjectDesc.dwStringsTableLen - pProjectData) <> lResultSize ThenGoTo CleanUp
' // Success
ReadProject = True
CleanUp:
If pLocalBuffer Then HeapFree GetProcessHeap(), HEAP_NO_SERIALIZE, pLocalBuffer
IfNot ReadProject And pProjectData Then
HeapFree GetProcessHeap(), HEAP_NO_SERIALIZE, pProjectData
EndIfEndFunction
Как можно увидеть я использую кучу процесса вместо массивов. Вначале загружается ресурс с проектом - PROJECT и копируется в кучу, затем производится декомпрессия используя функцию RtlDecompressBuffer. Эта функция не возвращает необходимый размер буфера поэтому мы пытаемся распаковать буфер увеличивая выходной размер буфера пока декомпрессия не будет успешно выполнена. После декомпрессии проверяются все параметры и инициализируются глобальные указатели проекта.
Если проект успешно загружен то вызывается функция CopyProcess которая распаковывает все файлы из хранилища, согласно данным проекта:
' // Copying processFunction CopyProcess() AsBooleanDim bItem As BinStorageListItem: Dim index AsLongDim pPath AsLong: Dim dwWritten AsLongDim msg AsLong: Dim lStep AsLongDim isError AsBoolean: Dim pItem AsLongDim pErrMsg AsLong: Dim pTempString AsLong' // Set pointer
pItem = pStoragesTable
' // Go thru file listFor 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 executableIf index <> ProjectDesc.storageDescriptor.iExecutableIndex Then' // Normalize path
pPath = NormalizePath(pStringsTable + bItem.ofstDestPath, pStringsTable + bItem.ofstFileName)
' // Error occursIf pPath = 0 Then
pErrMsg = GetString(MID_ERRORWIN32)
MessageBox 0, pErrMsg, 0, MB_ICONERROR Or MB_SYSTEMMODAL
GoTo CleanUp
ElseDim hFile AsLongDim disp As CREATIONDISPOSITION
' // Set overwrite flagsIf bItem.dwFlags And FF_REPLACEONEXIST Then disp = CREATE_ALWAYS Else disp = CREATE_NEW
' // Set number of subroutine
lStep = 0
' // Run subroutinesDo' // Disable error flag
isError = False' // Free stringIf pErrMsg Then SysFreeString pErrMsg: pErrMsg = 0
' // Choose subroutineSelectCase lStep
Case 0 ' // 0. Create folderIfNot CreateSubdirectories(pPath) Then isError = TrueCase 1 ' // 1. Create file
hFile = CreateFile(pPath, FILE_GENERIC_WRITE, 0, ByVal 0&, disp, FILE_ATTRIBUTE_NORMAL, 0)
If hFile = INVALID_HANDLE_VALUE ThenIf GetLastError = ERROR_FILE_EXISTS ThenExitDo
isError = TrueEndIfCase 2 ' // 2. Copy data to fileIf WriteFile(hFile, ByVal pFilesTable + bItem.ofstBeginOfData, _
bItem.dwSizeOfFile, dwWritten, ByVal 0&) = 0 Then isError = TrueIf dwWritten <> bItem.dwSizeOfFile Then
isError = TrueElse
CloseHandle hFile: hFile = INVALID_HANDLE_VALUE
EndIfEndSelect' // If error occurs show notification (retry, abort, ignore)If isError Then' // Ignore errorIf bItem.dwFlags And FF_IGNOREERROR ThenExitDo
pTempString = GetString(MID_ERRORCOPYINGFILE)
pErrMsg = StrCat(pTempString, pPath)
' // Cleaning
SysFreeString pTempString: pTempString = 0
SelectCase MessageBox(0, pErrMsg, 0, MB_ICONERROR Or MB_SYSTEMMODAL Or MB_CANCELTRYCONTINUE)
Case MESSAGEBOXRETURN.IDCONTINUE: ExitDoCase MESSAGEBOXRETURN.IDTRYAGAIN
CaseElse: GoTo CleanUp
EndSelectElse: lStep = lStep + 1
EndIfLoopWhile lStep <= 2
If hFile <> INVALID_HANDLE_VALUE Then
CloseHandle hFile: hFile = INVALID_HANDLE_VALUE
EndIf' // Cleaning
SysFreeString pPath: pPath = 0
EndIfEndIfNext' // 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
EndIfEndFunction
Эта процедура проходит по всем элементам хранилища и распаковывает их одна за одной исключая главный исполняемый файл. Функция NormalizePath заменяет подстановочные знаки на реальные пути. Также существует функция CreateSubdirectories которая создает промежуточные директории (если необходимо) по переданному в качестве параметра пути. Затем вызывается функция CreateFile для создания файла затем через WriteFile данные пишутся в файл. Если происходит ошибка то выводится стандартное сообщение с предложением повторить, отменить или игнорировать.