Module 2.4

Поиск дефектов

Эта часть курса посвящена дальнейшему обнаружению дефектов в программном обеспечении.

Программное обеспечение считается безопасным, если оно продолжает вести себя корректно даже в неожиданных ситуациях (при этом можно спорить о том, что именно считать корректным). Однако для создания безопасного программного обеспечения необходимо понимать, как оно ломается. Когда вы понимаете, как оно ломается, вы знаете его слабые места. Эти слабые места могут быть использованы для создания эксплойта против программного обеспечения. Один из способов исправить ситуацию — устранить слабые места, но иногда исправление может вернуть вас к этапу проектирования и заставить переработать систему.

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

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

Бинарные эксплойты

Здесь мы используем термин «бинарный эксплойт» как обобщающий термин для эксплуатации скомпилированных приложений способом, который приводит процессор к нежелательному поведению. Это включает доступ к памяти приложения или ее изменение (либо к областям памяти за пределами предполагаемых границ приложения).

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

Как работает ошибка Heartbleed

(источник xkcd: How the heartbleed bug works.)

Перед выполнением программа загружается в память. После загрузки в память программа может быть выполнена: для этого операционная система создает процесс. Операционная система также выделяет процессу участок памяти для использования.

Участок памяти делится на четыре секции: стек, кучу, код и данные.

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

Секция кода содержит исполняемый код программы, а секция данных хранит глобальные и статические переменные.

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

Обратите внимание, что описанное выше сильно зависит от способа реализации выполнения.

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

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

Поиск вредоносного кода можно упростить, записав перед вредоносным бинарным кодом большой набор инструкций NOP (No Operation Instruction). Инструкция NOP ничего не делает, а только продвигает выполнение к следующей инструкции. Если мы переходим в такой большой блок NOP, указатель инструкций просто сдвигается вперед, пока не достигнет нашего вредоносного кода — такая техника называется NOP slide.

Один из источников потенциальных бинарных эксплойтов — неправильное управление памятью. В некоторых языках, например C, динамической памятью вручную управляет программист. Нежелательное поведение может возникнуть, например, когда программист забывает выделить (зарезервировать) память, выделяет неправильный объем памяти, забывает освободить память (что приводит к утечке памяти) или пытается использовать память, которая уже была освобождена.

Бинарные эксплойты и языки более высокого уровня

Можно подумать, что о бинарных эксплойтах не нужно беспокоиться, если вы пишете на языке более высокого уровня, таком как Ruby, Python или Java. К сожалению, ваше приложение все равно может быть уязвимо к тем же проблемам.

Если язык более высокого уровня интерпретируемый, интерпретатор может использовать небезопасный язык. Например, Ruby и Python реализованы на C. Если в их коде есть уязвимость безопасности, например переполнение буфера, все программное обеспечение, использующее уязвимую часть, также уязвимо к той же проблеме.

Многие программы, написанные на языках более высокого уровня, используют так называемые нативные расширения. Нативные расширения — это функциональность, реализованная на языке более низкого уровня, в основном по соображениям производительности. Большинство таких расширений написано на C и подвержено тем же рискам безопасности, что и все остальные C-программы.

Фаззинг

Как проверять наличие таких дефектов? Разработчик может прочитать код и попытаться найти проблемы либо использовать автоматизированный инструмент статического анализа кода для поиска подозрительных конструкций. Другой способ найти проблемы — попробовать как можно больше различных входных данных. Это называется фаззингом (по доктору Миллеру, в начале 1990-х годов).

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

Некоторые хорошие начальные значения, которые фаззер должен учитывать:

  • Длинные строки
  • Пустые строки
  • Максимальные значения
  • Минимальные значения
  • Малые значения
  • Большие значения
  • Максимум, деленный на 2, 3 и 4, а также близкие значения
  • null
  • Символы новой строки
  • Точки с запятой и другие разделители
  • Значения форматных строк (%s)
  • Ключевые слова

Фаззеры полезны, потому что при небольших усилиях они могут создавать входные данные, проверяющие самые очевидные случаи. Фаззинг может найти ошибки, пропущенные при ручном ревью кода, и дает общее представление об устойчивости кода. Однако фаззеры не могут найти ошибки, которые изменяют выполнение программы, но не приводят к сбою. Кроме того, когда вы находите сбой, вам все еще нужно определить его настоящую причину. Самый простой способ обнаруживать сбои — отслеживать использование CPU процессом либо запускать приложение под отладчиком или другим тестовым стендом.

Слепой фаззинг

