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

Статья Наказуемая беспечность. Автоматизируем поиск уязвимостей, вызванных некорректным использованием функций alloc/free с 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. Но если в прошлый раз мы искали ошибки в циклах и переполнения буфера, то в этот раз устроим охоту на баги, связанные с функциями выделения/освобождения памяти.

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

Данными в динамической памяти оперируют функции выделения памяти. С семействами функций alloc и free связаны четыре категории уязвимостей: целочисленное переполнение, игнорирование проверки возвращаемого значения, повторное освобождение памяти и использование освобожденной памяти. Задачу проверки на эти категории можно проиллюстрировать схемой, представленной на рисунке 1.

X8nHnsK.png

Рис. 1. Схема проверки четырех категорий уязвимостей

РАЗМЕР ИМЕЕТ ЗНАЧЕНИЕ
Арифметические операции с параметром alloc-функций приводят к тому, что выделяется недостаточное количество памяти для буфера. Следствием этого является переполнение в куче. На примере из рисунка 2 можно увидеть, что с параметром malloc происходит интересное.

CsCDTAB.png

Рис. 2. Арифметическая ошибка в браузерном протоколе Steam

Подобная арифметическая ошибка была найдена недавно в браузерном протоколе Steam, в обработчике графических файлов TGA.

Код:
add eax,eax
add eax,eax
push eax
call malloc_wrapper

Для идентификации этого паттерна используется, как обычно, трассировка и анализ операндов инструкций. Задача предельно проста: от начальной точки — функции выделения памяти, трассируя вверх, обращать внимание на математические инструкции, где нулевой операнд — трассируемое значение. Но одной функцией malloc семейство аллокаторов не ограничено. Сюда относятся также LocalAlloc, SysAllocString и так далее. Для скрипта-помощника важно лишь, что во всех этих функциях присутствует слово «alloc» и что у части из них параметр «размер» не один. Это обуславливает дополнительную небольшую задачу для скрипта — найти нужный push. Например, объект трассировки у LocalAlloc — нулевой операнд второго push’а, а у malloc — первого и единственного push’а. В задаче проверки на integer overflow одна из вкусностей в том, что размер выделяемых байтов для функции выделения может вычисляться динамически (например, быть возвращаемым strlen-like функцией). Подобный пример ты можешь увидеть на рисунке 3.

TcNEafS.png

Рис. 3. Размер выделяемых байтов для функции вычисляется динамически

Код, выполняющий простейший анализ

Python:
# Список арифметических инструкций
maths=['inc','add','mul','imul','lea','movsx','dec','sub','shl','shr']
# Для функции malloc ищем первый push
for step in range(5):
     ea=RfirstB(ea)
     # Покраска удобна для отладки и наглядности
     SetColor(ea,CIC_ITEM,0xcbe4e4)
     if GetMnem(ea)=='push':
         # Проверка на константу
         if GetOpnd(ea,0)==5:
             break
         # Находим объект трассировки
         traceval=GetOpnd(ea,0)
         break
     step=+1
while ea!=parent:
     SetColor(ea,CIC_ITEM,0xcbe4e4)
     ea=RfirstB(ea)
     # Ищем трассируемый операнд
     if GetOpnd(ea,0)==traceval:
         # Влияют ли математические инструкции на будущий
         # размер
         if GetMnem(ea) in maths:
             if GetMnem(ea)=='lea':
                 # lea причастна только при сложении
                 if '+' not in GetOpnd(ea,1):
                     break
                     # Сообщаем о возможной арифметической ошибке
                     print 'La vida Alloca at address',hex(ea)

ОГЛЯДЫВАЯСЬ НАЗАД

Если возвращаемое значение неправильно интерпретируется или попросту игнорируется, то поведение программы может стать непредсказуемым. Мы должны осматривать функции выделения памяти, поскольку многие уязвимости были связаны с отсутствием проверки возвращаемого значения. Книга по исследованию уязвимостей The Art of Software Security Assessment

