Любые программные системы включают в себя нужные и не очень нужные пакеты. Получается огромный объём кода (для одного несложного сайта npm list -a выдаёт список из 4256 зависимостей). А так как «весь код — это ваш код», то такие зависимости надо тестировать. И регулятор требует, да и просто собственные продукты хочется защитить от вторжений, утечек и других неприятностей.

Что там внутри semver? Может ли он вызвать проблемы?
Что там внутри semver? Может ли он вызвать проблемы?

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

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

Если поверхность атаки заранее не проанализировать, а сразу начать искать баги, придётся изучать предупреждения статического анализатора для всей кодовой базы (а их счёт будет идти на многие тысячи). Или фаззить все-все функции. Получится ситуация, как с теми жандармами, которые искали у Ленина запрещённую литературу по всей библиотеке. Если бы они знали нужную полку, то смогли бы что-то найти. Вместо этого устали только.

Жандармы ищут уязвимости, не определив заранее поверхность атаки.
Жандармы ищут уязвимости, не определив заранее поверхность атаки.

Оказывается, точную поверхность атаки найти невозможно

Но всё же так хорошо начиналось. Почему невозможно? Всё дело в фундаментальных ограничениях методов анализа, статических и динамических.

Статический анализ — это исследование программы без её запуска. По исходному или бинарному коду. И тут сразу появляется проблема — входные данные программы неизвестны, пока её не запустят. И даже не только всей программы, но и отдельных функций.

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

sprintf(name, “prefix_%s_suffix.so“, input);
h = dlopen(name, RTLD_NOW);

Получается, что статический анализ никак не сможет определить эту зависимость. Но постойте. Если использовать динамический анализ, при котором программа запускается, можно же получить точную информацию о её поведении?

Оказывается, и тут без недостатков не обходится. Во-первых, не все ветви кода будут выполняться.

if (rand() == 42)
    recvfrom(sfd, buf, BUF_SIZE, 0, &addr, &addrlen);

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

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

x = read();
y = x + 3;
z = y – x;

Вот тут для вычисления переменной z вроде бы используются входные данные. Но если приглядеться, переменная всегда будет равна 3.

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

Интерфейсы

Самая очевидная часть поверхности атаки — это интерфейсы, то есть способы взаимодействия программы (или системы) с внешним миром. Очевидная потому, что, их легче всего обнаружить. Например, открываемые файлы легко определить с помощью strace, а открытые сетевые порты с помощью nmap.

Ещё один популярный тип интерфейса — это web API. Программа принимает внешние запросы через HTTP(s) и рассылает ответы. Точки входа и их параметры (передаваемые в пакете строки вроде /user/add?name=ada) распознаются веб-сервером и приводят к активации нужных функций.

Чтобы найти все эти точки входа, существует множество сканеров вроде Pentest tools, BurpSuite, Nikto.

Pentest tools нашёл точку входа, уязвимую для SQL-инъекций
Pentest tools нашёл точку входа, уязвимую для SQL-инъекций

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

Если речь идёт о целой системе, то можно взять auditd. Это почти как strace, только он отслеживает все программы, а также может сразу сообщать об обращениях к неправильным файлам или программам. Правда правила для поиска таких обращений придётся придумать самостоятельно (или поискать в интернете).

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

Полный список отслеживаемых сущностей
  • File system (static snapshot and live monitoring available)

  • User accounts

  • Services

  • Network Ports

  • Certificates

  • Registry

  • COM Objects

  • Event Logs

  • Firewall Settings

  • Wifi Networks

  • Cryptographic Keys

  • Processes

  • TPM Information

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

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

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

Ручной анализ поверхности атаки

Чтобы найти код, ответственный за работу нужного интерфейса, можно покопаться в программе вручную. Это не всегда легко. Например, в репозитории keycloak — 34 мегабайта исходников. Хорошо, что они на Java, там не будет указателей на void. Тем не менее, чтобы разобраться с работой приложения, понадобится много времени.

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

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

Для успешной отладки всегда нужно терпение.
Для успешной отладки всегда нужно терпение.

Способ хороший, но трудоёмкий. К тому же, если данные копируются в разные места, как при разборе сложной структуры данных, разбираться придётся долго. А если нужно поверхность атаки тестировать для каждого релиза (вдруг новые утечки появились), придётся перепроверять её вручную. Однако, когда надо просто один раз поковыряться в конкретной версии конкретной программы, отладчик вполне подойдёт.

Инструмент для определения поверхности атаки

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

Тут подойдёт инструмент Natch. Он предназначен именно для определения поверхности атаки. Отслеживая входные данные программ, Natch определяет, куда эти данные пошли, и как были использованы. Например, сетевой пакет проходит через web-сервер, затем попадает в backend на питоне, а после этого часть пакета записывается в базу данных.

Большое количество программ обмениваются большим количеством данных.
Большое количество программ обмениваются большим количеством данных.

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

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

При помощи отслеживания потоков данных, аналитик может оценить наличие ненужных интерфейсов в программе, так как Natch видит всё, что происходит в системе. И если программа открывает файл /etc/passwd, обращается к серверам «телеметрии», запускает фоновые процессы, можно это обнаружить и исправить.

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

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

Либо, если помечены были чувствительные данные, тогда стоит искать, не обрабатываются ли они там, где не следует. Например, в заимствованном коде, который ещё непонятно как устроен. И тогда этот код надо либо тестировать, либо удалить.

Дерево вызовов для нативного приложения.
Дерево вызовов для нативного приложения.

Natch анализирует не только нативные приложения. Также поддерживается анализ программ на Python и Java, для них тоже можно посмотреть дерево вызовов.

Дерево вызовов для Java-приложения.
Дерево вызовов для Java-приложения.

Последние два отчёта помогают не только увидеть поверхность атаки, но и выбрать цели для фаззинга. Ведь если недоверенный пользователь может «заслать» данные во внутреннюю функцию программы, то надо протестировать её тщательнее, чем всё остальное. Без тестирования всплывают очень неожиданные баги, которые как минимум могут привести к DOS-атаке.

Заключение

Если удаётся покрыть тестами 100% своего кода, это очень хорошо. Поверхность атаки искать не нужно, можно расслабиться. Главное при этом не забыть и про заимствованный код тоже, ведь он выполняется наравне с собственноручно написанным.

Ссылки

  1. Руководство пользователя Natch

  2. Телеграм-канал поддержки Natch

  3. Кейсы реального использования Natch

Комментарии (3)


  1. positroid
    09.07.2024 06:51

    Если удаётся покрыть тестами 100% своего кода, это очень хорошо. Поверхность атаки искать не нужно, можно расслабиться.

    Но ведь тесты обычно покрывают функциональную часть кода, успешное прохождение тестов ведь не означает, что он безопасен?


    1. Dovgaluk Автор
      09.07.2024 06:51
      +2

      Если глобально, то никакое тестирование не гарантирует ни безопасность, ни отсутствие ошибок.

      Но покрытие используется как некий индикатор качества, за неимением лучшего.


  1. andrettv
    09.07.2024 06:51

    Тест может показать наличие ошибок, но ни один тест не может доказать их отсутствия.

    (c) Дейкстра