• 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.

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

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

VvuEGjf (1).png

Рис. Структура цикла

Идентифицировать цикл через построение дерева доминаторов способен плагин к IDA Loop Detection, скрипт findloop из Immunity Debugger, Loop colorizer от Ильфака Гуильфанова. Но найти – это не то же самое, что и понять. Поэтому перейдем к примерам уязвимостей в циклах и их анализу.

КОЛЕСО ПЕРЕРОЖДЕНИЙ
Самая известная уязвимость в рассматриваемом паттерне — это переполнение буфера в интерфейсе RPC DCOM. Печально известная уязвимость стала результатом непроверяемого цикла копирования строки при выделении имен серверов из путей в формате UNC.

Код:
mov ax, [еах+4]
cmp ах, ʹ\ʹ
jz short loc_761AE698
sub edx, ecx
beginloop:
mov [ecx], ax ;пишем
inc ecx ;итератор
inc ecx
mov ax, [ecx+edx]
cmp ax, ʹ\ʹ ;проверка с ʹ\ʹ
jnz short beginloop

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

Подобный натюрморт наблюдается в дефектном коде бюллетеня MS08-067:

Код:
begin_loop:
     mov eax, dword ptr [ebx]
     movzx ecx,word ptr [eax]
     cmp ecx,5Ch ;проверка с ʹ\ʹ
     je out_of_loop ;на выход
     mov eax,dword ptr [ebx]
     cmp eax,dword ptr [esi]
je out_of_loop ;на выход
     mov eax,dword ptr [ebx]
     sub eax,2 ;итератор
     mov dword ptr [ebx],eax ;пишем
jmp begin_loop

Но и современность также балует уязвимыми циклами. Взглянем на недавнюю уязвимость в SAP NetWeaver (CVE-2012-2611):

Код:
begin_loop:
    cmp edx,2
    mov [ebp+DataEnd], TraceInfo
    jnz copy_with_unicode_conversion ;переход внутри
                                      ;тела
    mov dx, TraceInfo
    mov [ebp+eax*2+var_d],dx ;запись в память
    jmp loop_end
copy_with_unicode_conversion:
    movzx cx, byte ptr [TraceInfo]
    mov [ebp+eax*2+var_d],cx ;запись в память
loop_end:
    cmp [ebp+eax*2+var_d],0 ;пока не встретится
                             ;ноль
    jz out_of_loop
     …
    add eax,1 ;инкремент
    add TraceInfo,edx
jmp begin_loop

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

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

Код:
begin_loop:
     xor ecx,ecx
     mov edx,[edi]
     mov cx,[ebp+e]
     imul ecx,eax ;операция умножения с
                  ;условиями выхода
     mov [edx+eax*4],ecx
     mov ecx,[edi+8]
     inc eax
     cmp eax,ecx
    jl short begin_loop

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

Подведем промежуточный итог. Что же такое уязвимость цикла? Исходя из корня слова «определение», обозначим границы контроля за циклом. Из приведенных выше листингов дефектного кода следует, что интересность цикла — это фактор записи + фактор контроля за условием выхода из цикла.

В следующем примере показаны варианты контроля за циклом. Речь об уязвимости в Microsoft Vector Graphic rendering Engine (CVE-2006-4868). Вот код:

Код:
begin_loop:
     mov edx, [ecx+8]
     mov ebx, [ecx]
     mov dx, [ebx+edx*2]
     test dx, dx
     jz short loc_5DEDED2F ;пока не 0
     cmp dx, 20h
     jnz short loc_5DEDED1E ;пока не 20h
     test esi, esi
      jg short loc_5DEDED33
      jmp short loc_5DEDED24
loc_5DEDED1E:
      mov [edi], dx
     inc esi
     inc edi
      inc edi
loc_5DEDED24:
      inc dword ptr [ecx+8]
      mov edx, [ecx+8]
      cmp edx, [ecx+4]
jl short begin_loop

