Статья SEH – фильтр необработанных исключений

Marylin

Marylin

Mod.Assembler
Red Team
05.06.2019
131
328
Во времена рассвета DOS программист имел полную власть над системой. Находясь в реальном режиме работы CPU, у нас не было никаких ограничений и мы могли использовать в программах буквально все инструкции и регистры процессора, общее число которых на Р6 стремилось преодолеть барьер в ~200 единиц. Это была отличная школа программирования, где вся ответственность за то или иное действие возлагалась полностью на наши плечи. Всё приходилось делать вручную, в том числе и обрабатывать аппаратные исключения с ошибками. Одним словом, царила атмосфера свободы и демократии, столь редкая в наши дни.

А что мы имеем сейчас в защищённом режиме? Microsoft отобрала у нас все права и вместо них вручила более 5000 системных API, но забыла толком объяснить, для чего они и как ими пользоваться. При малейщем чихе гигантская система валится в BSOD подымая в небеса тонны пыли, не забыв прихватить с собой наши не сохранённые данные. Ладно если был-бы реальный повод, так ведь может и без веских на то причин.

В данной статье предлагаю рассмотреть способы перехвата системных исключений в защищённом режиме. Мы постараемся отобрать у системы то, что принадлежит нам по праву – это управление при возникновении различного рода эксепшенов. В системе, заправляет ими диспетчер исключений, который имеет своего секретаря "Windows Error Reporting" – именно WER при исключениях стреляет в нас модальными окнами со-знакомой всем посмертной надписью "Программа допустила ошибку и будет закрыта":

SEH – фильтр необработанных исключений


Что мы можем сделать в этой ситуации, когда большой Билли зажал нас между двумя кнопками ОК и Отмена? Выбрав "ОК" отправимся прямиком в царство Аида, иначе всплывёт отладчик (если таковой привязан), который никак не спасёт уже наше приложение – судьба его предрешена в любом случае. Но не это сейчас главное..

Если система "тасует мечеными", значит мы тоже имеем право мухлевать. В наших планах умышленное генерирование ошибок, на основе которых можно выстроить защитный механизм программ. Например все отладчики взводят TRAP-флаг, что заставляет проц после каждой инструкции генерить аппаратное исключение #DB. В реальном режиме, этот эксепшен обрабатывает прерывание биос INT_01h – отладчик перехватывает его и выводит на экран текущее состояние регистров и прочую информацию о своём пассажире.

В защищённом режиме ничего не меняется – Оля и её братия так-же взводят флаг TF, только юзер остаётся здесь в пролёте, т.к. отладчик маскирует сей факт. Как-бы мы не пытались из своего приложения взвести или сбросить TF, Оля не даст нам этого сделать, а значит мы должны забрать его силой. Тут и пригодится движок фиктивных исключений, которые мы будем сами генерить, и сами-же их отлавливать. Нужно сказать, что это не мы такие, а жизнь заставляет.. Приходится писать защитную малвару, грабить караваны, и только тогда наша советь будет чиста перед самим собой.


Устройство системного диспетчера исключений

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

Диспетчером пользовательских эксепшенов является неэкспортируемая из Ntdll.dll ядерная функциия KiUserExceptionDispatcher(). Она лежит в окопах и ждёт, когда юзер сделает то, на что программист никак не рассчитывал, и заранее не предугадал его действий. Как только наступает этот радужный для диспетчера момент, он вытаскивает из своих/широких штанин список обрабатываемых им исключений и по номерам сверяет его с возникшим. Если текущее исключение диспетчеру знакомо, он пофиксит его, иначе просто прихлопнет глючное приложение по ExitProcess() и всё. Вот это алгоритм.. всем-бы так работать.

Обработкой известных диспетчеру исключений, по-умолчанию занимается функция UnhandledExceptionFilter() из kernel32.dll. В стеке исполняемого потока имеется 8-байтный фрейм под названием SEH. Он состоит из двух 4-байтных слов, одно из которых указывает на функцию UnhandledExceptionFilter() где-то в нёдрах kernel32, а второе слово – это маркер окончания цепочки со-значением 0xFFFFFFFF. В отладчике это выглядит так:

SEH – фильтр необработанных исключений


Здесь, главное слово – цепочка "chain". Она даёт нам право добавлять к системному SEH свои обработчики, предлагая системному потесниться. В результате, управление будет получать уже не системный, а наш обработчик исключений. Эту рульную фишку должна иметь любая уважающая себя программа. Когда мы получим управление, в нашем распоряжении окажется код исключения, состояние регистров на этот момент, адрес глючной инструкции и прочая техническая инфа. Всё это предоставит нам диспетчер KiUserExceptionDispatcher() по следующему алгоритму..

