Добро пожаловать в форум, Guest  >>   Войти | Регистрация | Поиск | Правила | В избранное | Подписаться
Все форумы / Visual Basic Новый топик    Ответить
 Загрузчик, шеллкод, без рантайма...  [new]
ATM-TURBO 2
Member

Откуда:
Сообщений: 165


Всем привет! Когда-то давно я исследовал 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 item
    Private Type BinStorageListItem
        ofstFileName        As Long            ' // Offset of file name
        ofstDestPath        As Long            ' // Offset of file path
        dwSizeOfFile        As Long            ' // Size of file
        ofstBeginOfData     As Long            ' // Offset of beginning data
        dwFlags             As FileFlags       ' // Flags
    End Type
    
    ' // Execute list item
    Private Type BinExecListItem
        ofstFileName        As Long            ' // Offset of file name
        ofstParameters      As Long            ' // Offset of parameters
        dwFlags             As ExeFlags        ' // Flags
    End Type
    
    ' // Storage descriptor
    Private Type BinStorageList
        dwSizeOfStructure   As Long            ' // Size of structure
        iExecutableIndex    As Long            ' // Index of main executable
        dwSizeOfItem        As Long            ' // Size of BinaryStorageItem structure
        dwNumberOfItems     As Long            ' // Number of files in storage
    End Type
    
    ' // Execute list descriptor
    Private Type BinExecList
        dwSizeOfStructure   As Long            ' // Size of structure
        dwSizeOfItem        As Long            ' // Size of BinaryExecuteItem structure
        dwNumberOfItems     As Long            ' // Number of items
    End Type
    
    ' // Base information about project
    Private Type BinProject
        dwSizeOfStructure   As Long            ' // Size of structure
        storageDescriptor   As BinStorageList  ' // Storage descriptor
        execListDescriptor  As BinExecList     ' // Command descriptor
        dwStringsTableLen   As Long            ' // Size of strings table
        dwFileTableLen      As Long            ' // Size of data table
    End Type
    

    Структура BinProject размещается в начале ресурсов. Заметьте что проект сохраняется как RT_RCDATA с именем PROJECT. Поле dwSizeOfStructure определяет размер структуры BinProject. storageDescriptor и execListDescriptor определяют описатели хранилища и команд соответственно. Поле dwStringsTableLen показывает размер строковой таблицы. Строковая таблица содержит все имена и команды в формате UNICODE. Поле dwFileTableLen определяет размер всех данных в хранилище. И хранилище BinStorageList и списки команд BinExecList также имеют поля dwSizeOfItem и dwSizeOfStructure которые определяют размер структуры описателя и размер одного элемента в списке. Эти структуры также содержат поле dwNumberOfItems которое показывает количество элементов в списке. Поле iExecutableIndex содержит индекс исполняемого файла в хранилище. Общая структура показана на рисунке:
    Картинка с другого сайта.
    Любой элемент может ссылаться на таблицу строк и таблицу файлов. Для этой цели используется смещение относительно начала таблицы. Все итемы расположены одна за другой. Теперь мы знаем внутренний формат проекта и можем поговорить о том как постороить загрузчик который будет содержать эти данные. Как я уже сказал мы сохраняем данные в ресурсы загрузчика. О самом загрузчике я расскажу позднее, а сейчас я хотел бы заметить одну важную особенность. Когда мы ложим данные проекта в EXE файл загрузчика то это не затрагивает другие данные в ресурсах. Для примера, если запустить такой EXE то информация хранящаяся в ресурсах внутреннего EXE не будет загружена. Тоже самое относится к иконкам и версии приложения. Для избежания данных проблем нужно скопировать все ресурсы из внутреннего EXE в загрузчик. WinAPI предоставляет набор функций для замены ресурсов. Для того чтобы получить список ресурсов нам нужно распарсить EXE файл и извлечь данные. Я написал функцию LoadResources которая извлекает все ресурсы EXE файла в массив.

    PE формат.

    Для того чтобы получить ресурсы из EXE файла, запустить EXE из памяти и хорошо разбираться в структуре EXE фала мы должны изучить PE (portable executable) формат. PE формат имеет довольно сложную структуру. Когда загрузчик запускает PE file (exe или dll) он делает довольно много работы. Каждый PE файл начинается со специальной структуры IMAGE_DOS_HEADER aka. DOS-заглушка. Поскольку и DOS и Windows приложения имеют расширение exe существует возможность запуска exe файла в DOS, но если попытаться сделать это в DOS то он выполнит это заглушку. Обычно в этом случае показываетсясообщение: "This program cannot be run in DOS mode", но мы можем написать там любую программу:
    Картинка с другого сайта.
    Type IMAGE_DOS_HEADER
        e_magic                     As Integer
        e_cblp                      As Integer
        e_cp                        As Integer
        e_crlc                      As Integer
        e_cparhdr                   As Integer
        e_minalloc                  As Integer
        e_maxalloc                  As Integer
        e_ss                        As Integer
        e_sp                        As Integer
        e_csum                      As Integer
        e_ip                        As Integer
        e_cs                        As Integer
        e_lfarlc                    As Integer
        e_ovno                      As Integer
        e_res(0 To 3)               As Integer
        e_oemid                     As Integer
        e_oeminfo                   As Integer
        e_res2(0 To 9)              As Integer
        e_lfanew                    As Long
    End Type
    

    Но поскольку мы не пишем DOS программы для нас эта структура не важна. Нам интересно только поля e_magic и e_lfanew. Первое поле должно содержать сигнатуру 'MZ' aka. IMAGE_DOS_SIGNATURE а второе смещение до очень важной структуры IMAGE_NT_HEADERS:
    Type IMAGE_NT_HEADERS
        Signature                       As Long
        FileHeader                      As IMAGE_FILE_HEADER
        OptionalHeader                  As IMAGE_OPTIONAL_HEADER
    End Type
    

    Первое поле этой структуры содержит сигнатуру 'PE\0\0' (aka. IMAGE_NT_SIGNATURE). Следующее поле описывает исполняемый файл и имеет следующий формат:
    Type IMAGE_FILE_HEADER
        Machine                         As Integer
        NumberOfSections                As Integer
        TimeDateStamp                   As Long
        PointerToSymbolTable            As Long
        NumberOfSymbols                 As Long
        SizeOfOptionalHeader            As Integer
        Characteristics                 As Integer
    End Type
    

    Поле Machine определяет архитектуру процессора и должно иметь значение IMAGE_FILE_MACHINE_I386 в нашем случае. Поле NumberOfSections определяет количество секций в PE файле.

  • Любой EXE файл содержит секции. Каждая секция занимает место в адресном пространстве процесса и опционально в файле. Секция может содержать как код так и данные (инизиализированные или не), а также имеет имя. Наиболее распространенные имена: .text, .data, .rsrc. Обычно секция .text содержит код, .data инициализированные данные, а .rsrc - ресурсы. Можно изменять это поведение используя дериктивы линкера. Каждая секция имеет адрес называемый виртуальным адресом. В общем в PE формате существует несколько типов адресации. Первый - относительный виртуальный адрес (RVA). Из-за того что PE фал может быть загружен по любому адресу все ссылки внутри PE файла имеют относительную адресацию. RVA - это смещение относительно базового адреса (адреса первого байта PE-образа в памяти). Сумма RVA и базового адреса называется виртуальным адресом (VA). Также существует RAW-смещение которое показывает смещение относительно начала файла относительно RVA. Заметьте что RVA <> RAW. Когда модуль загружается каждая секция размещается по виртуальному адресу. Для примера модуль может иметь секцию что не имеет инициализированных данных. Такая секция не будет занимать место в PE-файле, но будет в памяти. Это очень важный момент поскольку мы будем работать с сырым EXE файлом.

    Поле TimeDateStamp содержит дату создания PE модуля в формате UTC. Поля PointerToSymbolTable and NumberOfSymbols содержат информацию о символах в PE файлах. В общем эти поля содержат нули, но эти поля всегда используються в объектных файлах (*.OBJ, *.LIB) для разрешения ссылок во время линковки а также содержат отладочную информацию для PE модуля. Следующее поле SizeOfOptionalHeader содержит размер структуры расположенной после IMAGE_FILE_HEADER так называемой IMAGE_OPTIONAL_HEADER которая всегда присутствует в PE файлах (хотя может отсутствовать в OBJ файлах). Эта структура являеться очень важной для загрузки PE модуля в память. Заметьте что эта структура различается в 32 битных и 64 битных PE-модулях. И наконец поле Characteristics содержит PE-аттрибуты.
    Структура IMAGE_OPTIONAL_HEADER имеет следующий формат:
    Type IMAGE_OPTIONAL_HEADER
        Magic                           As Integer
        MajorLinkerVersion              As Byte
        MinorLinkerVersion              As Byte
        SizeOfCode                      As Long
        SizeOfInitializedData           As Long
        SizeOfUnitializedData           As Long
        AddressOfEntryPoint             As Long
        BaseOfCode                      As Long
        BaseOfData                      As Long
        ImageBase                       As Long
        SectionAlignment                As Long
        FileAlignment                   As Long
        MajorOperatingSystemVersion     As Integer
        MinorOperatingSystemVersion     As Integer
        MajorImageVersion               As Integer
        MinorImageVersion               As Integer
        MajorSubsystemVersion           As Integer
        MinorSubsystemVersion           As Integer
        W32VersionValue                 As Long
        SizeOfImage                     As Long
        SizeOfHeaders                   As Long
        CheckSum                        As Long
        SubSystem                       As Integer
        DllCharacteristics              As Integer
        SizeOfStackReserve              As Long
        SizeOfStackCommit               As Long
        SizeOfHeapReserve               As Long
        SizeOfHeapCommit                As Long
        LoaderFlags                     As Long
        NumberOfRvaAndSizes             As Long
        DataDirectory(15)               As IMAGE_DATA_DIRECTORY
    End Type
    

    Первое поле содержит тип образа (x86, x64 или ROM образ). Нас интересует только IMAGE_NT_OPTIONAL_HDR32_MAGIC который представляет собой 32 битное приложение. Следующие 2 поля не являются важными (они использовались на старых системах) и содержат 4. Следующая группа полей содержит размер всех секций с кодом, инициализированными данными и неинициализированными данными. Эти значения должны быть кратными значению SectionAlignment этой структуры (см. далее). Поле AddressOfEntryPoint является очень важным RVA значением которое определяет точку входа в программу. Мы будем использовать это поле когда загрузим PE образ в память для запуска кода. Следующим важным полем является ImageBase которое задает предпочитаемый виртуальный адрес загрузки модуля. Когда загрузчик начинает загружать модуль, то он старается сделать это по предпочитаемому виртуальному адресу (находящимся в ImageBase). Если этот адрес занят, то загрузчик проверяет поле Characteristics структуры IMAGE_FILE_HEADER. Если это поле содержит флаг IMAGE_FILE_RELOCS_STRIPPED то модуль не сможет быть загружен. Для того чтобы загрузить такие модули нам нужно добавить информацию о релокации которая позволит загрузчику настроить адреса внутри PE-образа если модуль не может загрузится по предпочитаемому базовому адресу. Мы будем использоват это поле вместе с SizeOfImage для того чтобы зарезервировать память под распакованный EXE. Поля SectionAlignment and FileAlignment содержат выравнивание секций в памяти и в файле соответственно. Изменяя файловое выравнивание можно уменьшить размер PE файла, но система может не загрузить данный PE файл. Выравнивание секций обычно равно размеру страницы в памяти. Поле SizeOfHeaders задает размер всех заголовков (DOS Заголовок, NT заголовок, заголовки секций) выровненное на FileAlignment. Значения SizeOfStackReserve и SizeOfStackCommit определяют общий размер стека и начальный размер стека. Тоже самое и для полей SizeOfHeapReserve и SizeOfHeapCommit, но для кучи. Поле NumberOfRvaAndSizes содержит количество элементов в массиве DataDirectory. Это поле всегда равно 16. Массив DataDirectory является также очень важным поскольку в нем содержатся каталоги данных которые содержат нужную информацию об импорте, экспорте, ресурсах, релокациях и т.д. Мы будем использовать только несколько элементов из этого каталога которые используются VB6 компилятором. Я расскажу о каталогах немного позже, давайте посмотрим что находится за каталогами. За каталогами содержаться описатели секций. Количество этих описателей, если вспомнить, мы получили из структуры IMAGE_FILE_HEADER. Рассмотрим формат заголовка секции:
    Type IMAGE_SECTION_HEADER
        SectionName(7)              As Byte
        VirtualSize                 As Long
        VirtualAddress              As Long
        SizeOfRawData               As Long
        PointerToRawData            As Long
        PointerToRelocations        As Long
        PointerToLinenumbers        As Long
        NumberOfRelocations         As Integer
        NumberOfLinenumbers         As Integer
        Characteristics             As Long
    End Type
    

    Первое поле содержит имя секции в формате UTF-8 c завершающим нуль-терминалом. Это имя ограничено 8-ю символами (если имя секции имеет размер 8 символов то нуль-терминатор игнорируется). COFF файл может иметь имя больше чем 8 символов в этом случае имя начинается с символа '/' за которым следует ASCII строка с десятичным значением смещения в строковой таблице (поле IMAGE_FILE_HEADER). PE файл не поддерживает длинные имена секций. Поля VirtualSize и VirtualAddress содержат размер секции в памяти и адрес (RVA). Поля SizeOfRawData и PointerToRawData содержат RAW адрес данных в файле (если секция содержит инициализированные данные). Это ключевой момент потому что мы можем вычислить RAW адрес с помощью относительного виртуального адреса используя информацию из заголовка секций. Я написал функцию для перевода RVA адресации в RAW смещение в файле:
    ' // RVA to RAW
    Function RVA2RAW( _
                     ByVal rva As Long, _
                     ByRef sec() As IMAGE_SECTION_HEADER) As Long
        Dim index As Long
        
        For index = 0 To UBound(sec)
            
            If rva >= sec(index).VirtualAddress And _
               rva < sec(index).VirtualAddress + sec(index).VirtualSize Then
                RVA2RAW = sec(index).PointerToRawData + (rva - sec(index).VirtualAddress)
                Exit Function
            End If
            
        Next
        
        RVA2RAW = rva
        
    End Function
    

    Эта функция перечисляет все секции и проверяет если переданный адрес находится в пределах секции. Следующие 5 полей используються только в COFF файлах и не важны в PE файлах. Поле Characteristics содержит атрибуты секции такие как права доступа к памяти и управление. Мы будем использовать это поле для защиты памяти exe файла в загрузчике.
    Давайте теперь вернемся к каталогам данных. Как мы видели существует 16 элементов в данном каталоге. Обычно PE файл не использует их все. Давайте рассмотрим структуру элемента каталога:
    Private Type IMAGE_DATA_DIRECTORY
        VirtualAddress                  As Long
        Size                            As Long
    End Type
    

    Эта структура содержит два поля. Первое поле содержит RVA адрес данных каталога, воторое - размер. Когда элемент каталога не представлен в PE файле то оба поля содержат нули. Вообще большинство VB6-компилируемых приложений имеют только 4 каталога: таблица импорта, таблица ресурсов, таблица связанного импорта и таблица адресов импорта (IAT). Сейчас мы рассмотрим таблицу ресурсов которая имеет индекс IMAGE_DIRECTORY_ENTRY_RESOURCE потому что мы работаем с этой информацией в проекте Compiler.
    Все ресурсы в EXE файле представлены в виде трехуровнего дерева. Первый уровень определяет тип ресурса (RT_BITMAP, RT_MANIFEST, RT_RCDATA, и т.д.), следующий - идентификатор ресурса и наконец третий - язык. В стандартном редакторе ресурсов VB Resource Editor можно изменять только первые 2 уровня. Все ресурсы размещаются таблице ресурсов расположенной в секции .rsrc EXE файла. Благодаря такой структуре мы можем изменять ресурсы даже в готовом EXE файле. Для того чтобы добраться до самих данных в секции ресурсов нам сначала нужно прочитать IMAGE_DIRECTORY_ENTRY_RESOURCE из опционального хидера. Поле VirtualAddress содержит RVA таблицы ресурсов которая имеет следующий формат:
    Type IMAGE_RESOURCE_DIRECTORY
        Characteristics             As Long
        TimeDateStamp               As Long
        MajorVersion                As Integer
        MinorVersion                As Integer
        NumberOfNamedEntries        As Integer
        NumberOfIdEntries           As Integer
    End Type
    

    Эта структура описывает все ресурсы в PE файле. Первые 4 поля не важны для нас; поле NumberOfNamedEntries и NumberOfIdEntries содержат количество именованных записей и записей с числовыми идентификаторами соответственно. Для примера, когда мы добавляем картинку в стандартном редакторе это добавит запись с числовым идентификатором равным 2 (RT_BITMAP). Сами записи расположены сразу после IMAGE_RESOURCE_DIRECTORY и имеют следующую структуру:
    Type IMAGE_RESOURCE_DIRECTORY_ENTRY
        NameId                      As Long
        OffsetToData                As Long
    End Type
    

    Первое поле этой структуры определяет является ли это именованной запись либо это запись с числовым идентификатором в зависимости от старшего бита. Если этот бит установлен то остальные биты определяют смещение от начала ресурсов к структуре IMAGE_RESOURCE_DIR_STRING_U которая имет следующий формат:
    Type IMAGE_RESOURCE_DIR_STRING_U
        Length                      As Integer
        NameString                  As String
    End Type
    

    Заметьте что это не правильная VB-структура и показана для наглядности. Первые два байта являются беззнаковым целым которые показывают длину строки в формате UNICODE (в символах) которая следует за ними. Таким образом для того чтобы получить строку нам нужно прочитать первые два байта с размером, выделить память для строки согласно этого размера и прочитать данные в строковую переменную. Напротив, если старший бит поля NameId сброшен то оно содержит числовой идентификатор ресурса (RT_BITMAP в примере). Поле OffsetToData имеет также двойную интерпретацию. Если старший бит установлен то это смещение (от начала ресурсов) до следующего уровня дерева ресурсов, т.е. до структуры IMAGE_RESOURCE_DIRECTORY. Иначе - это смещение до структуры IMAGE_RESOURCE_DATA_ENTRY:
    Type IMAGE_RESOURCE_DATA_ENTRY
        OffsetToData                As Long
        Size                        As Long
        CodePage                    As Long
        Reserved                    As Long
    End Type
    

    Наиболее важными для нас являются поля OffsetToData and Size которые содержат RVA и размер сырых данных ресурса. Теперь мы можем извлечь все данные из ресурсов любого PE файла.

    Компиляция.

    Итак, когда мы начинаем компиляцию проекта то вызывается метод Compile объекта класса clsProject. Вначале упаковываются все элементы хранилища и команд в бинарный формат (BinProject, BinStorageListItem, и т.д.) и формируются таблица строк и файловая таблица. Строковая таблица сохраняется как набор строк разделенных нуль-терминалом. Я использую специальный класс clsStream для безопасной работы с бинарными данными. Этот класс позволяет читать и писать любые данные или потоки в двоичный буфер, сжимать буфер. Я использую функцию RtlCompressBuffer для сжатия потока которая использует LZ-сжатие. После упаковки и сжатия проверяется выходной формат файла. Поддерживаются 2 типа файлов: бинарный (сырые данные проекта) и исполняемый (загрузчик). Двоичный формат не интересен поэтому мы будем рассматривать исполняемый формат. Вначале извлекаются все ресурсы из главного исполняемого файла в трехуровневый каталог. Эта операция выполняется с помощью функции ExtractResorces. Имена-идентификаторы сохраняются в строковом виде с префиксом '#'. Потом клонируется шаблон загрузчика в результирующий файл, начинается процесс модификации ресурсов в EXE файле используя функцию BeginUpdateResource. После этого последовательно копируются все извлеченные ресурсы (UpdateResource), двоичный проект и манифест (если нужно) в результирующий файл и применяются изменения функцией EndUpdateResource. Опять повторюсь, бинарный проект сохраняется с именем PROJECT и имеет тип RT_DATA. В общем все.

    Загрузчик.

    Итак. я думаю это наиболее интересная часть. Итак, нам нужно избегать использование рантайма. Как этого добится? Я дам некоторые правила:

  • Установить в качестве стартовой функции пользовательскую функцию;
  • Избегать любых объектов и классов в проекте;
  • Избегать непосредственных массивов. Массивы фиксированного размера в пользовательских типах не запрещены;
  • Избегать строковых переменных а также Variant/Object переменных. В некоторых случаях Currency/Date;
  • Избегать API функции задекларированые с помощью ключевого слова Declare;
  • Избегать VarPtr/StrPtr/ObjPtr и некоторые стандартные функции;
  • ...
  • ...

    Это неполный список ограничений, а во время выполнения шеллкода добавляются дополнительные ограничения.
    Итак, начнем. Для того чтобы избежать использования строковых переменных я храню все строковые переменные как Long указатели на строки. Существует проблема с загрузкой строк поскольку мы не можем обращаться к любой строке чтобы загрузить ее. Я решил использовать ресурсы в качестве хранилища строк и загружать их по числовому идентификатору. Таким образом мы можем хранить указатель в переменной Long без обращения к рантайму. Я использовал TLB (библиотеку типов) для всех API функций без атрибута usesgetlasterror чтобы избежать объявление через Declare. Для установки стартовой функции я использую опции линкера. Стартовая функция в загрузчике - Main. Обратите внимание, если в IDE выбрать стартовую функцию Main на самом деле это не будет стартовой функцией приложения потому что VB6-скомпилированное приложение начинается с функции __vbaS которая вызывает функцию ThunRTMain из рантайма, которая инициализирует рантайм и поток.
    Загрузчик содержит три модуля:

  • modMain - стартовая функция и работа с хранилищем;
  • modConstants - работа со строковыми константами;
  • modLoader - загрузчик EXE файла.

    Когда загрузчик запустился выполняется функция Main:
    ' // Startup subroutine
    Sub Main()
    
        ' // Load constants
        If Not LoadConstants Then
            MessageBox 0, GetString(MID_ERRORLOADINGCONST), 0, MB_ICONERROR Or MB_SYSTEMMODAL
            GoTo EndOfProcess
        End If
        
        ' // Load project
        If Not ReadProject Then
            MessageBox 0, GetString(MID_ERRORREADINGPROJECT), 0, MB_ICONERROR Or MB_SYSTEMMODAL
            GoTo EndOfProcess
        End If
        
        ' // Copying from storage
        If Not CopyProcess Then GoTo EndOfProcess
        
        ' // Execution process
        If Not ExecuteProcess Then GoTo EndOfProcess
        
        ' // If main executable is not presented exit
        If ProjectDesc.storageDescriptor.iExecutableIndex = -1 Then GoTo EndOfProcess
        
        ' // Run exe from memory
        If Not RunProcess Then
            ' // Error occrurs
            MessageBox 0, GetString(MID_ERRORSTARTUPEXE), 0, MB_ICONERROR Or MB_SYSTEMMODAL
        End If
        
    EndOfProcess:
        
        If pProjectData Then
            HeapFree GetProcessHeap(), HEAP_NO_SERIALIZE, pProjectData
        End If
        
        ExitProcess 0
        
    End Sub
    

    Вначале вызывается функция LoadConstants для того чтобы загрузить все необходимые константы из ресурсов:
    ' // modConstants.bas - main module for loading constants
    ' // © Krivous Anatoly Anatolevich (The trick), 2016
    
    Option Explicit
    
    Public Enum MessagesID
        MID_ERRORLOADINGCONST = 100     ' // Errors
        MID_ERRORREADINGPROJECT = 101   '
        MID_ERRORCOPYINGFILE = 102      '
        MID_ERRORWIN32 = 103            '
        MID_ERROREXECUTELINE = 104      '
        MID_ERRORSTARTUPEXE = 105       '
        PROJECT = 200                   ' // Project resource ID
        API_LIB_KERNEL32 = 300          ' // Library names
        API_LIB_NTDLL = 350             '
        API_LIB_USER32 = 400            '
        MSG_LOADER_ERROR = 500
    End Enum
    
    ' // Paths
    
    Public pAppPath  As Long            ' // Path to application
    Public pSysPath  As Long            ' // Path to System32
    Public pTmpPath  As Long            ' // Path to Temp
    Public pWinPath  As Long            ' // Path to Windows
    Public pDrvPath  As Long            ' // Path to system drive
    Public pDtpPath  As Long            ' // Path to desktop
    
    ' // Substitution constants
    
    Public pAppRepl  As Long
    Public pSysRepl  As Long
    Public pTmpRepl  As Long
    Public pWinRepl  As Long
    Public pDrvRepl  As Long
    Public pDtpRepl  As Long
    Public pStrNull  As Long            ' // \0
    
    Public hInstance    As Long         ' // Base address
    Public lpCmdLine    As Long         ' // Command line
    Public SI           As STARTUPINFO  ' // Startup parameters
    Public LCID         As Long         ' // LCID
    
    ' // Load constants
    Function LoadConstants() As Boolean
        Dim lSize   As Long
        Dim pBuf    As Long
        Dim index   As Long
        Dim ctl     As tagINITCOMMONCONTROLSEX
        
        ' // Load windows classes
        ctl.dwSize = Len(ctl)
        ctl.dwICC = &H3FFF&
        InitCommonControlsEx ctl
        
        ' // Get startup parameters
        GetStartupInfo SI
        
        ' // Get command line
        lpCmdLine = GetCommandLine()
        
        ' // Get base address
        hInstance = GetModuleHandle(ByVal 0&)
        
        ' // Get LCID
        LCID = GetUserDefaultLCID()
        
        ' // Alloc memory for strings
        pBuf = SysAllocStringLen(0, MAX_PATH)
        If pBuf = 0 Then Exit Function
        
        ' // Get path to process file name
        If GetModuleFileName(hInstance, pBuf, MAX_PATH) = 0 Then GoTo CleanUp
        
        ' // Leave only directory
        PathRemoveFileSpec pBuf
        
        ' // Save path
        pAppPath = SysAllocString(pBuf)
        
        ' // Get Windows folder
        If GetWindowsDirectory(pBuf, MAX_PATH) = 0 Then GoTo CleanUp
        pWinPath = SysAllocString(pBuf)
        
        ' // Get System32 folder
        If GetSystemDirectory(pBuf, MAX_PATH) = 0 Then GoTo CleanUp
        pSysPath = SysAllocString(pBuf)
        
        ' // Get Temp directory
        If GetTempPath(MAX_PATH, pBuf) = 0 Then GoTo CleanUp
        pTmpPath = SysAllocString(pBuf)
        
        ' // Get system drive
        PathStripToRoot pBuf
        pDrvPath = SysAllocString(pBuf)
        
        ' // Get desktop path
        If SHGetFolderPath(0, CSIDL_DESKTOPDIRECTORY, 0, SHGFP_TYPE_CURRENT, pBuf) Then GoTo CleanUp
        pDtpPath = SysAllocString(pBuf)
        
        ' // Load wildcards
        For index = 1 To 6
            If LoadString(hInstance, index, pBuf, MAX_PATH) = 0 Then GoTo CleanUp
            Select Case index
            Case 1: pAppRepl = SysAllocString(pBuf)
            Case 2: pSysRepl = SysAllocString(pBuf)
            Case 3: pTmpRepl = SysAllocString(pBuf)
            Case 4: pWinRepl = SysAllocString(pBuf)
            Case 5: pDrvRepl = SysAllocString(pBuf)
            Case 6: pDtpRepl = SysAllocString(pBuf)
            End Select
        Next
        
        ' // vbNullChar
        pStrNull = SysAllocStringLen(0, 0)
    
        ' // Success
        LoadConstants = True
        
    CleanUp:
        
        If pBuf Then SysFreeString pBuf
        
    End Function
    
    ' // Obtain string from resource (it should be less or equal MAX_PATH)
    Public Function GetString( _
                    ByVal ID As MessagesID) As Long
                    
        GetString = SysAllocStringLen(0, MAX_PATH)
        
        If GetString Then
        
            If LoadString(hInstance, ID, GetString, MAX_PATH) = 0 Then SysFreeString GetString: GetString = 0: Exit Function
            If SysReAllocString(GetString, GetString) = 0 Then SysFreeString GetString: GetString = 0: Exit Function
            
        End If
        
    End Function
    

    Функция LoadConstants загружает все необходимые переменные и строки (hInstance, LCID, командная строка, подстановочные символы, пути по умолчанию, и т.д.). Все строки сохраняются в формате UNICODE-BSTR. Функция GetString загружает строку из ресурсов по ее идентификатору. Перечисление MessagesID содержит некоторые строковые идентификаторы нужные для работы программы (сообщения об ошибках, имена библиотек, и.т.д.). Когда все константы загрузятся вызывается функция ReadProject которая загружает проект:
    ' // Load project
    Function ReadProject() As Boolean
        Dim hResource       As Long:                Dim hMememory       As Long
        Dim lResSize        As Long:                Dim pRawData        As Long
        Dim status          As Long:                Dim pUncompressed   As Long
        Dim lUncompressSize As Long:                Dim lResultSize     As Long
        Dim tmpStorageItem  As BinStorageListItem:  Dim tmpExecuteItem  As BinExecListItem
        Dim pLocalBuffer    As Long
        
        ' // Load resource
        hResource = FindResource(hInstance, GetString(PROJECT), RT_RCDATA)
        If hResource = 0 Then GoTo CleanUp
        
        hMememory = LoadResource(hInstance, hResource)
        If hMememory = 0 Then GoTo CleanUp
        
        lResSize = SizeofResource(hInstance, hResource)
        If lResSize = 0 Then GoTo CleanUp
        
        pRawData = LockResource(hMememory)
        If pRawData = 0 Then GoTo CleanUp
        
        pLocalBuffer = HeapAlloc(GetProcessHeap(), HEAP_NO_SERIALIZE, lResSize)
        If pLocalBuffer = 0 Then GoTo CleanUp
        
        ' // Copy to local buffer
        CopyMemory ByVal pLocalBuffer, ByVal pRawData, lResSize
        
        ' // Set default size
        lUncompressSize = lResSize * 2
        
        ' // Do decompress...
        Do
            
            If pUncompressed Then
                pUncompressed = HeapReAlloc(GetProcessHeap(), HEAP_NO_SERIALIZE, ByVal pUncompressed, lUncompressSize)
            Else
                pUncompressed = HeapAlloc(GetProcessHeap(), HEAP_NO_SERIALIZE, lUncompressSize)
            End If
            
            status = RtlDecompressBuffer(COMPRESSION_FORMAT_LZNT1, _
                                         ByVal pUncompressed, lUncompressSize, _
                                         ByVal pLocalBuffer, lResSize, lResultSize)
            
            lUncompressSize = lUncompressSize * 2
            
        Loop While status = STATUS_BAD_COMPRESSION_BUFFER
        
        pProjectData = pUncompressed
        
        If status Then GoTo CleanUp
    
        ' // Validation check
        If lResultSize < LenB(ProjectDesc) Then GoTo CleanUp
        
        ' // Copy descriptor
        CopyMemory ProjectDesc, ByVal pProjectData, LenB(ProjectDesc)
        
        ' // Check all members
        If ProjectDesc.dwSizeOfStructure <> Len(ProjectDesc) Then GoTo CleanUp
        If ProjectDesc.storageDescriptor.dwSizeOfStructure <> Len(ProjectDesc.storageDescriptor) Then GoTo CleanUp
        If ProjectDesc.storageDescriptor.dwSizeOfItem <> Len(tmpStorageItem) Then GoTo CleanUp
        If ProjectDesc.execListDescriptor.dwSizeOfStructure <> Len(ProjectDesc.execListDescriptor) Then GoTo CleanUp
        If ProjectDesc.execListDescriptor.dwSizeOfItem <> Len(tmpExecuteItem) Then GoTo CleanUp
        
        ' // Initialize pointers
        pStoragesTable = pProjectData + ProjectDesc.dwSizeOfStructure
        pExecutesTable = pStoragesTable + ProjectDesc.storageDescriptor.dwSizeOfItem * ProjectDesc.storageDescriptor.dwNumberOfItems
        pFilesTable = pExecutesTable + ProjectDesc.execListDescriptor.dwSizeOfItem * ProjectDesc.execListDescriptor.dwNumberOfItems
        pStringsTable = pFilesTable + ProjectDesc.dwFileTableLen
        
        ' // Check size
        If (pStringsTable + ProjectDesc.dwStringsTableLen - pProjectData) <> lResultSize Then GoTo CleanUp
        
        ' // Success
        ReadProject = True
        
    CleanUp:
        
        If pLocalBuffer Then HeapFree GetProcessHeap(), HEAP_NO_SERIALIZE, pLocalBuffer
        
        If Not ReadProject And pProjectData Then
            HeapFree GetProcessHeap(), HEAP_NO_SERIALIZE, pProjectData
        End If
        
    End Function
    

    Как можно увидеть я использую кучу процесса вместо массивов. Вначале загружается ресурс с проектом - PROJECT и копируется в кучу, затем производится декомпрессия используя функцию RtlDecompressBuffer. Эта функция не возвращает необходимый размер буфера поэтому мы пытаемся распаковать буфер увеличивая выходной размер буфера пока декомпрессия не будет успешно выполнена. После декомпрессии проверяются все параметры и инициализируются глобальные указатели проекта.
    Если проект успешно загружен то вызывается функция CopyProcess которая распаковывает все файлы из хранилища, согласно данным проекта:
    ' // Copying process
    Function CopyProcess() As Boolean
        Dim bItem       As BinStorageListItem:  Dim index       As Long
        Dim pPath       As Long:                Dim dwWritten   As Long
        Dim msg         As Long:                Dim lStep       As Long
        Dim isError     As Boolean:             Dim pItem       As Long
        Dim pErrMsg     As Long:                Dim pTempString As Long
        
        ' // Set pointer
        pItem = pStoragesTable
        
        ' // Go thru file list
        For index = 0 To ProjectDesc.storageDescriptor.dwNumberOfItems - 1
    
            ' // Copy file descriptor
            CopyMemory bItem, ByVal pItem, Len(bItem)
            
            ' // Next item
            pItem = pItem + ProjectDesc.storageDescriptor.dwSizeOfItem
            
            ' // If it is not main executable
            If index <> ProjectDesc.storageDescriptor.iExecutableIndex Then
            
                ' // Normalize path
                pPath = NormalizePath(pStringsTable + bItem.ofstDestPath, pStringsTable + bItem.ofstFileName)
                
                ' // Error occurs
                If pPath = 0 Then
                
                    pErrMsg = GetString(MID_ERRORWIN32)
                    MessageBox 0, pErrMsg, 0, MB_ICONERROR Or MB_SYSTEMMODAL
                    GoTo CleanUp
                    
                Else
                    Dim hFile   As Long
                    Dim disp    As CREATIONDISPOSITION
                    
                    ' // Set overwrite flags
                    If bItem.dwFlags And FF_REPLACEONEXIST Then disp = CREATE_ALWAYS Else disp = CREATE_NEW
                    
                    ' // Set number of subroutine
                    lStep = 0
                    
                    ' // Run subroutines
                    Do
                        ' // Disable error flag
                        isError = False
                        
                        ' // Free string
                        If pErrMsg Then SysFreeString pErrMsg: pErrMsg = 0
                        
                        ' // Choose subroutine
                        Select Case lStep
                        Case 0  ' // 0. Create folder
                        
                            If Not CreateSubdirectories(pPath) Then isError = True
                            
                        Case 1  ' // 1. Create file
                        
                            hFile = CreateFile(pPath, FILE_GENERIC_WRITE, 0, ByVal 0&, disp, FILE_ATTRIBUTE_NORMAL, 0)
                            If hFile = INVALID_HANDLE_VALUE Then
                                If GetLastError = ERROR_FILE_EXISTS Then Exit Do
                                isError = True
                            End If
                            
                        Case 2  ' // 2. Copy data to file
                        
                            If WriteFile(hFile, ByVal pFilesTable + bItem.ofstBeginOfData, _
                                         bItem.dwSizeOfFile, dwWritten, ByVal 0&) = 0 Then isError = True
                                         
                            If dwWritten <> bItem.dwSizeOfFile Then
                                isError = True
                            Else
                                CloseHandle hFile: hFile = INVALID_HANDLE_VALUE
                            End If
                            
                        End Select
                        
                        ' // If error occurs show notification (retry, abort, ignore)
                        If isError Then
                        
                            ' // Ignore error
                            If bItem.dwFlags And FF_IGNOREERROR Then Exit Do
    
                            pTempString = GetString(MID_ERRORCOPYINGFILE)
                            pErrMsg = StrCat(pTempString, pPath)
                            
                            ' // Cleaning
                            SysFreeString pTempString: pTempString = 0
                            
                            Select Case MessageBox(0, pErrMsg, 0, MB_ICONERROR Or MB_SYSTEMMODAL Or MB_CANCELTRYCONTINUE)
                            Case MESSAGEBOXRETURN.IDCONTINUE: Exit Do
                            Case MESSAGEBOXRETURN.IDTRYAGAIN
                            Case Else:  GoTo CleanUp
                            End Select
                            
                        Else: lStep = lStep + 1
                        End If
                        
                    Loop While lStep <= 2
                            
                    If hFile <> INVALID_HANDLE_VALUE Then
                        CloseHandle hFile: hFile = INVALID_HANDLE_VALUE
                    End If
                    
                    ' // Cleaning
                    SysFreeString pPath: pPath = 0
                    
                End If
                
            End If
            
        Next
        
        ' // Success
        CopyProcess = True
        
    CleanUp:
        
        If pTempString Then SysFreeString pTempString
        If pErrMsg Then SysFreeString pErrMsg
        If pPath Then SysFreeString pPath
        
        If hFile <> INVALID_HANDLE_VALUE Then
            CloseHandle hFile
            hFile = INVALID_HANDLE_VALUE
        End If
        
    End Function
    

    Эта процедура проходит по всем элементам хранилища и распаковывает их одна за одной исключая главный исполняемый файл. Функция NormalizePath заменяет подстановочные знаки на реальные пути. Также существует функция CreateSubdirectories которая создает промежуточные директории (если необходимо) по переданному в качестве параметра пути. Затем вызывается функция CreateFile для создания файла затем через WriteFile данные пишутся в файл. Если происходит ошибка то выводится стандартное сообщение с предложением повторить, отменить или игнорировать.
    ' // Create all subdirectories by path
    Function CreateSubdirectories( _
                    ByVal pPath As Long) As Boolean
        Dim pComponent As Long
        Dim tChar      As Integer
        
        ' // Pointer to first char
        pComponent = pPath
        
        ' // Go thru path components
        Do
        
            ' // Get next component
            pComponent = PathFindNextComponent(pComponent)
            
            ' // Check if end of line
            CopyMemory tChar, ByVal pComponent, 2
            If tChar = 0 Then Exit Do
            
            ' // Write null-terminator
            CopyMemory ByVal pComponent - 2, 0, 2
            
            ' // Check if path exists
            If PathIsDirectory(pPath) = 0 Then
            
                ' // Create folder
                If CreateDirectory(pPath, ByVal 0&) = 0 Then
                    ' // Error
                    CopyMemory ByVal pComponent - 2, &H5C, 2
                    Exit Function
                End If
                
            End If
            
            ' // Restore path delimiter
            CopyMemory ByVal pComponent - 2, &H5C, 2
            
        Loop
        
        ' // Success
        CreateSubdirectories = True
        
    End Function
    
    ' // Get normalize path (replace wildcards, append file name)
    Function NormalizePath( _
                    ByVal pPath As Long, _
                    ByVal pTitle As Long) As Long
        Dim lPathLen    As Long:    Dim lRelacerLen As Long
        Dim lTitleLen   As Long:    Dim pRelacer    As Long
        Dim lTotalLen   As Long:    Dim lPtr        As Long
        Dim pTempString As Long:    Dim pRetString  As Long
        
        ' // Determine wildcard
        Select Case True
        Case IntlStrEqWorker(0, pPath, pAppRepl, 5): pRelacer = pAppPath
        Case IntlStrEqWorker(0, pPath, pSysRepl, 5): pRelacer = pSysPath
        Case IntlStrEqWorker(0, pPath, pTmpRepl, 5): pRelacer = pTmpPath
        Case IntlStrEqWorker(0, pPath, pWinRepl, 5): pRelacer = pWinPath
        Case IntlStrEqWorker(0, pPath, pDrvRepl, 5): pRelacer = pDrvPath
        Case IntlStrEqWorker(0, pPath, pDtpRepl, 5): pRelacer = pDtpPath
        Case Else: pRelacer = pStrNull
        End Select
        
        ' // Get string size
        lPathLen = lstrlen(ByVal pPath)
        lRelacerLen = lstrlen(ByVal pRelacer)
        
        ' // Skip wildcard
        If lRelacerLen Then
            pPath = pPath + 5 * 2
            lPathLen = lPathLen - 5
        End If
        
        If pTitle Then lTitleLen = lstrlen(ByVal pTitle)
        
        ' // Get length all strings
        lTotalLen = lPathLen + lRelacerLen + lTitleLen
        
        ' // Check overflow (it should be les or equal MAX_PATH)
        If lTotalLen > MAX_PATH Then Exit Function
        
        ' // Create string
        pTempString = SysAllocStringLen(0, MAX_PATH)
        If pTempString = 0 Then Exit Function
        
        ' // Copy
        lstrcpyn ByVal pTempString, ByVal pRelacer, lRelacerLen + 1
        lstrcat ByVal pTempString, ByVal pPath
    
        ' // If title is presented append
        If pTitle Then
    
            ' // Error
            If PathAddBackslash(pTempString) = 0 Then GoTo CleanUp
    
            ' // Copy file name
            lstrcat ByVal pTempString, ByVal pTitle
            
        End If
        
        ' // Alloc memory for translation relative path to absolute
        pRetString = SysAllocStringLen(0, MAX_PATH)
        If pRetString = 0 Then GoTo CleanUp
        
        ' // Normalize
        If PathCanonicalize(pRetString, pTempString) = 0 Then GoTo CleanUp
        
        NormalizePath = pRetString
        
    CleanUp:
        
        If pTempString Then SysFreeString pTempString
        If pRetString <> 0 And NormalizePath = 0 Then SysFreeString pRetString
        
    End Function
    
    ' // Concatenation strings
    Function StrCat( _
                    ByVal pStringDest As Long, _
                    ByVal pStringAppended As Long) As Long
        Dim l1 As Long, l2 As Long
        
        l1 = lstrlen(ByVal pStringDest): l2 = lstrlen(ByVal pStringAppended)
        StrCat = SysAllocStringLen(0, l1 + l2)
        
        If StrCat = 0 Then Exit Function
        
        lstrcpyn ByVal StrCat, ByVal pStringDest, l1 + 1
        lstrcat ByVal StrCat, ByVal pStringAppended
        
    End Function
    

    После извлечения файлов вызывается функция ExecuteProcess которая запускает выполнение команд используя функцию ShellExecuteEx:
    ' // Execution command process
    Function ExecuteProcess() As Boolean
        Dim index       As Long:                Dim bItem       As BinExecListItem
        Dim pPath       As Long:                Dim pErrMsg     As Long
        Dim shInfo      As SHELLEXECUTEINFO:    Dim pTempString As Long
        Dim pItem       As Long:                Dim status      As Long
    
        ' // Set pointer and size
        shInfo.cbSize = Len(shInfo)
        pItem = pExecutesTable
        
        ' // Go thru all items
        For index = 0 To ProjectDesc.execListDescriptor.dwNumberOfItems - 1
        
            ' // Copy item
            CopyMemory bItem, ByVal pItem, ProjectDesc.execListDescriptor.dwSizeOfItem
            
            ' // Set pointer to next item
            pItem = pItem + ProjectDesc.execListDescriptor.dwSizeOfItem
            
            ' // Normalize path
            pPath = NormalizePath(pStringsTable + bItem.ofstFileName, 0)
            
            ' // Fill SHELLEXECUTEINFO
            shInfo.lpFile = pPath
            shInfo.lpParameters = pStringsTable + bItem.ofstParameters
            shInfo.fMask = SEE_MASK_NOCLOSEPROCESS Or SEE_MASK_FLAG_NO_UI
            shInfo.nShow = SW_SHOWDEFAULT
            
            ' // Performing...
            status = ShellExecuteEx(shInfo)
            
            ' // If error occurs show notification (retry, abort, ignore)
            Do Until status
                
                If pErrMsg Then SysFreeString pErrMsg: pErrMsg = 0
                
                ' // Ignore error
                If bItem.dwFlags And EF_IGNOREERROR Then
                    Exit Do
                End If
                            
                pTempString = GetString(MID_ERROREXECUTELINE)
                pErrMsg = StrCat(pTempString, pPath)
                
                SysFreeString pTempString: pTempString = 0
                
                Select Case MessageBox(0, pErrMsg, 0, MB_ICONERROR Or MB_SYSTEMMODAL Or MB_CANCELTRYCONTINUE)
                Case MESSAGEBOXRETURN.IDCONTINUE: Exit Do
                Case MESSAGEBOXRETURN.IDTRYAGAIN
                Case Else: GoTo CleanUp
                End Select
    
                status = ShellExecuteEx(shInfo)
                
            Loop
            
            ' // Wait for process terminaton
            WaitForSingleObject shInfo.hProcess, INFINITE
            CloseHandle shInfo.hProcess
            
        Next
        
        ' // Success
        ExecuteProcess = True
        
    CleanUp:
    
        If pTempString Then SysFreeString pTempString
        If pErrMsg Then SysFreeString pErrMsg
        If pPath Then SysFreeString pPath
        
    End Function
    

    Эта функция похожа на предыдущую за исключением того что здесь используется функция ShellExecuteEx вместо извлечения. Обратите внимание что каждая операция выполняется синхронно, т.е. каждый вызов процедуры ShellExecuteEx ждет окончания выполнения команды.
    Если предыдущая функция выполнилась успешно тогда вызывается функция RunProcess которая подготовливает данные для исполнения главного исполняемого файла из памяти:
    ' // 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 функцию:
    ' // 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 которая сплайсит все функции в шеллкоде:
    ' // 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 функции, теперь мы должны использовать наши "трамплины":
    ' // 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:
    ' // 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. Эта функция размещает все заголовки в выделенную память, извлекает информацию о всех секциях и копирует все данные в выделенную для них память. Если какая-либо секция имеет неинициализированные данные то этот регион заполняется нулями:
    ' // 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:
    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 которая описывает все загруженные модули в процессе:
    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:
    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 которая описывает базовую информацию о процессе:
    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 и повторяет заново пока структура не будет извлечена:
    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:
    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 это означает что список структур также окончен.
    ' // 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:
    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 заканчивается структурой где все поля заполнены нулями:
    ' // 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 с определенными атрибутами памяти:
    ' // 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).

    Исходники.
  • 14 сен 16, 21:18    [19666256]     Ответить | Цитировать Сообщить модератору
    Между сообщениями интервал более 1 года.
     Re: Загрузчик, шеллкод, без рантайма...  [new]
    kealon(Ruslan)
    Member

    Откуда: Нижневартовск
    Сообщений: 4346
    ATM-TURBO 2,

    занятно, а зачем вам понадобилась такая вещь?
    4 фев 19, 12:44    [21801190]     Ответить | Цитировать Сообщить модератору
     Re: Загрузчик, шеллкод, без рантайма...  [new]
    Roman Mejtes
    Member

    Откуда: г. Пермь
    Сообщений: 3226
    kealon(Ruslan),

    некромант.

    для настоящего программиста Барсика нет такого вопроса "Зачем?". Потому, что я могу.
    99% кода на VB6 написано непонятно зачем и непонятно для чего.
    По большей части для самоутверждения в том, что Basic (и всегда его порождения) может всё, как и любой другой язык
    Подобная картина наблюдалась раньше и в QB, и наверное в более ранних версиях этого языка.
    Придумать очередной хак или сделать возможность работы с PNG, которая нативно не поддерживается, пожалуйста, достаточно глянуть в этом форуме соответствующую тему. Многопоточность, легко, то есть нет.
    И так везде и всюду, когда дело касается Basic'а. Кто во что горазд.
    Программирование в VB6 это всегда превозмогания, возможно именно этого и хотят те, кто на нём работает :D
    4 фев 19, 14:11    [21801288]     Ответить | Цитировать Сообщить модератору
     Re: Загрузчик, шеллкод, без рантайма...  [new]
    ATM-TURBO 2
    Member

    Откуда:
    Сообщений: 165
    kealon(Ruslan)
    занятно, а зачем вам понадобилась такая вещь?

    В самом начале написано:
    ATM-TURBO 2
    ВСЕ ЭТО БЫЛО СДЕЛАНО ДЛЯ ЭКСПЕРИМЕНТАЛЬНЫХ ЦЕЛЕЙ ДЛЯ ТОГО ЧТОБЫ ПРОВЕРИТЬ ТАКУЮ ВОЗМОЖНОСТЬ НА VB6.


    Roman Mejtes
    99% кода на VB6 написано непонятно зачем и непонятно для чего.

    Откуда статистика?

    Roman Mejtes
    Придумать очередной хак или сделать возможность работы с PNG, которая нативно не поддерживается, пожалуйста, достаточно глянуть в этом форуме соответствующую тему.

    Что значит нативно? Нативно C поддерживает возможность работы с PNG?

    Roman Mejtes
    Многопоточность, легко, то есть нет.

    Легко.

    Roman Mejtes
    Программирование в VB6 это всегда превозмогания, возможно именно этого и хотят те, кто на нём работает :D

    Очередной "превозмогальщик".
    4 фев 19, 16:59    [21801451]     Ответить | Цитировать Сообщить модератору
     Re: Загрузчик, шеллкод, без рантайма...  [new]
    kealon(Ruslan)
    Member

    Откуда: Нижневартовск
    Сообщений: 4346
    ATM-TURBO 2
    kealon(Ruslan)
    занятно, а зачем вам понадобилась такая вещь?

    В самом начале написано:
    ATM-TURBO 2
    ВСЕ ЭТО БЫЛО СДЕЛАНО ДЛЯ ЭКСПЕРИМЕНТАЛЬНЫХ ЦЕЛЕЙ ДЛЯ ТОГО ЧТОБЫ ПРОВЕРИТЬ ТАКУЮ ВОЗМОЖНОСТЬ НА VB6.
    т.е. первоначально был плюсовой вариант?
    4 фев 19, 17:44    [21801481]     Ответить | Цитировать Сообщить модератору
     Re: Загрузчик, шеллкод, без рантайма...  [new]
    ATM-TURBO 2
    Member

    Откуда:
    Сообщений: 165
    kealon(Ruslan)
    т.е. первоначально был плюсовой вариант?

    Нет. Все сразу писалось на VB6.
    4 фев 19, 17:52    [21801487]     Ответить | Цитировать Сообщить модератору
     Re: Загрузчик, шеллкод, без рантайма...  [new]
    Eolt
    Member

    Откуда:
    Сообщений: 1402
    kealon(Ruslan)
    ATM-TURBO 2,

    занятно, а зачем вам понадобилась такая вещь?


    Думаю это из разряда спортивного программирования.
    4 фев 19, 17:54    [21801490]     Ответить | Цитировать Сообщить модератору
     Re: Загрузчик, шеллкод, без рантайма...  [new]
    Shocker.Pro
    Member

    Откуда: ->|<- :адуктО
    Сообщений: 20206
    Eolt
    Думаю это из разряда спортивного программирования.
    раскрыт секрет!
    автор
    За последние 17 лет российские программисты 11 раз выигрывали мировое первенство по программированию АСМ ICPC, а в последние пять лет кубки уезжали в Россию ежегодно.
    4 фев 19, 17:56    [21801492]     Ответить | Цитировать Сообщить модератору
    Все форумы / Visual Basic Ответить