Пример от остальных мало чем отличается, а приведен, чтобы показать то, что называется false positives — отрицательный результат анализа. ZERT (Zeroday Emergency Response Team) добавила проверку значения, находящегося по адресу [ecx+4], тем самым условие выхода из цикла стало «прибито гвоздями» (захардкодено). Microsoft же в своем патче добавила проверку итератора. Здесь их парочка, но проверки одного достаточно:

Код:
cmp esi, 0FEh
jnb short skip_copy
…
mov [edi], dx
inc esi
inc edi
inc edi

Отметим, что, если значение esi контролируется, при знаковом переходе после сравнения можно было бы по-прежнему «обладать телом цикла». Но использование беззнакового перехода jnb закрыло путь-дорожку к эксплуатации. В патче от ZERT также семейка знаковых переходов была обделена вниманием.

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

Код:
cmp reg32, imm32 ;сравнение итератора
…
inc|add|sub|dec reg32 ;итератор

и

Код:
cmp reg32, imm32 ;сравнение операнда проверки
…
cmp 0opnd, 1opnd ;где reg32 — один из операндов
jump out_of_loop ;на выход из цикла

Куда же без основной задачи реверс-инжиниринга: «будет ли Х иметь значение Y после заданного набора инструкций?». Наряду с inline memcpy, анализ циклов также нуждается в трассировке регистров.

Продолжая тему false positive (истинно негативных) признаков, следует обратить внимание на то, что цикл может быть подобен одной из инструкций stosb/stosw/stosd, пишущих в память константу.

Но вернемся к пониманию прекрасного — свойствам потенциально уязвимого цикла:
• записи в память непостоянного значения;
• отсутствию корректной проверки выхода из цикла.

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

КОНТРОЛИРУЕМЫЙ ВЫХОД ИЗ ТЕЛА. АСТРАЛЬНЫЕ ПРАКТИКИ
Предлагаемое заклинание кода имеет следующие этапы:
1. Поиск тела цикла (пикап). Составляем список всех условных переходов и адресов тела.
2. Поиск паттерна записи.
3. Поиск условий выхода.
• Поиск условных переходов, указывающих на выход из тела.
• Поиск и анализ инструкций сравнения.
4.1. Трассировка операнда инструкции сравнения.
4.2.Поиск итератора и трассировка его операнда.

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

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

Python:
while addr!=startaddr:
    # Получаем ссылки
    xref1=Rfi rstB(addr)
    xref2=Rnext(addr,0)
    # Отсев ненужных ссылок
    if xref2!=0xffffffff and GetMnem(addr)!="call" and GetMnem(addr)!="jmp":
        # Добавляем адрес с условным переходом в список
        branchpoints.append(addr)
    # Проверка на пропасть
    if xref1 == 0xffffffff:
        break
    addr=xref1
    # Добавляем текущий адрес к списку с телом цикла
    bodyaddr.append(addr)

Затем, используя список адресов, находим паттерн записи в память — инструкцию mov, где 0-й операнд — память, а 1-й — регистр (прим.: mov [eax],ecx). Темная сторона этого свойства (деготь в меде) проявляется, когда 1-й операнд — постоянное значение (прим.: 0xBAADF00D — потеря аппетита от такой пищи гарантирована).

Python:
for addr in bodyaddr:
     if GetMnem(addr)=="mov":
     # Список "памятных" типов: базовый
     # регистр + индексный регистр, базовый
     # регистр + индексный регистр + смещение
     memtypes=[3,4]
     if GetOpType(addr,0) in memtypes:
         # Если 1-й операнд регистр
         if GetOpnd(addr,1)==1:
             # Цикл пишущий, вернем истину
             return 1

Далее ищутся условные переходы, указывающие за пределы цикла.

Python:
for addr in branchpoints:
     # Получаем ссылки
     xref1=Rfi rst(addr)
     xref2=Rnext(addr,0)
     # Если одна ссылка указывает на тело
     if xref1 in bodyaddr:
         # займемся второй
         # Функция-ищейка попытается найти тело за 20
         # шагов и помашет флажком
         fl ag=SearchBodyAddr(xref2)
     else:
         fl ag=SearchBodyAddr(xref1)
     if fl ag==1:
         # Удалим переход-инсайдер
         branchpoints.remove(addr)

