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

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

Примерно в то же время, когда я начал свою научную работу в этой области (2012 год), был опубликован довольно удивительный инструмент символьного выполнения, который не просто крашил программы, но автоматически их взламывал, под названием Mayhem. Хотя это было более десяти лет назад, с тех пор я не перестаю удивляться успеху Mayhem. Как уже было сказано, Mayhem обещал стать чем-то вроде универсального инструмента для поиска ошибок, который, как я давно считал, можно реализовать с помощью символьных методов, но, похоже, на рынке его не существовало.

Недавно я узнал, что с помощью Mayhem можно бесплатно тестировать программное обеспечение по предложению компании ForAllSecure (это название — игра терминологии, используемой в математических моделях компьютерных программ, я почти уверен). В эти выходные я воспользовался возможностью немного поиграть с платформой ForAllSecure Mayhem. Результаты порадовали и разочаровали.

После перехода на веб-сайт Mayhem и создания новой учетной записи с помощью одного из провайдеров единого входа, что было безболезненно и просто, мне был представлен экран, на котором мне предлагалось указать Mayhem на образ контейнера в Docker Hub:

Я был немного удивлен, что не было никакого механизма для входа в другой реестр контейнеров (или учетных данных реестра), но, к счастью, Mayhem предоставляет пару примеров проектов. Я выбрал первый, который оказался уязвимым образом http-сервера.

После выбора уязвимого образа мне был представлен экран для настройки «Mayhemfile», который будет контролировать мой тестовый запуск. Прежде чем идти дальше, я переключил пользовательский интерфейс в темный режим, который не был помечен как экспериментальный, но, вероятно, должен быть таковым. Как вы можете видеть на изображениях ниже, некоторые из основных элементов пользовательского интерфейса трудно (или даже невозможно) прочитать.

Я оставил все настройки в их состоянии по умолчанию/предварительно заполненном и решил, что, поскольку это было указано в качестве примера, все, вероятно, будет в порядке. Я ничуть не разочаровался: за минуту Mayhem нашла «недостаток» в уязвимом сервере:

Это круто! Сервер lighttpd является более сложным, чем все, что я когда-либо тестировал с помощью Klee, и, похоже, он работал без специальной компиляции, без сложной системы тестирования и т. д. Просто наведите инструмент на образ контейнера, и он мгновенно найдет ошибку.

Удовлетворенный тем, что я могу управлять Mayhem достаточно хорошо, чтобы фаззить сетевые входы на контейнерный сервер, я решил попробовать свой собственный пример. В качестве первой жертвы я выбрал пример RESTful API, написанный на языке программирования Go с использованием фреймворка Gin. У меня нет реального опыта работы с этим фреймворком или примером, но он кажется достаточно простым для контейнеризации. Я следовал руководству на своем локальном компьютере, чтобы убедиться, что оно работает, а затем следовал официальным инструкциям Docker по контейнеризации приложения Golang. В итоге я получил следующий файл main.go и файл Dockerfile.

package main

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

// album represents data about a record album.
type album struct {
    ID     string  `json:"id"`
    Title  string  `json:"title"`
    Artist string  `json:"artist"`
    Price  float64 `json:"price"`
}

// albums slice to seed record album data.
var albums = []album{
    {ID: "1", Title: "Blue Train", Artist: "John Coltrane", Price: 56.99},
    {ID: "2", Title: "Jeru", Artist: "Gerry Mulligan", Price: 17.99},
    {ID: "3", Title: "Sarah Vaughan and Clifford Brown", Artist: "Sarah Vaughan", Price: 39.99},
}

func main() {
    router := gin.Default()
    router.GET("/albums", getAlbums)
    router.GET("/albums/:id", getAlbumByID)
    router.POST("/albums", postAlbums)

    router.Run("localhost:8080")
}

// getAlbums responds with the list of all albums as JSON.
func getAlbums(c *gin.Context) {
    c.IndentedJSON(http.StatusOK, albums)
}

// postAlbums adds an album from JSON received in the request body.
func postAlbums(c *gin.Context) {
    var newAlbum album

    // Call BindJSON to bind the received JSON to
    // newAlbum.
    if err := c.BindJSON(&newAlbum); err != nil {
        return
    }

    // Add the new album to the slice.
    albums = append(albums, newAlbum)
    c.IndentedJSON(http.StatusCreated, newAlbum)
}

// getAlbumByID locates the album whose ID value matches the id
// parameter sent by the client, then returns that album as a response.
func getAlbumByID(c *gin.Context) {
    id := c.Param("id")

    // Loop through the list of albums, looking for
    // an album whose ID value matches the parameter.
    for _, a := range albums {
        if a.ID == id {
            c.IndentedJSON(http.StatusOK, a)
            return
        }
    }
    c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not found"})
}
FROM golang:1.20

WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY *.go ./

RUN CGO_ENABLED=0 GOOS=linux go build -o /go-api

EXPOSE 8080
ENV GIN_MODE=release

CMD ["/go-api"]

Как вы можете видеть из приведенных выше файлов, это создает экземпляр сервера, который прослушивает HTTP-запросы на порту 808. Я запустил контейнер локально, затем выполнил команду curl внутри контейнера и успешно получил ответ. Удовлетворенный тем, что небольшой пример сработал, я продолжил свое путешествие по тестированию.

Отправив образ в Docker Hub, я пошел и создал новый запуск в Mayhem. Mayhem автоматически обнаружил открытый порт (8080, а не порт 80 из примера lighttpd) и соответствующим образом изменил Mayhemfile. Взволнованный, я начал прогон, но мои надежды на быстрый тест быстро рухнули.

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

  • Рассмотрите возможность перезапуска цикла позже. Если проблема не устранена, обратитесь к администратору. Как пользователь бесплатного уровня, я понятия не имею, кто мой администратор и как с ним связаться. Мой запуск завершился неудачно из-за нехватки ресурсов на стороне сервера или он неправильно настроен? Если он не работает из-за моей конфигурации, почему его запуск позже будет другим?
  • Smoketesting обнаружил ошибку, которую не смог устранить. Пожалуйста, проверьте журнал событий для получения дополнительной информации. Интересно — интересно, где находится журнал событий? В конце концов я нашел его в меню с многоточием рядом с названием прогона, но оно предоставило мне ту же информацию, что и сообщения об ошибках на экране. Облом!
  • Похоже, что ваша цель настроена как сетевой сервер, но подходящие соединения не обнаружены. А, вот мы и добрались до нужного места. Демонстрация Gin API прослушивает только петлевое соединение, но если Mayhem собирается протестировать это вне контейнера, мне нужно быть немного более смелым в моей конфигурации.

Я изменил localhost на 0.0.0.0 в своем файле main.go, чтобы сервер прослушивал все IP-адреса, построил и отправил образ, а также создал новый запуск в Mayhem. Я был быстро вознагражден новым набором сообщений об ошибках, все еще в той красивой розовой коробке:

На этот раз Mayhem предложила мне автоматическое решение, призванное решить мои проблемы. Mayhem сообщает, что мой Mayhemfile имеет новые значения: в частности, URL-адрес сети был заменен неуказанным адресом для IPv6, [::]. Это сообщение об ошибке было в лучшем случае неинтуитивным:

  • Формулировка сообщения звучит так, будто Mayhem уже применил изменения к моему файлу Mayhemfile — оно даже предупреждает меня, что это может повлиять на производительность.
  • Неуказанный адрес (0.0.0.0 для IPv4, [::] для IPv6) обычно используется для привязки сервера к любому IP-адресу. Однако он не будет использоваться для обращения к этому серверу: для этого вы захотите использовать петлевой адрес (127.0.0.1 или [::1], в зависимости от вашей версии IP).

Из-за формулировки ошибки и того факта, что мои локальные тесты прошли нормально (обращение к конечным точкам API с использованием IPv4-адреса, назначенного контейнеру), я потратил довольно много времени на возню со своим контейнером, прежде чем загрузить файл strace. из пользовательского интерфейса Mayhem. Там я обнаружил несколько вызовов bind, которые выглядели так:

bind(8,{sin_family=AF_INET6,sin_port=htons(8080),sin_addr=inet_addr("0000:0000:0000:0000:0000:0000:0000:0000")}, 28) = 0

Ага! Когда Mayhem запускает мой контейнер, похоже, что контейнер привязывается, используя только IPv6. Это странно, потому что контейнер отлично работает локально, а пример lighttpd вполне устраивал localhost в качестве адреса, но, видимо, в этом контейнере есть что-то другое (и среда выполнения Mayhem по сравнению с моей локалью). Я изменил запись cmd.network.url Mayhemfile с tcp://localhost:8080 Mayhem по умолчанию на tcp://[::1]:8080 и снова запустил задание. Это привело к успешному запуску, когда Mayhem предоставлял сетевые входные данные для моего контейнера в течение 10 минут, ни разу не сбив его:

Честно говоря, на данный момент я очень разочарован пользовательским интерфейсом Mayhem и Mayhem в целом. Это не была утопия символического выполнения, которую я искал: я не хочу устранять проблемы с IPv4 и IPv6, я просто хочу запускать тесты! Что действительно меня заводит, так это качество сообщений об ошибках: если параметры на самом деле не изменились, не сообщайте пользователю, что «параметры имеют новые значения». И если вы собираетесь указывать новые значения, убедитесь, что они правильные, а не просто близкие.