Отсутствие проверки значения, возвращаемого функцией выделения памяти, часто служит причиной захвата потока управления дефектной программы. В Windows существует несколько различных функций выделения памяти. Из них в user mode — LocalAlloc, SysAllocString, realloc и в kernel mode — ExAllocatePoolWithTag. Также присутствует множество оберток функций, выделяющих память, наподобие MIDL_user_allocate. В примере ниже значение, отданное LocalAlloc (в регистре еах) без проверки, служит аргументом для функции NtAdjustPrivilegesToken.

Код:
push eax ; uBytes
push ebx ; uFlags
mov [ebp+arg_4], eax
call ds:LocalAlloc(x,x)
lea ecx, [ebp+uBytes]
push ecx
push eax ; all input are evil!
push [ebp+arg_4]
mov [ebp+hMem], eax
push [ebp+var_4]
push ebx
push [ebp+var_8]
call edi ; NtAdjustPrivilegesToken(x,x,x,x,x,x)

Задача автоматического анализа этого паттерна сводится к проверке, является ли регистр еах операндом инструкции сравнения (cmp, test). В этом случае будет ясно, что возвращаемое значение сравнивается с нулем. Код, выполняющий эту нехитрую задачу:

Python:
tests=['cmp','test']
for step in range(5):
     ea=Rfirst(ea)
     SetColor(ea,CIC_ITEM,0xcbe4e4)
     if GetMnem(ea) in tests:
         if GetOpnd(ea,0)=='eax' or GetOpnd(ea,1)=='eax':
             break
         print 'No check return value at address',hex(ea)
     step=+1

Отправной точкой для анализа вышеописанных паттернов уязвимостей служит, конечно же, одна из функций выделения памяти. Для примера ниже приведен код, обходящий всю базу IDA в поисках вызовов malloc:

Python:
for seg_ea in Segments():
     for ea in Heads(seg_ea, SegEnd(seg_ea)):
         if isCode(GetFlags(ea)):
             if GetMnem(ea) == "call":
                 if re.match('.*malloc.*',GetOpnd(ea,0)):
                     allox.append(ea)

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

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

PRAY-AFTER-FREE
Use-after-free — весьма распространенный баг, сущность которого кроется в самом названии — использование после освобождения. Возникает вследствие некорректных операций программы с указателем (как и в случае с double free). В последнее время этот вид уязвимостей в адвизори-лентах светится все чаще — CVE2012-0469, CVE-2012-1529, CVE-2012-1889. Идея эксплуатации этого бага состоит в том, чтобы после освобождения объекта в памяти заставить программу выделить фейковый кусок памяти (или переполнить буфер), а затем программа сама «использует» объект (на благо атакующего). Эксплуатация этого типа уязвимостей выполняется посредством техники распрыскивания кучи, которая и создаст подложный объект. Этого пациента можно идентифицировать через проверку доступа к указателю после освобождения. Трассировка «вниз» позволяет локализовать места доступа к указателю.

Взглянем на иллюстрацию, представленную на рисунке 4.

Ch5f4xA.png

Рис. 4. Указатель на освобожденную память ptr читается в регистр еах

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

Python:
# Пока не встретилась пропасть
while ea!=0xFFFFFFFF:
     ea=Rfirst(ea)
     SetColor(ea,CIC_ITEM,0xcbe4e4)
     # Кто-нибудь использует трассируемое значение?
     if GetOpnd(ea,0)==traceval or GetOpnd(ea,1)==traceval:
         print "may be used after free",traceval,hex(ea)

Ошибка double free подобна use-after-free тем, что после освобождения определенного фрагмента памяти также пытается использовать указатель.