Теперь от каждого jump-а, указывающего из тела, ведется поиск компараторов — инструкций сравнения cmp, test. Нулевой операнд каждого «сравнивателя» скармливается функции трассировки, дабы познать значение регистра-операнда.

Python:
# Список инструкций сравнения
cmps=['cmp','test']
# за небольшое кол-во шагов
for count in range(5):
     # пока не найден сравниватель
     while GetMnem(addr) not in cmps:
         addr=Rfi rstB(addr)
         if GetMnem(addr)=="test":
             # Если "внетелесный джамп" один-одинешенек,
             # то имеем дело с проверкой на ноль
             # (test reg,reg эквивалентно cmp reg,0)
             if len(branchpoints)==1:
                 # Добрые вести
                 print "input is exit condition!"
                 vulncount+=1
              break
          # Встречая cmp, где 1-й операнд != константе
         if GetMnem(addr)=="cmp" and GetOpType(addr,1)!=5:
             # передаем нулевой операнд трассировщику
             reg=GetOpnd(addr,0)
             TraceVal(reg,addr,iters)
              break
          # Нашла коса на камень
          if GetMnem(addr)=="cmp" and GetOpType(addr,1)==5:
                # Печалька
                 hardcoded=1
                 # Не теряя надежду, шагая вниз, ищем j[gl]
                 for count in range(5):
                     addr=Rfi rst(addr)
                 if GetMnem(addr) in signjumps:
                     vulncount+=1

Трассировка нужна и в отношении операнда итератора (счетчика). Если итераторов несколько, то необходимо проверить операнд каждого из них, в связи с возможностью присутствия проверки количества прохождений цикла. Трассировка итератора отлична от трассировки нулевого операнда инструкций сравнения cmp и test. Как видно из примеров уязвимых циклов, если операнд итератора сравнивается с константой и переход после сравнения беззнаковый, то это однозначный провал операции по захвату тела. При знаковом переходе надежда еще есть.

Код:
inc reg32
cmp reg32, imm32
jnb out_loop

Составляем список итераторов тела

Python:
# Список возможных итераторов
iterlist=["inc","add","sub","dec"]
# Припасаем вместилище
iters=[]
# В списке адресов тела ищем цели
for addr in bodyaddr:
     if GetMnem(addr) in iterlist:
     # Ищущий да зааппендится
     iters.append(addr)
return iters

Затем для каждого элемента списка итераторов вызываем трассирующую функцию

Код:
for addr in iters:
     reg=GetOpnd(addr,0)
     TraceVal(reg,endaddr)

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

Python:
# Подозревая signed/unsigned mismatch
suspectedins=["movsx","sub","add"]
# Посыльщики
movers=["mov","movzx"]\
# Виды адресации [ebp+esi],[ebp+esi+8]
memtypes=[3,4]
# Берем адрес начала функции
parent = GetFunctionAttr(addr,0)
while addr != parent or addr!=0xffffffff:
     # На случай чанкед-функций
     # ищем начало фрагмента
     if GetMnem(addr)=="push" and GetOpnd(addr,0)=="ebp":
         print "prolog"
         break
     # Получаем ссылку
     addr = Rfi rstB(addr)
     # Ищем пациента
     if GetOpnd(addr,0)==reg:
         # Если значение возвращается функцией
         if GetMnem(addr)=="call" and reg=="eax":
             print reg,"returned by call"
             break
         # Обнуление счетчика — частое событие
         if GetMnem(addr)=="xor":
             print reg,"xored"
             hardcoded=1
             break
         # Встреча с подозреваемыми без алиби
         if GetMnem(addr) in suspectedins and addr not in iters:
             print "suspected ins",reg,"at addr",hex(addr)
             vulncount+=1
         # Или объект трассировки меняется
         # или разводим руками: из памяти пришло,
         # в память уйдет
         if GetMnem(addr) in movers:
             if GetOpType(addr,1)==1:
                 reg=GetOpnd(addr,1)
             if GetOpType(addr,1) in memtypes:
                 print reg,"from memory"
                 break

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

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