Самый простой способ фаззинга — создавать входные данные случайно, без каких-либо знаний о корректных входных данных или контексте. Это называется слепым фаззингом. Самая простая реализация — передавать /dev/random в Linux в качестве входа. Это эффективно, потому что требует очень мало усилий для создания фаззинговых данных. Такой вид фаззинга можно использовать, например, против алгоритмов сжатия. С другой стороны, полная случайность одновременно является его слабым местом. Многие приложения ожидают определенную структуру входных данных. Рассмотрим приложения для обработки изображений: они ожидают, что считываемый файл (входные данные) начинается определенным образом. Например, JPEG File Interchange Format (JFIF) указывает, что файл начинается с FF D8 в шестнадцатеричном виде. Если первые байты не распознаны, приложение должно сообщить пользователю о некорректном формате файла. В качестве другого примера, сетевые протоколы ожидают, что данные будут представлены в некоторой структуре, содержащей пары ключ-значение и даже длины полей. Таким приложениям нужен более интеллектуальный фаззинг, чем просто слепой фаззинг. Фаззер должен иметь хотя бы базовое знание о том, что делает входные данные корректными; это усложняет создание фаззинговых данных и превращается в баланс между объемом работы, который вы вкладываете в создание фаззинга, и полнотой покрытия входных данных, которую вы хотите получить для целевого приложения.

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

Умные фаззеры

Если фаззинг на основе мутаций называют dumb-fuzzing, то фаззинг на основе генерации называют интеллектуальным фаззингом. При фаззинге на основе генерации входные данные создаются с нуля на основе используемой спецификации или формата входных данных целевого приложения или протокола. Затем создание входных данных разделяется на фрагменты, которые строятся по порядку и независимо. Случайность вводится внутри фрагментов. Благодаря этому входные данные не полностью сломаны, но содержат отдельные фаззированные части. Способ разделения входных данных на фрагменты обычно определяется форматом входных данных: например, поля вроде пар ключ-значение хорошо подходят в качестве точек разделения.

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

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

Вероятно, наиболее сложным и трудоемким является фаззинг на основе моделей, также известный как синтаксическое тестирование. При фаззинге на основе моделей входные данные моделируются с помощью контекстно-свободных языков. Среди часто используемых языков есть, например, Backus-Naur Form или Backus Normal Form (BNF), Abstract Syntax Notation (ASN.1) или doctype Extensible Markup Language (XML). С помощью этих языков моделируются поля синтаксиса, затем они изменяются и передаются целевому приложению в качестве входных данных. Моделирование с помощью контекстно-свободных языков может быть трудоемким и сложным, но сложные протоколы, такие как Transport Layer Security (TLS), было бы трудно фаззить другими методами.

Фаззинг в вебе

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

Далее мы подробнее рассмотрим инструменты для этого.

Раздел инструментов

В этом разделе дается краткое введение в несколько распространенных инструментов, используемых на практике. Это лишь небольшой обзор, а дальнейшие руководства и примеры можно найти в Интернете.

Фаззинг

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

  • Собрать примеры входных данных
  • Фаззить примеры
  • Запустить тесты
  • Дождаться сбоя
  • Начать заново

Что касается сбора набора примерных файлов, можно обойтись генерацией примеров из ничего случайным образом. Однако лучшие результаты можно получить, если начать с набора примеров. Примеры должны представлять особенности формата, который нужно фаззить. В случае сетевых протоколов хорошими отправными точками являются записи реального трафика (с помощью таких инструментов, как wireshark или tcpdump). Иногда подойдут и случайные файлы из веба. Например, набор доступных PDF из веба для тестирования PDF-инструментов или изображения кукурузы из веба для тестирования инструментов конвертации изображений.

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

Radamsa — хороший пример фаззера, созданного в Университете Оулу. Radamsa принимает набор корректных примеров и фаззит их. Вывод может содержать набор некорректных файлов. Radamsa можно получить в виде текущего Git-снимка со страницы Akihe в Gitlab (работает как минимум на GNU/Linux и Mac OS X с установленным git). Собрать ее можно так:

git clone https://gitlab.com/akihe/radamsa.git
cd radamsa; make
bin/radamsa --help

Заставить radamsa производить фаззированный вывод довольно просто. В следующем примере строка передается по конвейеру в radamsa, а radamsa печатает ее мутированную версию. Создавать примеры по одному утомительно. К счастью, radamsa предоставляет опцию -n, с помощью которой можно задать, сколько примеров нужно создать.

[user@laptop ~]$ echo "Cyber Security Base 101" | ./radamsa
Cyber Security Base 17011

[user@laptop ~]$ echo "Cyber Security Base 101" | ./radamsa -n 10
Cyber Security Base 1
Cyber Security Base 1
Cyber Security Base 340282366920938463463374607431768211455
Cyber Security Base 101
Cyber Security Base -1601859698619483283371
Cyber Security Base -1097337118446744073709551617
Cyber Security Base 0
Cyber Security Base 128
Cyber Security Base 102
Cyber Secu‪rity Base 101
Cyber Secu‪rity Base 128

С файлами проще использовать опцию -r, чтобы radamsa прочитала все примеры, и опцию -o, чтобы записать мутированные примеры в некоторую папку. С помощью знака % можно добавить номер итерации к имени файла.

radamsa -n 100 -o output-samples/img-%n.jpg -r orig-samples/