Возможно, настоящая проблема здесь в том, что я использовал веб-интерфейс вместо CLI. Я решил загрузить интерфейс командной строки и посмотреть, не будет ли этот опыт лучше — к тому же, мне показалось, что это простой способ увидеть 165 тестовых случаев, сгенерированных Mayhem для сервера API.

Установить CLI было так же просто, как скопировать команду из документации, но дальше опыт пошел не так.

После загрузки инструмента CLI mayhem и входа в систему (что неожиданно потребовало от меня создания токена API без явной даты истечения срока действия!), я выполнил то, что казалось естественным первым шагом в соответствии со страницей Команды CLI Mayhem: я использовал mayhem list для перечислите проекты и цели, которые я запускал. К этому моменту я создал несколько проектов и запусков, устраняя неполадки в примере Go API, описанном выше. Каково же было мое удивление, когда я получил от команды список из 137 проектов!

Похоже, мои несколько проектов были в этом удивительно большом списке, поэтому я запустил mayhem download против одного из них и был вознагражден успешной загрузкой во временный каталог. Но как насчет всех остальных? Я немного волновался, что каким-то образом столкнулся с проблемой контроля доступа на сервере Mayhem, но решил копнуть немного дальше.

Я попытался загрузить все проекты в списке, но для тех, которые не были моими, я получил сообщение об ошибке Такого проекта не существует. Вау! Может быть, есть проблема, позволяющая мне перечислить чужой проект, но, по крайней мере, я не могу его прочитать.

Не зная, что делать дальше, я просто набрал mayhem, чтобы получить справочное сообщение. Это показало мне, что я могу запустить команду списка с аргументом --verbosity debug, чтобы получить дополнительную информацию. Я так и сделал, и в награду получил список всех вызовов API, которые mayhem делал от моего имени. Это показало мне, что сотни с лишним неожиданных записей принадлежат другим пользователям. Вернувшись к веб-интерфейсу, я повозился с URL-адресом одного из моих запусков и смог увидеть проект, о котором мне говорил интерфейс командной строки. Кроме того, я просмотрел ответы API из моей команды подробного списка, и ответ API убедительно сообщает мне, что эти проекты имеют атрибут access_level, равный "AUTHENTICATED", а не “PRIVATE”, как это было установлено в моих проектах. Итак, никаких проблем с контролем доступа здесь, в конце концов, нет. Но этот список очень раздражает. Просматривая Документацию Mayhem CLI на веб-сайте, я обнаружил полезную информацию о команде mayhem list:

Я любезно вызвал mayhem list -n crcady, чтобы отфильтровать только мои проекты. Я получил следующую очень полезную ошибку: беспредел: ошибка: нераспознанные аргументы: -n crcady. Ха, странно, я взял это прямо со страницы официальной документации. Решил проверить справку локально:

$ mayhem list --help

usage: mayhem list [-h] [--owner OWNER] [--url URL] [--token TOKEN] [-k] [--cacert CACERT] [--timeout TIMEOUT]

List projects and targets you have run.

optional arguments:
  -h, --help         Show this help message and exit.
  --owner OWNER      Filter to only show results for the provided owner (user or organization).
  --url URL          URL to running Mayhem API.
  --token TOKEN      Authentication token for accessing Mayhem API.
  -k, --insecure     Disable SSL verification.
  --cacert CACERT    Path to the mayhem server's certificate.
  --timeout TIMEOUT  Seconds to wait for API responses (useful for slow connections).

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

Запуск mayhem list --owner crcady не потерпел неудачу, как это было с коротким вариантом, но он также фактически не фильтрует только мои проекты: я все еще получаю 137 проектов из CLI.

Итак, подытожу мой опыт работы с CLI:

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

На данный момент я практически забыл, зачем я пришел сюда (на самом деле посмотреть на результаты символического выполнения), поэтому я думаю, что приберегу это для другого поста. Надеюсь, к тому времени некоторые проблемы с пользовательским интерфейсом Mayhem будут решены!

Я очень, очень хочу быть в восторге от Mayhem от ForAllSecure. Я люблю символическое выполнение, и это самое близкое, что я видел, к тому, чтобы на самом деле «создать» очень сложную задачу. При этом похоже, что ForAllSecure нужно нанять несколько дизайнеров в свою команду и перестать позволять инженерам выполнять работу с пользовательским интерфейсом.

Это все еще больше расстраивает, потому что, если бы это был продукт с открытым исходным кодом, я был бы рад помочь улучшить его для сообщества. К сожалению, это проприетарный продукт, который просто использует массу основ OSS, некоторые из которых пользовались обильным государственным финансированием (а в случае BAP — некоторой спонсорской поддержкой самой ForAllSecure).

В конце концов, с помощью этого инструмента я получил довольно интересные идеи, даже на самом простом примере API, и я расскажу об этом в своем следующем посте!