Python:
#A script for search vuln loop. A public version.
from idaapi import *
from idautils import *
import string
bodyaddr=[]
branchpoints=[]
endaddr=get_screen_ea()
iterlist=[]
iters=[]
signjumps=["jg","jge","jl","jle"]
vulncount=0
hardcoded=0

def Pickuper(addr,startaddr):
    global branchpoints
    global bodyaddr
    infinityflag=0
    #collects addresses of path(body loop)
    bodyaddr.append(addr)
    while addr!=startaddr:
                   xref1=RfirstB(addr)
                   xref2=Rnext(addr,0)
                   #otsev call
                   if xref2!=0xffffffff and GetMnem(addr)!="call" and GetMnem(addr)!="jmp":
                   #in massiv branchpoints stored points of exit of basic blocks - jumpmassiv
                        branchpoints.append(addr)
                        print "branchpoints.append(ea)",hex(addr),hex(xref1),hex(xref2),"start",hex(startaddr)
                        #contra infinity path
                        for id in branchpoints:
                            if branchpoints.count(id) >1:
                                infinityflag=1
                                branchpoints=UniqListCreator(branchpoints)
                   if xref1 == 0xffffffff:
                        break
                   addr=xref1
                   if infinityflag==1:
                        addr=xref2
                   #collect addresses of path(body loop)
                   bodyaddr.append(addr)
                   comment=hex(addr)
                   MakeComm(addr, comment)
                   #paint
                   SetColor(addr,CIC_ITEM,0xe5f3ff)
                   if addr==startaddr:
                        print "found startaddr!"
                        return
    return

def UniqListCreator(list):
    list2=[]
    for id in list:
        if id not in list2:
            list2.append(id)
    return list2


def Writesearcher():
    global bodyaddr
    #search the first wr-pattern
    #don`t search all patterns
    for addr in bodyaddr:
        if GetMnem(addr)=="mov":
            #print "mov at addr",hex(addr)
            #if memory reference
            memtypes=[3,4]
            if GetOpType(addr,0) in memtypes:
                #if 1opnd register
                if GetOpType(addr,1)==1:
                    print "This is WRiting loop"
                    #this is writing loop
                    return 1

def Outloop_jumps_searcher(endaddr):
    global branchpoints
    global bodyaddr
    outloopjumps=[]
    fakejumps=[]
    flag=0
    for addr in branchpoints:
        xref1=Rfirst(addr)
        xref2=Rnext(addr,0)
        if xref1 in bodyaddr:
            flag=SearchBodyAddr(xref2)
        else:
            flag=SearchBodyAddr(xref1)
        if flag==1:
            #remove jumps-insiders
            fakejumps.append(addr)
    for id in fakejumps:
        if id in branchpoints:
            branchpoints.remove(id)


def SearchBodyAddr(xref):
    #try to search loopbody
    flag=0
    global bodyaddr
    for count in range(20):
        xref=Rfirst(xref)
        comment=hex(xref)
        MakeComm(xref, comment)
        #paint
        SetColor(xref,CIC_ITEM,0xcbe4e4)
        if xref in bodyaddr:
            flag=1
            break
        print "searchendloop",hex(xref)
        if xref==0xffffffff:
            flag=0
            break
        count=+1
    print "flag of insideloop jump",flag
    return flag

def CmpSearch(addr):
    global branchpoints
    global iters
    global signjumps
    global vulncount
    global hardcoded
    #for 5 steps search cmp,test ins
    cmps=['cmp','test']
    for count in range(5):
        while GetMnem(addr) not in cmps:
            addr=RfirstB(addr)
            if GetMnem(addr)=="test" and GetOpnd(addr,0)==GetOpnd(addr,1):
                print "test at", hex(addr)
                if len(branchpoints)==1:
                    print "input is exit condition!"
                    vulncount+=1
                break
            if GetMnem(addr)=="cmp" and GetOpType(addr,1)!=5:
                print "cmp at", hex(addr)
                reg=GetOpnd(addr,0)
                TraceVal(reg,addr,iters)
                break
            if GetMnem(addr)=="cmp" and GetOpType(addr,1)==5:
                hardcoded=1
                for count in range(5):
                    addr=Rfirst(addr)
                    print hex(addr)
                    if GetMnem(addr) in signjumps:
                        vulncount+=1
                        return

                break

        count=+1

    return vulncount
def SearchFuncProlog(addr):
    if GetMnem(addr)=="push" and GetOpnd(addr,0)=="ebp":
        return 1

def TraceVal(reg,addr,iters):
    hardcoded=0
    global vulncount
    global signjumps
    suspectedins=["movsx","sub","add"]
    movers=["mov","movzx"]
    #mem constants[ebp+arg_0],[eax],[eax+8]
    memtypes=[2,3,4]
    print reg, hex(addr)
    parent = GetFunctionAttr(addr,0)
    while addr != parent or addr!=0xffffffff:
        if SearchFuncProlog(addr)==1:
            print "prolog"
            break
        comment=hex(addr)
        MakeComm(addr, comment)
        addr = RfirstB(addr)
        if GetOpnd(addr,0)==reg:
            comment=hex(addr)
            MakeComm(addr, comment)
            if GetMnem(addr)=="lea":
                break
            if GetMnem(addr)=="call" and reg=="eax":
                print "at call ins stop trace"
                break
            if GetMnem(addr)=="xor":
                print reg,"xored"
                hardcoded=1
                break
            if GetMnem(addr) in suspectedins and addr not in iters:
                print "suspectedins", hex(addr),reg
                vulncount+=1
            #if cmp reg,imm
            if GetMnem(addr)=="cmp" and GetOpType(addr,1)==5:
                hardcoded=1
                for count in range(5):
                    addr=Rfirst(addr)
                    if GetMnem(addr) in signjumps:
                        vulncount+=1
                        break
                break

            if GetMnem(addr) in movers:
                #if mov
                if GetOpType(addr,1)==1:
                    reg=GetOpnd(addr,1)
                if GetOpType(addr,1) in memtypes:
                    print reg,"from memory"
                    break

    return vulncount,hardcoded

def IterSearch():
    global bodyaddr
    global iters
    iterlist=["inc","add","sub","dec"]
    for addr in bodyaddr:
        if GetMnem(addr) in iterlist:
            iters.append(addr)
    return iters


def main(endaddr):
    global branchpoints
    global bodyaddr
    startaddr=0
    vulncount=0
    global hardcoded
    global signjums
    #searching startaddr
    xref1=Rfirst(endaddr)
    xref2=Rnext(endaddr,0)
    if xref1 < endaddr:
        startaddr=xref1
    if xref2 < endaddr:
        startaddr=xref2
    else:
        print "Try other loop"
        return
    print "startaddr", hex(startaddr),hex(xref1),hex(xref2),"endaddr",hex(endaddr)
    #search bodyloop. Takes start and end addresses of loop
    Pickuper(endaddr,startaddr)
    for id in branchpoints:
        print "branchpoint:",hex(id)
    for id in bodyaddr:
        print "body:",hex(id)
    #search for writing loop

    if Writesearcher()==1 and len(branchpoints)>1:
        Outloop_jumps_searcher(endaddr)

    elif Writesearcher()!=1:
        print "loop is not interesting"
        return
    for addr in branchpoints:
        vulncount=CmpSearch(addr)
        if hardcoded==1:
            print "vulncount=",vulncount,"hardcoded=",hardcoded
            return
    iters=IterSearch()
    for id in iters:
        print "iter", hex(id)
    for addr in iters:
        reg=GetOpnd(addr,0)
        vulncount,hardcoded=TraceVal(reg,endaddr,iters)
        print "vulncount=",vulncount,"hardcoded=",hardcoded
    for addr in branchpoints:
        if GetMnem(addr) in signjumps:
            vulncount+=1
    print "Loop are analyzed. Vulncount=",vulncount,"hardcoded=",hardcoded


if __name__ == "__main__":
    main(endaddr)

Источник
 
Последнее редактирование:


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