Поиск клонов в исходном коде

Материал из Википедии — свободной энциклопедии
Перейти к навигации Перейти к поиску

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

Введение[править | править код]

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

Способы анализа исходного кода[править | править код]

Существует множество способов поиска клонированного кода, но в основном они подразделяются на синтаксические, структурные и семантические. У каждого подхода есть свои достоинства и недостатки. В научных работах и на практике в настоящее время чаще используется комбинированный подход, который позволяет получить более точный результат. Для упрощения анализа используются, так называемые отпечатки(fingerprint). Отпечатком называется информация, особым образом собранная из исследуемого программного объекта и пригодная для сравнения с другими подобными отпечатками. Данная информация представляет собой уникальное и компактное представление функциональности кода. Алгоритм получения отпечатков - это процедура, которая произвольному элементу данных большого размера ставит в соответствие гораздо более короткую битовую строку. Функции, которые выполняют эквивалентные задачи должны быть представлены похожими отпечатками. Кроме того, результирующие отпечатки должны быть устойчивы к несоответствиям на уровне байтов и терпимы к незначительным изменениям в функции. Также, вероятность возникновения конфликта между отпечатками различных функций должна быть минимальной. То есть не должны возникать ситуации, когда для различных по смыслу функций генерируются идентичные отпечатки. В терминологии отпечатков, перед разработчиком стоят следующие вопросы: каким образом сгенерировать отпечаток, как сравнивать два отпечатка и каким образом оценить результаты.

Структурный подход[править | править код]

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

Синтаксический подход[править | править код]

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

Семантический подход[править | править код]

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

Алгоритмы анализа исходного кода[править | править код]

Алгоритм Хескела[1][править | править код]

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

BinDiff[2][править | править код]

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

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

Для построения изоморфизма вводятся такие понятия, как селектор и свойство.

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

Создание узлов.[править | править код]

Если дан селектор s, то изоморфизм , можно построить задав начальный изоморфизм , который затем будет последовательно улучшаться, пока это возможно. Начальный изоморфизм строится простым образом с помощью селектора: . Это определение можно улучшить в случае, если нам задан набор свойств. Пусть { } - заданный набор свойств. Тогда улучшенный начальный изоморфизм можно построить следующим образом:

    for do
        (K, L) ;
        for do
            define  
        end
    end

Добавление узлов.[править | править код]

Если дан начальный изоморфизм , дальнейшие улучшенные изморфизмы можно построить по следующему алгоритму:

    Input: 
    Result: 
    { };
    for do
        P up
        K up;
        for do
            if then
                define  
            end
        end
    end

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

Применение.[править | править код]

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

BinSign[3][править | править код]

Основной целью BinSign является предоставление точного и масштабируемого решения для снятия отпечатков двоичного кода. Данный алгоритм анализирует как структурные, так и семантические свойства. Для генерации отпечатка сначала извлекаются глобальные особенности функции такие как, количество и размер аргументов, константы, возвращаемое значения, строки, ссылки, количество базовых блоков. Далее на основе графа потока управления строятся tracelet’ы. Каждый tracelet состоит из пары связанных блоков, каждый из блоков обладает своими особенностями: количество инструкций, группы инструкций, константы, количество констант. Таким образом, учитываются структурные особенности. Далее генерируется отпечаток, пригодный для сравнения. Данный отпечаток имеет множество применений, таких как идентификация компилятора, идентификация библиотечной функции, анализ авторства, обнаружение клонов, обнаружение уязвимостей, анализ происхождения, обнаружение и классификация вредоносных программ и т.д.

Tracelet[4][править | править код]

Алгоритм для вычисления сходства между функциями. Основная задача состоит в том, чтобы правильно определить понятие сходства, которое выходит за рамки прямого синтаксического сопоставления и способно находить измененные версии кода, а не только точные совпадения. Алгоритм основан на разложении функции в tracelets - краткий фрагмент трассы исполнения компьютерной программы, используемый в автоматизированных системах, пытающихся понять и оптимизировать исходный код. Для установления сходства применяется простой механизм перезаписи. Этот механизм выравнивает данные, чтобы сопоставить регистры и адреса памяти между tracelet'ами, устраняя различия между tracelet'ами, которые в остальном похожи.

Алгоритм для оценки сходства двух функций.[править | править код]

