уязвимости в программном коде и борьба с ними

neon nadpis privetstvie 139784 1280x720 Игры для детей

По следам Petya: находим и эксплуатируем уязвимость в программном обеспечении

c81fa143ad644823bc46baad00fb208b

Нашумевшие события последних месяцев наглядно показывают, насколько актуальной является проблема критических уязвимостей в программном обеспечении. Массовые атаки на пользователей с помощью вирусов-шифровальщиков WannaCry и Petya осуществлялись путем удалённой эксплуатации уязвимостей нулевого дня в сетевых сервисах Windows – SMBv1 и SMBv2. Нахождение и эксплуатация уязвимостей для удаленного выполнения кода – задачка явно не из легких. Однако лучшим из лучших специалистов по информационной безопасности всё по зубам!

В одном из заданий очного тура NeoQuest-2017 необходимо было найти уязвимости в доступной по сети программе-интерпретаторе команд, и с помощью их эксплуатации выполнить код на удаленном сервере. Для доказательства взлома нужно было прочитать содержимое файлового каталога на сервере – там, по условию, размещался ключ задания. Участникам был доступен бинарный файл интерпретатора. Итак, лёд тронулся!

Начинаем исследование

Для начала запустим бинарник и посмотрим, что он собой представляет. Становится ясно, что исполняемый файл интерпретатора формата PE и предназначен для архитектуры Intel x64. Также ясно, что он скомпилирован с поддержкой DEP/ASLR, но без CFG. Сторонние библиотеки также не подгружаются.

920280444f514378b03b9b09fab71e7e

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

968f03bdf1ee46e7a5a95a42cd0d7dea

С поверхностным анализом закончено, пора реверсить – тур-то очный, время поджимает!

А что подавать на вход?

Первая промежуточная задача, которую необходимо решить – точное определение поверхности атаки. Уточним количество и формат команд, загрузив бинарник в IDA Pro. Функция main легко находится путем поиска выводимых на экран строк.

Анализ функции main позволяет утверждать следующее:

1. 1. Сначала адрес массива array на стеке функции main заносится в переменную array_base. В цикле ячейки массива инициализируются нулями. Условие выхода из цикла позволяет узнать размер массива – 100 (0x64) целочисленных ячеек.

190da17d5b124c24bdda51f26c728ff3

2. 2. Происходит вывод на экран приветствия и считывание команды пользователя. Присутствие строк «cls» и «whoami» говорит о вызове функции system (в дальнейшем это нам сильно пригодится).

7aa49d030a9c49f8b8403ff856cf6d63

3. Инициализируются переменные-регулярные выражения, для проверки корректности синтаксиса. Видно, что недокументированные команды отсутствуют. Кроме того, становится ясно множество допустимых параметров каждой инструкции.

654307690a3a4c1b868579bb63d6119c

4. Последовательно в бесконечном цикле производится проверка введенной команды на соответствие регулярным выражениям. Если соответствие какой-либо команде найдено – вычисляются аргументы у команды и вызывается её обработчик.

1127f1d2dce7435db539eda324902225

8b285e2202a343a5a53f587aa92caf0f

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

Уязвимость №1

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

18b278e5ccd147bbb364a5c021620122

Сначала при величинах, больших размера массива, мы наблюдаем нормальное поведение – выход с сообщением об ошибке. Однако при значениях индекса больших, чем 2147483647, программа то падает, то выдает какие-то значения.

3b902d81f083475cb79043b060bdcf51

Похоже, что индексы, которые трактуются как отрицательные числа, проходят проверку границ и выдают содержимое памяти по отрицательному индексу вне границ массива. Уязвимость найдена!

Почему так происходит? Уязвимость кроется в неверной обработке индекса в командах set/get. Численный индекс в массиве считывается как параметр команды set и переводится из строковой в численную форму с помощью вызова stoul. Эта функция возвращает беззнаковое целое.

e2aaa00b1321482f9dfbd861110e54e3

Однако при передаче параметра в функции SET/GET это же значение ошибочно приводится к знаковому целому – оно сравнивается с размером массива и влияет на результат команды знакового сравнения jl. Команда GET выдает значение ячейки памяти, отсчитанное от начала массива.

ce57f5e8fcc04243b48d1333f5f6f664

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

0a856a469f1f4d8d9f4d0bf7a66c23fd

