Загрузчик, шеллкод, без рантайма...

Автор обещает много интересных штучек.

Модератор: The trick

The trick
Постоялец
Постоялец
 
Сообщения: 774
Зарегистрирован: 26.06.2010 (Сб) 23:08

Загрузчик, шеллкод, без рантайма...

Сообщение The trick » 14.09.2016 (Ср) 18:34


Всем привет! Когда-то давно я исследовал 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
UA6527P

The trick
Постоялец
Постоялец
 
Сообщения: 774
Зарегистрирован: 26.06.2010 (Сб) 23:08

Re: Загрузчик, шеллкод, без рантайма...

Сообщение The trick » 14.09.2016 (Ср) 18:36

Эта функция похожа на предыдущую за исключением того что здесь используется функция 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).
Вложения
VBLoader.zip
(176.44 Кб) Скачиваний: 225
UA6527P


Вернуться в The trick

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и гости: 4

    TopList