SEH на службе контрреволюции

       

Кратко о структурных исключениях


Будучи вполне легальным механизмом взаимодействия с операционной системой, структурная обработка исключений неплохо документирована (во всяком случае нас будет интересовать именно документированная часть).

Внимательнейшим образом проштудируйте раздел "Frequently Asked Questions: Exception Handling" из MSDN. Там же вы найдете замечательную статью Мэтта Питерека "A Crash Course on the Depths of Win32 Structured Exception Handling". Из русскоязычных авторов лучше всего о структурных исключениях рассказывает Volodya– читайте "Об Упаковщиках В Последний Раз", что лежит на wasm'e – (http://www.wasm.ru/article.php?article=packlast01 и http://www.wasm.ru/article.php?article=packers2). Много интересного содержит и заголовочный файл EXCPT.H, входящий в состав SDK. Учитывая, что читатель может быть незнаком со структурными исключениями вообще, кратко введем его в курс дела.

Адрес текущего SEH-фрейма содержится в двойном слове по смещению ноль от селектора FS, для извлечения которого можно воспользоваться следующей ассемблерной абракадаброй: mov eax,FS:[00000000h]/mov my_var,eax. Он указывает на структуру типа EXCEPTION_REGISTRATION, прототип которой описывается так:

_EXCEPTION_REGISTRATION struc

       prev          dd     ?      ; адрес предыдущего SEH-фрейма

       handler              dd     ?      ; адрес SEH-обработчика

_EXCEPTION_REGISTRATION ends

Листинг 1 описание структуры EXCEPTION_REGISTRATION

При возбуждении исключения, управление передается текущему SEH-обработчику. Проанализировав ситуацию, SEH-обработчик, кстати говоря представляющий собой обычную cdecl-функцию, должен возвратить либо ExceptionContinueExecution, сообщая операционной системе, что исключение успешно обработано и исполнение программы может быть продолжено, либо ExceptionContinueSearch, если он не знает, что с этим исключением делать и тогда операционная система переходит к следующему обработчику в цепочке (собственно говоря, возвращать управление необязательно, и SEH-обработчик может удерживать его хоть до Мамаева пришествия, как обработчики, установленные shell-кодом обычно и поступают).


Последним идет обработчик, назначенный операционной системой по умолчанию. Видя, что дело труба и никто с исключением не справляется, он лезет в реестр, извлекает оттуда ключ HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug и, в зависимости от его состояния, либо прихлопывает сбойнувшее приложение, либо передает управление отладчику (или, как вариант, Доктору Ватсону).



При создании нового процесса, операционная система автоматически добавляет к нему первичный SEH-фрейм с обработчиком по умолчанию, лежащий практически на самом дне стековой памяти, выделенной процессу. "Дотянуться" до него последовательным переполнении практически нереально, т. к. для этого потребуется пересечь весь стек целиком! Таких катастрофических переполненений старожилы не встречали уже лет сто!

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

Разработчик может назначать и свои обработчики, автоматически создающиеся при упоминании "волшебных" слов try и except (такие обработчики мы будем называть "пользовательскими"). Несмотря на все усилия Microsoft'а, основная масса программистов совершенно равнодушна к структурной обработке исключений (некоторые из них даже такого слова не слышали!), поэтому, вероятность встретить в уязвимой программе "пользовательский" SEH-фрейм достаточно невелика, но все же они встречаются! В противном случае, для подмены SEH-обработчика (а первичный SEH-обработчик в нашем распоряжении есть всегда), придется прибегнуть к индексному переполнению или псевдофункции poke, которую мы обсуждали в предыдущих статьях.



Рисунок 1 глобальное развертывание цепочки структурных исключений (рисунок позаимствован из MSDN).


1 – возникла исключительная ситуация;
2 – операционная система анализирует TIB (Thread Information Bock – Информационный Блок Потока) для поиска первого SEH-фрейма в цепочке;
3 – операционная система передает управление первому SEH-обработчику;
4 – обработчик прикидывается шлагом и уходит в отказ;
5 – операционная система переходит к следующему фрейму в цепочке;
6 – операционная система передает управление SEH-обработчику;
7 – и этот обработчик не знает, что делать с исключением;
8 – операционная система переходит к следующему фрейму;
9 – операционная система передает управление SEH-обработчику;
10 – этот обработчик обрабатывает исключение (не обработать его он не может, т.к. это первичный обработчик, просто прихлопывающий приложение от безысходности)

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

main(int argc, char **argv)

{

       int *a, xESP;

       __try{

              __asm{

                     mov eax,fs:[0];

                     mov a,eax

                     mov xESP, esp

              } printf(     "ESP                            : %08Xh\n",xESP);

             

              while((int)a != -1)

              {

                     printf(       "EXCEPTION_REGISTRATION.prev    :%08Xh\n"\

                           "EXCEPTION_REGISTRATION.handler :%08Xh\n\n", a, *(a+1));

                     a = (int*) *a;

              }

       }

       __except (1 /*EXCEPTION_EXECUTE_HANDLER */) {

              printf("exception\x7\n");

       }

       return 0;

}

Листинг 2 простой визуализатор SEH-фреймов

Откомпилировав программу и запустив ее на выполнение, мы получим следующий результат (естественно, адреса SEH-фреймов и обработчиков в вашем случае скорее всего будут другими):

ESP                            : 0012FF54h      ; текущий указатель вершины стека



EXCEPTION_REGISTRATION.prev    : 0012FF70h      ; "пользовательский" SEH-фрейм

EXCEPTION_REGISTRATION.handler : 004011C0h      ; "пользовательский" SEH-обработчик

EXCEPTION_REGISTRATION.prev    : 0012FFB0h      ; SEH-фрейм стартового кода

EXCEPTION_REGISTRATION.handler : 004011C0h      ; SEH-обработчик стартового кода

EXCEPTION_REGISTRATION.prev    : 0012FFE0h      ; первичный SEH-фрейм

EXCEPTION_REGISTRATION.handler : 77EA1856h      ; SEH-обработчик по умолчанию

Листинг 3 раскладка SEH-фреймов в памяти

Смотрите, "пользовательский" SEH-фрейм, сформированный ключевым словом try, лежит в непосредственной близости от вершины стека текущей функции и его отделяют всего 1Сh байт (естественно, конкретное значение зависит от размера памяти, выделенной под локальные переменные, ну и еще кое от чего).

Следующим в цепочке идет фрейм, сформированный стартовым кодом. Он расположен намного ниже – от вершины стека его отделяют аж 5Сh байт и это-то в демонстрационной программе, содержащей минимум переменных!!!

Первичный фрейм, назначаемый операционной системой, отстоит от вершины стека на целых 8Сh байт, а в реальных полновесных приложениях и того больше (идентифицировать первичный фрейм можно по "ненормальному" адресу SEH-обработчика, лежащего в старших адресах первой половины адресного пространства). Его линейный адрес, равный 12FFE0h, идентичен для первого потока всех процессов, запущенных в данной версии операционной системы, что создает благоприятные условия для его подмены. Однако, для гарантированного перехвата управления, shell-код должен перехватывать текущий, а не первичный обработчик, поскольку до первичного обработчика исключение может и не дожить. Проверьте: если при переполнении буфера бессмысленной строкой наподобие "XXXXX…", возникает стандартное диалоговое окно критической ошибки, подменять первичный обработчик можно, в противном случае, его перезапись ничего не даст и shell-код сдохнет прежде, чем успеет получить управление.



Первичный фрейм всех последующих потоков располагается на dwStackSize байт выше предыдущего фрейма, где dwStackSize – размер памяти, выделенной потоку (по умолчанию: 4 Мбайт на первый поток и по 1 Мбайту на все последующие). Доработаем нашу тестовую программу, включив в нее следующую строку:

CreateThread(0, 0, (void*) main, 0,0, &xESP); gets(&xESP);

Листинг 4 исследования раскладки SEH-фреймов во многопоточной среде

Результат ее прогона будет выглядеть приблизительно так:

ESP                            : 0012FF48h      ; текущая вершина стека 1го потока

EXCEPTION_REGISTRATION.prev    : 0012FF70h      ; "пользовательский" SEH-фрейм 1го потока

EXCEPTION_REGISTRATION.handler : 00401244h

EXCEPTION_REGISTRATION.prev    : 0012FFB0h      ; SEH-фрейм стартового кода всех потоков

EXCEPTION_REGISTRATION.handler : 00401244h

EXCEPTION_REGISTRATION.prev    : 0012FFE0h      ; первичный SEH-фрейм 1го потока

EXCEPTION_REGISTRATION.handler : 77EA1856h

ESP                            : 0051FF7Ch      ; текущая вершина стека 2го потока

EXCEPTION_REGISTRATION.prev    : 0051FFA4h      ; "пользовательский" SEH-фрейм 2го потока

EXCEPTION_REGISTRATION.handler : 00401244h

EXCEPTION_REGISTRATION.prev    : 0051FFDCh      ; первичный SEH-фрейм 2го потока

EXCEPTION_REGISTRATION.handler : 77EA1856h

ESP                            : 0061FF7Ch      ; текущая вершина стека 3го потока

EXCEPTION_REGISTRATION.prev    : 0061FFA4h      ; "пользовательский" SEH-фрейм 3го потока

EXCEPTION_REGISTRATION.handler : 00401244h

EXCEPTION_REGISTRATION.prev    : 0061FFDCh      ; первичный SEH-фрейм 3го потока

EXCEPTION_REGISTRATION.handler : 77EA1856h

ESP                            : 0071FF7Ch      ; текущая вершина стека 4го потока

EXCEPTION_REGISTRATION.prev    : 0071FFA4h      ; "пользовательский" SEH-фрейм 4го потока

EXCEPTION_REGISTRATION.handler : 00401244h

EXCEPTION_REGISTRATION.prev    : 0071FFDCh      ; первичный SEH-фрейм 4го потока

EXCEPTION_REGISTRATION.handler : 77EA1856h

Листинг 5 раскладка SEH-фреймов в памяти

Заметно, что первичный SEH-фрейм всех потоков находится на идентичном расстоянии от текущей вершины стека, что существенно облегчает задачу его подмены. Первичные фреймы первого и второго потоков разнесены на 4 Мбайта (51FFDCh – 12FFE0h == 0x3EFFFC ~4 Мбайт), а остальные – на 1 Мбайт (61FFDCh – 51FFDCh == 71FFDCh – 61FFDCh == 10.00.00 == 1 Мбайт), ну в общем разобраться можно.

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


Содержание раздела