Кроме того, в дальнейшем при эксплуатации нам понадобится адрес самого буфера на стеке. Как его найти? Взглянем на начало функции main и увидим, что переменная array_base хранит указатель на начало буфера.

02fe972eab704f1c9db914a5776ab8fb

На стеке эта переменная расположена по адресу rsp+0x358-0x328, а сам буфер начинается с адреса rsp+0x358-0x198.

e62f89d9771149fe8dd584abc7a73cfe

28fbe92a3eb848aaab2fb283f6f5f091

Значит, необходимо отступить (0x328-0x198)/4 = 100 четырехбайтовых ячеек от начала массива, чтобы прочесть переменную array_base. Искомые смещения: 4294967196, 4294967197.

9f04592d9e6c46b68c92d5157f614fc2

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

Уязвимость №2

До сего момента мы не исследовали функцию интерпретатора load. Посмотрим на неё повнимательнее. Очень скоро здесь обнаруживается классическое переполнение буфера на стеке. Введенная с клавиатуры строка — аргумент команды load — выделяется и передается как первый параметр функции-обработчика LOAD. Вторым параметром идет адрес искомого буфера.

f53ec1ce60864c1bbf82c0db844f3756

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

4eadd0334b6542d4a037ae164bf3e6a6

Передав достаточно большое количество символов на вход команде load, возможно переписать адрес возврата на стеке и с выходом из интерпретатора передать управление по контролируемому нами адресу. С учетом размера и расположения буфера на стеке, адрес первичной передачи управления необходимо размещать со смещением 4 * (100 + 2) байта (дополнительные 8 байт переписывают сохраненное на стеке значение регистра rbp).

Поскольку переполненный буфер расположен на стеке, а не в куче, то ROP-цепь будем располагать там же целиком – начиная с смещения 4 * (100 + 2) во входном параметре команды load.

f7fc16058b6f4097941f4868eb3215ce

Pwn it!

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

69918b66b3e94e07a0e1310483f39869

Очевидно, что функция, которая это делает и принимает в качестве параметров строки «cls» и «whoami», ведет себя подобно библиотечной функции system. Необходимо вызвать эту функцию, но в качестве выполняемой команды взять «dir» – вывод на экран содержимого локального каталога на сервере.

Построим ROP-цепь, позволяющую это сделать. Последовательность операций следующая:

071b79c0b8794bad9486e1b32d37b7c8

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

Теперь необходимо найти смещения нужных гаджетов в исполняемом файле интерпретатора. Первый необходим для помещения в rcx адреса строки «dir», второй — для вызова функции system_func. По смещениям 0x24c00 и 0x16ce6 соответственно в секции кода исполняемого файла интерпретатора находятся нужные участки кода.

27ba2d9df5284db0bfd99ff714fedd62

fcc2a6a87ae1485fb20aa065c15f8798

Строку «dir» мы поместим после гаджетов. Её адрес, таким образом, будет на 4*(100 + 2) + 6*4 байт больше адреса буфера.

Ниже приведен наш вариант скрипта для генерации содержимого буфера:

Всё готово, пора идти за ключом!

3ca9c59a9d6e4dd8a57003a49cabdffa

Искомый ключ: fb520eb552747437c09f2770a9a282ea.

Источник

Программные уязвимости

Термин «уязвимость» часто упоминается в связи с компьютерной безопасностью, во множестве самых различных контекстов.

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

Предпринималось много попыток четко определить термин «уязвимость» и разделить два его значения. MITRE, исследовательская группа, финансируемая федеральным правительством США, занимающаяся анализом и разрешением критических проблем с безопасностью, разработала следующие определения:

Согласно терминологии MITRE CVE:

[…] Уязвимость — это состояние вычислительной системы (или нескольких систем), которое позволяет:

Предпринималось много попыток четко определить термин «уязвимость» и разделить два его значения.

В MITRE считают, что атака, производимая вследствие слабой или неверно настроенной политики безопасности, лучше описывается термином «открытость» (exposure).

Открытость — это состояние вычислительной системы (или нескольких систем), которое не является уязвимостью, но:

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

Поиск

Продукты для дома

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

Бесплатные утилиты

Наши бесплатные утилиты помогают обеспечить защиту ваших устройств на базе Windows, Mac и Android.

О компании

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