1. Перехватив ошибку, диспетчер сбрасывает в стек породившего исключение потока состояние всех его регистров CPU – этот т.н. "Context потока" и для его описания имеется специальная структура, где указывается очерёдность следования регистров в дампе. Среди этих регистров есть и доступный для модификации регистр EIP, который позволит нашему коду после исключения отправиться в будущее, настоящее или прошлое – это мы должны будем предусмотреть заранее, посредством метки в коде.​

2. Теперь диспетчер засылает в стек ещё одну структуру "ExceptRecord", с подробными деталями возникшего эксепшена. Эта структура хранит код ошибки, адрес породившей исключение инструкции и некоторые флаги, которые рассматриваются ниже.​

3. На заключительном этапе, поместив в стек два указатели на структуры Context и ExceptRecord, диспетчер отдаёт управление опять нашей программе, чтобы свои проблемы она попыталась решить сама, посредством установленного SEH-обработчика. Если пользовательского обработчика в стеке нет, то всю эту кухню на грудь принимает системный SEH, который представлен на рисунке выше.​

Таким образом, когда наша прожка примет от диспетчера проанализированное управление, в стеке окажется фрейм "EXCEPTION_POINTER" из 4-х указателей. Потянув за них мы можем получить голограмму данной ошибки, через указанные структуры диспетчера. Прототип фрейма вполне легальный и описывается так:

C-подобный:
struct EXCEPTION_POINTERS    ;// Стековый фрейм диспетчера исключений
   pExceptRecord  dd  0        ; линк на тех-информацию об исключении
   pExceptFrame   dd  0        ; линк на установленный нами SEH-фрейм
   pContext       dd  0        ; линк на контекст регистров процессора
   pParam         dd  0        ; резерв..
ends
Этой информации нам будет достаточно, чтобы описав пару кругов над краем пропасти, вытянуть приложение с того света.


Формат обработчика пользовательского SEH

SEH означает "Structured Exception Handling", или структурный обработчик исключений. Есть ещё VEH (векторный), но он пока не нужен. По своей природе, seh-обработчик представляет собой Callback-функцию, т.е. должен возвращать кому-то некое значение (callback в дословном переводе – перезвони). В данном случае, абонентом на линии является диспетчер исключений, который в регистре EAX ждёт от нас один из двух возможных флагов:

EAX=0 (Execute Handler) – предписывает диспетчеру ребутнуть контекст регистров, и продолжить исполнение нашей программы. Если мы возвращаем этот флаг, то должны предварительно подправить в структуре Context нужные нам регистры и сказать диспетчеру фас! Тогда он прервёт SEH-цепочку и сразу передаст управление опять нашей программе, вдохнув в неё новую жизнь.

EAX=1 (Continue Search) – мы отказываемся от обработки данного эксепшена (фильтруем по коду исключения в структуре Record), и просим диспетчера передать управление следующему SEH-фрейму в цепочке. Как-правило, этим следующим является системный SEH-handler, который сфинализирует нашу прожку по известному алго – R.I.P. и к праотцам.

Осталось рассмотреть содержимое структур CONTEXT и EXCEPTION_RECORD.
Чтобы подключить их к своему проекту, мы напишем специальный инклуд и будем звать его по мере необходимости.

C-подобный:
struct EXCEPTION_RECORD       ;// Техническая информация об исключении
  ExceptionCode      dd  ?      ; код ошибки - см.сишный хидер "ntstatus.h"
  ExceptionFlags     dd  ?      ; флаг управления данным обработчиком
  NestedRecord       dd  ?      ; указатель на предыдущий SEH-фрейма
  ExceptionAddress   dd  ?      ; адрес инструкции вызвавшей исключение
  NumberParameters   dd  ?      ; число доп.параметров в сл.поле "ExceptionInfo"
  ExceptionInfo      rd  15     ; дополнительные параметры.
ends
• ExceptionCode – системный код исключения. Ознакомиться с по истине огромной коллекцией этих кодов можно в сишном заголовке ntstatus.h. На всякий-пожарный, я прикрепил его в скрепке. Смысловую нагрузку кода можно разделить на 4-части – непосредственно код, источник ошибки, флаг клиента, и важность:

SEH – фильтр необработанных исключений


• ExceptionFlag – передаёт диспетчер и указывает на то, какие значения может возвращать наш обработчик: EAX=0 или EAX=1. При значении(0), диспетчер разрешает нам обработать данное исключение, после чего можно будет перезагрузить контекст регистров с EAX=0. Если флаг(1), значит дело труба и случилось что-то непоправимое. В этом случае ребут контекста не возможен и мы должны вернуть коллбэком EAX=1.

