Предлагаю на суд мою маленькую статью о SEH. Ответьте, если я где-то ошибся или что-то перепутал: SEH Структурная обработка исключений (Structured Exception Handle) Вступление Механизм обработки исключений языка Си++ позволяет обрабатывать возникающие в программе обрабатываемые исключения. Обрабатываемые исключения по своему механизму похожи на события или прерывания, которые может обработать код приложения. То, что мы будем рассматривать здесь, относится не к механизму обработки исключений в языке Си++, а относится к обработке необрабатываемых исключений средствами самой операционной системы. О необрабатываемых исключениях 1. Что такое необрабатываемые исключения? Система Windows позволяет приложению обрабатывать возникающие в нем критические ошибки. Эти ошибки называются необрабатываемыми исключениями микропроцессора. Эти исключение возникает в результате ошибки в коде программы, из-за которой процессор не может выполнять программу дальше. Например, произошло деление на ноль (div eax, ebx;, где ebx=0), процессор встретил команду обращения к ещё несозданной странице памяти (mov [00000010h], eax), команду, на выполнение которой у приложения нет прав (out 10h, ecx) или вообще неизвестную команду (FF FF E4h). В результате процессор не знает - что ему делать с этой "неправильной" командой, поэтому он посылает прерывание операционной системе. Та, узнав о такой ситуации просто убивает приложение от безысходности, не позволяя ни вывести на экран сообщения, ни сохранить данные на диске. Эти исключения как раз и называются необрабатываемыми потому, что их нельзя обработать. Посмотрите - в команде mov [00000010h], eax в качестве адреса приёмника указана константа 00000010h. Поскольку первая страница памяти, в которой находится адрес 10h, всегда остается не созданной, эта команда всегда будет вызывать исключение. Команды out 10h, ecx и FF FF E4h процессор вообще не сможет выполнить. Как видите, такие ситуации нельзя обработать! Примечание: Единственное как можно исправить эти ошибки - это изменить сами команды, заменив их на правильные. Но этого делать нельзя - это внесёт изменения в код программу, из-за чего она перестанет правильно работать. Однако в некоторых случаях можно исправить ошибку, изменив значение регистра, виновного в возникновении исключения. Например, рассмотрим команду div eax, ebx, перед выполнением которой значение регистра ebx равнялось нулю. В результате этой команды происходит деление на ноль и процессор генерирует программное прерывание, повествующиее о возникновении необрабатываемого исключения. Данную ошибку можно исправить, если значение ebx заменить на 1. Тогда деления на ноль не будет и программа заработает дальше. Как видите, мы смогли исправить эту ошибку и уберечь программу от гибели. Данную конструкцию div eax, ebx может использоваться в программе для того, чтобы не проверять каждый раз перед делением - равно ebx нулю или нет. Программа может спокойно делить на ebx, а при возникновении деления на ноль, пропустить эту команду и работать дальше. 2. Зачем система сообщает о них программе? Как я уже сказал, система Windows позволяет приложению узнавать о возникающих в нем необрабатываемых исключениях. Делается это для того чтобы: 1) При возникновении исключения, которое нельзя исправить (например, команды mov [00000010h], eax), программа смогла вывести на экран сообщение для пользователя и сохранить всю нужную информацию на диске, прежде чем система убъет приложение. 2) Программа сама могла исправить исключение или выполнить запланированную операцию (как в случае с div eax, ebx). Глава 1. SEH-обработчики Итак, мы убедились в том, что при возникновении в процессе необрабатываемого исключения система обязана сообщать о нем самому процессу. Теперь о том как она это делает. 1. Где и как хранятся SEH-обработчики Windows, как и многие другие системы, является многопоточной системой. В таких системах процесс является общим адресным пространством для всех потоков, выполняющих его код. Из этого следует, что исключение вызывает не процесс, а сам поток, который выполняет неправильную команду. Поэтому именно поток получает от системы извещение о возникшем исключении. Для каждого потока может быть установлено несколько обработчиков необрабатываемых исключений. Все обработчики для одного потока хранятся в его стеке в виде однонаправленного списка, каждый элемент которого выглядит следующим образом: SEHhandler { DWORD next; DWORD handler; }; Здесь handler является указателем на код обработчика исключений, а next указывает на следующий элемент списка. Значение поля next, равное FFFFFFFFh, повествует о конце списка. Каждый поток в Windows обладает собственной структурой, лежащей по адресу 0 от селектора FS, так называемой Thread Environment Block - Блок Окружения Потока (или сокращенно TEB). Пусть вас не смущает FS - к этой структуре можно получить доступ и через другие селекторы, например, у меня адресу FS:0 соответствует виртуальный адрес 7FFDE000h (на вашем компьютере этот адрес может быть другим). Так вот, в самом начале этой TIB-структуры (т. е. по адресу FS:0) хранится адрес первого элемент списка SEH-обработчиков потока. То есть ячейка памяти FS:0 является "головой" списка. Помимо извещения об исключении, система передает обработчику дополнительную информацию. А именно - передает через стек значения всех регистров потока и информацию о возникшем исключении, полученную от процессора. Это делается для того, чтобы обработчик мог определить тип исключения (н-р, деление на ноль, обращение к неоткрытой странице памяти) и при возможности изменить значение одного или нескольких регистров чтобы исправить возникшую ошибку. 2. Первичный обработчик структурных исключений Перед созданием нового потока система регистрирует в нем новый SEH-обработчик, расположенный по адресу 77E9BB86h (у вас он может быть другим). Этот адрес принадлежит библиотеке kernel32.dll и на него ссылается функция по адресу 77E774E1h. При рассморении этой функции оказывается что именно она устанавливает для потока SEH-обработчик: push 77E9BB86h mov eax, fs:[0] push eax mov fs[0], esp причем на эту функцию я обнаружил целых 192 ссылки из других функций этой библиотеки. При получении управления этот первичный обработчик: - Открывает в раздел реестра HKLM\Software\Microsoft\Windows NT\CurrentVersion\AeDebug - Если в этом разделе ключ Auto равен 1, тогда система сразу там же открывает ключ Debugger и запускает программу, чей путь записан в этом ключе, при этом вместо первой комбинации %ld подставляет номер процесса, а вместо второй комбинации %ld - номер потока, в котором возникло исключение. Выходит, что тем самым система передает этот поток в руки отладчика чтобы пользователь мог попробовать решить проблему. - Если ключ Auto равен 0, тогда на экран выводится сообщение о том, что произошла критическая ошибка. Если вы нажмете кнопку "OK" - обработчик убъет поток, а если "Отмена" - то запустится отладчик как описано выше. 3. Как система использует SEH-обработчики? При возникновении в потоке необрабатываемого исключения: 1) Система передает управление на первый обработчик исключений этого потока. Для этого она: - из его TIB-блока берет указатель на первый элемент списка, который расположен по адресу 0 от селектора FS. - Обращаясь к этому указателю, она берет 2-ой адрес (handler), по которому и узнает адрес первого SEH-обработчика. - Заносит на стек содержимое всех регистров потока, значение типа исключения, полученного от процессора и другую информацию. - и передает на него управление. В результате работы SEH-обработчик должен проанализировать переданные ему значения регистров потока, тип исключения и решить - может он исправить возникшую ошибку в потоке или нет. 2) Если обработчик исправил ошибку (изменил значение одного из регистров), то он должен вернуть 0: mov eax, 0h ret Тогда система устанавливает для потока измененные значения регистров (они остались на стеке) и продолжает его выполнение. 3) Если обработчик не знает что делать с исключением, тогда он должен вернуть 1. В этом случае система переходит к следующему обработчику в списке: - Берёт указатель на следующий элемент списка (это поле prev в текущем элементе списка) - Если этот адрес равен FFFFFFFFh (то есть текущий обработчик был последним), система вызывает функцию KiUserExceptionDispatcher из ntdll.dll, которая убивает программу без базара. - Если адрес следующего обработчика не равен FFFFFFFFh, то система вызывает его, передавая ему те же самые регистры потока и информацию об исключении. на этом пока все.
Для процессора нет такого понятия, есть исключение двойной ошибки, например когда при возникновении исключений процессор не может обратится к ТСС, в таком случае процессор останавливается и вывести его из этого состояния можно только жёстким ресетом. Это по дефолту вызовет исключение, многие программы используют обращения к невыделенной памяти с изменённым селектором, что не вызовет исключение, ибо значение в этой инструкции является смещением от начала базы, определённой в GDT/LDT, на которую и ссылается сегментный регистр в инструкции. Виртуальная память по своему определению выгружаемая, ядро активно использует подкачку сегментов, в общем случае если поток обратится к выгруженной в своп памяти, диспетчер подгрузит страницу и перезапустит инструкцию. Для каждого потока в процессе адрес TEB уникален. При возникновении исключения ядро передаёт управление на KiUserExceptionDispatcher, отсюда и начинает поток обрабатывать исключение. Кривое описание у тебя, еслиб я не знал, то ничего бы не понял)
AndreyMust19 полностью согласен на счёт: имхо ты слишком закопался во второстепенные детали оставив далеко за бортом то что действительно стоит популярно изложить А уж каламбурчики: как и само понятие "необрабатываемое исключение" в твоём изложении это нечто )) На самом деле действительно необрабатываемое исключение встречается редко, а то что ты перечислил можно обработать (в крайних случаях направив программу на путь аварийного сохранения данных и лога ошибки или отказав юзеру в выполнении сбоящей функции предложить ему продолжать пользоваться только не сбоящей частью проги
AndreyMust19 Кстати, вчера реализовал одну идею(стотья на VT). Не каждый поймёт как оно работает, хэндлер исключений вызывает сам себя и трассирует себя, довольно замудрёная рекурсия. Это будет хорошей задачей для твоего ума. Код (Text): GetDeltaOffset macro Reg32 Local l1 Call l1 l1: pop Reg32 sub Reg32,offset l1 endm ENTER_SEH macro ExceptionHandler Local Delta1, Delta2 assume fs:nothing push ebp Call Delta1 Delta1: add dword ptr [esp],(ExceptionExit_ - Delta1) Call Delta2 Delta2: add dword ptr [esp],(ExceptionHandler - Delta2) push dword ptr fs:[0] ;TEB.Tib.ExceptionList mov dword ptr fs:[0],esp endm LEAVE_SEH macro Frame clc ExceptionExit_: pop dword ptr fs:[0] lea esp,[esp + 4*3 + Frame] endm QueryVectoredExceptionHead: ENTER_SEH offset ExceptionHandler push EFLAGS_TF Int 2Ah popfd Breakpoint_: cli LEAVE_SEH 0 assume fs:nothing mov eax,dword ptr fs:[PAGE_SIZE - 4] ret ExceptionHandler proc uses ebx ExceptionRecord:PEXCEPTION_RECORD, EstablisherFrame:PSEH, Context:PCONTEXT, DispatcherContext:PVOID GetDeltaOffset Ebx mov edx,ExceptionRecord assume edx:PEXCEPTION_RECORD mov ecx,Context assume ecx:PCONTEXT cmp [edx].ExceptionCode,STATUS_SINGLE_STEP je break_ cmp [edx].ExceptionCode,STATUS_PRIVILEGED_INSTRUCTION jne next_ lea eax,[ebx + offset Breakpoint_] cmp [edx].ExceptionAddress,eax jne next_ inc [ecx].regEip jmp continue_ break_: mov eax,[edx].ExceptionAddress cmp byte ptr [eax],0Fh jne step_ cmp byte ptr [eax + 1],85h jne step_ mov eax,[ecx].regEdi assume fs:nothing mov dwor ptr fs:[PAGE_SIZE - 4],eax continue_: and [ecx].regEFlags,not EFLAGS_TF xor eax,eax ret step_: or [ecx].regEFlags,EFLAGS_TF xor eax,eax ret next_: mov eax,EXCEPTION_EXECUTE_HANDLER ;1 ret ExceptionHandler endp Как работоет этот кодес ?
Clerk Клевый код. Только там есть опечатка: И ещё по-моему бесполезный код, всегда возвращающий в регистре 0: Как до конца разберусь, опубликую здесь сообщение (надеюсь ты подписался)
AndreyMust19 Там была переменная, изза чего страница должна была быть доступна для записи, переделол немного чтобы переменная была в TEB. Это чтобы указатель в дальнейшем найти. С нетерпением ждём ответ.
Clerc'у: Не пойму что такое QueryVectoredExceptionHead. Ссылок нет, как будто ниоткуда не вызывается.
Clerk Вот алгоритм работы твоего кода (здесь не всё): 1) если исключение вызвано отладкой (SINGLE STEP), тогда 1.1. проверяем команду, на которой возникло исключение 1.2. BREAK: если эта команда = jnz 6h (0F 85h), тогда в качестве адреса текущего SEH-обработчика пишем адрес следующей после jnz 6h команды, отключаем трассировку и продолжаем выполнение исключения. ИНАЧЕ: 1.3. Устанавливаем трассировку (она автом. снимается) и продолжаем выполнение исключения. ИНАЧЕ: 2) если исключение вызвано привилегированной командой (int 2Ah) и адрес исключения == точке останова 2.1. увеличиваем eip исключения на 1, отключаем трассировку и продолжаем выполнение исключения (управление передается на Breakpoint_). - ИНАЧЕ: 2.2. передаем управление на след. обработчик ENTER_SEH macro ExceptionHandler Этот макрос: 1) формирует на стеке SEH-фрейм, который в качестве адреса обработчика регистрирует переданные ему адрес (ExceptionHandler), а в качестве следующего обработчика - ExceptionExit_. 2) сохраняет на стеке адрес старого SEH, а потом регистрирует новый, только что подготовленный SEH. ExceptionExit_: восстанавливает старый SEH и удаляет выделенную память (увеличивает esp). Breakpoint_: Вызывает ExceptionExit_ P.S. В veh.rar, который ты мне дал работы ещё 2-3 часа.
AndreyMust19 Клерк временно забанен за слишком чёткое выражение мысли ъ:->, пока я за него. Вобщем всё написанное смысл не открывает. Эта инструкция не привилегированная, она только чтобы обмануть отладчик. Догодался в чём смысл ?
CrystalIC Ну отладчик, наверное, его поглощает и не дает программе самой обработать это исключение. А программа ещё и себя трассирует (ещё один убитый заяц) Я не очень разбираюсь в вашем сленге, но "забанен", наверное, означает "наказан на нецензурные / недопустимые выражения на форуме", и он не может какое-то время входить на форум?
AndreyMust19 Всё иначе, принцип в следующем. Код оформлен таким образом, чтобы не имел значения его адрес в памяти(шелкод). > ENTER_SEH offset ExceptionHandler Устанавливает сех-фрейм, далее при исключении будет вызван его обработчик ExceptionHandler(). > push EFLAGS_TF Int 2Ah popfd Этот код просто взводит TF в флажках так, что при трассировке инструкция popfd не будет искажена(трассировщик её пропустит, см. "Методы обнаружения трассировки"). Далее выполнятеся инструкция Cli, она привилегированная, что должно вызывать исключение. Но тут есть одна особенность, а именно - баг в обработчике исключений в ядре. Если выполнить эту инструкцию с взведённым TF(не именно эту, просто я выбрал) вначале возникнет исключение привилегированной инструкции, поток вернётся в юзермод на диспетчер исключений KiUserExceptionDispatcher, но изза бага будет взведён TF в флажках, что вызовет трассировочное исключение после исполнения первой инструкции диспетчера. Затем есчё раз ядро вернёт управление на диспетчер, но уже TF будет сброшен. Вот отсюда и начинается трассировка диспетчера. Может показаться странным подобное, инструкция хэндлера исключений вызывает его пошагово.. но это не проблема, ибо при передаче управления на диспетчер новый фрейм(EXCEPTION_POINTERS) находится в стеке ниже, чем указатель Esp на момент возникновения исключения. Блокировку позволяет избежать тот факт, что при нормальном входе в диспетчер сбрасывается TF и отладочное исключение возникает после исполнения инструкции. Итак после исполнения каждой инструкции диспетчера вызывается наш хэндлер, который вновь взводит TF и возвращает управление(NtContinue) на следующую инструкцию. Эта трассировка выполняется до тех пор, пока не встретится инструкция условного перехода, тогда читается регистр Edi из контекста потока(он сохранятся в TEB), после чего сбрасывается TF и диспетчер продолжает исполнятся уже без трассировки. После хэндлер исключений получает есчо раз управление, изза предыдущего входа в диспетчер изза исключения по исполнению привилегированной инструкции, тут пропускается инструкция Cli, сбрасывается TF и удалятся из стека предыдущий сех-фрейм. Таков этот механизм. Оригинальная стотья тут: http://www.virustech.org/f/viewtopic.php?id=43
Кошмар. Такой статьей и убить можно! Я всего лишь бумажный SEH-самолетик в воздух запустил, а вы целыми лайнерами разбрасываетесь. Бросайте вещи полегче (откда мне знать про ошибку в ядре с TF-флагом?).