Solar inCode умеет обнаруживать уязвимости в байткоде Java. Но показать инструкцию байткода, которая содержит уязвимость, мало. Как показать уязвимость в исходном коде, которого нет?

На практике при использовании инструмента поиска уязвимостей к каждой найденной уязвимости нужно применить одно из трех действий:

  • устранить;
  • принять риски;
  • доказать, что это не уязвимость (false positive).

Все три действия требуют некоторого анализа уязвимости (например, нужно понять, как ее устранить и какие риски она несет).

Как проводить такой анализ, если мы находим уязвимости в байткоде Java, а исходного кода у нас нет?

Основным методом поиска уязвимостей при создании inCode был выбран статический анализ — поиск уязвимостей без выполнения кода. При статическом анализе нужно:

  • построить модель кода (промежуточное представление);
  • дополнить модель информацией о данных с применением алгоритмов статического анализа (анализа потока данных, потока управления — dataflow analysis, taint analysis);
  • применить правила поиска уязвимостей (правила говорят, где в модели кода находятся уязвимости, в терминах этой модели и информации, которой она дополнена).

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

Первыми приложениями, в которых Solar inCode искал уязвимости, были Android- и Java-приложения. Поиск уязвимостей в исполняемых файлах достаточно востребован:

  1. заказчику по условиям контракта могут не передавать исходный код;
  2. даже если исходный код передали, на боевой стенд (или в Google Play) может уйти исполняемый код, не соответствующий переданному исходному;
  3. разработчики используют сторонние компоненты без исходного кода, такой код также нужно контролировать.

Поэтому для Android- и Java-приложений в качестве промежуточного представления для статического анализа мы выбрали байткод Java. После компиляции исходного кода мобильного приложения в байткод Java компилятор Dalvik объединяет class-файлы и перекомпилирует код в байткод для Dalvik, получая исполняемый dex-файл. Исполняемый файл вместе с ресурсами и файлом конфигурации упаковывается в пакет apk, который распространяется через Google Play. Существуют средства, реализующие обработку и преобразования пакетов apk: распаковку, расшифровку ресурсов и файлов конфигурации, трансляцию кода Dalvik в байткод Java (apktool, dex2jar).

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

Байткод Java можно декомпилировать, при этом получить код достаточно высокого качества. Есть множество декомпиляторов для Java (JD, fernflower, Procyon). Мы не стали использовать восстановленный Java-код в качестве промежуточного представления, поскольку любые средства декомпиляции допускают ошибки, что может сказаться на качестве поиска уязвимостей.

Итак, мы нашли уязвимости в байткоде Java (о том, как это делается, мы будем писать в следующих статьях). Что делать с результатами?

Мы должны показать их в терминах «исходного» кода, точнее, восстановленного кода высокого уровня. Под уязвимостью здесь мы понимаем набор позиций инструкций в байткоде, которые определяют уязвимость (небезопасный вызов метода, набор инструкций, через который проходит поток небезопасных данных). Таким образом, каждой инструкции в байткоде мы должны сопоставить номер строки в восстановленном коде. В class-файле (файл байткода, соответствующий классу в исходном коде) существует атрибут LineNumberTable, в котором хранится отображение позиций в байткоде на номера строк в исходном коде. Таким образом, для отображения уязвимостей в терминах языка Java нужно, чтобы в байткоде был атрибут LineNumberTable.

При анализе байткода (в том числе полученного из apk-файла) атрибута LineNumberTable может не оказаться. Он может удаляться при компиляции или при обратной трансляции из apk. Хотя это и не так важно — удаленный из байткода LineNumberTable соответствовал исходному коду, который написал разработчик, а не восстановленному «исходному» коду. Это означает, что необходимо восстановить атрибут LineNumberTable в анализируемом байткоде, который будет указывать на восстановленный код.

Базовый алгоритм основывается на построении абстрактного синтаксического дерева декомпилируемого кода (AST) по байткоду Java и выводе исходного кода в процессе обхода AST. В процессе обхода AST (обход в глубину) также происходит сохранение информации о соответствии номеров строк восстанавливаемого кода и позиций инструкций в методах байткода.

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

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

Формализация восстанавливаемого отображения и алгоритм восстановления



На практике мы часто получаем проекты, содержащие и исходный код (тогда мы можем получить байткод с таблицей номеров строк путем компиляции), и байткод (различные сторонние компоненты, библиотеки и так далее). Для анализа таких проектов в inCode реализована комбинированная предобработка проекта на Java. Она состоит из следующих шагов:

  • в проекте обнаруживаются все class-файлы, файлы с исходным кодом на java и scala, jar/war файлы с байткодом;
  • в зависимости от настройки сканирования проекта, которую задает пользователь, в список class-файлов включаются class-файлы из jar/war-файлов (чаще всего это означает, что проект анализируется вместе с библиотеками) ;
  • из class-файлов и файлов исходного кода мы получаем полные имена классов, с помощью имен классов происходит сопоставление файлов исходного кода и файлов с байткодом;
  • файлы байткода, для которых не нашлись файлы исходного кода, подвергаются декомпиляции с описанной выше процедурой восстановления информации о номерах строк.

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

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

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

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