Рассмотрим общую структуру алгоритма сравнения (принцип работы используемых функций будет рассмотрен далее):

    Input: - функции, которые нужно сравнить (target, reference)
           - метод нормировки
           -  длина tracelet
           - пороговые значения
    Output: - можно ли считать функции одинаковыми
            - оценка сходства
    Alghoritm 
        RefTracelets = ExtractTracelets(R,k)
        TargetTracelets = ExtractTracelets(T,k)
        MatchCount = 0
        foreach do
            foreach do
                AlignedInsts = AlignTracelets(r, t)
                t' = RewriteTracelet( AlignedInsts, r, t)
                S = CalcScore(r, t')
                RIdent = CalcScore(r, r)
                TIdent = CalcScore(t', t')
                if then
                    MatcCount++
            end
        end
        SimilarityScore = MathCount / |RefTracelets|
        IsMath = SimilarityScore    

Алгоритм начинается с разложения каждой из двух функций в набор из tracelet'ов длины k (функция ExtractTracelets). Мы рассматриваем каждый tracelet как ограниченную последовательность инструкций без промежуточных переходов. После того, как каждая функция была представлена в виде набора tracelet'ов, начинается попарное сравнения tracelet'ов. Сравнение производится следующим образом: сначала пары traceletов сравниваются с помощью функции AlignTracelets, затем t tracelet перезаписывается с учетом r. Сходство tracelet вычисляется с помощью функции CalcScore. Два tracelets считаются одинаковыми если их показатель сходства выше порогового значения . Наконец, две функции считаются похожими, если их оценка сходства превышает пороговое значение .

Разложение функции на tracelets.[править | править код]

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

    Input: -  граф потока управления
           -  длина tracelet
    Output: - список всех tracelet в графе
    Alghoritm 
        result = 
        foreach do
           result Extract(b, k)
        end
        return result

    Function 
        bCode = {stripJumps(b)}
    if k = 1 then 
        return bCode
    else
        return 
    end

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

Алгоритм для оценки сходства двух tracelets.[править | править код]

В данном алгоритме каждая инструкция сборки рассматривается как буква (например, pop eax; является буквой). Составляется специальная таблица соответствий, которая на самом деле является мерой сходства между инструкциями сборки. При этом очень высокая оценка дается при идеальном совпадении.

    Input: -  tracelets, которые нужно сравнить (target, reference)
    Output: - оценка сходства данных tracelets
    Alghoritm 
        A = InitMAtrix(|T|, |R|)
        for do
           for do
               A[i, j] = Max(Sim(T[i], R[j]) + A[i+1, j+1],
                             A[i+1, j],
                             A[i, j+1])
           end
        end
        Score = A[0, 0]       

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

Перезапись заданного кода с учетом другого кода.[править | править код]

Для дальнейшего улучшения процесса сопоставления используется перераспределение аргументов и метод перезаписи.

    Input: -  набор записей с выровненными инструкциями
    -  tracelets, которые нужно сравнить (target, reference)
    Output: - измененный код
    Alghoritm 
        foreach do
           for do
               
               
               
               
               if then
                  
               else if then
                  
           end
        end
        
        foreach do
           
           foreach do
               if then
                   
               end
           end
           
        end          


Сначала мы перебираем выровненные пары команд из T и R. Для каждой пары команд выполнено (достигается путем применения AlignTracelets). Для каждой пары выровненных аргументов мы абстрагируем аргумент в T tracelet'e. Это делается с помощью newVarName, который генерирует уникальные имена временных переменных в соответствии с типом аргумента. Затем создаётся перекрестное ограничение между новой переменной и значением аргумента из r инструкции, которое добавляется к ограничению . После с помощью функции read(t) мы узнаем используется ли аргумент для чтения. Затем алгоритм использует вспомогательную структуру данных lastWrite() для определения имени последней временной переменной, в которую записан аргумент . Создается ограничение данных от последней записи в чтение. В противном случае, алгоритм с помощью функции write(t) проверяет является ли командой записи и обновляет значение lastWrite(). Наконец, после создания всех переменных и ограничений, вызывается функция solve, которая разрешит все конфликты и вернет минимальное решение, необходимое чтобы переписать tracelet.

Источники[править | править код]

  1. Paul Heckel. A technique for isolating differences between files // Communications of the ACM. — 1978-04-01. — Т. 21, вып. 4. — С. 264–268. — ISSN 0001-0782. — doi:10.1145/359460.359467.
  2. Источник. Дата обращения: 9 декабря 2018. Архивировано 10 декабря 2018 года.
  3. Lina Nouh, Ashkan Rahimian, Djedjiga Mouheb, Mourad Debbabi, Aiman Hanna. BinSign: Fingerprinting Binary Functions to Support Automated Analysis of Code Executables // ICT Systems Security and Privacy Protection. — Cham: Springer International Publishing, 2017. — С. 341–355. — ISBN 9783319584683, 9783319584690.
  4. Yaniv David, Eran Yahav. Tracelet-based code search in executables // Proceedings of the 35th ACM SIGPLAN Conference on Programming Language Design and Implementation - PLDI '14. — New York, New York, USA: ACM Press, 2013. — ISBN 9781450327848. — doi:10.1145/2594291.2594343.