Эту статью я перенес сюда со своего личного сайта. Первоначально она была опубликована 4/13/19
В своем предыдущем посте я рассказал об обработке данных, необходимой для превращения кучи двоичных файлов в набор данных для использования в классификаторе машинного обучения. Хотя я сказал, что обсуждение основ машинного обучения выходит за рамки этой серии, я все же хочу немного рассказать о том, как работает классификатор Naive Bayes, почему я выбрал именно его и как я планирую использовать особый способ обработки данных, чтобы сделать несколько интересных вещей.
Если вы помните, у нас есть набор данных, строки которого выглядят примерно так:
Naive Bayes — это алгоритм классификации на основе умозаключений. Вывод опирается на логические свойства, чтобы сделать предположение о том, насколько вероятно, что данное наблюдение принадлежит к определенному классу. В нашем случае Naive Bayes будет говорить нам, насколько вероятно, что определенный базовый блок принадлежит к одному из наших классов CWE. Он будет делать это, рассматривая опкоды, присутствующие в этом базовом блоке, сравнивая их с базовыми блоками, которые он видел, когда обучался, и возвращая вероятность для каждого возможного класса CWE, указывая, насколько вероятно, что это CWE. Мы можем использовать это, чтобы сделать что-то умное.
В этих данных есть что-то принципиально неправильное. Если вы этого еще не заметили, не волнуйтесь, это очень тонко. Когда я создавал набор данных, я решил создать много наблюдений из одного двоичного числа. А именно, я создал по одному наблюдению для каждого базового блока в этом бинарном файле. Уязвимости обычно локализуются на небольшом наборе базовых блоков в двоичном файле — однако я пометил целые двоичные файлы классом CWE. Это означает, что существует множество базовых блоков, т.е. наблюдений, которые обозначены неверно, поскольку на самом деле они не содержат уязвимости. Такая неправильная маркировка повлияет на обучение классификатора и, в свою очередь, негативно скажется на точности.
Однако, зная, что я знаю о двоичных файлах, я могу использовать наш классификатор, чтобы немного схитрить. Я знаю, что двоичные файлы часто имеют схожий код, например, код установки и удаления, код манипулирования строками, консольный или сетевой код и т.д. Двоичные файлы, которые служат схожим целям, часто используют схожий код и поэтому имеют больше схожих блоков, и наоборот, двоичные файлы, которые делают кардинально разные вещи, имеют меньше общих блоков. Я выдвинул гипотезу, что блоки, общие для двоичных файлов разных классов CWE, не указывают на уязвимость, в то время как блоки, уникальные для класса CWE, являются хорошими индикаторами этой уязвимости. Поскольку наивный Байес — это алгоритм вывода, и он возвращает нам вероятность для каждого класса CWE при классификации базового блока, мы можем проанализировать это распределение вероятностей во время классификации и определить, сильно ли этот блок склоняется к одному классу или классификатор считает, что видел его раньше среди многих разных классов. Например…
Если у нас есть пять различных классов CWE, по которым мы классифицируем, блок, который уже встречался во всех пяти классах CWE, может получить вероятность по наивному Байесу примерно такую:
Class 1: 20%
Class 2: 23%
Class 3: 21%
Class 4: 19%
Class 5: 17%
С другой стороны, базовый блок, который ранее встречался только в одной категории, может получить следующее распределение:
Class 1: 2%
Class 2: 3%
Class 3: 93%
Class 4: 1%
Class 5: 1%
Стоит отметить, что, хотя я говорю «базовый блок, который уже встречался…», в действительности любой базовый блок, который похож на базовый блок, который уже встречался, т.е. отличается лишь небольшим количеством характеристик, будет классифицирован таким же образом. В этом и заключается сила машинного обучения — мы обучаем наш классификатор распознавать то, чего он никогда раньше не видел, используя данные, которые, как мы надеемся, являются репрезентативными для проблемного пространства.
Итак, теперь, когда мы понимаем, как используется классификатор, давайте попробуем!
Испытание 1: Начните с простого
Для первой попытки я сделал все как можно проще. Я проигнорировал все те умные вещи, которые я упоминал об использовании вероятностей для классификации блоков как значимых или нет. Хотя у меня была интуиция, что эти вещи будут важны, важно оспаривать и проверять свои предположения, прежде чем действовать в соответствии с ними. Поэтому для этого испытания я рандомизировал весь набор данных, не заботясь о том, чтобы образцы из одного и того же бинара были вместе, и просто попросил свой классификатор предсказать CWE, которые, по его мнению, были у каждого блока.
Все прошло не очень хорошо.
Я имею в виду, что могло быть и хуже. Мой классификатор достиг ~25% точности, предсказав 27 классов CWE. Я построил матрицу путаницы, чтобы увидеть, не делает ли он что-нибудь сомнительное, например, классифицирует образцы только как класс 121 или 122 (потому что, если вы помните, данные были перекошены в сторону этих классов).
Ошибки встречаются повсеместно. Большинство классов действительно ошибочно классифицируются как 121 чаще, чем другие, но не слишком часто. Тот факт, что ошибки так разбросаны по разным классам, обнадеживает, поскольку это может указывать на то, что мой подход заслуживает доверия. Возможно, что основные блоки, которые неправильно классифицируются, довольно часто встречаются в большинстве классов, и поэтому их неправильно классифицируют как любой из них.
Чтобы выяснить, так ли это, я посмотрел на вероятности, которые были получены в процессе классификации. Я хотел получить общее представление о том, как выглядит разброс вероятностей, поэтому я рассчитал несколько основных статистических показателей.
Highest probability ever seen: 0.9560759838758044
Lowest probabilities ever seen: 2.4691830828330134e-09
Average difference between in-sample highest and lowest probabilities: 0.32504693947187246
Standard deviation of in-sample probability differences: 0.06998244753425269
Хм. Похоже, что в среднем классификатор довольно уверен в том, что он классифицирует. Но среднее значение может быть искажено тем, что он уверенно классифицирует блоки с уязвимостями и гораздо менее уверен в остальных. Если бы это было так, я бы ожидал увидеть бимодальное распределение разницы вероятностей в выборке. Итак, давайте построим график.
Это можно считать бимодальным распределением. Не совсем то разделение, на которое я надеялся, между двумя модами, но на графике определенно есть второй всплеск. Вы заметите, что наше среднее значение находится прямо между двумя пиками, примерно на уровне 0,3.
На данный момент я все еще не знаю, классифицируют ли точки данных с более высокой разницей вероятностей лучше, чем те, которые не классифицируют. Однако есть один простой способ выяснить это — попробовать. Я написал код для отбрасывания классифицированных блоков, у которых разница вероятностей в выборке была меньше 0,4, а затем посмотрел на результаты.
Испытание 2: Проверка вероятности
Correctly identified 90/347
Accuracy: 0.259365994236
Не очень убедительно. Давайте попробуем увеличить порог до 0,45.
Correctly identified 42/137
Accuracy: 0.306569343066
Точность увеличивается, но не так сильно, как хотелось бы. И матрицы путаницы все еще имеют тот же общий разброс. Честно говоря, совсем не похоже, что разница между самой высокой и самой низкой вероятностью в предсказании коррелирует с точностью классификатора. Я попробовал еще раз при 0,5.
Correctly identified 17/67
Accuracy: 0.253731343284
…и точность снизилась. Я готов поверить, что это просто не даст эффекта.
Пришло время оценить, где мы находимся, и изменить подход. Похоже, что мой подход, использующий предсказанные вероятности для отсеивания неинформативных базовых блоков, может не сработать, однако я хотел бы попробовать не разделять двоичные файлы на обучающие и тестовые данные, чтобы исключить вероятность того, что это мешает классификатору узнать, какими являются общие базовые блоки. Это кажется маловероятным, но это возможно. Также возможно, что наши данные нуждаются в лучшей маркировке, чтобы быть полезными — это потребует времени, которого, как мне кажется, у меня нет для этого проекта. Мы можем перефразировать данные, включив в них подсчет количества раз, когда инструкция встречалась в базовом блоке, а не только то, встречалась она или нет. Это даст классификатору больше информации для работы, что может повысить его точность.
Далее: часть 3