• ExceptionInfo – дополнительная информация, непостоянна и зависит от конкретного кода исключения. К примеру если код = 0хC0000005 (ошибка доступа), здесь будет лежать следующая информация - в первом слове 0= ошибка при чтении, 1= при записи. Во-втором слове будет прописан адрес, при попытке доступа к которому произошёл сбой.


Установка пользовательского SEH

Добавить свой обработчик исключений в системную цепочку можно тремя инструкциями ассемблера. В структуре ТЕВ потока по-смещению нуль имеется поле ExceptionList, которое хранит указатель на первый SEH-фрейм в цепочке. На начало ТЕВ всегда указывает регистр FS. Значит чтобы вклиниться в цепочку обработчиков нам нужно оформить свой фрейм так, чтобы текущее значение ExceptionList стало следующим, а вместо него подставить указатель на свой обработчик. Код ниже демонстрирует сказанное:

C-подобный:
   push    mySeh                ;// первый dword нашего SEH-фрейма = указатель на обработчик
   push    dword[fs:0]          ;// второй dword – текущее значение ExceptionList
   mov     dword[fs:0],esp      ;// подменяем ExceptionList на свой фрейм!
С этого момента наш обработчик "mySeh" будет отлавливать буквально все системные исключения, а дальше мы должны их фильтровать. Например если хотим парсить только исключения типа "Ошибка доступа к памяти" с кодом 0хС0000005 (см.хидер ntstatus.h), значит проверяем код-ошибки в структуре Exception_Record на это значение. После установки пользовательского обработчика, стек потока примет такой вид:

SEH – фильтр необработанных исключений


Здесь видно, что в стеке появился опознанный Олей ещё один SEH-фрейм, после которого следует системный по-умолчанию с маркером окончания цепочки -1. Поскольку в TEB.ExceptionList прописан верхний фрейм, то при исключениях он получит управление первым. Системному может что-то перепасть только в том случае, если мы из своего коллбэка вернём диспетчеру единицу.


Практика – перехват исключений

Чтобы подкрепить вышесказанное на практике, напишем небольшое консольное приложение, которое будет отлавливать исключения любого рода и ориентации. Можно было замутить и гуй, только с выводом на экран там проблемы и затмит весь полезный код. В консоль-же будем выводить одним выстрелом дробью, через printf().

Значит ловим исключение, и провожаем его на задний двор. Дальше парсим структуры диспетчера CONTEXT и EXCEPTION_RECORD, по ходу сбрасывая нарытую информацию на консоль. Можно было вывести на экран все регистры, только их много и сути дела это не меняет.

C-подобный:
;// EXCEPT.INC - инклуд fasm'a,
;// для пользовательской SEH-обработки внутрипоточных исключений
;// ------------------------------------------------------------
EXCEPTION_MAXIMUM_PARAMETERS = 15
MAXIMUM_SUPPORTED_EXTENSION  = 512
SIZE_OF_80387_REGISTERS      = 80

struct FLOATING_SAVE_AREA
  ControlWord          dd ?
  StatusWord           dd ?
  TagWord              dd ?
  ErrorOffset          dd ?
  ErrorSelector        dd ?
  DataOffset           dd ?
  DataSelector         dd ?
  RegisterArea         rb SIZE_OF_80387_REGISTERS
  Cr0NpxState          dd ?
ends

struct CONTEXT
  ContextFlags         dd ?
  iDr0                 dd ?
  iDr1                 dd ?
  iDr2                 dd ?
  iDr3                 dd ?
  iDr6                 dd ?
  iDr7                 dd ?
  FloatSave            FLOATING_SAVE_AREA
  regGs                dd ?
  regFs                dd ?
  regEs                dd ?
  regDs                dd ?
  regEdi               dd ?
  regEsi               dd ?
  regEbx               dd ?
  regEdx               dd ?
  regEcx               dd ?
  regEax               dd ?
  regEbp               dd ?
  regEip               dd ?
  regCs                dd ?
  regFlag              dd ?
  regEsp               dd ?
  regSs                dd ?
  ExtendedRegisters    rb MAXIMUM_SUPPORTED_EXTENSION
ends

struct EXCEPTION_RECORD
  ExceptionCode        dd ?
  ExceptionFlags       dd ?
  pExceptionRecord     dd ?
  ExceptionAddress     dd ?
  NumberParameters     dd ?
  ExceptionInformation rd EXCEPTION_MAXIMUM_PARAMETERS
ends

struct EXCEPTION_POINTERS
   pExceptionRecord    dd ?
   pExceptionFrame     dd ?
   pExceptionContext   dd ?
   pParam              dd ?
ends