Пробные версии

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

Связаться с нами

Наша главная цель – обеспечить вашу безопасность. Мы всегда готовы ответить на ваши вопросы и оказать техническую поддержку.

Источник

Ищем уязвимости в коде: теория, практика и перспективы SAST

Не будет большим преувеличением сказать, что рынок средств статического тестирования защищенности приложений (Static Application Security Testing, SAST) в наше время переживает самый настоящий бум.

f3ccc9aca7ecc510e3f531ce9983ad4a

Автор: Владимир Кочетков
Positive Technologies

Не будет большим преувеличением сказать, что рынок средств статического тестирования защищенности приложений (Static Application Security Testing, SAST) в наше время переживает самый настоящий бум. Не проходит и пары месяцев между публикациями очередных научных работ на эту тему, ежегодно на рынок выводятся все новые и новые инструменты статического анализа защищенности, а месту SAST в процессе разработки ПО отводятся целые секции на международных ИБ-конференциях. В условиях непрерывного информационного прессинга со стороны поставщиков инструментария SAST, нелегко разобраться в том, что есть правда, а что − не более, чем маркетинговые уловки, слабо коррелирующие с действительностью. Давайте попробуем понять, что же действительно под силу инструментам SAST и как быть с тем, что им «не по зубам». Для этого нам придется немного погрузиться в теорию, лежащую в основе современных средств статического анализа защищенности кода.

Тьюринг, Райс — вот эти вот все

TL/DR: задача статического тестирования защищенности программ алгоритмически неразрешима.

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

Теперь представьте, что одна из таких программ (назовем ее h) является анализатором, умеющим отвечать на простой вопрос: зависает ли произвольная программа p из множества P на заданном наборе данных n? Очевидно, что отвечать на этот вопрос h сможет только завершая свою работу и тем самым сообщая, что p зависает на n. Иными словами, если p(n) не останавливается, то h(p(n)) должна завершить свою работу за конечное число шагов, а если p(n) останавливается, то h(p(n)) должна зависнуть.

Ну, а теперь представьте, что произойдет, если мы попробуем ответить с помощью такого анализатора на вопрос: зависнет ли он сам, в результате анализа самого себя, анализирующего самого себя (ведь p может быть любой программой из P, значит она может быть и самой h)? В этом случае получается, что если h(h(n)) остановится, то анализ h(n) зависает, а если h(h(n))) зависает, то анализ h(n) останавливается. Но ведь h как раз и есть h(n), а, следовательно, мы здесь имеем противоречие и анализатор подобный h не имеет права на существование.

Описанное является вольным изложением доказательства Теоремы останова, сформулированной Алланом Тьюрингом (основоположником современной теоретической информатики) в далеком 1936-м. Данная теорема утверждает, что не существует такой программы, которая могла бы проанализировать другую программу и ответить на вопрос, остановится ли та на заданном наборе входных данных. Хорошо, но можем ли мы построить такую программу, которая дает ответ на вопрос о каких-либо других свойствах программ?

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

Давайте представим, что есть некоторый анализатор q, который принимает на вход произвольную программу p множества P и останавливается, если p относится к одному из классов. Пусть, для определенности, это будет класс A. Пусть pa — программа, относящаяся к классу A и зацикливающаяся на любом входе. Выберем также из класса B произвольную программу pb. Для каждой программы p определим программу p’, получающую на вход данные x и выполняющую следующий алгоритм:
1. p(p)
2. pb(x)

Теперь построим программу q’, которая получает на вход произвольную программу p, строит для нее p’ и вычисляет q(p’).

Если p’ зависает на первом шаге, значит p’ функционально тождественна pa (и относится к классу A), а, следовательно, q’ должна немедленно остановиться. Если p’ проходит первый шаг, то p’ функционально тождественна pb (и относится к классу B), а, следовательно, q’ должна зависнуть. Таким образом, для любой программы p, q'(p) останавливается тогда, когда p(p) не останавливается. Но в роли p может оказаться и сама q’, следовательно, p(p) останавливается только тогда, когда p(p) не останавливается. Снова пришли к противоречию.

