• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Охота на счетчик. Автоматизация поиска уязвимостей с помощью IDAPython

weaver

31 c0 bb ea 1b e6 77 66 b8 88 13 50 ff d3
Забанен
Регистрация
19.12.2018
Сообщения
3 301
Решения
11
Реакции
4 622
Депозит
0.0001
Пожалуйста, обратите внимание, что пользователь заблокирован
Автоматизация поиска уязвимостей в ПО без исходных кодов — это весьма вкусная плюшка черной магии багхантинга. Но в столь ответственном деле без надежного помощника не обойтись. Встречайте IDAPython — связующий элемент между языком Python и легендарным дизассемблером IDA Pro. Он прекрасно справится с такой задачей.

ЗНАКОМСТВО С АНАЛИЗОМ ПО
Методы обнаружения уязвимостей на абстрактном уровне различаются по цвету — «белый ящик», что светел знанием об исходном коде; «черный ящик» олицетворяет тьму незнания устройства подопытной программы. Там, где свет и тьма сходятся, рождается метод «серого ящика», применяемый при реверс-инжиниринге. Цель багоискателя — открыть ящик Пандоры с помощью реверсинга. Одна из категорий реверсинга — бинарный анализ. Это общее название методик постижения алгоритмов работы программы, восстановления исходного кода, поиска и анализа так называемых ключевых мест в коде, о которых будет сказано ниже.

Принцип различия динамического и статического подходов к изучению программ кроется в вопросе «to run, or not to run?» — то есть запускается программа на выполнение или нет. Следующая парочка — автоматический и ручной методы, которые различаются степенью относительного вмешательства Homo Logicus в процесс анализа. Статический анализ заклинаниями кода превращает трассу, полученную от покрытия кода, в источник информации (хотя бы и поверхностной) о присутствии потенциальных уязвимостей. Такой источник делает умнее фаззинг в памяти.

Этим материалом я начинаю знакомить читателя с нектаром логики анализа кандидатов (паттернов) уязвимостей. Поиск эксплуатируемых багов начинается с осмотра таких пациентов, как небезопасные функции работы со строками (strcpy, strcat), функции форматирования (семейка printf и так далее). Более сложен анализ циклов, inline memcpy и других паттернов, где могут обрабатываться входные данные, и обрабатываться ошибочно. Одним из таких ключевых мест, паттернов уязвимостей, является конструкция inline memcpy — ассемблерный аналог функции memcpy.

КОНСТРУКЦИЯ INLINE MEMCPY
Ассемблерно-абстрактно конструкция inline memcpy представляет собой набор следующих команд:

Код:
mov ecx, счетчик (количество байт для копирования)
mov esi, указатель на память, откуда копировать
mov edi, указатель, куда копировать
rep movsd, инструкция копирования

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

Итак, у нас нарисовался вектор автоматизации — это обратная трассировка счетчика. Место действия — дизассемблерный листинг IDA. Для анализа потенциально уязвимых мест небезопасных функций с использованием дизассемблера IDA существуют такой комплект скриптов, как Bugscam, скрипт getenv(), Bug Detector, предназначенный для поиска дыр в одноименной функции, Inlined Strlen — скрипт для анализа ассемблерных конструкций, аналогичных strlen. Все они написаны на встроенном скриптовом языке IDA. Как ты уже понял, мы будем писать IDAPython-скрипт, упрощающий поиск уязвимостей в inline memcpy. Скрипт будет состоять из трассировщика и анализатора. В задачи трассировщика будут входить поиск пути до начала функции и маркировка точки отсчета. Задача анализатора будет состоять в трассировке счетчика с целью выяснить, откуда кладется в него значение, происходят ли опасные операции со значением и не помещается ли в счетчик жестко закодированное значение. К опасным операциям относятся арифметические операции ввиду своей возможности привести к ошибке целочисленных преобразований (signed/unsigned mismatch), что, в свою очередь, как известно, влечет переполнение буфера, если, конечно, значение, которое использовал программист, является размером в операции копирования (затирания) памяти.

Англоязычные термины, отображающие поставленные задачи скрипта, — это backtracing и dataflow analysis.

Памятуя слова Л. Торвальдса: «Болтовня ничего не стоит. Покажите мне код», приступим к реализации.

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

А МНЕ ДОСТАЛАСЬ ТРАССА…
Для того чтобы научить трассировщик хождению по дизассемблерному листингу, мы будем использовать функцию RfirstB. «RfirstB возвращает адрес следующего источника в списке ссылок, то есть предыдущий адрес», — сказано в документации IDA. По сути, эта функция будет являться важнейшей частью «шагательного» механизма. Задача этого механизма состоит в том, чтобы, используя ссылки, «идти вверх», вплоть до начала функции, вызывая анализатор на каждом шаге.