C-подобный:
format   pe console
include 'win32ax.inc'
include 'except.inc'         ;// Подключаем инклуд обработки эксепшенов
entry    start
;//------
.data
capt     db   13,10,' R.I.P. example v0.1'
         db   13,10,' *******************************',0
report   db   13,10,' Exception code...: %08X'
         db   13,10,' Exception address: %08X'
         db   13,10,' Exception flag...: %08X'
         db   13,10,10,' ======= Registers dump ========'
         db   13,10,' CS....: %04X'
         db   13,10,' EIP...: %08X'
         db   13,10,' ESP...: %08X'
         db   13,10,' EFLAGS: %08X'
         db   13,10,' EAX...: %08X   EBX: %08X'
         db   13,10,' ECX...: %08X   EDX: %08X',0
frmt     db   '%s',0
;//------
.code
start:   invoke  printf,capt

;//-- Устанавливаем своей обработчик исключений
         push    mySeh                    ; указатель на обработчик
         push    dword[fs:0]              ; указатель на следующй SEH-фрейм
         mov     dword[fs:0],esp          ; подмена в TEB.ExceptionList

;//-- Вызываем исключение!
         xor     ebx,ebx                  ; EBX:=0
         div     ebx                      ; попытка деления EAX на нуль!
                                          ;  ..получим исключение типа 0хС0000094.
;//======================
;//         mov     eax,[ebx]               ; можно прочитать с нулевого указателя!
;//                                         ;  ..получим исключение 0хС0000005.
;//======================
;//         pushf                           ; можно взвести флаг трассировки TF
;//         pop     eax                     ;  ..берём EFLAGS в EAX
;//         or      ah,1                    ;    ..взводим бит(8) TRAP-flag
;//         push    eax                     ;      ..обновляем EFLAGS новым значением
;//         popf                            ;        ..получим исключение 0x80000004.
;//======================

next:    invoke  scanf,frmt,capt          ;// на метку "NEXT" мы натравим EIP в структуре CONTEXT
         invoke  exit,0                   ;

;//=====================================
;//=== Пользовательский SEH-обработчик
;//==== в качестве аргументов передаём структуру "EXCEPTION_POINTERS" диспетчера
proc   mySeh  pRecord, pFrame, pContext, pParam

         mov     esi,[pRecord]
         mov     eax,[esi+EXCEPTION_RECORD.ExceptionCode]
         mov     ebx,[esi+EXCEPTION_RECORD.ExceptionAddress]
         mov     ecx,[esi+EXCEPTION_RECORD.ExceptionFlags]

         mov     esi,[pContext]

         invoke  printf,report,eax,ebx,ecx,\
                        [esi+CONTEXT.regCs],[esi+CONTEXT.regEip],\
                        [esi+CONTEXT.regEsp],[esi+CONTEXT.regFlag],\
                        [esi+CONTEXT.regEax],[esi+CONTEXT.regEbx],\
                        [esi+CONTEXT.regEcx],[esi+CONTEXT.regEdx]

;//=== ВНИМАНИЕ! Правим регистр EIP, чтобы код продолжился с метки NEXT
         mov     esi,[pContext]               ; указатель на структуру диспетчера
         mov     eax,next                     ; указатель на метку перехода
         mov     [esi+CONTEXT.regEip],eax     ; фиксим EIP в структуре "CONTEXT"

;//=== Callback диспетчеру EAX = 0 (перезагрузить и продолжить)
         xor     eax,eax                      ;
         ret                                  ;
endp                                          ;
;//==========================================
;//=== Секция импорта =======================
data     import
library  msvcrt, 'msvcrt.dll'
import   msvcrt, printf,'printf',scanf,'scanf',exit,'exit'
end      data
SEH – фильтр необработанных исключений


Если посмотреть на значение регистра EIP, то оно совпадает с адресом глючной инструкции верхнего блока. Значит диспетчер сдампил регистры правильно. Диспетчер вернул нам флаг(0), тем-самым давая добро на обработку исключения. Если-бы там лежала единица, то мы должны были через callback вернуть тоже единицу, что означает необрабатываемое исключение. Для наглядности, здесь я этой проверкой принебрёг.


Заключение

В исходнике кода есть вариант установки флага трассировки TF, без которого не могут жить отладчики. Что будет, если мы взведём этот флаг? Тогда без отладчика мастдай сгенерит исключение 0х80000004, а мы его перехватим. А вот под отладчиком никакого исключения не будет, т.к. он маскирует этот флаг. Такой поворот событий может определить факт трассировки нашего приложения. Вообще этот флаг открывает перед нами большие возможности, что будет темой для следующего разговора.
 

Вложения

Последнее редактирование:
Мы в соцсетях: