Эту статью я перенес сюда со своего личного сайта. Первоначально она была опубликована 4/13/19
Последние пару месяцев я был сильно погружен в курс машинного обучения для выпускников в рамках своей программы обучения. Я не писал о своей работе, потому что это позволило бы другим жульничать (tsk tsk), но сейчас я работаю над своим финальным проектом и хотел бы неофициально поделиться своим опытом обучения классификатора машинного обучения для выявления уязвимостей в двоичных файлах.
Прежде чем мы углубимся в эту тему, пожалуйста, имейте в виду, что это текущий проект, и я не знаю, будет ли эта техника работать. Эта серия предназначена для того, чтобы дать некоторое представление о практическом применении машинного обучения, независимо от того, будут ли результаты положительными (это наука!).
Методы машинного обучения, которые мы рассматривали в этом курсе, считаются классическим машинным обучением. Мы рассмотрели контролируемое и неконтролируемое обучение и реализовали несколько известных алгоритмов с нуля. В этом посте я не собираюсь обучать вас основам машинного обучения — если вы хотите узнать, как выбрать классификатор или как работают алгоритмы машинного обучения, я рекомендую просмотреть этот список учебных пособий. Поскольку курс был посвящен классическим методам, я решил также сосредоточить свое внимание на них и проигнорировать более блестящие варианты нейронных сетей и глубокого обучения. Цель моего проекта — показать, что простая техника вывода, Naive Bayes, может быть использована для получения ценной информации для реальной проблемы. Эта проблема — выявление уязвимостей в двоичном коде.
Двоичный код — это 0 и 1, из которых состоит программа. Если вы разработчик программного обеспечения, это то, во что компилируется ваш исходный код. Если вы обычный пользователь, то это то, на что вы дважды щелкаете, чтобы запустить приложение. Двоичный код состоит из машинных инструкций, которые выполняет ваш процессор. Эти инструкции, по сути, состоят из двух частей: операционного кода и любых аргументов.
Все эти инструкции и опкоды упакованы в двоичный файл одна за другой. Разные инструкции занимают больше места, чем другие, а их аргументы могут быть разной длины. Если вы откроете двоичный файл в текстовом редакторе, он будет выглядеть примерно так:
Поскольку размер инструкций и их аргументов может быть разным, нам нужен специальный инструмент, называемый дизассемблером, для декодирования двоичного файла, чтобы мы могли просмотреть операционные коды. Дизассемблеры делают и кое-что другое, что тоже интересно, а именно показывают, как логика проходит через вашу программу, разлагая ее на блоки, называемые «базовыми блоками», и рисуя стрелки между этими блоками, где есть переходы или условия, которые связывают их.
Некоторые базовые блоки из бинарного файла CGC «yolodex», дизассемблированные BinaryNinja
Мой подход к обнаружению уязвимостей основывается на этой блочной декомпозиции. Я разбиваю двоичные файлы на основные блоки, перечисляю опкоды, присутствующие в этих блоках, и использую эти данные на уровне блоков в качестве одного наблюдения. Я считаю, что это может сработать, потому что именно последовательность инструкций, которые выполняет программа, делает ее уязвимой. Конкретные последовательности и связанные с ними аргументы могут сделать программу уязвимой для переполнения буфера, использования после освобождения или многих других уязвимостей. На уровне блоков у нас есть довольно приличная картина того, какие инструкции связаны друг с другом в последовательности, и, таким образом, мы можем сделать некоторые выводы о том, является ли программа уязвимой.
Различные виды уязвимостей, которые могут проявляться в программах, хорошо перечислены в Common Weakness Enumeration (CWE). Мой подход пытается классифицировать, какие из этих CWE могут проявляться в данном бинарном файле. Конечно, чтобы обучить классификатор машинного обучения распознаванию уязвимостей, я должен иметь некий набор обучающих данных. В качестве основы для тренировочных данных я использую набор двоичных файлов Cyber Grand Challenge от DARPA. Эти двоичные файлы были написаны так, чтобы содержать уязвимости, и имеют файлы readme, в которых подробно описаны уязвимости, включая то, под какие CWE они подпадают. Первым шагом на пути к созданию классификатора является обработка двоичных файлов, как я описал выше.
Как я уже говорил, обычный текстовый редактор не подойдет для чтения двоичных файлов, поэтому мне нужно было выбрать дизассемблер, чтобы разделить двоичные файлы на основные блоки. Я решил использовать Binary Ninja, потому что у него очень простой в использовании Python API, и он недорогой на уровне любителя (для сравнения, стандартный промышленный дизассемблер — IDA Pro, который вам продадут примерно за руку и будут продолжать отрывать пальцы на руках и ногах, взимая плату за продление). Я начал с написания быстрого сценария, который просматривает один двоичный файл и выводит опкоды, встречающиеся в каждом блоке, просто чтобы убедиться, что я смог получить нужные мне данные.
Вывод сценария Python, использующего API Binary Ninja для печати основных блоков
Великолепно. Шаг 1 завершен. Но ни один из этих данных не помечен. Я хочу пометить свои базовые блоки метками CWE, которые содержит бинарный файл. Все CWE описаны в куче файлов README.md, не имеющих стандартного формата. Я ненадолго задумался о написании скрипта для извлечения меток CWE из README, но решил, что количество времени, которое я потрачу на отладку побочных ситуаций и проверку того, что скрипт действительно берет правильные номера CWE, будет как минимум не меньше, чем если бы я делал это вручную. Итак, я начал, вставляя номера CWE в электронную таблицу, чтобы создать CSV, сопоставляющий двоичные файлы с их CWE.
Электронная таблица двоичных файлов CGC с метками их CWE
Это заняло у меня несколько часов, из которых я потратил в основном на просмотр YouTube (разборки bigclive всегда хороший способ провести время). Непродуманные задачи — это хорошая возможность научиться чему-то новому. Или посмотреть сериал — выбирайте на свой вкус.
Первое, что я заметил, просматривая свои метки, это то, что некоторые двоичные файлы помечены более чем одним CWE. Это логично, но я не учел этого в своем первоначальном подходе. Чтобы упростить проблему, я принял (несколько неудачное) решение отбрасывать образцы, помеченные более чем одним CWE. Есть лучшие способы решения этой проблемы, но у меня есть только три выходных дня на обучение, оценку и презентацию классификатора, поэтому, к сожалению, мне приходится срезать некоторые углы. Я подсчитал некоторые основные статистические данные по моему набору данных, чтобы получить некоторое представление о его составе.
Number of binaries:
108
Number of unique CWEs:
27
Распределение двоичных файлов по отношению к CWEs
Большинство моих образцов относятся к CWEs 121 и 122. Важно отметить, что это не обязательно означает, что мой набор данных нереалистично смещен. На самом деле, CWE 121 и 122 — это «переполнение буфера на основе стека» и «переполнение буфера на основе кучи», то есть две очень распространенные уязвимости. Тем не менее, стоит помнить об этом перекосе, поскольку он повлияет на то, как наш классификатор будет обучаться.
Если собрать все это вместе, у нас теперь есть набор данных, где каждая строка описывает инструкции в одном базовом блоке, помеченном CWE, представленным бинарным файлом, из которого был взят базовый блок. Мы готовы попробовать обучить классификатор.
Образцы строк из CSV данных с перфектурами
Далее: часть 2