На пути трассировки нас ждут разные встречи. Например, цикл, легко превращающий трассировку в белку в колесе. Чтобы не уйти в бесконечный цикл и не превратиться в белку, будем использовать функции SetColor и GetColor. Функция SetColor устанавливает указываемый цвет на строку, функцию или сегмент. Функция GetColor(ea, what) является частично «обратной» — параметр color ей не требуется. Функцией SetColor мы будем окрашивать адреса, которые уже «прошагал» наш скрипт, а GetColor’ом проверять. Стоит отметить, что две эти функции отсутствуют в языке IDA IDC. Вот таким простым хаком мы обережем себя от зацикливания на одном и том же участке кода.

Кстати сказать, такой окрасочно-распознавательный механизм отлично выручает при отладке скрипта.

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

Реализация представлена ниже:

Python:
def tracer(ea,reg):
     global vulncount [#1]
     parent = GetFunctionAttr(ea,0) [#2]
     while ea != parent:
         xref = Rfi rstB(ea) [#3]
         commented = idaapi.get_cmt(movsaddr,0)
         if commented != None: [#4]
             return
         if GetColor(ea,CIC_ITEM)==0xe5f3ff: [#5]
             return
         else:
             SetColor(ea,CIC_ITEM,0xe5f3ff) [#6]
             reg=Analyzer(ea,reg,xref) [#7]
             ea = xref [#8]
             if xref == 0xffffffff: [#9]
                 return
         tracer(ea,reg) [#10]
         return reg
     if ea == parent: [#11]
         comment="unknown. Vulncount=%s" % vulncount [#12]. MakeComm(addrofmovs, comment)
         return
     return reg

Немного поясню суть кода. Объявляем глобальную переменную для счетчика «подозрительных встреч» [#1]. В parent помещаем адрес начала родительской функции [#2]. В цикле, где ea — адрес rep movsd, получаем адрес-ссылку [#3], проверяем наличие комментария [#4], след [#5]. Если окрашено или закомментировано — выходим, иначе машем кисточкой [#6]. Вызываем анализатор, помещая трассируемое значение в reg [#7]. Делаем шаг [#8]. Проверяем, а вдруг пропасть [#9]? «Чтобы понять рекурсию, нужно сперва понять рекурсию» (с) [#10]. Если тупик уж под ногами [#11], не забыв про vulncount, комментируем [#12].

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

Предназначение анализатора — понимание «отношений» между инструкциями и операндами в процессе потока данных (dataflow). «Органы чувств» анализатора — это функция GetMnem(ea), возвращающая мнемонику инструкции по заданному адресу, GetOpnd(ea, n) — возвращающая операнд, GetOpType(ea,n) — возвращающая тип операнда. Анализатор использует счетчик подозрительных признаков, располагающийся в глобальной переменной. Счетчик — хранитель репортов об оперировании со счетчиком математическими инструкциями, а также размещении в нем результатов функций вычисления длины. Оба признака повышают вероятность наличия уязвимости.

Настала пора рассмотреть механизм анализа.

ТАМ, НА НЕВЕДОМЫХ ДОРОЖКАХ

DEF IFMOV()
Встречая команды пересылки mov, lea, movzx, проверяем, что помещается в трассируемый регистр: значение другого регистра, значение из памяти или же жестко закодированное значение. Взглянем на следующий код:

Код:
sub eax, esi
mov ecx, eax ; в ecx помещается eax
mov edx, ecx
shr ecx, 2
rep movsd

Логика ассемблера: в есх помещается eax. Логика анализатора: eax — объект трассировки. Ранее с регистром eax вступала в отношения команда sub, грозящая арифметической ошибкой.

Таковы особенности данного inline memcpy. Кстати, этот код — слабое звено и причина переполнения стека в Outlook Express (MS05-030).

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

Python:
if GetMnem(ea) in movers:
     if GetMnem(ea) == "lea": [#1]
         reg = GetOpnd(ea,1)
         reg = reg[1:4]
         return reg
         if GetOpType(ea,1)==5: [#2]
             print "value of counter is hardcoded!:("
             comment="uninterested!"
             MakeComm(addrofmovs, comment)
             return reg
         if GetOpType(ea,1)==1:
             reg=GetOpnd(ea,1) [#3]
             return reg
         # если: mov ecx,[ebp+arg_4] [#5]
         if re.match('.*arg.*',GetOpnd(ea,1)
             print "Count from function argument"
             comment="Сount from arg or local var. \
             Vulncount=%s" % vulncount # подпись
             MakeComm(addrofmovs, comment)
         else:
             comment="Unknown. Vulncount=%s" % vulncount [#4]
             MakeComm(addrofmovs, comment)
             return reg
     return reg

Пересыльщики могут в трассируемый регистр поместить постоянное значение, значение из указателя, значение из аргумента функции, другой регистр. Инструкция lea часто воздействует на reg следующим образом: lea reg, [reg32+reg32], где reg32 — другой регистр. В этом случае трассируемым становится другой регистр [#1]. Постоянное значение в счетчик помещается, как правило, с использованием пары инструкций push/pop, но на всякий случай обработка mov reg,imm32 необходима (в недрах mshtml.dll это присутствует) [#2]. Если в reg помещается другой регистр, то он и становится объектом пристального внимания ;) [#3]. В случае помещения значения из указателя трассировка прекращается и данному inline memcpy присваивается статус «unknown» [#4]. Помещение значения из аргумента функции, стековой переменной, встречается весьма часто и представляет для исследователя интересный пример наивного доверия входным данным [#5]. При нацеливании на такую функцию «фаззинга в памяти» падение исследуемой программы очень даже вероятно.

DEF ISPOP()
Пересыльщиками жестко закодированного значения чаще всего служат стековые инструкции push и pop. Алгоритм обнаружения и анализа этой парочки таков:

Python:
if GetMnem(ea)=="pop":
     maxsteps=6
     count=0
     while count != maxsteps:
         xref = Rfi rstB(ea)
         ea=xref
         if ea != -1:
             if GetMnem(ea)=="push": [#1]
                 if GetOpType(ea,0)==5: [#2]
                     print "value of counter is hardcoded!:(("comment="uninterested!" MakeComm(addrofmovs, comment)
                     return
             SetColor(ea,CIC_ITEM,0xe5f3ff)
             count=count+1
         else:
             break
             print "unknown value"
             print "vulncount =%s " % vulncount
             comment="unknown"
             MakeComm(addrofmovs, comment)
             return reg
     return reg

В цикле ищем push [#1]. Как правило, ему сопутствует жестко закодированное значение [#2]. А значит, данный паттерн обозначаем как неинтересный.

С пересыльщиками разобрались. Далее рассмотрим, как call может влиять на счетчик.

DEF IFCALL()
Смак здесь в том, что если вызов осуществляется к одной из функций, возвращающих длину строки, — strlen, wcslen и тому подобным — и трассируемый регистр — eax, то из этого следует, что значение счетчика очень может зависеть от входных данных в программу, а значит, существует вероятность захвата контроля над ним.

В приведенном ниже листинге происходит следующее: получаем имя функции [#1]; если eax — трассируемый регистр и слово «len» присутствует в названии функции [#2], то увеличиваем счетчик подозрительных признаков, выводим результирующую информацию и добавляем комментарий к rep movsd. При встрече с другими функциями отчитываемся о неизвестности [#3].

Python:
if GetMnem(ea)=="call":
     funcname=GetOpnd(ea,0) [#1]
     if reg=="eax":
         if re.match('.*len.*',funcname , re.IGNORECASE): [#2]
             print "Len in funcname"
             vulncount = vulncount+1
             print "length calculated dynamically!"
             print "vulncount =%s " % vulncount
             comment="length calculated dynamically!"
             MakeComm(addrofmovs, comment)
              return
     else:
         print "unknown value" [#3]
         print "vulncount =%s " % vulncount
         comment="Unknown. Vulncount=%s" % vulncount
         MakeComm(addrofmovs, comment)
         return reg
     return reg

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

DEF IFMATH()
Ошибки целочисленных преобразований случаются при выполнении арифметических операций, а также знакового расширения. Такие ошибки являются квинтэссенцией уязвимостей integer overflow/underflow. То есть цели для GetMnem() — это инструкции sub, add, dec, inc, mul, imul, movsx.

В качестве примера приведу упомянутый Microsoft Outlook Express NNTP Response Parsing Buffer Overflow Vulnerability:

Код:
sub eax, esi ; математическая инструкция работает
 ; с будущим счетчиком
mov ecx, eax
mov edx, ecx
shr ecx, 2
rep movsd

Знаковое расширение, производимое инструкцией movsx, часто является фатальной операцией для безопасности ПО. Один из наглядных примеров этого — уязвимый код, вызывающий целочисленное переполнение в Apple QuickTime Player H.264 Codec:

Код:
movsx edx,cx ; знаковое расширение
mov ecx,edx
mov ebx,ecx
shr ecx,0x2
lea esi,[eax+0x8]
lea edi,[esp+0x18]
rep movsd

Здесь мы видим, что с регистром edx происходит знаковое расширение. Это значит, что значение edx принимает вид 0xFFFFxxxx.

Код, проверяющий присутствие арифметических команд и комментирующий адрес точки отправки — инструкции rep movsd, очень прост:

Python:
mathmassiv = ["inc","add","sub","dec","mul","imul","movsx"]
if GetMnem(ea) in mathmassiv:
     vulncount = vulncount+1
     print "May be here signed/unsigned mismatch!"
     print "Vulncount =%s " % vulncount
     return reg

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

К integer overflow причастны не только арифметические команды, но и подход к операции сравнения. Если сравниваются числа со знаком и один из операндов сравнения — трассируемый регистр, то такое обстоятельство заслуживает пристального внимания. Итак, прицел на инструкцию cmp, за которой следует знаковый переход.

DEF ISCMP()
«…Когда меняется суть. Когда меньшее становится большим. Когда мир переворачивается» — это не начало эпической саги, а то, что происходит со счетчиком, когда его значение неправильно интерпретируется и условный переход кидает счетчик с огромным размером в операцию rep movsd.

Виновники рокового перехода — условные знаковые переходы — jg, jl, jge, jle, jng, jnge, jnl, jnle. В дикой природе группа «джампов», зависящих от знака, всегда где-то рядом с операцией сравнения. Ниже представлен найденный багоискателем Луиджи Аурьеммой (Luigi Auriemma) в недрах AngelServer листинг, в котором видна такая искомо-целевая ситуация:
Код:
cmp esi,imm ; esi под контролем
rep stosd
jge AngelSer.004023DE ; знаковый переход
mov ecx,esi
lea esi,[ebp+0xc]
mov edx,ecx
lea edi, [esp+0x24]
shr ecx,2
rep movsd

Искомая, потому что о ней идет речь. Целевая, потому что на такие трофеи анализатор тоже будет нацелен. Скелет зверя таков:

Код:
cmp tracereg, imm/reg ; сравнение трассируемого
 ; регистра с непосредственным
 ; значением или другим регистром
…
j[gl] ; группа переходов, представленных
 ; с помощью магии регулярных выражений

Если от cmp, «вниз шагая» [#2], находим один из знаковых переходов [#3], заключенных в массив [#1], то рапортуем, плюсуем. Если переход беззнаковый и происходит сравнение трассируемого регистра непосредственно с жестко закодированным значением [#4], то маркируем данный inline memcpy как неинтересный.

Python:
if GetMnem(ea)=="cmp":
     interestingjumps = ["jg","jl","jge","jle","jng", "jnge","jnl","jnle"] [#1]
     for count in range(5):
         ea = Rfi rst(ea) [#2]
         mnem = GetMnem(ea)
         if mnem in interestingjumps: [#3]
             vulncount = vulncount+1
             break
             return reg
     if GetOpType(ea,1)==5: [#4] # если 1-й операнд —
         # постоянное значение
         comment="uninterested!"
         MakeComm(addrofmovs, comment)
         SetColor(xref,CIC_ITEM,0xe5f3ff)
         return
     return reg

Таков нехитрый анализ, связанный с инструкцией cmp.

ЗАКЛЮЧЕНИЕ
Трассировщик и анализатор в сборе. Естественно, эту сборку придется «допилить» под себя, но я уверен, что для тебя это не составит труда. IDAPython ждет твоих указаний. Happy Hunting!

WWW
целочисленное переполнение в Apple QuickTime Player H.264 Codec
VULNERABILITY IN MY HEART (CVE2010-3970)
AngelServer stack overflow

Полезная литература
• Greg Hoglund and Gary McGraw. Exploiting Software: How to Break Code
• Mark Dowd, John McDonald, Justin Schuh. The Art of Software Security Assessment: Identifying and Preventing Software Vulnerabilities
• Tobias Klein. A Bug Hunter’s Diary: A Guided Tour Through the Wilds of Software Security

tracer.py - Исходный код скрипта, разработанный автором в процессе подготовки статьи "IDAPython vs memcpy"
Python:
from idaapi import *
from idautils import *
import string
import re
ea = get_screen_ea()
movsaddr=ea
vulncount=0

def main():
    global vulncount
    reg="ecx"
    tracer(ea,reg)
    #cleancolor=-1
    #Cleaner(movsaddr,cleancolor)

def tracer(ea,reg):
    global vulncount
    parent = GetFunctionAttr(ea,0)
    while ea != parent:
        xref = RfirstB(ea)
        commented = idaapi.get_cmt(movsaddr,0)
        print "comment = %s" % commented
        if commented != None:
            return
        if GetColor(ea,CIC_ITEM)==0xe5f3ff:
            return
        else:
            SetColor(ea,CIC_ITEM,0xe5f3ff)
            reg=Analyzer(ea,reg,xref)
            ea = xref
            if xref == 0xffffffff:
                return
        tracer(ea,reg)
        return reg
    if ea == parent:
        comment="unknown.  Vulncount=%s" % vulncount
        MakeComm(movsaddrddr, comment)
        return
    return reg

def Analyzer(ea,reg,xref):
    global vulncount
    movers = ["mov","movzx","lea"]
    if GetMnem(ea)=="call":
        funcname=GetOpnd(ea,0)
        if "eax" in reg:
            if re.match('.*len.*',funcname , re.IGNORECASE):
                print "Len in funcname"
                vulncount = vulncount+1
                print "Length calculated dynamically!"
                print "vulncount =%s " % vulncount
                comment="Length calculated dynamically!"
                MakeComm(movsaddr, comment)
                return
        else:
            print "unknown value"
            print "vulncount =%s " % vulncount
            comment="unknown. stopping on call. Vulncount=%s" % vulncount
            MakeComm(movsaddr, comment)
            return reg
        return reg
    if reg in GetOpnd(ea,0):
        mathmassiv = ["inc","add","sub","dec","movsx","mul","imul"]
        if GetMnem(ea) in mathmassiv:
            vulncount = vulncount+1
            print "may be here signed/unsigned mismatch!"
            print "vulncount =%s " % vulncount
            return reg
    if GetMnem(ea)=="pop":
        for count in range(5):
            xref = RfirstB(ea)
            ea=xref
            if ea != -1:
                if GetMnem(ea)=="push":
                    if GetOpType(ea,0)==5:
                        print "value of counter is hardcoded!:(("
                        comment="uninterested!"
                        MakeComm(movsaddr, comment)
                        return
                SetColor(ea,CIC_ITEM,0xe5f3ff)
                count=count+1
            else:
                break
                print "unknown value"
                print "vulncount =%s " % vulncount
                comment="unknown"
                MakeComm(movsaddr, comment)
                return reg
        return reg
    if GetMnem(ea)=="cmp":
        for count in range(5):
            ea = Rfirst(ea)
            print ea
            interesting_jumps = ["jg","jl","jge","jle","jng","jnge","jnl","jnle"]
            mnem = GetMnem(ea)
            print mnem
            if mnem in interesting_jumps:
                print "may be here signed/unsigned mismatch!"
                vulncount = vulncount+1
                print "vulncount =%s " % vulncount
                break
                return reg
            uninteresting_jumps = ["jmp","jz","jnz","jna","ja","jnb","je","jbe","jnbe","jne"]
            if mnem in uninteresting_jumps:
                break
                return reg
        if GetOpType(ea,1)==5:
            comment="uninterested!"
            MakeComm(movsaddr, comment)
            SetColor(xref,CIC_ITEM,0xe5f3ff)
            return
        return reg
    if GetMnem(ea) in movers:
        if GetMnem(ea) == "lea":
            reg =  GetOpnd(ea,1)
            reg = reg[1:4]
            if "x" in reg:
                return reg
            else:
                comment="unknown. Vulncount=%s" % vulncount
                MakeComm(movsaddr, comment)
                return reg
        if GetOpType(ea,1)==5:
            print "value of counter is hardcoded!:("
            comment="uninterested!"
            MakeComm(movsaddr, comment)
            return reg
        if GetOpType(ea,1)==1:
            reg=GetOpnd(ea,1)
            return reg
        if re.match('.*arg.*',GetOpnd(ea,1) , re.IGNORECASE):
            print "count from function argument"
            comment="size from function argument. Vulncount=%s" % vulncount
            MakeComm(movsaddr, comment)
        else:
            comment="unknown. Vulncount=%s" % vulncount
            MakeComm(movsaddr, comment)
            return reg
        return reg
    return reg

def Cleaner(ea,cleancolor):
    for seg_ea in Segments():
        for ea in Heads(seg_ea, SegEnd(seg_ea)):
            SetColor(ea,CIC_ITEM,0xFFFFFFFF)

if __name__ == "__main__":
    main()

Источник
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх