12 мая 2012 г.

Исключения в Windows.

А сегодня поковыряемся в темных закоулках исключений Windows. Они созданы для того, чтобы обрабатывать всякие ошибки, например если возникает деление на ноль или запись в ячейку памяти которая защищена от записи. По умолчанию мы получим сообщение типа "Application Error.", а в Windows Vista/Seven вообще можно ничего не получить, программа просто закроется.

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

В Windows существует механизм структурной обработки исключений(Structured Exception Handling) сокращенно SEH. Он обрабатывает возникшие исключения в программе и позволяет сделать попытку исправить ошибку возникшую в ходе выполнения программы.
SEH сам по себе выглядит в форме цепочки, где каждое следующее звено указывает на предыдущее. Структура выглядит приблизительно так:


EXCEPTION_REGISTRATION struc
     prev    dd      ?
     handler dd      ?
EXCEPTION_REGISTRATION ends
Где prev - это адрес предыдущего исключения, а handler - обработчик исключения.
Чтобы прервать цепочку и дальше она не продолжалась необходимо в prev записать -1.
Адрес на конец цепочки по умолчанию находиться в регистре FS(хотя бывают рукотворные исключения), тоесть для того, чтобы посмотреть на конец цепочки можно просто сделать следующее:


assume fs:nothing
mov eax, DWORD PTR fs:[0]

или если не хочется лезть напрямую к регистру, то можно воспользоваться функцией GetThreadContext, вот так:
invoke    GetThreadContext, NULL, ADDR contexts
mov        ecx, [ecx]
В ecx сообственно будет адрес обработчика исключений.

Итак, что мы можем с этим сделать, для начала мы можем добавить свое звено в цепочку ну или просто перезаписать стандартный обработчик(что в принципе не очень рекомендуется). Для чего нужны звенья и т.п. будет чуть ниже.
Добавляем:
assume fs:nothing
push    offset except_one
push    DWORD PTR fs:[0]
mov        DWORD PTR fs:[0], esp

Или более грубо ломимся и перезаписываем:
assume fs:nothing
mov        eax, DWORD PTR fs:[0]
mov        ecx, offset except
add        eax, 4h
mov        [eax], ecx

Итак вопрос и нахрена все это надо? Ну для начала мы можем запутать логику программы, например вызывая необходимые процедуры методом деления на ноль. Бывает крышу рвет у антивирусов и отладчиков и они не знают, что делать и как. Конечно для опытного крякера это не помеха, но поломать бошку придется, особенно если например захучить SetUnhandledExceptionFilter и сделать свой обработчик BasepCurrentTopLevelFilter. Можно сорвать стек у одной программы и например внедрить свой код при помощи исключения. Ну и главное можно сделать целую логику программы на исключениях. Например делаем деление на ноль - выполняется одна процедура, а если при расшифровке данных сломали джамп и не ввели правильный ключ, то получаем другую процедуру. Ну и небольшое колдунство которое я написал для примера, как это может работать:
.386
.model flat, stdcall

option casemap: none

      include \masm32\include\windows.inc
      include \masm32\macros\macros.asm

      uselib kernel32, user32, masm32

.data
    textMSG1    db "Exception divine by ZERO!",0 ; сообщение для деления на ноль
    textMSG2    db "Exception ACCESS violation!",0 ; сообщение для нет доступа к памяти
    header        db "ALL OK!",0 ; заголовок
    pContext     equ [esp+0Ch] ; смещение где лежит структура EXCEPTION_RECORD

.data?


.code
start:

    assume fs:nothing
    push    offset except_one ; пишем адрес процедуры обрабатывающей деление на ноль
    push    DWORD PTR fs:[0] ; создаем второе звено(первое - дефолтный обработчик)
    mov        DWORD PTR fs:[0], esp ; заносим ее в регистр fs
    push    offset except_two ; пишем адрес процедуры обрабатывающей доступ к памяти
    push    DWORD PTR fs:[0] ; создаем третье звено SEH
    mov        DWORD PTR fs:[0], esp ; заносим адрес в регистр fs

    xor        eax, eax ; обнуляем eax
    div        eax ; делим на eax где сейчас лежит 0 и вызываем обработчик исключения except_one

fixed_one:
    xor        eax, eax ; обнуляем eax
    mov        [eax], eax ; пытаемся записать по адресу 0 и вызываем исключение except_two
    pop        fs:[0] ; востанавливаем стек чтобы ничего не сломалось
    pop        eax ; востанавливаем стек чтобы ничего не сломалось
    pop        fs:[0] ; востанавливаем стек чтобы ничего не сломалось
    pop        eax ; востанавливаем стек чтобы ничего не сломалось

    jmp        prog_exit ; выход из программы
    
fixed_two:
    pop        fs:[0] ; востанавливаем стек чтобы ничего не сломалось
    pop        eax ; востанавливаем стек чтобы ничего не сломалось
    pop        fs:[0] ; востанавливаем стек чтобы ничего не сломалось
    pop        eax ; востанавливаем стек чтобы ничего не сломалось
    jmp        prog_exit prog_exit ; выход из программы

prog_exit:
    invoke    ExitProcess, NULL ; выход из программы
    
except_one proc
    mov        eax,DWORD PTR [esp+4h] ; заносим в eax адрес структуры EXCEPTION_RECORD
    mov        eax, (EXCEPTION_RECORD PTR [eax]).ExceptionCode ; считываем код исключения
    cmp        eax, 0C0000094h ; если код исключения не равено "STATUS_INTEGER_DIVIDE_BY_ZERO"
    jnz        nextExcpOne ; то прыгаем на nextExcpOne
    invoke  MessageBox, NULL, ADDR textMSG1, ADDR header,MB_OK ; иначе выводим сообщение 1
    mov        eax, pContext ; в eax запишем адрес на структуру pContext 
    mov        (CONTEXT PTR [eax]).regEip, offset fixed_one ; правим eip на продолжение программы после ошибки
    mov     eax,ExceptionContinueExecution ; возвращаем флаг, что ошибку починили и необходимо продолжить выполнение
    ret
nextExcpOne:
    mov     eax,ExceptionContinueSearch ; этот обработчик не подошел, требуем продолжение поиска в другом обработчике
    Ret
except_one EndP

except_two proc
    mov        eax,DWORD PTR [esp+4h] ; заносим в eax адрес структуры EXCEPTION_RECORD
    mov        eax, (EXCEPTION_RECORD PTR [eax]).ExceptionCode ; считываем код исключения
    cmp        eax, 0C0000005h ; если код исключения не равено "STATUS_ACCESS_VIOLATION"
    jnz        nextExcpTwo ; то прыгаем на nextExcpTwo
    invoke  MessageBox, NULL, ADDR textMSG2, ADDR header,MB_OK ; иначе выводим сообщение 2
    mov        eax, pContext  в eax запишем адрес на структуру pContext
    mov        (CONTEXT PTR [eax]).regEip, offset fixed_two ; правим eip на продолжение программы после ошибки
    mov     eax,ExceptionContinueExecution ; возвращаем флаг, что ошибку починили и необходимо продолжить выполнение
    Ret
nextExcpTwo:
    mov     eax,ExceptionContinueSearch ; этот обработчик не подошел, требуем продолжение поиска в другом обработчике
    Ret
except_two EndP


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

 А теперь бонус трек. Метод стырин вот от сюда. В IDA нельзя посмотреть SEH Chain как в OllyDbg и это есть прискорбно да и в стеке там обычно некоторая каша, которая не дает подсказки как OllyDbg. Можно воспользоваться Task Information Block сокращенно TIB. Для этого запускаем программу в отладчике IDA. И нажимаем CTRL+S  должно получиться, что то типа такого экрана где ищем упоминания TIB:
Встаем на TIB и тыкаем 2 раза на нее и попадаем на конец цепочки SEH, нажимем "o", получается более удобоваримый вид, както так:
   
 Если еще раз тыкнуть на получившийся адрес, то перейдем на следующее звено и там уже можно(опять же нажав кнопку "o" для удосбтва) увидеть какой адрес вызывается в случаее исключения первым. Както так:
 
А дальше еще раз тыкаем 2 раза и оказываемся в процедурке которую вызывает исключение:
 
Таким образом обойдя всю цепочку SEH можно прокоментировать и описать все исключения, что поможет в дальнейшем разобраться как работает программа. Ну вот в принципе на сегодня и все...

Комментариев нет:

Отправить комментарий