Утверждение о том, что не существует такой программы, которая могла бы давать ответ на вопрос о наличии любых нетривиальных инвариантных свойств у произвольно взятой программы, доказал ученый Генри Райс в 1953 году. Фактически, его работа обобщает Теорему останова, поскольку свойство останавливаться на заданном наборе данных является нетривиальным и инвариантным. Теорема Райса имеет бесконечное множество практических значений, в зависимости от рассматриваемых свойств: «невозможно с помощью программы классифицировать алгоритм, реализуемый другой программой», «невозможно с помощью программы доказать, что две других программы реализуют один и тот же алгоритм», «невозможно с помощью программы доказать, что другая программа на любых наборах данных не входит в какие-либо состояния…» и т.п. И вот на последнем примере стоит остановиться подробнее.

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

А что есть уязвимость? Это возможность с помощью входных данных заставить процесс выполнения войти в такое состояние, которое приведет к реализации какой-либо из угроз в отношении обрабатываемой процессом информации. Следовательно, можно определить свойство защищенности любой программы, как ее способность в каждый момент времени оставаться, вне зависимости от изначальных входных данных, в рамках заранее определенного множества допустимых состояний, определяющего политику ее безопасности. При этом, задача анализа защищенности программы очевидно сводится к анализу невозможности ее перехода в любое неразрешенное политикой безопасности состояние на произвольном наборе входных данных. То есть, к той самой задаче, алгоритмическая неразрешимость которой была давным-давно доказана Генри Райсом.

Так получается, что же… весь рынок инструментария SAST – это индустрия обмана? В теории – да, на практике же, всё как обычно — возможны варианты.

Теория SAST на практике

Даже оставаясь в теоретическом поле, вполне возможно сделать несколько послаблений утверждению Райса для реальных программ, выполняющихся в реальных средах. Во-первых, в теоретической информатике под «программой» подразумевается математическая абстракция, эквивалентная машине Тьюринга (МТ) – самому мощному из вычислительных автоматов. Однако же, в реальных программах далеко не каждый фрагмент их кода эквивалентен МТ. Ниже по иерархии вычислительной мощности находятся линейно-ограниченные, стековые и конечные автоматы. Анализ защищенности двух последних вполне возможен, даже в рамках самой теоретической теории.

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

В-третьих, вычисление свойств программы по Райсу, является разрешимой проблемой для ряда малых МТ, имеющих небольшое количество состояний и возможных переходов между ними. Сложно себе представить реальную программу, имеющую от 2 до 4 состояний. Однако, такой *фрагмент* программы представить себе гораздо легче.

Следовательно, возможен эффективный анализ отдельных фрагментов кода программы, попадающих под перечисленных критерии. На практике, это означает, что:

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

DAST, IAST и все-все-все

В противовес статическому подходу, работающему с кодом программы без его фактического выполнения, динамический (Dynamic Application Security Testing, DAST) подразумевает наличие развернутой среды выполнения приложения и ее прогон на наиболее интересных с точки зрения анализа наборах входных данных. Упрощая, его можно охарактеризовать, как метод «осознанного научного тыка» («давайте передадим программе вот такие данные, характерные вот для такой атаки и посмотрим, что же из этого выйдет»). Его недостатки очевидны: далеко не всегда есть возможность быстро развернуть анализируемую систему (а зачастую и просто собрать), переход системы в какое-либо состояние может быть следствием обработки предыдущих наборов данных, да и для всестороннего анализа поведения реальной системы количество наборов входных данных должно быть настолько велико, что о его конечности можно рассуждать исключительно теоретически.

Относительно недавно перспективным считался подход, комбинирующий преимущества SAST и DAST – интерактивный анализ (Interactive…, IAST). Отличительной особенностью этого подхода является то, что SAST используется для формирования наборов входных данных и шаблонов ожидаемых результатов, а DAST выполняет тестирование системы на этих наборах, опционально привлекая к процессу человека-оператора в неоднозначных ситуациях. Ирония этого подхода заключается в том, что он вобрал в себя как преимущества, так и недостатки SAST и DAST, что не могло не сказаться на его практической применимости.

Но кто сказал, что в случае динамического анализа нужно выполнять всю программу целиком? Как было показано выше, вполне реально проанализировать значительную часть кода с помощью статического подхода. Что же мешает проанализировать с помощью динамического только оставшиеся фрагменты? Звучит, как план…

А внутре у ней неонка

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

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