Сама по себе Radamsa не передает плохие данные целевому программному обеспечению. Это нужно сделать каким-либо способом. Ниже приведен простой пример Bash-скрипта, который берет изображения, созданные в предыдущем примере, передает их инструменту командной строки ImageMagick, немного изменяет их размер и выполняет для них конвертацию.

#!/bin/bash
for f in output-samples/*
do
	convert "$f" -resize 99% imgmgck-output/$(basename "$f" | cut -d . -f1).png
done

ImageMagick будет жаловаться на некорректно сформированные изображения и т. п. (примерно половина мутированных изображений была повреждена). Но как понять, что что-то действительно пошло не так и программа аварийно завершилась? Один из способов — проверить, что вернула convert, с помощью $?, которая дает возвращаемое значение последней команды. С классической командой test можно проверить, был ли результат больше 127 (признак фатального завершения программы), и прервать выполнение цикла.

#!/bin/bash
for f in output-samples/*
do
	convert "$f" -resize 99% imgmgck-output/$(basename "$f" | cut -d . -f1).png
	test $? -gt 127 && break
done

Более подробные инструкции можно найти на странице Akihe в Gitlab.

Тестовые стенды и отладка

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

GNU Project debugger (GDB) — это отладчик, который позволяет нам проверять, что происходит внутри другой программы во время ее выполнения. С помощью GDB можно устанавливать точки останова в коде, останавливать выполнение при заданных условиях или исследовать, что произошло при сбое программы.

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

Наконец, strace и ltrace — это диагностические, отладочные и обучающие утилиты пользовательского пространства для Linux. Strace используется для мониторинга взаимодействий между процессами и ядром Linux, включая системные вызовы, доставку сигналов и изменения состояния процесса, тогда как ltrace используется для трассировки вызовов библиотек.

Loading

Burp Suite

До этого момента эта часть курса была сосредоточена на низкоуровневых инструментах для тестирования безопасности приложений. Теперь пора представить первую платформу тестирования безопасности веб-приложений. На самом деле это набор инструментов, которые работают вместе для создания тестов. Он покрывает тесты для картирования и анализа уязвимостей и даже для их эксплуатации. Burp Suite можно легко расширить плагинами, например Bradamsa (radamsa для Burp Suite).

Burp Suite может работать как прокси между браузером и целевым приложением, перехватывая и изменяя трафик между ними. Он также содержит такие инструменты, как spider для обхода содержимого и функциональности, scanner для автоматического обнаружения известных уязвимостей, и intruder, который можно использовать для выполнения специализированных атак. Здесь мы немного рассмотрим прокси-инструмент и принцип его работы. В конце этого раздела инструментов мы кратко посмотрим на intruder и конкретно на его режим sniper с bradamsa.

Burp proxy можно использовать для перехвата трафика между браузером и целевым приложением. Он работает очень похоже на атаки man-in-the-middle. Чтобы проверить эту возможность, у нас есть простая страница входа, которая запрашивает ваши учетные данные. Чтобы Burp Suite захватывал трафик, режим intercept должен быть включен. В режиме intercept у вас есть возможность пересылать или отбрасывать пакеты.

Прокси захватывает HTTP-заголовки и сведения о cookie. Также в захвате видны имя пользователя и пароль формы ("user" / "passwd"). Обратите внимание, что это был HTTP, а не HTTPS (для HTTPS потребовалось бы немного больше работы с такими инструментами, как SSLstrip script).

Инструмент intruder используется для автоматизации атак против веб-приложений. Этот инструмент состоит из четырех типов атак: sniper, battering ram, pitchfork и cluster bomb. Чтобы эти атаки работали, инструмент автоматически ищет все позиции полезной нагрузки в захваченных пакетах (см. выделения на изображении ниже, заключенные в знаки §). Эти позиции используются атаками.

Затем эти позиции заменяются в атаке. Чем они заменяются, зависит от атакующего. Самый простой способ — составить список "хороших предположений", например использовать список распространенных паролей, которые легко найти в сети (небольшой фрагмент ниже). Также можно использовать такие инструменты, как Bradamsa, расширение Burp Suite, которое использует Radamsa для генерации полезных нагрузок intruder.

123456
password
12345678
qwerty
123456789
12345
1234
111111
1234567
dragon
123123
baseball
abc123
football
monkey
letmein
696969
shadow
master
666666
qwertyuiop
123321

В атаке sniper последовательно изменяется только одна из позиций. В battering ram несколько параметров могут заменяться из одного и того же списка полезных нагрузок. В атаке pitchfork можно одновременно использовать несколько списков полезных нагрузок для нескольких параметров. В cluster bomb используется несколько списков полезных нагрузок, а также все их комбинации и параметры. В этом примере используется атака sniper с небольшим набором распространенных паролей выше, и изменяется только полезная нагрузка пароля.

Затем атака пробует все пароли и собирает полученные от сервера ответы (здесь только 501 ответ, потому что на другой стороне фактически не было ничего подходящего для обработки запросов).

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

Вы дошли до конца этого раздела!

Не забудьте проверить свои баллы в индикаторе в правом нижнем углу материала!

В этой части:
  1. 1. Поиск дефектов