Автор Анна Евкова
Преподаватель который помогает студентам и школьникам в учёбе.

Обзор существующих подходов обнаружения ВПО и способов противодействия им

Содержание:

Введение

В настоящее время получили распространение как глобальные, так и локальные сети. Большинство организаций, или людей в той или иной степени ими пользуется. С одной стороны, это удобно и практично, но с другой порождает проблемы, связанные с защитой информации. Важная информация для компании, или физического лица может быть потеряна, украдена и использована злоумышленниками.

Учитывая, что даже с самой совершенной защитой компьютерные системы нельзя назвать абсолютно неуязвимыми, то защита ценных данных, является одной из самых актуальных задач на сегодняшний день. В этих условиях необходимым становится своевременное обнаружение информационной атаки, и применение комплексных мер, направленных на её локализацию и предотвращение. На данный момент существует достаточно много антивирусных программ, решающих эти задачи. Однако антивирусные программы не совершенны. Поэтому актуально, на данный момент, создание программы для анализа эффективности работы антивирусных программ.

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

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

ГЛАВА 1. ОБЗОР СУЩЕСТВУЮЩИХ ПОДХОДОВ ОБНАРУЖЕНИЯ ВПО И СПОСОБОВ ПРОТИВОДЕЙСТВИЯ ИМ

1.1 Методы обнаружения вредоносных программ

Начать обзор метода обнаружения ВПО основанного на сигнатурах стоит с уточнения самого термина сигнатура. Классическая вирусная сигнатура — это непрерывная последовательность байтов, характерная для той или иной вредоносной программы. То есть она содержится в этом вредоносном файле и не содержится в чистых файлах. В наши дни с помощью таких классических сигнатур определить вредоносный файл достаточно проблематично — их создатели используют различные техники для того, чтобы запутать следы.

Поэтому современные антивирусы используют значительно более продвинутые методы. В силу того, что термин сигнатура появился еще в 80е годы 20го века многие пользователи считают технологию устаревшей, однако это не так. Антивирусная запись — это запись, а стоящая за ней технология может быть, как классической, простенькой, так и суперсовременной, и навороченной, нацеленной на детектирование самых запутанных и высокотехнологичных вредоносных файлов или даже целых семейств ВПО. Поэтому важно иметь в виду, что само по себе слово «сигнатура» на самом деле не говорит ничего о продвинутости или примитивности, однако все это продолжают по старинке называть сигнатурами. Теперь, имея представление о том, что такое сигнатура, вернемся к самому методу обнаружения. Обнаружение, основанное на сигнатурах ни что иное как сравнение исследуемого файла или пакета с записями в антивирусных базах – сигнатурами.

В случае соответствия какого-либо участка кода просматриваемого файла сигнатуре объект детектируется как вредоносный. Проверка объекта зачастую происходит при непосредственной работе с ним, например, при создании, открытии/закрытии файла, его изменении или пересылке посредствам компьютерных сетей. Не смотря на свой возраст, данный метод до сих пор является актуальным и одним из наиболее популярных, однако стоит учитывать, что для успешного использования этого подхода детектирования ВПО необходимо постоянное обновление антивирусных баз.

Статический программный анализ приложения – еще один из методов обнаружения ВПО. Данный метод заключается в анализе ПО без непосредственного запуска исследуемой программы. Зачастую подвергается анализу какая-либо версия исходного кода, однако анализ некоторых форм объектного кода также возможен. Хочется отметить, что наравне со статическим программным анализом существует ручной анализ – непрограммный. Несмотря на схожесть этих методов не стоит их путать. Главное отличие заключается в том, что статический программный анализ осуществляется при помощи специализированного ПО в то время как ручной анализ ни что иное как систематическая проверка исходного кода разработчиком.

Динамический программный анализ – метод обнаружения ВПО, выполняемый в процессе запуска приложений в среде с реальным или виртуальным процессором. Стоит отметить, что для повышения эффективности динамического анализа целевое приложение должно запускаться с достаточным количеством тестовых входных данных для инициации работы различных участков алгоритма.

Для понимания того, что из себя представляет метод обнаружения ВПО с использованием песочницы необходимо выяснить, а что же собственно такое песочница. Песочница представляет собой безопасную среду для разделения запущенных программ, которая обычно используется для запуска не тестированного и недостоверного программного кода из непроверенных источников: производителей, пользователей или веб-сайтов для того, чтобы избежать вреда для операционной системы.

Теперь имея представление о том, что такое песочница становится очевиден принцип работы метода обнаружения ВПО с ее использованием. Он заключается в запуске тестируемого приложения в изолированной среде для анализа его поведения.

Еще один из наиболее популярных в наши дни методов обнаружения ВПО - Эвристический анализ. Он используется для детектирования ранее неизвестных компьютерных вирусов, а также новых вариантов ВПО, уже существующих на просторах интернета. Данный вид анализа, по сути, представляет собой экспертную систему, которая определяет степень восприимчивости системы по отношению к конкретной угрозе/риску при помощи различных правил принятия решений или весовых методов.

Многокритериальный анализ – один из способов «взвешивания», который отличается от статического анализа, опирающегося на доступную информацию/статистику.

Метод обнаружения ВПО с использованием энтропии заключается в подсчете этой самой энтропии для исследуемого объекта. Энтропия представляет собой меру хаотичности информации, а это ни что иное как мера непредсказуемости некоторой системы. Таким образом, посчитав энтропию можно проанализировать ее отклонение от нормы и сделать соответствующие выводы.

1.2. Способы борьбы с методами обнаружения угроз

Первое, что приходит на ум и о чем сразу хочется упомянуть это – Обфускация. Все гениальное просто и яркий тому пример – обфускация кода. Если стоит задача не быть обнаруженным, то нет ничего проще и логичней, чем просто взять и «запутать» код с сохранением его функциональности. Сразу становится очевидно, что обнаружение с использованием сигнатур не способно оказать достойного сопротивления обфусцирофанному объекту. Статистический и ручной анализ также существенно осложняются, что также является плюсом для этого метода. Подводя итог, мы имеем метод, который затрудняет статический анализ и изменяет хеш-сигнатуры бинарного файла. Данная техника позволяет обойти приличное число антивирусов, но конечный успех зависит от глубины обфускации.

Использование упаковщика – следующий метод противодействия обнаружению. Его суть, как и в случае с обфускацией, довольно проста. Упаковщик сжимает исходный файл и объединяет сжатые данные с кодом распаковки в один исполняемый файл. Когда сжатый исполняемый файл запускается, вначале программа распаковки воссоздает первоначальный код из сжатого файла, после чего происходит выполнение. Стоит заметить, что при таком случае защиты от обнаружения для эффективной работы антивирусам необходимо, сканируя упакованный образец ВПО, определить алгоритм сжатия и распаковать исполняемый файл. В свою очередь злоумышленник будет стремиться сделать все, чтоб максимально осложнить жизнь антивирусным программам. Поскольку упакованные файлы сложнее проанализировать, отсюда следует немаловажное требование к самому упаковщику – непроприетарность. Подводя итог, мы имеем еще один метод, который значительно усложняет статистический анализ, а при должной подготовке (наличие «незасвеченного» упаковщика) оставляет метод обнаружения с использованием сигнатур без единого шанса на обнаружение. Однако стоит отметить тот факт, что при упаковке объекта значительно возрастает его энтропия, что делает объект довольно подозрительным для метода обнаружения ВПО с использованием энтропии.

Мы не будем подробно останавливаться на крипторах, пермутации и генерации мусора, а лишь упомянем об этих методах. Каждый из них имеет право на жизнь, но выделять их как самостоятельные методы в этой работе нецелесообразно, так как генерация мусора и пермутация напрямую относятся к методу обфускации кода, а использование крипторов ни что иное, как упаковка с использованием алгоритмов шифрования вместо алгоритмов сжатия. Для уточнения скажем, что генерация мусора, как следует из названия, метод усложняющий анализ кода посредствам добавления «ненужных» инструкций, а пермутация – это перестановка логических блоков в теле ВПО с целью повысить нагрузку на движок антивируса.

Следующий метод защиты от обнаружения является симбиозом вышеперечисленных методов. Он заключается в том, что для упаковки ВПО каждый раз используется новый ключ, чаще всего зависимый от каких-либо параметров атакуемого объекта, например, от объема файла. Также этот метод допускает использование технологии обфускации, генерации мусора и пермуатации. В итоге мы имеем метод, использование которого позволяет бороться почти со всеми вышеперечисленными методами обнаружения ВПО. Постоянно меняющаяся энтропия, изменения сигнатур и приемы обфускации создают серьезные проблемы в задаче обнаружения, однако не все способы обнаружения остались беззащитны. Все также опасным остается эвристический метод, а также существует вероятность выявления сигнатур декриптора, что позволит посредствам добавления их в базы антивирусов сделать метод обнаружения с использованием сигнатур снова эффективным.

Учитывая слабости полиморфизма, был придуман еще более надежный метод защиты от обнаружения. Им стала эволюционировавшая версия полиморфизма – метаморфизм. Стремясь избавиться от слабости в виде постоянных сигнатур декриптора, было найдено решение – генерация нового кода декриптора в каждом новом поколении вирусов. ВПО подобного типа получило название MtE-вирусы.

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

Имея существенную угрозу в виде песочницы, перед злоумышленниками стоял вопрос о методе противодействия. Таким методом стала «антипесочница». Идея метода, следующая: при попадании в систему необходимо было определить является ли она виртуальной. Если выяснялось, что имеет место виртуализация, проявление деструктивных свойств откладывалось либо по заданному таймеру, либо до выхода из виртуальной среды. В итоге, можно сделать вывод о том, что и наличие песочницы не гарантирует обнаружения ВПО. Метод «антипесочницы» почти не имеет ценности как самостоятельный алгоритм, зато является очень важным элементом при построении комплексной системы защиты от обнаружения.

1.3. Обзор существующих антивирусных программ и предпосылки к созданию приложения для их тестирования

Когда речь заходит об уменьшении вероятности детектирования, первое, что приходит на ум: упаковщики и обфускация кода. Эти инструменты и техники все еще позволяют оставаться незаметным для большинства антивирусных продуктов. Кибербезопасность развивается семимильными шагами и как заявляют производители антивирусного ПО большинство существующих методов и программ устарели и не могут сделать полностью недетектируемый вредонос. Встает вопрос, а так ли это на самом деле? Поскольку, не смотря на быстрые темпы развития, антивирусные программы не могут гарантировать абсолютную защищенность, встает вопрос о необходимости определения уязвимых мест, для последующей их доработки вплоть до полного устранения.

Антивирус лаборатории Касперского – Internet Security 2017

По описанию, представленному производителем на официальном сайте продукта - Kaspersky Internet Security 2017 — это надежная защита для компьютера. В состав продукта помимо базовой входит защита интернетсоединения, веб-камеры, платежей в интернете, детей от нежелательной информации, а также возможность поиска и установки обновлений программ и удаления неиспользуемых программ. Выделим основной функционал, который предоставляет данный продукт:

  • Выполняет защиту интернет-соединений благодаря встроенной виртуальной сети VPN
  • Производит защиту финансовых и банковских операций
  • Имеет встроенную функцию родительского контроля, благодаря которой можно ограничить доступ ребенка к нежелательному контенту

Avast Free Antivirus 2017

Приложение распространяется бесплатно и содержит обширную вирусную базу, благодаря чему настолько популярно не только среди пользователей России, но и во всем мире. В Avast встроены модули, которые не предусмотрены даже в некоторых платных программах. Выделим основной функционал, который предоставляет данный продукт:

  • Защита компьютера от вирусов
  • Безопасная работа в сети Интернет
  • Защита проведения финансовых и банковских операций
  • Проверка и контроль безопасности сети Wi-Fi
  • Поиск ненужных плагинов и расширений в веб-браузере

AVG Internet Security 2017

Платное приложение, подкупающее своим быстродействием. Тем не менее, имеет ярко выраженный минус - возникновение конфликтов при взаимодействии с другими антивирусными приложениями, например, с Windows Defender. Выделим основной функционал, который предоставляет данный продукт:

  • Выполняет защиту личной информации
  • Контролирует работу в сети интернет, предотвращая вирусные атаки
  • Позволяет осуществлять контроль над устройствами через интернет-соединение, которые защищены данным антивирусом Windows Defender

Встроенный в последние версии ОС Windows защитник от компании Microsoft, созданный для того, чтобы удалять, помещать в карантин или

предотвращать появление spyware-модулей в операционных системах Microsoft Windows. Выделим основной функционал, который предоставляет данный продукт:

  • Отслеживание подозрительных изменений в определённых сегментах системы в режиме реального времени
  • Удаление установленных приложений ActiveX
  • Возможность отправлять сообщения о подозрительных объектах в Microsoft для определения его возможной принадлежности к spyware

1.4. Постановка задачи

Таким образом можно сделать следующие выводы. На данный момент необходимым становится своевременное обнаружение ВПО, и применение комплексных мер, направленных на защиту от деструктивных свойств подобных объектов. Этой задачей занимаются антивирусные программы. На сегодняшний день остро встает вопрос о возможностях антивирусных программ. Кроме самих разработчиков антивирусного ПО, которым не выгодно выставлять минусы своей продукции на показ, если о них вообще известно, вопросом безопасности персональных данных в достаточных объемах почти никто не занимается.

ГЛАВА 2. РАЗРАБОТКА И ТЕСТИРОВАНИЕ ПО

2.1. Разработка

Прежде чем начать работу необходимо оговорить функционал разрабатываемого приложения. Главными требованиями к предлагаемому ПО функционалу определим реализацию упаковки исполняемых файлов и обфускацию бинарного кода файла. Почему именно такие требования? Вопервых, это два наиболее исторически возрастных подхода к сокрытию ВПО от АС, а значит и наиболее изученных, следовательно, борьба с данными методами противодействия обнаружению является минимальной планкой для современных АС, во-вторых именно эти способы являются наиболее универсальными, те не требуется реализация под конкретно взятый пример, и в-третьих относительно просты в реализации по сравнению с более современными способами противодействия обнаружению. Разработка приложения будет идти в два этапа: первый – упаковщик, второй – распаковщик. Схема работы разрабатываемого ПО приведена на рис.2.1.

Рисунок

Начать разработку стоит с понимания того, что такое упаковка и как это работает. (см.рис.2.1) Упаковка ни что иное, как уменьшение размера файла с сохранением его работоспособности.

Последовательность действий при упаковке следующая:

1. Считывание бинарного кода приложения в специально отведенный для этого буфер(массив) с последующим сжатием любым удобным для тех или иных целей алгоритмом.

2. Написание распаковщика, который впоследствии будет превращать упакованный на первом этапе массив в исходный байт-код и запускать.

3. Добавление полезной нагрузки в виде полученного на первом этапе массива к распаковщику.

Выглядит довольно просто для понимания, поэтому останавливаться на алгоритме работы упаковщика подробнее не имеет смысла. Приступим непосредственно к реализации, которая как мы поняли будет разбита на 2 этапа – упаковка файла и написание распаковщика. Начнем конечно же с написания упаковщика. Так как нашей конечной целью является обман АС необходимо выбрать такой алгоритм, который будет иметь достаточно высокую степень сжатия при довольно-таки быстрой распаковке. Остановимся на алгоритме LZO, чья эффективность не многим уступает, а распаковщик даже превосходит по скорости zip.

Реализовывать алгоритм LZO не имеет большого смысла, поэтому возьмем готовый реализованный алгоритм, в моем случае версии 2.06. Также при разработке проекта будем использовать готовую библиотеку для работы с PE-файлами. Первое на что следует обратить внимание – структура PE-файла. В силу особенностей строения исполняемых файлов в windows мы не можем просто сжать байт-код и при распаковке получить приложение с исходным функционалом. Со структурой строения PE-файла надо считаться, поэтому нам не остается ничего другого как разобраться в том, как устроены PEфайлы. На рис.2.2 приведено схематическое устройство PE-файла. Забегая вперед хочется отметить, что сохранение именно такой структуры является залогом успешной распаковки.

Рисунок

Итак, что же нам нужно учесть для успешной распаковки. Первое, на что следует обратить внимание – DOS заголовок. Для нашей работы не обязательно подробное изучение этой структуры, достаточно лишь упомянуть, что первое поле e_magic находится по смещению 0 от начала файла и всегда равно “MZ”, а последнее поле e_ifnew содержит в себе смещение PE-заголовка относительно начала файла. DOS-заглушка не представляет для нас никакого интереса, зачастую просто заполнена нулями, поэтому идем дальше. По смещению e_ifnew от начала файла располагается PE – сигнатура. Ее отличительной характеристикой являются неизменные для PE-файлов 4 байта, чьи значения ’P’, ‘E’, 0, 0. Далее идет PE-заголовок или заголовок файла, в нем указываются базовые характеристики файла. Затем опциональный заголовок, он является обязательным, содержит формат файла и необходимую для загрузки файла информацию. Заголовок секций содержит таблицы секций с соответствующими именами, размерами и адресами.

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

В этом нам поможет энтропия. Реализуем проверку энтропии сразу после открытия файла. Считается, что нормальная энтропия PE-файлов принимает значения до 6.8. Именно с этим значением мы и будем сравнивать полученную энтропию входного файла. Также было бы неплохо проверить не является ли входной файл .NET. Работать с такими файлами мы не будем. Алгоритм работы упаковщика можно описать следующей последовательностью действий:

  • Считываем все секции файла
  • Компоновка данных в один буфер с последующей упаковкой
  • Расположение буфера в новой секции
  • Удаление старых секций

Проиллюстрируем вышеупомянутую последовательность действий. (рис.2.3)

Рисунок

Ближайший план действий понятен, приступим непосредственно к реализации. Для начала напишем структуру, для хранения всех параметров существовавших секций оригинального файла, чтобы впоследствии распаковщик мог их восстановить.

Эту структуру мы будем записывать в любое место упакованного файла для каждой секции, а код распаковщика будет эти структуры считывать. В этой структуре будет храниться вся необходимая информация для восстановления секций PE-файла. Кроме того, нам пригодится структура, которая хранит различную полезную информацию об оригинальном файле, которая также понадобится распаковщику. Теперь мы можем приступать к считыванию секций файла. Как только мы это сделаем, нам ничто не помешает перейти непосредственно к упаковке.

Создаем два буфера - packed_sections_info и raw_section_data. Первый буфер хранит идущие подряд структуры packed_section, создаваемые и заполняемые нами для всех имеющихся в PE-файле секций. Второй хранит непосредственно данные всех секций, собранные вместе. Эти данные после распаковки мы без проблем сможем разделить и разложить по секциям заново, потому что информация о размере файловых данных секций хранится в первом буфере и будет доступна распаковщику. Далее будем упаковывать полученные буферы raw_section_data и packed_sections_info. Теперь мы можем создать новую секцию PE-файла, в которой и разместим наши упакованные данные.

Стоит уточнить, что имя секции безусловно должно быть «.rsrc» так как именно это имя зарезервировано под секцию ресурсов, в противном случае они не будут подгружены. И непосредственно сама упаковка. Остается только удалить уже ненужные нам секции PE-файла и добавить в него нашу новую секцию. Немного пояснений о том, что конкретно делаем. Начнем с того, что мы определили виртуальный адрес самой первой секции в PE-файле. Затем, зная выровненный виртуальный размер секций, путем несложных вычислений считаем виртуальный суммарный размер всех секций. Далее мы удалили все существующие секции PE-файла.

После этого добавили нашу секцию в PE-файл и получили ссылку на добавленную секцию с пересчитанными адресами и размерами. Именно с этой ссылкой мы и будем работать дальше. Затем мы меняем виртуальный размер добавленной секции на общий размер всех ранее существовавших секций. Виртуальный адрес добавленной секции будет вычислен автоматически по умолчанию. Но мы должны оставить себе достаточное количество памяти, чтобы потом в нее распаковать все исходные секции. Нас это не очень устраивает, отсюда необходимость в том, чтобы область в памяти, занимаемая нашей секцией, полностью совпала с областью, которую занимали все секции оригинального файла.

Функционал библиотеки для работы с PE-файлами позволяет явно указать виртуальный адрес секции, если она будет первой в файле. Это как раз то, что нам нужно. Теперь нам понадобится еще одна секция. В ней будет располагаться распаковщик. Итак, подытожим проделанные действия. Блок-схема работы упаковщика представлена на рисунке 2.4.

Рисунок

Затем удаляем все используемые директории из заголовков, кроме директории импорта. И сохраняем упакованный файл. Структурная схема полученного упакованного файла приведена на рис.2.5.

Рисунок

Для работы распаковщика нам потребуются две WinAPI-функции: LoadLibraryA и GetProcAddress. Встает вопрос о том, куда загрузчик будет записывать адреса этих двух функций. Решением этой проблемы станет расширение вышеупомянутой структуры packed_file_info. Теперь необходимо создать новую таблицу импорта. В этом нам сильно поможет библиотека для работы с PE-файлами. Поясним, как именно мы это делаем. Для каждой импортируемой библиотеке в PE-файлах создается структура, полями которой являются таблицы IAT и Orignal IAT.

В первую загрузчик записывает адреса импортируемых функций, вторая же хранит их имена или ординалы. Мы будем использовать одну таблицу. Вызов settings.save_iat_and_original_iat_rvas настраивает пересборщик таким образом, что он не будет создавать свои собственные IAT и Original IAT, а запишет все по тем адресам, которые уже указаны в каждой библиотеке. Далее мы просто пересобираем таблицу импортов. Теперь преступаем непосредственно к распаковщику. Стоит обратить внимание на то, что функция объявлена как naked. Это говорит компилятору о том, что не нужно создавать для этой функции пролог и эпилог (стековый фрейм) автоматически. Сделано это для того, чтобы можно было увеличить количество данных, выделяемых на стеке с мыслью о том, что локальных переменных много, и стандартных 128 байт может не хватить. Идем дальше и добавим в тело функции 2 переменные: Первая будет содержать действительный адрес загрузки образа, а вторая - относительный адрес самой первой секции. Далее нам необходимо получить значения полей структуры packed_file_info, которые заполняет загрузчик.

Из этих полей мы получаем адреса функций LoadLibraryA и GetProcAddress. Затем необходимо решить проблему со вставкой кода распаковщика в упакованный файл. Решение следующее - открываем получающийся после компиляции распаковщика файл, считываем данные из его единственной секции (по сути - код) и преобразовываем его в h-файл, на который мы сможем ссылаться в проекте с упаковщиком. Решение было взято уже реализованным, поэтому останавливаться на нем мы не будем. Дело осталось за малым, дописать тело распаковщика.

Итак, распаковщик настраивается и добавляется в упакованный файл. На этом разработка упаковщика закончена. Для проверки работоспособности достаточно добавить полезную нагрузку, например, в виде MessageBox’a в код распаковщика и запаковать, например, сам упаковщик. Однако окончание разработки не означает, что мы сможем пользоваться этим упаковщиком в дальнейшем. Он слишком прост и больше подходит под определение «работающего прототипа». Однако при помощи уже разработанных решений, можно доработать этот упаковщик. Именно доработанным упаковщиком мы и будем пользоваться в дальнейшем.

2.2. Тестирование

Для проверки работоспособности нашего упаковщика, достаточно добавить полезную нагрузку в код распаковщика в виде простенького MessageBox’a как уже было предложено выше, упаковать сам упаковщик и запустить упакованный файл. В итоге результатом запуска упакованного приложения является окошко с надписью: «Hello!». (рис.2.6)

Рисунок

ЗАКЛЮЧЕНИЕ

Результатом проведённой работы стало создание программы упаковщика, с помощью которой были произведены модификации нескольких образцов ВПО с целью тестирования работоспособности АС.

В ходе работы стало известно, что в современных условиях необходимо своевременное обнаружение ВПО и применение комплексных мер, направленных на предотвращение последствий их работы. Именно этим и занимаются Антивирусы.

Изучив состояние вопроса на данный момент, становится очевидным необходимость создания ПО для анализа уязвимостей АС. Примером такого ПО является разработанный в данной работе упаковщик. Принцип и результаты работы разработанной программы наглядно и подробно продемонстрированы.

СПИСОК ИСПОЛЬЗУЕМЫХ ИСТОЧНИКОВ

  1. Касперски К. Техника отладки программ без исходных текстов / К. Касперски. – Санкт-Петербург: БХВ-Петербург, 2005. – 823 с.
  2. Подпружников Ю. В. Классификация методов обнаружения неизвестного вредоносного программного обеспечения / Современные тенденции технических наук: материалы Междунар. науч. конф. (г. Уфа, октябрь 2011 г.). — Уфа: Лето, 2011. — С. 22-25. — URL https://moluch.ru/conf/tech/archive/5/1133/ (дата обращения: 13.01.2019).
  3. . Касперский Е. Компьютерное zловредство / Е. Касперский. – Санкт-Петербург: Питер, 2007. – 208 с.
  4. Денисов Т.В. «Антивирусная защита» // Мой Компьютер - №4 1999 г.
  5. Защита информации. Конфидент. - 1998. - №1. - 96 с.
  6. Peering Inside the PE: A Tour of the Win32 Portable Executable File Format — URL https://msdn.microsoft.com/en-us/library/ms809762.aspx (дата обращения: 22.01.2019).
  7. Касперски К. Компьютерные вирусы изнутри и снаружи / К.Касперски. – Санкт-Петербург: Питер, 2006. – 526 с.
  8. Касперски К. Записки исследователя компьютерных вирусов / Касперски К. – Санкт-Петербург: Питер, 2005. – 316 с.
  9. Ташков П.А. Защита компьютера. Сбои, ошибки и вирусы / Ташков П.А. – Санкт-Петербург: Питер, 2010. – 288 с.
  10. PE (Portable Executable): На странных берегах — URL https://habr.com/post/266831/ (дата обращения: 11.01.2019).
  11. Донцов Д. Как защитить компьютер от ошибок, вирусов, хакеров / Донцов Д. – Санкт-Петербург: Питер, 2008. – 160 с.

ПРИЛОЖЕНИЕ 1: ЛИСТИНГ РЕАЛИЗОВАННОГО ПРИЛОЖЕНИЯ

Packer: main.cpp

#include <iostream>

#include <fstream>

#include <vector>

#include <string>

#include <boost/scoped_array.hpp>

//Заголовочный файл библиотеки для работы с PE-файлами

#include <pe_32_64.h>

//Заголовочный файл алгоритма LZO1Z999

#include "../../lzo-2.06/include/lzo/lzo1z.h"

//Заголовочный файл со структурами

#include "structs.h"

//Заголовочный файл с параметрами распаковщика

#include "../unpacker/parameters.h"

//Тело распаковщика (авто)

#include "unpacker.h"

//Директивы для линкования с собранными библиотеками PE и LZO

#ifndef _M_X64

#ifdef _DEBUG

#pragma comment(lib, "../../Debug/pe_lib.lib")

#pragma comment(lib, "../Debug/lzo-2.06.lib")

#else

#pragma comment(lib, "../../Release/pe_lib.lib")

#pragma comment(lib, "../Release/lzo-2.06.lib")

#endif

#else

#ifdef _DEBUG

#pragma comment(lib, "../../x64/Debug/pe_lib.lib")

#pragma comment(lib, "../x64/Debug/lzo-2.06.lib")

#else

#pragma comment(lib, "../../x64/Release/pe_lib.lib")

#pragma comment(lib, "../x64/Release/lzo-2.06.lib")

#endif

#endif

int main(int argc, char* argv[])

{

//имя файла, который мы хотим упаковать

if(argc != 2)

{

std::cout << "Usage: simple_pe_packer.exe PE_FILE" << std::endl;

return 0;

}

//Открываем файл - его имя хранится в массиве argv по индексу 1

std::ifstream file(argv[1], std::ios::in | std::ios::binary);

if(!file)

{

//Если открыть файл не удалось - сообщим и выйдем с ошибкой

std::cout << "Cannot open " << argv[1] << std::endl;

return -1;

}

try

{

//Пытаемся открыть файл как 32-битный PE-файл

pe32 image(file, false, false);

//Проверим, не .NET ли

if(image.is_dotnet())

{

std::cout << ".NEt image cannot be packed!" << std::endl;

return -1;

}

//Просчитаем энтропию секций файла, чтобы убедиться, что файл не упакован

{

std::cout << "Entropy of sections: ";

double entropy = image.calculate_entropy();

std::cout << entropy << std::endl;

if(entropy > 6.8)

{

std::cout << "File has already been packed!" << std::endl;

return -1;

}

}

//Инициализируем библиотеку сжатия LZO

if(lzo_init() != LZO_E_OK)

{

std::cout << "Error initializing LZO library" << std::endl;

return -1;

}

std::cout << "Reading sections..." << std::endl;

//Получаем список секций PE-файла

const pe_base::section_list& sections = image.get_image_sections();

if(sections.empty())

{

//Если у файла нет ни одной секции, нам нечего упаковывать

std::cout << "File has no sections!" << std::endl;

return -1;

}

//Структура базовой информации о PE-файле

packed_file_info basic_info = {0};

//Получаем и сохраняем изначальное количество секций

basic_info.number_of_sections = sections.size();

//Запоминаем относительный адрес и размер

//оригинальной таблицы импорта упаковываемого файла

pe32 image(file, false, false);

//Проверим, не .NET ли

if(image.is_dotnet())

{

std::cout << ".NEt image cannot be packed!" << std::endl;

return -1;

}

//Просчитаем энтропию секций файла, чтобы убедиться, что файл не упакован

{

std::cout << "Entropy of sections: ";

double entropy = image.calculate_entropy();

std::cout << entropy << std::endl;

if(entropy > 6.8)

{

std::cout << "File has already been packed!" << std::endl;

return -1;

}

}

//Инициализируем библиотеку сжатия LZO

if(lzo_init() != LZO_E_OK)

{

std::cout << "Error initializing LZO library" << std::endl;

return -1;

}

std::cout << "Reading sections..." << std::endl;

//Получаем список секций PE-файла

const pe_base::section_list& sections = image.get_image_sections();

if(sections.empty())

{

//Если у файла нет ни одной секции, нам нечего упаковывать

std::cout << "File has no sections!" << std::endl;

return -1;

}

//Структура базовой информации о PE-файле

packed_file_info basic_info = {0};

//Получаем и сохраняем изначальное количество секций

basic_info.number_of_sections = sections.size();

//Запоминаем относительный адрес и размер

//оригинальной таблицы импорта упаковываемого файла

basic_info.original_import_directory_rva = image.get_directory_rva(IMAGE_DIRECTORY_ENTRY_IMPORT);

basic_info.original_import_directory_size = image.get_directory_size(IMAGE_DIRECTORY_ENTRY_IMPORT);

//Запоминаем его точку входа

basic_info.original_entry_point = image.get_ep();

//Запоминаем общий виртуальный размер всех секций

//упаковываемого файла

basic_info.total_virtual_size_of_sections = image.get_size_of_image();

//Строка, которая будет хранить последовательно Приложение 1

//структуры packed_section для каждой секции

std::string packed_sections_info;

{

//Выделим в строке необходимое количество памяти для этих стркуткр

packed_sections_info.resize(sections.size() * sizeof(packed_section));

//"Сырые" данные всех секций, считанные из файла и слепленные воедино

std::string raw_section_data;

//Индекс текущей секции

unsigned long current_section = 0;

//Перечисляем все секции

for(pe_base::section_list::const_iterator it = sections.begin(); it != sections.end(); ++it, ++current_section)

{

//Ссылка на очередную секцию

const pe_base::section& s = *it;

{

//Создаем структуру информации

//о секции в строке и заполняем ее

packed_section& info

= reinterpret_cast<packed_section&>(packed_sections_info[current_section * sizeof(packed_section)]);

//Характеристики секции

info.characteristics = s.get_characteristics();

//Указатель на файловые данные

info.pointer_to_raw_data = s.get_pointer_to_raw_data();

//Размер файловых данных

info.size_of_raw_data = s.get_size_of_raw_data();

//Относительный виртуальный адрес секции

info.virtual_address = s.get_virtual_address();

//Виртуальный размер секции

info.virtual_size = s.get_virtual_size();

//Копируем имя секции (оно максимально 8 символов)

memset(info.name, 0, sizeof(info.name));

memcpy(info.name, s.get_name().c_str(), s.get_name().length());

}

//Если секция пустая, переходим к следующей

if(s.get_raw_data().empty())

continue;

//А если не пустая - копируем ее данные в строку

//с данными всех секций

raw_section_data += s.get_raw_data();

}

//Если все секции оказались пустыми, то паковать нечего!

if(raw_section_data.empty())

std::cout << "All sections of PE file are empty!" << std::endl;

return -1;

}

packed_sections_info += raw_section_data;

}

//Новая секция

pe_base::section new_section;

new_section.set_name(".rsrc");

//Доступна на чтение, запись, исполнение

new_section.readable(true).writeable(true).executable(true);

//Ссылка на сырые данные секции

std::string& out_buf = new_section.get_raw_data();

//Мы используем тип lzo_align_t для того, чтобы

//память была выровняна как надо

//(из документации к LZO)

boost::scoped_array<lzo_align_t> work_memory(new lzo_align_t[LZO1Z_999_MEM_COMPRESS]);

//Длина неупакованных данных

lzo_uint src_length = packed_sections_info.size();

//Сохраним ее в нашу структуру информации о файле

basic_info.size_of_unpacked_data = src_length;

//Длина упакованных данных

//(пока нам неизвестна)

lzo_uint out_length = 0;

//Необходимый буфер для сжатых данных

//(длина исходя из документации к LZO)

out_buf.resize(src_length + src_length / 16 + 64 + 3);

//Производим сжатие данных

std::cout << "Packing data..." << std::endl;

if(LZO_E_OK !=

lzo1z_999_compress(reinterpret_cast<const unsigned char*>(packed_sections_info.data()),

src_length,

reinterpret_cast<unsigned char*>(&out_buf[0]),

&out_length,

work_memory.get())

)

{

//Если что-то не так, выйдем

std::cout << "Error compressing data!" << std::endl;

return -1;

}

std::cout << "Packing complete..." << std::endl;

//Сохраним длину упакованных данных в нашу структуру

basic_info.size_of_packed_data = out_length;

out_buf.resize(out_length);

//Собираем буфер воедино, это и будут

//финальные данные нашей новой секции

out_buf =

//Данные структуры basic_info

std::string(reinterpret_cast<const char*>(&basic_info), sizeof(basic_info))

//Выходной буфер

+ out_buf;

//Проверим, что файл реально стал меньше

if(out_buf.size() >= src_length)

{

std::cout << "File is incompressible!" << std::endl;

return -1;

}

{

//Сначала получим ссылку на самую первую

//существующую секцию PE-файла

const pe_base::section& first_section = image.get_image_sections().front();

//Установим виртуальный адрес для добавляемой секции (читай ниже)

new_section.set_virtual_address(first_section.get_virtual_address());

//Теперь получим ссылку на самю последнюю

//существующую секцию PE-файла

const pe_base::section& last_section = image.get_image_sections().back();

//Посчитаем общий размер виртуальных данных

DWORD total_virtual_size =

//Виртуальный адрес последней секции

last_section.get_virtual_address()

//Выровненный виртуальный размер последней секции

+ pe_base::align_up(last_section.get_virtual_size(), image.get_section_alignment())

//Минус виртуальный размер первой секции

- first_section.get_virtual_address();

//Удаляем все секции PE-файла

image.get_image_sections().clear();

//Изменяем файловое выравнивание, если вдруг оно было

//больше, чем 0x200 - это минимально допустимое

//для выровненных PE-файлов

image.realign_file(0x200);

//Добавляем нашу секцию и получаем ссылку на

//уже добавленную секцию с пересчитанными адресами и размерами

pe_base::section& added_section = image.add_section(new_section);

//Устанавливаем для нее необходимый виртуальный размер

image.set_section_virtual_size(added_section, total_virtual_size);

std::cout << "Creating imports..." << std::endl;

//Создаем импорты из библиотеки kernel32.dll

pe_base::import_library kernel32;

kernel32.set_name("kernel32.dll"); //Выставили имя библиотеки

//Создаем импортируемую функцию

pe_base::imported_function func;

func.set_name("LoadLibraryA"); //Ее имя

kernel32.add_import(func); //Добавляем ее к библиотеке

//И вторую функцию

func.set_name("GetProcAddress");

kernel32.add_import(func); //Тоже добавляем

//Получаем относительный адрес (RVA) поля load_library_a

//нашей структуры packed_file_info, которую мы расположили в самом

//начале добавленной секции

DWORD load_library_address_rva = pe_base::rva_from_section_offset(added_section,

offsetof(packed_file_info, load_library_a));

//Устанавливаем этот адрес как адрес

//таблицы адресов импорта (import address table)

kernel32.set_rva_to_iat(load_library_address_rva);

//Создаем список импортируемых библиотек

pe_base::imported_functions_list imports;

//Добавляем к списку нашу библиотеку

imports.push_back(kernel32);

//Настроим пересборщик импортов

pe_base::import_rebuilder_settings settings;

//Original import address table нам не нужна (пояснения ниже)

settings.build_original_iat(false);

//Будем переписывать IAT именно по тому адресу,

//которому указали (load_library_address_rva)

settings.save_iat_and_original_iat_rvas(true, true);

//Расположим импорты прямо за концом упакованных данных

settings.set_offset_from_section_start(added_section.get_raw_data().size());

//Пересоберем импорты

image.rebuild_imports(imports, added_section, settings);

}

{

//Новая секция

pe_base::section unpacker_section;

//Имя - exsite

unpacker_section.set_name("exsite");

//Доступна на чтение и исполнение

unpacker_section.readable(true).executable(true);

{

//Получаем ссылку на данные секции распаковщика

std::string& unpacker_section_data = unpacker_section.get_raw_data();

//Записываем туда код распаковщика

//Этот код хранится в авто файле

//unpacker.h, который мы подключили в main.cpp

unpacker_section_data = std::string(reinterpret_cast<const char*>(unpacker_data), sizeof(unpacker_data));

//Записываем по нужным смещениям адрес

//загрузки образа

*reinterpret_cast<DWORD*>(&unpacker_section_data[original_image_base_offset]) = image.get_image_base_32();

//и виртуальный адрес самой первой секции упакованного файла,

//в которой лежат данные для распаковки и информация о них

//В самом начале это секции, как вы помните, лежит

//структура packed_file_info

*reinterpret_cast<DWORD*>(&unpacker_section_data[rva_of_first_section_offset]) = image.get_image_sections().at(0).get_virtual_address();

}

//Добавляем и эту секцию

const pe_base::section& unpacker_added_section = image.add_section(unpacker_section);

//Выставляем новую точку входа - теперь она указывает

//на распаковщик, на самое его начало

image.set_ep(image.rva_from_section_offset(unpacker_added_section, 0));

}

//Удалим все часто используемые директории

image.remove_directory(IMAGE_DIRECTORY_ENTRY_BASERELOC);

image.remove_directory(IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT);

image.remove_directory(IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT);

image.remove_directory(IMAGE_DIRECTORY_ENTRY_EXPORT);

image.remove_directory(IMAGE_DIRECTORY_ENTRY_IAT);

image.remove_directory(IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG);

image.remove_directory(IMAGE_DIRECTORY_ENTRY_RESOURCE);

image.remove_directory(IMAGE_DIRECTORY_ENTRY_SECURITY);

image.remove_directory(IMAGE_DIRECTORY_ENTRY_TLS);

image.remove_directory(IMAGE_DIRECTORY_ENTRY_DEBUG);

//Урезаем таблицу директорий, удаляя все нулевые

//Урезаем не полностью, а минимум до 12 элементов, так как в оригинальном

//файле могут присутствовать первые 12 и использоваться

image.strip_data_directories(16 - 4);

//Удаляем стаб из заголовка, если какой-то был

image.strip_stub_overlay();

//Создаем новый PE-файл

//Вычислим имя переданного нам файла без директории

std::string base_file_name(argv[1]);

std::string dir_name;

std::string::size_type slash_pos;

if((slash_pos = base_file_name.find_last_of("/\\")) !=

std::string::npos)

{

dir_name = base_file_name.substr(0, slash_pos + 1); //Директория исходного файла

base_file_name = base_file_name.substr(slash_pos + 1); //Имя исходного файла

}

base_file_name = dir_name + "packed_" + base_file_name;

std::ofstream new_pe_file(base_file_name.c_str(), std::ios::out | std::ios::binary | std::ios::trunc);

if(!new_pe_file)

{

//Если не удалось создать файл - выведем ошибку

std::cout << "Cannot create " << base_file_name << std::endl;

return -1;

}

//Пересобираем PE-образ

//Урезаем DOS-заголовок, накладывая на него NT-заголовки

//(за это отвечает второй параметр true)

//Не пересчитываем SizeOfHeaders - за это отвечает третий параметр

image.rebuild_pe(new_pe_file, true, false);

std::cout << "Packed image was saved to " << base_file_name << std::endl;

}

catch(const pe_exception& e)

{

//Если по какой-то причине открыть его не удалось

std::cout << e.what() << std::endl;

return -1;

}

return 0;

}

Unpacker: unpacker.cpp

//Подключаем файл со структурами из проекта упаковщика

#include "../simple_pe_packer/structs.h"

//Алгоритм распаковки

#include "lzo_conf.h"

/* decompression */

LZO_EXTERN(int)

lzo1z_decompress ( const lzo_bytep src, lzo_uint src_len,

lzo_bytep dst, lzo_uintp dst_len,

lzo_voidp wrkmem /* NOT USED */ );

//Создадим функцию без пролога и эпилога

extern "C" void __declspec(naked) unpacker_main()

{

//Пролог вручную

__asm

{

push ebp;

mov ebp, esp;

sub esp, 256;

}

//Адрес загрузки образа

unsigned int original_image_base;

//Относительный адрес первой секции,

//в которую упаковщик кладет информацию для

//распаковщика и сами упакованные данные

unsigned int rva_of_first_section;

//Эти инструкции нужны только для того, чтобы

//заменить в сборщике распаковщика адреса на реальные__asm

{

mov original_image_base, 0x11111111;

mov rva_of_first_section, 0x22222222;

}

//Получаем указатель на структуру с информацией,

//которую для нас приготовил упаковщик

const packed_file_info* info;

//Она находится в самом начале

//первой секции упакованного файла

info = reinterpret_cast<const packed_file_info*>(original_image_base + rva_of_first_section);

//Два тайпдефа прототипов функций LoadLibraryA и GetProcAddress

typedef HMODULE (__stdcall* load_library_a_func)(const char* library_name);

typedef INT_PTR (__stdcall* get_proc_address_func)(HMODULE dll, const char* func_name);

//Считаем их адреса из структуры packed_file_info

//Их нам туда подложил загрузчик

load_library_a_func load_library_a;

get_proc_address_func get_proc_address;

load_library_a = reinterpret_cast<load_library_a_func>(info->load_library_a);

get_proc_address = reinterpret_cast<get_proc_address_func>(info->get_proc_address);

//Создаем буфер на стеке

char buf[32];

//kernel32.dll

*reinterpret_cast<DWORD*>(&buf[0]) = 'nrek';

*reinterpret_cast<DWORD*>(&buf[4]) = '23le';

*reinterpret_cast<DWORD*>(&buf[8]) = 'lld.';

*reinterpret_cast<DWORD*>(&buf[12]) = 0;

//Загружаем библиотеку kernel32.dll

HMODULE kernel32_dll;

kernel32_dll = load_library_a(buf);

//Тайпдеф прототипа функции VirtualAlloc

typedef LPVOID (__stdcall* virtual_alloc_func)(LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect);

//Тайпдеф прототипа функции VirtualProtect

typedef LPVOID (__stdcall* virtual_protect_func)(LPVOID lpAddress, SIZE_T dwSize, DWORD flNewProtect, PDWORD lpflOldProtect);

//Тайпдеф прототипа функции VirtualFree

typedef LPVOID (__stdcall* virtual_free_func)(LPVOID lpAddress, SIZE_T dwSize, DWORD dwFreeType);

//VirtualAlloc

*reinterpret_cast<DWORD*>(&buf[0]) = 'triV';

*reinterpret_cast<DWORD*>(&buf[4]) = 'Alau';

*reinterpret_cast<DWORD*>(&buf[8]) = 'coll';

*reinterpret_cast<DWORD*>(&buf[12]) = 0;

//Получаем адрес функции VirtualAlloc

virtual_alloc_func virtual_alloc;

virtual_alloc = reinterpret_cast<virtual_alloc_func>(get_proc_address(kernel32_dll, buf));

//VirtualProtect

*reinterpret_cast<DWORD*>(&buf[0]) = 'triV';

*reinterpret_cast<DWORD*>(&buf[4]) = 'Plau';

*reinterpret_cast<DWORD*>(&buf[8]) = 'etor';

*reinterpret_cast<DWORD*>(&buf[12]) = 'tc';

//Получаем адрес функции VirtualProtect

virtual_protect_func virtual_protect;

virtual_protect = reinterpret_cast<virtual_protect_func>(get_proc_address(kernel32_dll, buf));

//VirtualFree

*reinterpret_cast<DWORD*>(&buf[0]) = 'triV';

*reinterpret_cast<DWORD*>(&buf[4]) = 'Flau';

*reinterpret_cast<DWORD*>(&buf[8]) = 'eer';

//Получаем адрес функции VirtualFree

virtual_free_func virtual_free;

virtual_free = reinterpret_cast<virtual_free_func>(get_proc_address(kernel32_dll, buf));

//Относительный виртуальный адрес директории импорта

DWORD original_import_directory_rva;

//Виртуальный размер директории импорта

DWORD original_import_directory_size;

//Оригинальная точка входа

DWORD original_entry_point;

//Общий размер всех секций файла

DWORD total_virtual_size_of_sections;

//Количество секций в оригинальном файле

BYTE number_of_sections;

//Копируем эти значения из структуры packed_file_info,

//которую для нас записал упаковщик

original_import_directory_rva = info->original_import_directory_rva;

original_import_directory_size = info->original_import_directory_size;

original_entry_point = info->original_entry_point;

total_virtual_size_of_sections = info->total_virtual_size_of_sections;

number_of_sections = info->number_of_sections;

//Указатель на память, в которую

//мы запишем распакованные данные

LPVOID unpacked_mem;

//Выделяем память

unpacked_mem = virtual_alloc(

0,

info->size_of_unpacked_data,

MEM_COMMIT,

PAGE_READWRITE);

//Выходной размер распакованных данных

lzo_uint out_len;

out_len = 0;

//Производим распаковку алгоритмом LZO

lzo1z_decompress(

reinterpret_cast<const unsigned char*>(reinterpret_cast<DWORD>(info) + sizeof(packed_file_info)),

info->size_of_packed_data,

reinterpret_cast<unsigned char*>(unpacked_mem),

&out_len,

0);

//Указатель на DOS-заголовок файла

const IMAGE_DOS_HEADER* dos_header;

//Указатель на файловый заголовок

IMAGE_FILE_HEADER* file_header;

//Виртуальный адрес начала заголовков секций

DWORD offset_to_section_headers;

//Просчитываем этот адрес

dos_header = reinterpret_cast<const IMAGE_DOS_HEADER*>(original_image_base);

file_header = reinterpret_cast<IMAGE_FILE_HEADER*>(original_image_base + dos_header->e_lfanew + sizeof(DWORD));

//Вот по такой формуле

offset_to_section_headers = original_image_base + dos_header->e_lfanew + file_header->SizeOfOptionalHeader

+ sizeof(IMAGE_FILE_HEADER) + sizeof(DWORD) /* Signature */;

//Обнулим всю память первой секции

//эта область соответствует области памяти, которую

//в оригинальном файле занимают все секции

memset(

reinterpret_cast<void*>(original_image_base + rva_of_first_section),

0,

total_virtual_size_of_sections - rva_of_first_section);

//Изменим атрибуты блока памяти, в котором

//расположены заголовки PE-файла и секций

//Нам необходим доступ на запись

DWORD old_protect;

virtual_protect(reinterpret_cast<LPVOID>(offset_to_section_headers),

number_of_sections * sizeof(IMAGE_SECTION_HEADER),

PAGE_READWRITE, &old_protect);

//Теперь изменим количество секций

//в заголовке PE-файла на оригинальное

file_header->NumberOfSections = number_of_sections;

//Виртуальный адрес структуры заголовка секции

DWORD current_section_structure_pos;

current_section_structure_pos = offset_to_section_headers;

//Перечислим все секции

for(int i = 0; i != number_of_sections; ++i)

{

//Создаем структуру заголовка секции

IMAGE_SECTION_HEADER section_header;

//Обнуляем структуру

memset(&section_header, 0, sizeof(section_header));

//Заполняем важные поля:

//Характеристики

section_header.Characteristics = (reinterpret_cast<packed_section*>(unpacked_mem) + i)->characteristics;

//Смещение файловых данных

section_header.PointerToRawData = (reinterpret_cast<packed_section*>(unpacked_mem) + i)->pointer_to_raw_data;

//Размер файловых данных

section_header.SizeOfRawData = (reinterpret_cast<packed_section*>(unpacked_mem) + i)->size_of_raw_data;

//Относительный виртуальный адрес секции

section_header.VirtualAddress = (reinterpret_cast<packed_section*>(unpacked_mem) + i)->virtual_address;

//Виртуальный размер секции

section_header.Misc.VirtualSize = (reinterpret_cast<packed_section*>(unpacked_mem) + i)->virtual_size;

//Копируем оригинальное имя секции

memcpy(section_header.Name, (reinterpret_cast<packed_section*>(unpacked_mem) + i)->name, sizeof(section_header.Name));

//Копируем заполненный заголовок

//в память, где находятся заголовки секций

memcpy(reinterpret_cast<void*>(current_section_structure_pos), &section_header, sizeof(section_header));

//Перемещаем указатель на следующий заголовок секции

current_section_structure_pos += sizeof(section_header);

}

//Указатель на сырые данные секции

//Необходим для восстановления сжатых данных секций

//и расположения их по нужным местам

DWORD current_raw_data_ptr;

current_raw_data_ptr = 0;

//Восстановим указатель на заголовки секций

current_section_structure_pos = offset_to_section_headers;

//Снова перечисляем все секции

for(int i = 0; i != number_of_sections; ++i)

{

//Заголовок секции, который мы только что сами записали

const IMAGE_SECTION_HEADER* section_header = reinterpret_cast<const IMAGE_SECTION_HEADER*>(current_section_structure_pos);

//Копируем данные секции в то место памяти,

//где они должны располагаться

memcpy(reinterpret_cast<void*>(original_image_base + section_header->VirtualAddress),

reinterpret_cast<char*>(unpacked_mem) + number_of_sections * sizeof(packed_section) + current_raw_data_ptr,

section_header->SizeOfRawData);

//Перемещаем указатель на данные секции

//в распакованном блоке данных

current_raw_data_ptr += section_header->SizeOfRawData;

//Переходим к следующему заголовку секции

current_section_structure_pos += sizeof(IMAGE_SECTION_HEADER);

import_dir = reinterpret_cast<IMAGE_DATA_DIRECTORY*>(offset_to_directories + sizeof(IMAGE_DATA_DIRECTORY) * IMAGE_DIRECTORY_ENTRY_IMPORT);

//Записываем значения размера и виртуального адреса в соответствующие поля

import_dir->Size = original_import_directory_size;

import_dir->VirtualAddress = original_import_directory_rva;

//Если у файла имеются импорты

if(original_import_directory_rva)

{

//Виртуальный адрес первого дескриптора

IMAGE_IMPORT_DESCRIPTOR* descr;

descr = reinterpret_cast<IMAGE_IMPORT_DESCRIPTOR*>(original_import_directory_rva + original_image_base);

//Перечисляем все дескрипторы

//Последний - нулевой

while(descr->Name)

{

//Загружаем необходимую DLL

HMODULE dll;

dll = load_library_a(reinterpret_cast<char*>(descr->Name + original_image_base));

//Указатели на таблицу адресов и lookup-таблицу

DWORD* lookup, *address;

//Учтем, что lookup-таблицы может и не быть

lookup = reinterpret_cast<DWORD*>(original_image_base + (descr->OriginalFirstThunk ? descr->OriginalFirstThunk : descr->FirstThunk));

address = reinterpret_cast<DWORD*>(descr->FirstThunk + original_image_base);

//Перечисляем все импорты в дескрипторе

while(true)

{

//До первого нулевого элемента в лукап-таблице

DWORD lookup_value = *lookup;

if(!lookup_value)

break;

//Проверим, импортируется ли функция по ординалу

if(IMAGE_SNAP_BY_ORDINAL32(lookup_value))

*address = static_cast<DWORD>(get_proc_address(dll, reinterpret_cast<const char*>(lookup_value & ~IMAGE_ORDINAL_FLAG32)));

else

*address = static_cast<DWORD>(get_proc_address(dll, reinterpret_cast<const char*>(lookup_value + original_image_base + sizeof(WORD))));

//Переходим к следующему элементу

++lookup;

++address;

}

//Переходим к следующему дескриптору

++descr;

}

}

//Вернем атрибуты памяти заголовков, как было изначально

virtual_protect(reinterpret_cast<LPVOID>(offset_to_section_headers), number_of_sections * sizeof(IMAGE_SECTION_HEADER), old_protect, &old_protect);

//Эпилог вручную

_asm

{

//Переходим на оригинальную точку входа

mov eax, original_entry_point;

add eax, original_image_base;

leave;

//Вот так

jmp eax;

}

}

Converter: main.cpp

#include <pe_32_64.h> //Директивы для линкования с собранной библиотекой PE

#ifndef _M_X64

#ifdef _DEBUG

#pragma comment(lib, "../../Debug/pe_lib.lib")

#else

#pragma comment(lib, "../../Release/pe_lib.lib")

#endif

#else

#ifdef _DEBUG

#pragma comment(lib, "../../x64/Debug/pe_lib.lib")

#else

#pragma comment(lib, "../../x64/Release/pe_lib.lib")

#endif

#endif

int main(int argc, char* argv[])

{

//Подсказка по использованию

if(argc != 3)

{

std::cout << "Usage: unpacker_converter.exe unpacker.exe output.h" << std::endl;

return 0;

}

//Открываем файл

//Открываем файл unpacker.exe - его имя

//и путь к нему хранятся в массиве argv по индексу 1

std::ifstream file(argv[1], std::ios::in | std::ios::binary);

if(!file)

{

//Если открыть файл не удалось - сообщим и выйдем с ошибкой

std::cout << "Cannot open " << argv[1] << std::endl;

return -1;

}

try

{

std::cout << "Creating unpacker source file..." << std::endl;

//Пытаемся открыть файл как 32-битный PE-файл

pe32 image(file, false, false);

//Получаем список секций распаковщика

pe_base::section_list& unpacker_sections = image.get_image_sections();

//Убедимся, что она одна (так как в нем нет импортов, релокаций)

if(unpacker_sections.size() != 1)

{

std::cout << "Incorrect unpacker" << std::endl;

return -1;

}

//Получаем ссылку на данные этой секции

std::string& unpacker_section_data = unpacker_sections.at(0).get_raw_data();

//Удаляем нулевые байты в конце этой секции,

//которые компилятор добавил для выравнивания

pe_base::strip_nullbytes(unpacker_section_data);

//Открываем выходной файл для записи h-файла

//Его имя хранится в argv[2]

std::ofstream output_source(argv[2], std::ios::out | std::ios::trunc);

//Начинаем формировать исходный код

output_source << std::hex << "#pragma once" << std::endl << "unsigned char unpacker_data[] = {";

//Текущая длина считанных данных

unsigned long len = 0;

//Общая длина данных секции

std::string::size_type total_len = unpacker_section_data.length();

//Для каждого байта данных...

for(std::string::const_iterator it = unpacker_section_data.begin(); it != unpacker_section_data.end(); ++it, ++len)

{

//Добавляем необходимые переносы, чтобы

//получившийся код был читаемым

if((len % 16) == 0)

output_source << std::endl;

//Записываем значение байта

output_source

<< "0x" << std::setw(2) << std::setfill('0')

<< static_cast<unsigned long>(static_cast<unsigned char>(*it));

//И, если необходимо, запятую

if(len != total_len - 1)

output_source << ", ";

}

//Конец кода

output_source << " };" << std::endl;

}

catch(const pe_exception& e)

{

//Если по какой-то причине открыть его не удалось

//Выведем текст ошибки и выйдем

std::cout << e.what() << std::endl;

return -1;

}

return 0;

}

Structs.h

#pragma once

#include <Windows.h>

#pragma pack(push, 1)

//Структура, хранящая информацию об упакованной секции

struct packed_section

{

char name[8]; //Имя секции

DWORD virtual_size; //Виртуальный размер

DWORD virtual_address; //Виртуальный адрес (RVA)

DWORD size_of_raw_data; //Размер "сырых" данных

DWORD pointer_to_raw_data; //Файловое смещение сырых данных

DWORD characteristics; //Характеристики секции

};

//Структура, хранящая информацию об упакованном файле

struct packed_file_info

{

BYTE number_of_sections; //Количество секций в оригинальном файле

DWORD size_of_packed_data; //Размер упакованных данных

DWORD size_of_unpacked_data; //Размер оригинальных данных

DWORD total_virtual_size_of_sections; //Полный виртуальный размер всех секций оригинального файла

DWORD original_import_directory_rva; //Относительный адрес оригинальной таблицы импорта

DWORD original_import_directory_size; //Размер оригинальной таблицы импорта

DWORD original_entry_point; //Оригинальная точка входа

DWORD load_library_a; //Адрес процедуры LoadLibraryA из kernel32.dll

DWORD get_proc_address; //Адрес процедуры GetProcAddress из kernel32.dll

DWORD end_of_import_address_table; //Конец IAT

};

#pragma pack(pop)