такой анализатор выведет из построенной модели утвердительный ответ о достижимости потоком данных `Request.Params[«param»]` точки выполнения `Response.Write(filteredParam)` и существовании в данной точке уязвимости к XSS. В то время, как на самом деле, данный поток эффективно фильтруется и не может являться носителем вектора атаки. Существует множество способов покрыть частные случаи, связанные с предварительной обработкой потоков данных, но все они в конечном итоге сводятся к разумному балансу между ложными срабатываниями первого и второго типа.

65c72471d8ed49a593da4c0f12e8549f

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

Абстрактная интерпретация и символические вычисления

Допустим, перед нами стоит задача определить, число с каким знаком определяет выражение `-42 / 8 * 100500`. Самый простой способ — это вычислить данное выражение и убедиться, что получено отрицательное число. Вычисление выражения с вполне определенными значениями всех его аргументов называется конкретным вычислением. Но есть и другой способ решить эту задачу. Давайте на секунду представим, что по какой-то причине у нас нет возможности конкретно вычислить данное выражение. Например, если в него добавилась переменная `-42 / 8 * 100500 * x`. Определим абстрактную арифметику, в которой результат операций над числами определяется исключительно правилом знака, а значения их аргументов игнорируются:

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

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

Поиск уязвимостей на графе потока вычисления

Формализовав в терминах графа потока вычисления критерии уязвимости к тому или иному классу атак, мы сможем реализовать анализ защищенности кода через разрешение свойств конкретной модели, полученной в результате абстрактной интерпретации исследуемого кода. Например, критерии уязвимости к атакам любых инъекций (SQLi, XSS, XPATHi, Path Traversal и т.п.) можно формализовать примерно так:

Пусть C — граф потока вычисления исследуемого кода.

Пусть pvf(t) — достижимая вершина потока управления на C, являющаяся вызовом функции прямой или косвенной интерпретации текста t, соответствующего формальной грамматике G.

Пусть e — поток аргумента входных данных на С.

Пусть De — множество потоков данных на C, порождаемых от e.

Тогда приложение уязвимо к атакам инъекции в точке вызова pvf(t), если t принадлежит De и при изменении e происходит изменение структуры синтаксического дерева t.

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

Идеальный сферический анализатор защищенности кода в вакууме

Давайте теперь ненадолго отвлечемся от суровой реальности и чуть-чуть помечтаем о том, какой функциональностью должно обладать ядро гипотетического Идеального Анализатора (назовем его условно «IA»)?

Во-первых, оно должно вбирать в себя преимущества SAST и DAST, не включая при этом их недостатки. Из этого в частности следует, что IA должен уметь работать исключительно с имеющимся кодом приложения (исходным или бинарным), не требуя при этом его полноты или развертывания приложения в исполняющей среде. Иными словами, он должен поддерживать анализ проектов с отсутствующими внешними зависимостями или же какими-либо другими факторами, препятствующими сборке и развертыванию приложения. При этом, работа с фрагментами кода, имеющего ссылки на отсутствующие зависимости, должна быть реализована в настолько полной мере, насколько это возможно в каждом конкретном случае. С другой стороны, IA должен уметь эффективно «уворачиваться» не только от теоретических ограничений, накладываемых тьюринговой моделью вычислений, но и осуществлять сканирование за разумное время, потребляя разумное количество памяти и придерживаясь по возможности субэкспоненциальной «весовой категории».

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

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

Использование модели, основанной на символических вычислениях, позволяет реализовать все эти требования, что называется «by-design», за исключением той их части, которая касается теоретических ограничений и субэкспонент. И здесь, как нельзя кстати, придется наш план — использовать динамический анализ там, где не справился статический.

Частичные вычисления, обратные функции и отложенная интерпретация

Представьте себе, что IA содержит в себе некоторую базу знаний, описывающую семантику функций преобразования входных данных, реализованных в стандартной библиотеке языка или исполняющей среды приложения, наиболее популярных фреймворках и CMS. Например, что функции Base64Decode и Base64Encode являются взаимно-обратными, или что каждый вызов StringBuilder.Append добавляет новую строку к уже хранящейся в промежуточной переменной-аккумуляторе этого класса и т.п. Обладая такими знаниями IA будет избавлен от необходимости «проваливаться» в библиотечный код, анализ которого также попадает под все вычислительные ограничения:

