УДК: 004.424 ББК: 32.973
ПРИМЕНЕНИЕ ПРИНЦИПОВ ДИНАМИЧЕСКОГО ПОЛИМОРФИЗМА ПРИ КОМПИЛЯЦИИ ПРИЛОЖЕНИЯ С ЦЕЛЬЮ ПОВЫШЕНИЯ ЕГО КРИПТОСТОЙКОСТИ
КАРЭН РАФАЕЛОВИЧ АВЕТИСЯН,
адъюнкт кафедры информационной безопасности учебно-научного комплекса информационных технологий Московского университета МВД России им. В.Я. Кикотя;
АРТУР ВАСИЛЬЕВИЧ ВИДРУ, курсант факультета подготовки специалистов в области информационной безопасности Московского университета МВД России им. В.Я. Кикотя Научная специальность 05.25.05 — информационные системы и процессы Научный руководитель: доктор технических наук Овчинский А.С.
E-mail: karen-avetisyan-1989@bk.ru
Citation-индекс в электронной библиотеке НИИОН
Аннотация. Рассматриваются вопросы криптостойкости приложений на основе применения принципов динамического полиморфизма; прослеживается процесс шифрования и дешифрования. Отражены основные методы реверс-инженеринга и методы борьбы с ними.
Ключевые слова: динамический полиморфизм, крипторы, стеки, сплайсинг, реверс-инженеринг.
Annotation. In article is considered questions of cryptofiimness of appendices on the basis of application of the principles of dynamic polymorphism, process of enciphering and decoding is considered. The main methods a reverse-inzheneringa and methods of fight against them are reflected.
Keywords: dynamic polymorphism, kriptor, stacks, splaysing, reverse-inzhenering.
Самомодификация как методика управления кодом. Самомодифицирующийся код — методика правки образа приложения в памяти из управляемого им потока динамическим путём («на лету», по ходу исполнения файла). Самомодифицирующийся код широко используется во многих вредоносах, упаковщиках, эксплоитах и прочем подобном программном обеспечении.
Несмотря на то, что данная методика управления кодом не сложна в реализации, тем не менее, была неоднократно описана, к примеру, в статьях Jonathon T. Giffin Mihai Christodorescu Louis Kruger.
Существует мнение, что написание рабочего самомодифицирующегося кода в ОС Windows невозможно, связывая с использованием недокументированных возможностей системы, либо наличия требования по написанию L0 драйверов. Самомодификация применяется для достижения устойчивости к реверс-инжинирингу, и ускорения выполнения некоторых участков кода (к примеру, правки адреса и типа перехода «на лету»), или отключения части функционала на время отладки.
Рассвет эпохи самомодификации уже позади. Во
времена не интерактивных отладчиков (программ для проверки выполнения скомпилированного кода) типа debug.com и пакетных дизассемблеров типа Sourcer, самомодификация, серьезно затрудняла анализ, однако, с появлением IDA PRO и Syser Kernel debugger все изменилось. Как пример отличием может выступать возможность дизассемблирования байткода для IDA и поддержка отладки в r0.
Описываемый приём не способен воздействовать на трассировку кода, что даёт возможность осуществлять отладку самомодифицирующегося кода штатными средствами (динамическими отладчиками, подобными IDA Pro, OllyDebugger).[1]
Полученный список ассемблерных команд, используемых в программе, отражает состояние программы в момент снятия дампа путём анализа машинного кода, либо на точке входа, при выгрузке оригинала программы в пространство памяти отладчика. Он предполагает неизменность кода в процессе исполнения инструкций, иначе построенный листинг окажется неверным. При этом ручное исправление кода можно выполнить, не прилагая больших усилий.
Такой приём, как динамическая шифровка, используется более современными и совершенными средствами защиты полученных программ. Становится ясным, что для создания полного дизассемблирован-ного листинга, необходим полостью расшифрованный двоичный код. Кроме этого, отладка с подключением к процессу, так же невозможна в том случае, если используется множество анти отладочных приёмов.
Используемая как основная, в обычных навесных протекторах, статическая шифровка, в большинстве случаев, бесполезна. После окончания процедуры дешифровки, можно снять дамп, после чего исследовать его инструментарием реверс-инженера. Разумеется, имеются защитные механизмы. Скрытие таблицы импорта, её искажение, сплайсинг, модификация PE-заголовка, присвоение страницам памяти атрибутов NO_ACCESS. Однако, это общеизвестные приёмы, которые опытные исследователи обходят без серьёзных временных затрат. [2]
Большинство актуальных навесных протекторов могут быть сняты автоматически, а для наиболее сложных защитных механизмов могут быть применены методы полуавтоматической или ручной распаковки.
Для того, чтобы защита имела смысл, программный код, ни в одном участке, не должен быть полностью расшифрован, иначе именно этот участок будет использован для снятия дампа.
Механизм создания полиморфных крипторов. Расшифровщик должен быть сконструирован так, чтобы было невозможно использовать его вне контекста программы, так как это является простейшей и распространённой уязвимостью. При нахождении точки входа в процедуру расшифровщика, можно без проблем восстановить его прототип — шеллкод, вызываемый динамически или статически полученным на него указателем, после чего, расшифровать блоками весь оставшийся зашифрованным код. В случаях со слабым уровнем шифрования данных ситуация упрощается: достаточно найти место хранения ключей, либо, при использовании обратимого шифрования, установить алгоритм шифрования (UUEncode, Base64, Rot13) после чего выполнить процедуру дешифровки без правки каких-либо функций.[3]
При использовании полиморфно сгенерированных «на лету» процедур шифрования (динамически, нелинейным алгоритмом), автоматизировать дешифровку программы крайне затруднительно, однако, и создать такой механизм защиты сложно, что является одним из факторов, затрудняющих распространение полиморфных защитных механизмов — полиморфных крипторов.
В случае с процессорами, построенными на архитектуре x86, не поддерживающими когерентность машинного кода — связи, отслеживания конвейера, где отсутствовала технология отслеживания команд, находящихся на конвейере, отладчику было невозможно, в трассирующем режиме обнаружить правку команд в реальном времени.
Если модификация команд отслеживается, как это выполняется на процессорах семейства, начиная с Pentium, такие действия, как IF, OR выполняются кон-вейерно, программная длина равна нулю, что приводит к вызову исключения по выполнению на отладчике в старых защитных механизмах, не учитывающих данную задокументированную особенность поведения конвейера процессора.
В момент изменения инструкции, правится кэш данных, затем кодовый кэш экстренно сбрасывается и кэш-линейки загружаются с новыми значениями. Это расходует множество тактов процессора (действий), что необходимо учитывать при разработке собственных программ. При выполнении описываемых инструкций в цикле, непременно будет наблюдаться большой расход процессорного времени на выполнение алгоритма.
Существует категория программистов, считающих, что самомодифицирующий код может работать лишь в MS-DOS, по причине того, как ОС Windows работает с памятью, выставляя запрет на правку секций кода «на лету».
Утверждение о том, что, по умолчанию, секция, содержащая код, имеет атрибут КБ, верно, однако, существуют некоторые нюансы работы со страницами и сегментами памяти. Процессоры, построенные на архитектуре, подобной x86, поддерживают такие атрибуты доступа, как чтение, запись и исполнение для сегментов, а также доступ и запись для страниц. ОС Windows, с целью упрощения работы с моделью памяти и обеспечения совместимости, объединяет сегмент кода и данных. В результате этого они заключаются в общем адресном пространстве, что требует общих атрибутов доступа — чтение становится эквивалентно исполнению.
Целесообразно размещение исполняемого кода как в стеке — наборе элементов, смещающихся по принципу LIFO, так и «куче» — древовидная структура данных с максимальным или минимальным старшим элементом, так и области глобальных переменных. И стек и «кучи» имеют атрибуты, разрешающие запись по умолчанию, что делает их пригодными для размещения самомодифицирующегося кода.
Статичные переменные и глобальные константы находятся в секции .rdata, доступ к которой, по умолчанию, установлен только на чтение, однако, ввиду совмещения всего пространства памяти, возможно и исполнение. При запросах модификации, пробуждается исключение, что требует для работы самомодификации, скопировать рабочий код в стек, либо кучу и передать управление на него.
Некоторые аспекты процедур шифрования и дешифровки. Компиляторы высокоуровневых языков обычно не создают нужного кода, что может казаться сложным и объёмным. Мы же будем максимально избегать низкоуровневого программирования.
Создадим рабочую программу, содержащую за-
шифрованную функцию, предполагая, что порядок следования подпрограмм идентичен порядку объявления в исходном коде, зашифрованная процедура полностью перемещаема (без fixup^), что ограничивает лишь динамические библиотеки, в общем случае, в исполняемых файлах, код можно сделать полностью перемещаемым, в том случае, если он не сгенерирован таким заведомо. Отдадим предпочтение языку программирования «Си», однако, подобное можно реализовать на почти любом ином компилируемом языке.[4]
В Си можно оперировать указателями на объекты. Код, выполняющий описываемую операцию, синтаксически описывается примерно так:
'void *cat = (void *) dog;»
Это просто, однако, большую проблему представляет измерение длины функции. Штатных средств, предоставляемых известными компиляторами или стандартом языка, для измерения длины функции нет. Ввиду поддержки операций над указателями, видится возможным получить длину через разность адресов. Например, шифруемой функции и следующей за ней.
Зашифрованный код компилятор не генерирует, так что выполнять нужно будет при помощи hex-редактора, такого, как WinHex или HIEW. Осталось решить вопрос с поиском шифруемого кода. Точно получить границы функции можно при помощи отладочных символов и отладчика, но этот способ слишком медленный. Также можно использовать функции-маркеры, однако, это может создать некоторые проблемы с последующей разработкой и сборкой нашего приложения, однако, позволит сделать специальную утилиту, которая автоматизирует процесс шифрования.
Мы будем использовать маркеры, создавая уникальные последовательности байт в рамках программы. В наших условиях, доступна директория _emit, работающая как «DB» — являющаяся инструкцией резервирования данных размером 1 байт в ассемблере.
Располагать директивы необходимо только вне шифруемой функции.
Требований к алгоритму шифрования нет. Кто-то отдаст предпочтение Rinjael, кто-то Serpent.
После выполненных операций, полученный файл стал работоспособным. Если снять дамп до расшифровки и попытаться исследовать шифрованную функцию, то результат не будет достигнут. Процедура дешифрования будет выполнена, в случае сохранения состояния потока, однако, фиксировать состояние дешифровки реверс-инженеру будет необходимо отдельно при помощи отладчика, что будет трудной задачей в условиях многопоточного приложения с большим количеством антиотладочных технологий.
Но не стоит забывать о том, что защита секции кода действительно необходима, и сделана вовсе не для препятствия самомодификации из управляющего потока программы. По завершению дешифровки, не нужно забывать восстанавливать защиту от записи, причем, желательно снимать защиту только с тех страниц, где размещён шифрованный код. Для этого используется функция VirtualProtectEx.
Как итог хотелось бы заострить внимание на том, что самомодифицирующийся код способен работать лишь на компьютерах, построенных по архитектуре фон-Неймана, где в памяти совмещены код и данные. Процессоры Pentium лишь эмулируют архитектуру фон-Неймана, будучи построенными по Гарвардской архитектуре. Самомодифицирующийся код крайне снижает производительность в таких условиях. Ассемблер не имеет средств для работы с самомодифицирующимся кодом, кроме как директивы DB.[5]
1. URL:http://www.vuzllib.su/books/7040
2. URL:http://c2.com/cgi/wiki?SelfModifyingCode
3. URL:http://evetythmg.explamedtoday/Self-modfymg_
code
4. URL:http://e-burg-biz.ru/e-burg-biz-17
5. URL:http://ru.stackoverflow.com/questions/697