Всем привет! Когда-то давно я исследовал 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. Сейчас я расскажу о процедуре компиляции. Как я уже сказал вся информация об инсталляции сохраняется в ресурсы загрузчика. Вначале на нужно определить формат данных: Код (Visual Basic): ' // 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", но мы можем написать там любую программу: Код (Visual Basic): 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: Код (Visual Basic): 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). Следующее поле описывает исполняемый файл и имеет следующий формат: Код (Visual Basic): 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 имеет следующий формат: Код (Visual Basic): 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. Рассмотрим формат заголовка секции: Код (Visual Basic): 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 смещение в файле: Код (Visual Basic): ' // 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 файл не использует их все. Давайте рассмотрим структуру элемента каталога: Код (Visual Basic): 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 таблицы ресурсов которая имеет следующий формат: Код (Visual Basic): 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 и имеют следующую структуру: Код (Visual Basic): Type IMAGE_RESOURCE_DIRECTORY_ENTRY NameId As Long OffsetToData As Long End Type Первое поле этой структуры определяет является ли это именованной запись либо это запись с числовым идентификатором в зависимости от старшего бита. Если этот бит установлен то остальные биты определяют смещение от начала ресурсов к структуре IMAGE_RESOURCE_DIR_STRING_U которая имет следующий формат: Код (Visual Basic): 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: Код (Visual Basic): 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 файла. Когда загрузчик запустился выполняется функция Main: Код (Visual Basic): ' // 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 для того чтобы загрузить все необходимые константы из ресурсов: Код (Visual Basic): ' // 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 которая загружает проект: Код (Visual Basic): ' // 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 которая распаковывает все файлы из хранилища, согласно данным проекта: Код (Visual Basic): ' // 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 данные пишутся в файл. Если происходит ошибка то выводится стандартное сообщение с предложением повторить, отменить или игнорировать. Код (Visual Basic): ' // 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: Код (Visual Basic): ' // 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
Эта функция похожа на предыдущую за исключением того что здесь используется функция ShellExecuteEx вместо извлечения. Обратите внимание что каждая операция выполняется синхронно, т.е. каждый вызов процедуры ShellExecuteEx ждет окончания выполнения команды. Если предыдущая функция выполнилась успешно тогда вызывается функция RunProcess которая подготовливает данные для исполнения главного исполняемого файла из памяти: Код (Visual Basic): ' // Run exe from project in memory Function RunProcess() As Boolean Dim bItem As BinStorageListItem: Dim Length As Long Dim pFileData As Long ' // Get descriptor of executable file CopyMemory bItem, ByVal pStoragesTable + ProjectDesc.storageDescriptor.dwSizeOfItem * _ ProjectDesc.storageDescriptor.iExecutableIndex, Len(bItem) ' // Alloc memory within top memory addresses pFileData = VirtualAlloc(ByVal 0&, bItem.dwSizeOfFile, MEM_TOP_DOWN Or MEM_COMMIT, PAGE_READWRITE) If pFileData = 0 Then Exit Function ' // Copy raw exe file to this memory CopyMemory ByVal pFileData, ByVal pFilesTable + bItem.ofstBeginOfData, bItem.dwSizeOfFile ' // Free decompressed project data HeapFree GetProcessHeap(), HEAP_NO_SERIALIZE, pProjectData pProjectData = 0 ' // Run exe from memory RunExeFromMemory pFileData, bItem.dwFlags And FF_IGNOREERROR ' ---------------------------------------------------- ' // An error occurs ' // Clean memory VirtualFree ByVal pFileData, 0, MEM_RELEASE ' // If ignore error then success If bItem.dwFlags And FF_IGNOREERROR Then RunProcess = True End Function Эта процедура выделяет память в верхних областях виртуального адресного пространства (поскольку большинство EXE файлов грузятся по довольно низким адресам (обычно 0x00400000). После этого очишается память данных проекта поскольку если EXE файл запустится, то эта память не будет освобождена, затем вызывается функция RunExeFromMemory которая делает следующий шаг в загрузке EXE из памяти. Если по какой-либо причине загрузка EXE файла не состоялась то освобождается выделенная память и управление передается функции Main. Итак, для того чтобы загрузить EXE файл нам нужно освободить память загрузчика, т.е. выгрузить загрузчик. Нам нужно только оставить маленькуий кусочек кода который будет загружать EXE файл и запускать его. Для этого я решил использовать шеллкод, хотя можно использовать и DLL. Шеллкод - это маленький базонезависимый код (код который не ссылается к внешним данным). Но в любом случае нам придется обеспечить доступ к API функциям из шеллкода. Мы не можем вызывать API функции непосредственно из шеллкода поскольку наш главный исполняемый файл будет выгружен и любое обращение к таблице импорта вызовет креш. Второе ограничение - это то что инструкция call использует относительное смещение (это наиболее частый случай). Из этого следует что нам нужно инициализировать некие "трамплины" которые будут перебрасывать нас на API функции. Я решил делать это посредством сплайсинга. Я просто заменяю первые 5 байт функции пусттышки на ассемблерную инструкцию jmp которая ссылается на необходимую API функцию: Код (Visual Basic): ' // Run EXE file by memory address Function RunExeFromMemory( _ ByVal pExeData As Long, _ ByVal IgnoreError As Boolean) As Boolean Dim Length As Long: Dim pCode As Long Dim pszMsg As Long: Dim pMsgTable As Long Dim index As Long: Dim pCurMsg As Long ' // Get size of shellcode Length = GetAddr(AddressOf ENDSHELLLOADER) - GetAddr(AddressOf BEGINSHELLLOADER) ' // Alloc memory within top addresses pCode = VirtualAlloc(ByVal 0&, Length, MEM_TOP_DOWN Or MEM_COMMIT, PAGE_EXECUTE_READWRITE) ' // Copy shellcode to allocated memory CopyMemory ByVal pCode, ByVal GetAddr(AddressOf BEGINSHELLLOADER), Length ' // Initialization of shellcode If Not InitShellLoader(pCode) Then GoTo CleanUp ' // Splice CallLoader function in order to call shellcode Splice AddressOf CallLoader, pCode + GetAddr(AddressOf LoadExeFromMemory) - GetAddr(AddressOf BEGINSHELLLOADER) ' // Check ignore errors If Not IgnoreError Then ' // Alloc memory for messages table pMsgTable = VirtualAlloc(ByVal 0&, 1024, MEM_TOP_DOWN Or MEM_COMMIT, PAGE_READWRITE) If pMsgTable = 0 Then GoTo CleanUp ' // Skip pointers pCurMsg = pMsgTable + EM_END * 4 For index = 0 To EM_END - 1 ' // Load message string pszMsg = GetString(MSG_LOADER_ERROR + index) If pszMsg = 0 Then GoTo CleanUp Length = SysStringLen(pszMsg) lstrcpyn ByVal pCurMsg, ByVal pszMsg, Length + 1 ' // Store pointer CopyMemory ByVal pMsgTable + index * 4, pCurMsg, Len(pCurMsg) ' // Next message offset pCurMsg = pCurMsg + (Length + 1) * 2 SysFreeString pszMsg Next End If ' // Call shellcode CallLoader pExeData, pCode, pMsgTable CleanUp: If pMsgTable Then VirtualFree ByVal pMsgTable, 0, MEM_RELEASE End If If pCode Then VirtualFree ByVal pCode, 0, MEM_RELEASE End If End Function Как видно из кода он вычисляет размер шеллкода используя разницу между крайними функциями - ENDSHELLLOADER и BEGINSHELLLOADER. Эти функции должны окружать наш шеллкод и иметь разный прототип поскольку VB6 компилятор может объединять идентичные функции. Затем выделяется память для самого шеллкода и он копируется в эту область памяти.
После этого вызывается функция InitShellLoader которая сплайсит все функции в шеллкоде: Код (Visual Basic): ' // Shellcode initialization Function InitShellLoader( _ ByVal pShellCode As Long) As Boolean Dim hLib As Long: Dim sName As Long Dim sFunc As Long: Dim lpAddr As Long Dim libIdx As Long: Dim fncIdx As Long Dim libName As MessagesID: Dim fncName As MessagesID Dim fncSpc As Long: Dim splAddr As Long ' // +----------------------------------------------------------------+ ' // | Fixing of API addresses | ' // +----------------------------------------------------------------+ ' // | In order to call api function from shellcode i use splicing of | ' // | our VB functions and redirect call to corresponding api. | ' // | I did same in the code that injects to other process. | ' // +----------------------------------------------------------------+ splAddr = GetAddr(AddressOf tVirtualAlloc) - GetAddr(AddressOf BEGINSHELLLOADER) + pShellCode ' // Get size in bytes between stub functions fncSpc = GetAddr(AddressOf tVirtualProtect) - GetAddr(AddressOf tVirtualAlloc) ' // Use 3 library: kernel32, ntdll и user32 For libIdx = 0 To 2 ' // Get number of imported functions depending on library Select Case libIdx Case 0: libName = API_LIB_KERNEL32: fncIdx = 13 Case 1: libName = API_LIB_NTDLL: fncIdx = 1 Case 2: libName = API_LIB_USER32: fncIdx = 1 End Select ' // Get library name from resources sName = GetString(libName): If sName = 0 Then Exit Function ' // Get module handle hLib = GetModuleHandle(ByVal sName): If hLib = 0 Then Exit Function SysFreeString sName ' // Go thru functions Do While fncIdx libName = libName + 1 ' // Get function name sName = GetString(libName): If sName = 0 Then Exit Function ' // Because of GetProcAddress works with ANSI string translate it to ANSI sFunc = ToAnsi(sName): If sFunc = 0 Then Exit Function ' // Get function address lpAddr = GetProcAddress(hLib, sFunc) SysFreeString sName: SysFreeString sFunc ' // Error If lpAddr = 0 Then Exit Function ' // Splice stub Splice splAddr, lpAddr ' // Next stub splAddr = splAddr + fncSpc fncIdx = fncIdx - 1 Loop Next ' // Modify CallByPointer lpAddr = GetAddr(AddressOf CallByPointer) - GetAddr(AddressOf BEGINSHELLLOADER) + pShellCode ' // pop eax - 0x58 ' // pop ecx - 0x59 ' // push eax - 0x50 ' // jmp ecx - 0xFFE1 CopyMemory ByVal lpAddr, &HFF505958, 4 CopyMemory ByVal lpAddr + 4, &HE1, 1 ' // Success InitShellLoader = True End Function ' // Splice function Sub Splice( _ ByVal Func As Long, _ ByVal NewAddr As Long) ' // Set memory permissions VirtualProtect ByVal Func, 5, PAGE_EXECUTE_READWRITE, 0 CopyMemory ByVal Func, &HE9, 1 ' // JMP CopyMemory ByVal Func + 1, NewAddr - Func - 5, 4 ' // Relative address End Sub Вначале код вычисляет смещение первого "трамплина" (в нашем случае это функция tVirtualAlloc) относительно начала шеллкода, и вычисляет расстояние (в байтах) между функциями "трамплинами". Когда компилятор VB6 компилирует стандартный модуль он размещает функции в том же порядке в котором они определены в модуле. Необходимое условие - обеспечить уникальное возвращаемое значение для каждой функции. Затем код проходит по всем необходимым библиотекам (kernel32, ntdll, user32 - в этом порядке) и их функциям. Первая запись в ресурсах строк соответствует имени библиотеки за котором идут имена функций в этой библиотеке. Когда строка имени функции из ресурсов получена она транслируется в ANSI формат и вызывается функция GetProcAddress. Затем вызывается функция Splice которая собирает "трамплин" к необходимой функции из шеллкода. В конце модифицируется функция CallByPointer для того чтобы обеспечить прыжок из шеллкода на точку входа EXE файла. Далее функция RunExeFromMemory патчит функцию CallLoader для того чтобы обеспечить вызов шеллкода из загрузчика. После этой операции функция формирует таблицу сообщений об ошибках (если нужно) которая представляет из себя просто набор указателей на стоки сообщений. И наконец вызывается пропатченная CallLoader которая прыгает на функцию шеллкода LoadExeFromMemory которая больше не расположена внутри загрузчика, а находится в верхних адресах АП процесса. Внутри шеллкода.Итак, я сделал несколько функций внутри шеллкода: LoadExeFromMemory - стартовая функция шеллкода; GetImageNtHeaders - возвращает структуру IMAGE_NT_HEADERS и ее адрес по базовому адресу; GetDataDirectory - возвращает структуру IMAGE_DATA_DIRECTORY и ее адрес по базовому адресу и каталоговому индексу; EndProcess - показать сообщение об ошибке (если есть такое) и завершить процесс; ProcessSectionsAndHeaders - выделить память под все заголовки (DOS, NT, секции) и все секции. Скопировать данные в секции; ReserveMemory - зарезервировать необходимую память под EXE; ProcessRelocations - настроить адреса иесли EXE был загружен не по базовому адресу; ProcessImportTable - сканировать таблицу импорта EXE файла, загрузить необходимые библиотеки и заполнить таблицу адресов импорта (IAT); SetMemoryPermissions - настроить разрешения памяти для каждой секции; UpdateNewBaseAddress - обновить новый базовый адрес в системных структурах PEB и LDR. Из-за того что нельзя использовать функцию VarPtr, я сделалпохожую функцию используя функцию lstrcpyn - IntPtr. Итак, функция LoadExeFromMemory извлекает вначале заголовок NT и проверяет архитектуру процессора, является ли PE файл исполняемым и является ли он 32-битным приложением. Если проверка прошла успешно тогда шеллкод выгружает загрузчик из памяти используя функцию ZwUnmapViewOfSection. Если функция выполняется успешно EXE образ загрузчика больше не находится в памяти и занимаемая им память освобождается. Отныне мы не можем напрямую вызывать API функции, теперь мы должны использовать наши "трамплины": Код (Visual Basic): ' // Parse exe in memory Function LoadExeFromMemory( _ ByVal pRawData As Long, _ ByVal pMyBaseAddress As Long, _ ByVal pErrMsgTable As Long) As Boolean Dim NtHdr As IMAGE_NT_HEADERS Dim pBase As Long Dim index As Long Dim iError As ERROR_MESSAGES Dim pszMsg As Long ' // Get IMAGE_NT_HEADERS If GetImageNtHeaders(pRawData, NtHdr) = 0 Then iError = EM_UNABLE_TO_GET_NT_HEADERS EndProcess pErrMsgTable, iError Exit Function End If ' // Check flags If NtHdr.FileHeader.Machine <> IMAGE_FILE_MACHINE_I386 Or _ (NtHdr.FileHeader.Characteristics And IMAGE_FILE_EXECUTABLE_IMAGE) = 0 Or _ (NtHdr.FileHeader.Characteristics And IMAGE_FILE_32BIT_MACHINE) = 0 Then Exit Function ' // Release main EXE memory. After that main exe is unloaded from memory. ZwUnmapViewOfSection GetCurrentProcess(), GetModuleHandle(ByVal 0&) ' // Reserve memory for EXE iError = ReserveMemory(pRawData, pBase) If iError Then EndProcess pErrMsgTable, iError Exit Function End If ' // Place data iError = ProcessSectionsAndHeaders(pRawData, pBase) If iError Then EndProcess pErrMsgTable, iError Exit Function End If ' // Update new base address iError = UpdateNewBaseAddress(pBase) If iError Then EndProcess pErrMsgTable, iError Exit Function End If ' // Import table processing iError = ProcessImportTable(pBase) If iError Then EndProcess pErrMsgTable, iError Exit Function End If ' // Relocations processing iError = ProcessRelocations(pBase) If iError Then EndProcess pErrMsgTable, iError Exit Function End If ' // Set the memory attributes iError = SetMemoryPermissions(pBase) If iError Then EndProcess pErrMsgTable, iError Exit Function End If ' // Release error message table If pErrMsgTable Then tVirtualFree pErrMsgTable, 0, MEM_RELEASE End If ' // Call entry point CallByPointer NtHdr.OptionalHeader.AddressOfEntryPoint + pBase ' // End process EndProcess End Function
Затем шеллкод вызывает функцию ReserveMemory показанную ниже. Эта функция извлекает заголовок NT из загружаемого EXE и пытается зарезервировать регион памяти по адресу указанному в поле ImageBase размера SizeOfmage. Если регион по какой-то причине не был выделен функция проверяет имеет ли EXE файл таблицу релокаций. Если так, тогда функция пытается выделять память по любому адресу. Информация о релокациях позволяет загрузить EXE по любому адресу отличному от ImageBase. Она содержит все места в EXE файле где он использует абсолютную адресацию. Мы можем потом подкорректировать эти адреса используя разницу между реальным базовым адресом и адресом указанным в поле ImageBase: Код (Visual Basic): ' // Reserve memory for EXE Function ReserveMemory( _ ByVal pRawExeData As Long, _ ByRef pBase As Long) As ERROR_MESSAGES Dim NtHdr As IMAGE_NT_HEADERS Dim pLocBase As Long If GetImageNtHeaders(pRawExeData, NtHdr) = 0 Then ReserveMemory = EM_UNABLE_TO_GET_NT_HEADERS Exit Function End If ' // Reserve memory for EXE pLocBase = tVirtualAlloc(ByVal NtHdr.OptionalHeader.ImageBase, _ NtHdr.OptionalHeader.SizeOfImage, _ MEM_RESERVE, PAGE_EXECUTE_READWRITE) If pLocBase = 0 Then ' // If relocation information not found error If NtHdr.FileHeader.Characteristics And IMAGE_FILE_RELOCS_STRIPPED Then ReserveMemory = EM_UNABLE_TO_ALLOCATE_MEMORY Exit Function Else ' // Reserve memory in other region pLocBase = tVirtualAlloc(ByVal 0&, NtHdr.OptionalHeader.SizeOfImage, _ MEM_RESERVE, PAGE_EXECUTE_READWRITE) If pLocBase = 0 Then ReserveMemory = EM_UNABLE_TO_ALLOCATE_MEMORY Exit Function End If End If End If pBase = pLocBase End Function Если при вызове функции произошла ошибка то показывается сообщение о ней и приложение завершается. В противном случае вызывается функция ProcessSectionsAndHeaders. Эта функция размещает все заголовки в выделенную память, извлекает информацию о всех секциях и копирует все данные в выделенную для них память. Если какая-либо секция имеет неинициализированные данные то этот регион заполняется нулями: Код (Visual Basic): ' // Allocate memory for sections and copy them data to there Function ProcessSectionsAndHeaders( _ ByVal pRawExeData As Long, _ ByVal pBase As Long) As ERROR_MESSAGES Dim iSec As Long Dim pNtHdr As Long Dim NtHdr As IMAGE_NT_HEADERS Dim sec As IMAGE_SECTION_HEADER Dim lpSec As Long Dim pData As Long pNtHdr = GetImageNtHeaders(pRawExeData, NtHdr) If pNtHdr = 0 Then ProcessSectionsAndHeaders = EM_UNABLE_TO_GET_NT_HEADERS Exit Function End If ' // Alloc memory for headers pData = tVirtualAlloc(ByVal pBase, NtHdr.OptionalHeader.SizeOfHeaders, MEM_COMMIT, PAGE_READWRITE) If pData = 0 Then ProcessSectionsAndHeaders = EM_UNABLE_TO_ALLOCATE_MEMORY Exit Function End If ' // Copy headers tCopyMemory pData, pRawExeData, NtHdr.OptionalHeader.SizeOfHeaders ' // Get address of beginnig of sections headers pData = pNtHdr + Len(NtHdr.Signature) + Len(NtHdr.FileHeader) + NtHdr.FileHeader.SizeOfOptionalHeader ' // Go thru sections For iSec = 0 To NtHdr.FileHeader.NumberOfSections - 1 ' // Copy section descriptor tCopyMemory IntPtr(sec.SectionName(0)), pData, Len(sec) ' // Alloc memory for section lpSec = tVirtualAlloc(sec.VirtualAddress + pBase, sec.VirtualSize, MEM_COMMIT, PAGE_READWRITE) If lpSec = 0 Then ProcessSectionsAndHeaders = EM_UNABLE_TO_ALLOCATE_MEMORY Exit Function End If ' If there is initialized data If sec.SizeOfRawData Then ' // Take into account file alignment If sec.SizeOfRawData > sec.VirtualSize Then sec.SizeOfRawData = sec.VirtualSize ' // Copy initialized data to section tCopyMemory lpSec, pRawExeData + sec.PointerToRawData, sec.SizeOfRawData lpSec = lpSec + sec.SizeOfRawData sec.VirtualSize = sec.VirtualSize - sec.SizeOfRawData End If ' // Fill remain part with zero tFillMemory lpSec, sec.VirtualSize, 0 ' // Next section pData = pData + Len(sec) Next End Function Затем функция LoadExeFromMemory вызывает функцию UpdateNewBaseAddress которая обновляет новый базовый адрес в user-mode системных структурах. Windows создает специальную структуру называемую PEB (Process Environment Block) для каждого процесса. Это очень полезная структура которая позволяет получить очень много информации о процессе. Множество API функций берут информацию из этой структуры. Для примера GetModuleHandle(NULL) берет возвращаемое значение из PEB.ImageBaseAddress или GetModuleHandle("MyExeName") извлекает информацию из списка загруженных модулей - PEB.Ldr. Нам нужно обновить эту информацию согласно новому базовому адресу для того чтобы API функции возвращали корректное значение. Вот небольшая часть структуры PEB: Код (Visual Basic): Type PEB NotUsed As Long Mutant As Long ImageBaseAddress As Long LoaderData As Long ' // Pointer to PEB_LDR_DATA ProcessParameters As Long ' // .... End Type Нам интересно только поле ImageBaseAddress и LoaderData. Первое поле содержит базовый адрес EXE файла. Второе поле содержит указатель на структуру PEB_LDR_DATA которая описывает все загруженные модули в процессе: Код (Visual Basic): Type PEB_LDR_DATA Length As Long Initialized As Long SsHandle As Long InLoadOrderModuleList As LIST_ENTRY InMemoryOrderModuleList As LIST_ENTRY InInitializationOrderModuleList As LIST_ENTRY End Type
Эта структура содержит три двухсвязных списка что описывают каждый модуль. Список InLoadOrderModuleList содержит ссылки на элементы в порядке загрузки, т.е. ссылки в этом списке расположены в порядке загрузки (первый модуль в начале). Список InMemoryOrderModuleList тоже самое только в порядке расположения в памяти, а InInitializationOrderModuleList в порядке инициализации. Нам нужно получить первый элемент списка InLoadOrderModuleList который является указателем на структуру LDR_MODULE: Код (Visual Basic): Type LDR_MODULE InLoadOrderModuleList As LIST_ENTRY InMemoryOrderModuleList As LIST_ENTRY InInitOrderModuleList As LIST_ENTRY BaseAddress As Long EntryPoint As Long SizeOfImage As Long FullDllName As UNICODE_STRING BaseDllName As UNICODE_STRING Flags As Long LoadCount As Integer TlsIndex As Integer HashTableEntry As LIST_ENTRY TimeDateStamp As Long End Type Эта структура описывает один модуль. Первый элемент списка InLoadOrderModuleList является описателем главного исполняемого файла. Нам нужно изменить поле BaseAddress на новый базовый адрес и сохранить изменения. Итак, для того чтобы получить адрес структуры PEB мы можем использовать функцию NtQueryInformationProcess которая извлекает множество полезной информации о процессе (узнать подробнее можно в книге 'Windows NT/2000 Native API Reference' by Gary Nebbett). Структура PEB может быть получена из структуры PROCESS_BASIC_INFORMATION которая описывает базовую информацию о процессе: Код (Visual Basic): Type PROCESS_BASIC_INFORMATION ExitStatus As Long PebBaseAddress As Long AffinityMask As Long BasePriority As Long UniqueProcessId As Long InheritedFromUniqueProcessId As Long End Type Поле PebBaseAddress содержит адрес структуры PEB. Для того чтобы извлечь структуру PROCESS_BASIC_INFORMATION нам нужно передать в качестве параметра класса информации значение ProcessBasicInformation. Поскольку размер структуры может меняться в различных версиях Windows я использую кучу для извлечения структуры PROCESS_BASIC_INFORMATION. Если размер не подходит код увеличивает размер памяти для структуры PROCESS_BASIC_INFORMATION и повторяет заново пока структура не будет извлечена: Код (Visual Basic): Function UpdateNewBaseAddress( _ ByVal pBase As Long) As ERROR_MESSAGES Dim pPBI As Long: Dim PBIlen As Long Dim PBI As PROCESS_BASIC_INFORMATION: Dim cPEB As PEB Dim ntstat As Long Dim ldrData As PEB_LDR_DATA Dim ldrMod As LDR_MODULE ntstat = tNtQueryInformationProcess(tGetCurrentProcess(), ProcessBasicInformation, IntPtr(PBI.ExitStatus), Len(PBI), PBIlen) Do While ntstat = STATUS_INFO_LENGTH_MISMATCH PBIlen = PBIlen * 2 If pPBI Then tHeapFree tGetProcessHeap(), HEAP_NO_SERIALIZE, pPBI End If pPBI = tHeapAlloc(tGetProcessHeap(), HEAP_NO_SERIALIZE, PBIlen) ntstat = tNtQueryInformationProcess(tGetCurrentProcess(), ProcessBasicInformation, pPBI, PBIlen, PBIlen) Loop If ntstat <> STATUS_SUCCESS Then UpdateNewBaseAddress = EM_PROCESS_INFORMATION_NOT_FOUND GoTo CleanUp End If If pPBI Then ' // Copy to PROCESS_BASIC_INFORMATION tCopyMemory IntPtr(PBI.ExitStatus), pPBI, Len(PBI) End If ' // Get PEB tCopyMemory IntPtr(cPEB.NotUsed), PBI.PebBaseAddress, Len(cPEB) ' // Modify image base cPEB.ImageBaseAddress = pBase ' // Restore PEB tCopyMemory PBI.PebBaseAddress, IntPtr(cPEB.NotUsed), Len(cPEB) ' // Fix base address in PEB_LDR_DATA list tCopyMemory IntPtr(ldrData.Length), cPEB.LoaderData, Len(ldrData) ' // Get first element tCopyMemory IntPtr(ldrMod.InLoadOrderModuleList.Flink), ldrData.InLoadOrderModuleList.Flink, Len(ldrMod) ' // Fix base ldrMod.BaseAddress = pBase ' // Restore tCopyMemory ldrData.InLoadOrderModuleList.Flink, IntPtr(ldrMod.InLoadOrderModuleList.Flink), Len(ldrMod) CleanUp: ' // Free memory If pPBI Then tHeapFree tGetProcessHeap(), HEAP_NO_SERIALIZE, pPBI End If End Function После обновления базового адреса в системных структурах шеллкод вызывает функцию ProcessImportTable которая загружает необходимые библиотеки для работы EXE файла. Вначале извлекается директория IMAGE_DIRECTORY_ENTRY_IMPORT которая содержит RVA массива структур IMAGE_IMPORT_DESCRIPTOR: Код (Visual Basic): Type IMAGE_IMPORT_DESCRIPTOR Characteristics As Long TimeDateStamp As Long ForwarderChain As Long pName As Long FirstThunk As Long End Type Каждая такая структура описывает одну DLL. Поле pName содержит RVA ASCIIZ строки с именем библиотеки. Поле Characteristics содержит RVA таблицы импортируемых функций, а поле FirstThunk содержит RVA таблицы адресов импорта (IAT). Таблица имен представляет из себя массив структур IMAGE_THUNK_DATA. Эта структура представляет из себя 32 битное значение в котором если установлен старший бит остальные биты представляют из себя ординал функции (импорт по ординалу), иначе остальные биты содержат RVA имени функции предваренной значением Hint. Если же структура IMAGE_THUNK_DATA содержит 0 то значит список имен закончен. Если все поля структуры IMAGE_IMPORT_DESCRIPTOR равны 0 это означает что список структур также окончен. Код (Visual Basic): ' // Process import table Function ProcessImportTable( _ ByVal pBase As Long) As ERROR_MESSAGES Dim NtHdr As IMAGE_NT_HEADERS: Dim datDirectory As IMAGE_DATA_DIRECTORY Dim dsc As IMAGE_IMPORT_DESCRIPTOR: Dim hLib As Long Dim thnk As Long: Dim Addr As Long Dim fnc As Long: Dim pData As Long If GetImageNtHeaders(pBase, NtHdr) = 0 Then ProcessImportTable = EM_UNABLE_TO_GET_NT_HEADERS Exit Function End If ' // Import table processing If NtHdr.OptionalHeader.NumberOfRvaAndSizes > 1 Then If GetDataDirectory(pBase, IMAGE_DIRECTORY_ENTRY_IMPORT, datDirectory) = 0 Then ProcessImportTable = EM_INVALID_DATA_DIRECTORY Exit Function End If ' // If import table exists If datDirectory.Size > 0 And datDirectory.VirtualAddress > 0 Then ' // Copy import descriptor pData = datDirectory.VirtualAddress + pBase tCopyMemory IntPtr(dsc.Characteristics), pData, Len(dsc) ' // Go thru all descriptors Do Until dsc.Characteristics = 0 And _ dsc.FirstThunk = 0 And _ dsc.ForwarderChain = 0 And _ dsc.pName = 0 And _ dsc.TimeDateStamp = 0 If dsc.pName > 0 Then ' // Load needed library hLib = tLoadLibrary(dsc.pName + pBase) If hLib = 0 Then ProcessImportTable = EM_LOADLIBRARY_FAILED Exit Function End If If dsc.Characteristics Then fnc = dsc.Characteristics + pBase Else fnc = dsc.FirstThunk + pBase ' // Go to names table tCopyMemory IntPtr(thnk), fnc, 4 ' // Go thru names table Do While thnk ' // Check import type If thnk < 0 Then ' // By ordinal Addr = tGetProcAddress(hLib, thnk And &HFFFF&) Else ' // By name Addr = tGetProcAddress(hLib, thnk + 2 + pBase) End If ' // Next function fnc = fnc + 4 tCopyMemory IntPtr(thnk), fnc, 4 tCopyMemory dsc.FirstThunk + pBase, IntPtr(Addr), 4 dsc.FirstThunk = dsc.FirstThunk + 4 Loop End If ' // Next descriptor pData = pData + Len(dsc) tCopyMemory IntPtr(dsc.Characteristics), pData, Len(dsc) Loop End If End If End Function
Функция ProcessRelocation вызывается после обработки импорта. Эта функция настраивает все абсолютные ссылки (если таковые имеются). Извлекается каталог IMAGE_DIRECTORY_ENTRY_BASERELOC который содержит RVA массива структур IMAGE_BASE_RELOCATION. Каждый элемент этого масива содержит настройки в пределах 4Кб относительно адреса VirtualAddress: Код (Visual Basic): Type IMAGE_BASE_RELOCATION VirtualAddress As Long SizeOfBlock As Long End Type Поле SizeOfBlock содержит размер элемента в байтах. Массив 16-битных значений дескрипторов расположен после каждой структуры IMAGE_BASE_RELOCATION. Мы можем вычислить количество этих значений по формуле: (SizeOfBlock - Len(IMAGE_BASE_RELOCATION)) \ Len(Integer). Каждый элемент массива дескрипторов имеет следующуюю структуру: Верхние 4 бита содержат тип настройки. Нам интересна только настройка IMAGE_REL_BASED_HIGHLOW которая означает что нам нужно добавить разницу (RealBaseAddress - ImageBaseAddress) к значению Long которое расположено по адресу VirtualAddress + 12 младших бит дескриптора. Массив струкутр IMAGE_BASE_RELOCATION заканчивается структурой где все поля заполнены нулями: Код (Visual Basic): ' // Process relocations Function ProcessRelocations( _ ByVal pBase As Long) As ERROR_MESSAGES Dim NtHdr As IMAGE_NT_HEADERS: Dim datDirectory As IMAGE_DATA_DIRECTORY Dim relBase As IMAGE_BASE_RELOCATION: Dim entriesCount As Long Dim relType As Long: Dim dwAddress As Long Dim dwOrig As Long: Dim pRelBase As Long Dim delta As Long: Dim pData As Long ' // Check if module has not been loaded to image base value If GetImageNtHeaders(pBase, NtHdr) = 0 Then ProcessRelocations = EM_UNABLE_TO_GET_NT_HEADERS Exit Function End If delta = pBase - NtHdr.OptionalHeader.ImageBase ' // Process relocations If delta Then ' // Get address of relocation table If GetDataDirectory(pBase, IMAGE_DIRECTORY_ENTRY_BASERELOC, datDirectory) = 0 Then ProcessRelocations = EM_INVALID_DATA_DIRECTORY Exit Function End If If datDirectory.Size > 0 And datDirectory.VirtualAddress > 0 Then ' // Copy relocation base pRelBase = datDirectory.VirtualAddress + pBase tCopyMemory IntPtr(relBase.VirtualAddress), pRelBase, Len(relBase) Do While relBase.VirtualAddress ' // To first reloc chunk pData = pRelBase + Len(relBase) entriesCount = (relBase.SizeOfBlock - Len(relBase)) \ 2 Do While entriesCount > 0 tCopyMemory IntPtr(relType), pData, 2 Select Case (relType \ 4096) And &HF Case IMAGE_REL_BASED_HIGHLOW ' // Calculate address dwAddress = relBase.VirtualAddress + (relType And &HFFF&) + pBase ' // Get original address tCopyMemory IntPtr(dwOrig), dwAddress, Len(dwOrig) ' // Add delta dwOrig = dwOrig + delta ' // Save tCopyMemory dwAddress, IntPtr(dwOrig), Len(dwOrig) End Select pData = pData + 2 entriesCount = entriesCount - 1 Loop ' // Next relocation base pRelBase = pRelBase + relBase.SizeOfBlock tCopyMemory IntPtr(relBase.VirtualAddress), pRelBase, Len(relBase) Loop End If End If End Function После настройки релокаций шеллкод вызывает функцию SetMemoryPermissions которая настраивает разрешения памяти согласно полю Characteristics структуры IMAGE_SECTION_HEADER. Для этого просто вызывается функция VirtualProtect с определенными атрибутами памяти: Код (Visual Basic): ' // Set memory permissions Private Function SetMemoryPermissions( _ ByVal pBase As Long) As ERROR_MESSAGES Dim iSec As Long: Dim pNtHdr As Long Dim NtHdr As IMAGE_NT_HEADERS: Dim sec As IMAGE_SECTION_HEADER Dim Attr As MEMPROTECT: Dim pSec As Long Dim ret As Long pNtHdr = GetImageNtHeaders(pBase, NtHdr) If pNtHdr = 0 Then SetMemoryPermissions = EM_UNABLE_TO_GET_NT_HEADERS Exit Function End If ' // Get address of first section header pSec = pNtHdr + 4 + Len(NtHdr.FileHeader) + NtHdr.FileHeader.SizeOfOptionalHeader ' // Go thru section headers For iSec = 0 To NtHdr.FileHeader.NumberOfSections - 1 ' // Copy section descriptor tCopyMemory IntPtr(sec.SectionName(0)), pSec, Len(sec) ' // Get type If sec.Characteristics And IMAGE_SCN_MEM_EXECUTE Then If sec.Characteristics And IMAGE_SCN_MEM_READ Then If sec.Characteristics And IMAGE_SCN_MEM_WRITE Then Attr = PAGE_EXECUTE_READWRITE Else Attr = PAGE_EXECUTE_READ End If Else If sec.Characteristics And IMAGE_SCN_MEM_WRITE Then Attr = PAGE_EXECUTE_WRITECOPY Else Attr = PAGE_EXECUTE End If End If Else If sec.Characteristics And IMAGE_SCN_MEM_READ Then If sec.Characteristics And IMAGE_SCN_MEM_WRITE Then Attr = PAGE_READWRITE Else Attr = PAGE_READONLY End If Else If sec.Characteristics And IMAGE_SCN_MEM_WRITE Then Attr = PAGE_WRITECOPY Else Attr = PAGE_NOACCESS End If End If End If ' // Set memory permissions If tVirtualProtect(sec.VirtualAddress + pBase, sec.VirtualSize, Attr, IntPtr(ret)) = 0 Then SetMemoryPermissions = EM_UNABLE_TO_PROTECT_MEMORY Exit Function End If ' // Next section pSec = pSec + Len(sec) Next End Function В конце концов очищается таблица сообщений об ошибках (если нужно) и вызывается точка входа загруженного EXE. В предыдущей версии загрузчика я выгружал шеллкод тоже, но некоторые EXE не вызывают ExitProcess следовательно это могло вызывать креши. Загрузчик готов. Хотя мы написал загрузчик без использвания рантайма, компилятор VB6 добавляет его все-равно поскольку все OBJ файлы имеют ссылки на MSVBVM60 во время компиляции. Нам придется удалить рантайм из таблицы импорта загрузчика вручную. Для этого я сделал специальную утилиту - Patcher которая ищет рантайм в таблице импорта и таблице связанного импорта и удаляет его оттуда. Эта утилита также была полезна для драйвера режима ядра. Я не буду описывать ее работу поскольку она использует те же концепции PE-формата что я уже описал здесь. В общем и целом мы сделали рабочий EXE который не использует MSVBVM60 на целевой машине. Для того чтобы использовать загрузчик нужно скомпилировать его затем с помощью патчера пропатчить его. После этог можно использовать его в компиляторе. Я надеюсь вам понравилось. Спасибо за внимание! С уважением, Кривоус Анатолий (The trick).