Но что делать, если в коде встречается вызов функции, не описанной в базе знаний IA? Давайте представим, что в распоряжении IA есть некая виртуальная среда-песочница, позволяющая запустить произвольный фрагмент анализируемого кода в заданном контексте и получить результат его выполнения. Назовём это «частичным вычислением». Тогда, перед тем, как честно «провалиться» в неизвестную функцию и начинать её абстрактно интерпретировать, IA может попробовать проделать трюк, называемый «частичным фаззингом». Его общая идея заключается в предварительной подготовке базы знаний по библиотечным трансформирующим функциям и сочетаниям их последовательных вызовов на заранее известных наборах пробных данных. Имея такую базу, можно выполнить неизвестную функцию на тех же наборах данных и сравнить полученные результаты с образцами из базы знаний. Если результаты выполнения неизвестной функции совпадут с результатами выполнения известной цепочки библиотечных функций, то это будет значить, что IA теперь известна семантика неизвестной функции и в ее интерпретации нет необходимости.

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

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

Описанные выше подходы позволяют справиться с анализом значительной части фрагментов тьюринг-полного кода, но требуют существенной инженерной проработки как в части наполнения базы знаний и оптимизации эмулирования семантики стандартных типов, так и в части реализации песочницы для частичного выполнения кода (никто не захочет, чтобы в процессе анализа внезапно выполнилось что-то вроде File.Delete в цикле), а также поддержки фаззинга n-местных неизвестных функций, интеграции концепции частичного вычисления с SMT-солвером и т.п. Однако же, никаких существенных ограничений на их реализацию нет, в отличии от граблей классического SAST.

Когда гадкий duck-typing становится лебедем

e6662beae7c6422691ead2ec61472de4

Представьте, что нам необходимо проанализировать следующий код:

Человек без труда увидит здесь достижимую уязвимость к XSS. А вот большинство существующих статических анализаторов ее благополучно прошляпят в связи с тем, что им ничего не известно о типе UnknownType. Однако все, что здесь требуется от IA — это забыть о статической типизации и перейти к утиной. Семантика интерпретации таких конструкций должна полностью зависеть от контекста их использования. Да, интерпретатор ничего не знает о том, чем является `UnknownType.Property1` — свойством, полем, или даже делегатом (ссылкой на метод в C#). Но поскольку операции с ней осуществляются как с переменной-мембером какого-то типа, интерпретатору ничего не мешает обрабатывать их именно таким образом. А если, к примеру, далее по коду встретится конструкция `UnknownType.Property1()`, то ничто не мешает интерпретировать вызов того метода, ссылка на который была ранее присвоена Property1. И так далее, в лучших традициях заводчиков уток-чемпионов.

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

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

«Talk is cheap. Show me the code.»

Было бы странным не завершить статью небольшим примером кода, позволяющим проверить степень идеальности того или иного анализатора на практике. Voila — ниже представлен код, включающий в себя все базовые кейсы, покрываемые описанным подходом к абстрактной интерпретации, но не покрываемые более примитивными подходами. Каждый кейс реализован настолько тривиально, насколько это возможно и с минимальным количеством инструкций языка. Это пример для C#/ASP.Net WebForms, но не содержит какой-либо специфики и легко может быть транслирован в код на любом другом ООП-языке и под любой web-фреймворк.

Результатом анализа данного кода должно являться сообщение о единственной уязвимости к атакам XSS в выражении `pvo(parm1)`. Вступить и компилировать с готовым к сканированию проектом можно на GitHub(zip)

Но, как говорится, «лучше один раз увидеть. » и, в первую очередь, мы проверили на соответствие IA разрабатываемый нами анализатор, по чистой случайности называющийся AI:

bd11736f7f0447b795979ad4861907f3

А вы — уже проверили свой? 😉

На правах бонуса для дочитавших до конца:

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

3ffe98d0a8e94273852ed6a03cc7ea56

Скачать утилиту можно на официальном сайте. Перед ее использованием обязательно ознакомьтесь с лицензионным соглашением. В ходе анализа, Approof собирает неконфиденциальную статистику по проекту (CLOC, типы файлов, используемые фреймворки и т.д.) и, опционально, отправляет ее на сервер PT. Отключить отправку статистики или ознакомиться с сырым json, содержащим собранные данные, можно в разделе About приложения.

Источник

Сказочный портал
Adblock
detector