ОСВОБОДИТЬ ОСВОБОЖДЕННОГО
Читатель, исследующий бинарный код, уже отметил, что на иллюстрации также присутствует уязвимость double-free — повторное освобождение памяти (goo.gl/9z5Fb). Уязвимость подобного рода возникает при попытке освободить тот фрагмент памяти, который система уже считает освобожденным. Уязвимость класса double free так же, как и use-after-free, используется для манипуляций с метаданными кучи. Пресловутый указатель ptr используется в качестве аргумента к free дважды. Приведенный выше пример из IDA является результатом простого С-кода, представленного на рисунке 5.

LanLsFh.png

Рис. 5. Указатель ptr используется в качестве аргумента к free дважды

В дебрях ассемблера поиск потенциально дважды освобожденного сводится к поиску указателя, повторно используемого в качестве аргумента. Отправная и конечная точка поиска — вызов free. Объект трассировки — указатель. Различия в количестве параметров у разных «освободителей» типа HeapFree, free, VirtualFree в плане идентификации указателя функции легко решаются — просто от начала вызова ищется определенный push. Задача идентификации ошибки повторного освобождения памяти, на первый взгляд, простая. Но ограничение статического анализа порой состоит в невозможности найти родительскую функцию (в случае с виртуальными функциями). Все же этот вид уязвимости можно локализовать с помощью приведенного ниже кода. Смысл его — двигаясь от вызова free вверх, искать другой вызов функции освобождения, чтобы сравнить указатель с трассируемым.

Python:
# Пропасть вводит IDA в бесконечный цикл
while ea!=0xFFFFFFFF:
     # Красим путь
     SetColor(ea,CIC_ITEM,0xe5f3ff)
     # Получить ссылку
     ea=RfirstB(ea)
     # Проверка: были ли мы тут
      if GetColor(ea,CIC_ITEM)==0xe5f3ff:
         break
     # Ищем вызов free
     if GetMnem(ea)=='call':
         if 'free' in GetOpnd(ea,0):
             for step in range(5):
                 ea=RfirstB(ea)
                 SetColor(ea,CIC_ITEM,0xcbe4e4)
                 if GetMnem(ea)=='push':
                     # В переменную операнд push’a
                     val=GetOpnd(ea,0)
                     break
                 step=+1
             # Пять шагов на поиск возможного источника
             for step in range(5):
                 ea=RfirstB(ea)
                 SetColor(ea,CIC_ITEM,0xcbe4e4)
                 if GetMnem(ea)=='mov':
                     val=GetOpnd(ea,1)
                     break
                 step=+1
             # Ключевая проверка
             if val==traceval:
                 print 'double free', hex(ea)

При глобальном анализе, по всей базе дизассемблера IDA (idbфайлу), целесообразно после работы с функцией освобождения памяти перекрашивать за собой пройденный окрашенный путь, поскольку алгоритм построен так, что анализирующие функции используют покраску, чтобы не попасть в бесконечный цикл. Чистильщик представлен ниже:

Python:
def Cleaner(ea):
     # Бэкапим адрес
     downea=ea
     # Обеление
     SetColor(ea,CIC_ITEM, 0xFFFFFFFF)
     while ea!=0xFFFFFFFF:
         # Идем вверх
         ea=RfirstB(ea)
         # Вы еще не отбелились?
         if GetColor(ea,CIC_ITEM)!=0xFFFFFFFF:
             # Тогда мы идем к вам
             SetColor(ea,CIC_ITEM, 0xFFFFFFFF)
         # Иначе уходим
         else:
             break
     while downea!=0xFFFFFFFF:
         # Идем вниз
         downea=Rfirst(downea)
         # Аналогично
         if GetColor(downea,CIC_ITEM)!=0xFFFFFFFF:
             SetColor(downea,CIC_ITEM, 0xFFFFFFFF)
         else:
             break

Ошибки повторного освобождения памяти гораздо проще находить динамическим анализом

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

ВВЕДЕНИЕ В МЕЖПРОЦЕДУРНЫЙ АНАЛИЗ
Говоря простым языком, межпроцедурный анализ — тот анализ, который на основе исследования входных и выходных данных подпрограмм рисует картину взаимодействия между функциями. На рисунке 6 мы видим функцию func2, где один из аргументов — указатель ptr.

k7cvqK8.png

Рис. 6. Функция func2 собственной персоной

Вызывается эта функция из листинга, представленного на рисунке 7.

5LL2KNB.png

Рис. 7. Враппер, вызывающий функцию func2

Согласно конвенции вызовов stdcall, ptr попадает в func2 через стек с помощью второго снизу push’a. Дотянуться до трассируемого значения межпроцедурно в данном случае очень просто. Ступени к победе таковы:

1. Найти трассируемого в фрейме функции. То есть вычислить положение аргумента (относительно других аргументов, переменных) в стековом фрейме функции func2 (вызываемой функции). Для примера приведу код, выводящий на экран содержимое фрейма функции:

Python:
# Получаем фрейм функции
stack_frame = GetFrame(get_screen_ea())
# Запрашиваем размер
frame_size = GetStrucSize(stack_frame)
# Массив для работы с содержимым фрейма
stk_vars=[]
while frame_counter < frame_size:
     # Перебираем имена переменных
     stack_var = GetMemberName(stack_frame, frame_counter)
     if stack_var!=None:
         print " Stack Variable: %s " % (stack_var)
         # Сохраняем имена
         stk_vars.append(stack_var)
         frame_counter += 1
         # Вывод результата
for var in stk_vars:
     print "stack var:",var

2. Найти путь наверх. То есть с помощью перекрестных ссылок найти, какая функция вызывает текущую.

3 Найти трассируемого в прошлой жизни предыдущей функции (в примере ptr был регистром еах).

Перед реализацией этих шагов стоит почитать пост автора IDA Pro о межпроцедурном анализе через анализ стековых переменных (www.hexblog.com/?p=42). Отмечу, что у скрипта, который ты найдешь на прилагаемом к журналу диске, помимо отсутствия взаимодействия меж функциями, существуют следующие ограничения: неполный охват кода, отсутствие проверки операций с указателем (в деле поиска ошибок double free), работа с ограниченным количеством представителей семейств функций выделения и освобождения памяти (malloc и free). На этом всё. Удачи!

Файл, предоставленный автором статьи.

Python:
from idaapi import *
from idautils import *
import string
import re
#buggy demo script

vulncount=0

def main():


    frees=[]
    allox=[]
    for seg_ea in Segments():
        for ea in Heads(seg_ea, SegEnd(seg_ea)):
            if isCode(GetFlags(ea)):
                if GetMnem(ea) == "call":
                    if 'free' in GetOpnd(ea,0):
                        #print "free at",hex(ea),GetOpnd(ea,0)
                        SetColor(ea,CIC_ITEM,0xcbe4e4)
                        frees.append(ea)
                    if re.match('.*malloc.*',GetOpnd(ea,0)):
                        #print "malloc at",hex(ea),GetOpnd(ea,0)
                        SetColor(ea,CIC_ITEM,0xcbe4e4)
                        allox.append(ea)

    print "Search done"

    for id in allox:
        print hex(id),GetOpnd(id,0)
        CheckAllocSize(id)
        CheckAllocRet(id)
    for id in frees:
        print hex(id),GetOpnd(id,0)
        freez(id)


def freez(ea):
    startaddr=ea
    #search push
    for step in range(3):
        ea=RfirstB(ea)
        SetColor(ea,CIC_ITEM,0xcbe4e4)
        if GetMnem(ea)=='push':
            traceval=GetOpnd(ea,0)
            print "found push",traceval
            break
        step=+1
    #search pointer
    for step in range(5):
        ea=RfirstB(ea)
        SetColor(ea,CIC_ITEM,0xcbe4e4)
        if GetMnem(ea)=='mov':
            if GetOpnd(ea,0)==traceval:
                traceval=GetOpnd(ea,1)
                #print "found push",traceval
                break
        step=+1

    CheckUseAfterFree(startaddr,traceval)
    CheckDoubleFree(startaddr,traceval)
    Cleaner(startaddr)


def CheckAllocSize(allocaddr):
    #print 'allocsize'
    ea=allocaddr
    parent = GetFunctionAttr(ea,0)
    maths=['inc','add','mul','imul','lea','movsx','dec','sub','shl','shr']
    #search push. For malloc it 1st push
    for step in range(5):
        ea=RfirstB(ea)
        SetColor(ea,CIC_ITEM,0xcbe4e4)
        if GetMnem(ea)=='push':
            if GetOpnd(ea,0)==5:
                break
            traceval=GetOpnd(ea,0)
            break
        step=+1

    while ea!=parent:
        SetColor(ea,CIC_ITEM,0xcbe4e4)
        ea=RfirstB(ea)
        if GetOpnd(ea,0)==traceval:
            if GetMnem(ea) in maths:
                if GetMnem(ea)=='lea':
                    if '+' not in GetOpnd(ea,1):
                        break
                print 'La vida Alloca at address',hex(ea)
                break

        if ea==0xffffffff:
            break
        if GetColor(ea,CIC_ITEM)==0xcbe4e4:
            break

def CheckAllocRet(ea):
    #print 'allocret'
    tests=['cmp','test']
    for step in range(5):
        ea=Rfirst(ea)
        SetColor(ea,CIC_ITEM,0xcbe4e4)
        if GetMnem(ea) in tests:
            if GetOpnd(ea,0)=='eax' or GetOpnd(ea,1)=='eax':
                #print 'checked'
                break
            print 'No check return value at address',hex(ea)
        step=+1


def CheckUseAfterFree(startaddr,traceval):
    ea=startaddr
    #going down
    while ea!=0xFFFFFFFF:
        ea=Rfirst(ea)
        SetColor(ea,CIC_ITEM,0xcbe4e4)
        if GetOpnd(ea,0)==traceval or GetOpnd(ea,1)==traceval:
            if GetMnem(ea)=='pop':
                #print 'false'
                break
            if '[' not in traceval:
                #print 'false'
                break
            else:
                print "used after freed",traceval,hex(ea)
                break


def CheckDoubleFree(ea,traceval):
    ea=RfirstB(ea)
    while ea!=0xFFFFFFFF:

        SetColor(ea,CIC_ITEM,0xe5f3ff)
        ea=RfirstB(ea)
        if GetColor(ea,CIC_ITEM)==0xe5f3ff:
            print 'recurse'
            break
        if GetMnem(ea)=='call':
            #print 'call at',hex(ea)
            if 'free' in GetOpnd(ea,0):
                print 'free'
                for step in range(5):
                    ea=RfirstB(ea)
                    SetColor(ea,CIC_ITEM,0xcbe4e4)
                    if GetMnem(ea)=='push':
                        val=GetOpnd(ea,0)
                        print "found push",traceval,val
                        break
                    step=+1

                for step in range(5):
                    ea=RfirstB(ea)
                    SetColor(ea,CIC_ITEM,0xcbe4e4)
                    if GetMnem(ea)=='mov':
                        val=GetOpnd(ea,1)
                        print "found push",traceval
                        break
                    step=+1
                if val==traceval:
                    print 'double free',hex(ea)
                    break

def Cleaner(ea):
    print 'cleaner'
    print hex(ea)
    downea=ea
    SetColor(ea,CIC_ITEM,0xFFFFFFFF)
    while ea!=0xFFFFFFFF:
        ea=RfirstB(ea)
        if GetColor(ea,CIC_ITEM)!=0xFFFFFFFF:
            SetColor(ea,CIC_ITEM,0xFFFFFFFF)
        else:
            break
    while downea!=0xFFFFFFFF:
        downea=Rfirst(downea)
        if GetColor(downea,CIC_ITEM)!=0xFFFFFFFF:
            SetColor(downea,CIC_ITEM,0xFFFFFFFF)
        else:
            break



if __name__ == "__main__":
    main()

Источник
 


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