На этапе проектирования софта, как-правило предусматривают в нём модуль обработки ошибок и исключений. На жаргоне программистов его называют «защитой от дурака», хотя не исключён вариант, что дураком является здесь как-раз сам разработчик, поскольку плохо продумал пользовательский интерфейс. В любом случае «Exception Handler» должен присутствовать в коде, но с переходом на Win-64 мы сталкиваемся с проблемой. Дело в том, что основанный на стековых фреймах привычный механизм SEH больше не работает – на х64 он подвергся полной «прокачке» и требует теперь абсолютно иного подхода. В данной статье попробуем освятить достоинства и недостатки усовершенствованного механизма SEH-х64.
Оглавление:
1. Введение и терминология
Интеллект центрального процессора заключается в способности отлавливать им допущенные как пользователем, так и программистом ошибки. Если глюк глобальный и может отрицательно повлиять на дальнейший ход событий, при помощи ловушек Trap система пытается сама устранить его, иначе проц забивается в угол и от безысходности генерит исключение Еxception. В сишном хидере Ntstatus.h можно найти коды 37 эксепшенов, которые предоставляет диспетчер в надежде на то, что в системе найдётся заинтересованное лицо и решит всё-таки возникшую проблему. В противном случае ОС выстрелит в нас своим окном «Ошибка приложения», с бесполезным предложением найти ответ в интернете.
Чтобы получить от диспетчера код-исключения и повесить на него свой обработчик, все системы класса Win предлагают нам два механизма – это векторный и структурный, причём в силу своей простоты последний используется чаше собрата, и звучит как «Structured Exception Handler» SEH. В классическом виде, он держится всего на двух указателях – первый хранит адрес сл.фрейма в цепочке, второй – адрес обработчика. Связывание SEH-фреймов в цепочки Chain дают возможность обрабатывать исключения разного типа, в зависимости от их кодов.
Такая схема верой и правдой прослужила нам без малого пол-века, но за такой период нажила себе и врагов. Как и следовало ожидать, уязвимым местом стало использование ею системного стека. Переполняющая буфера малварь могла запросто затереть стековый SEH-фрейм, в результате чего вся конструкция по обработке исключений с треском падала, в очередной раз доказывая свою несостоятельность.
Учитывая сей факт, на системах Win64 инженеры выбрали другой подход. Теперь в SEH-фреймах прописываются не указатели, а сразу несколько связанных между собой структур. В базовом варианте обязательны лишь две из них – это основная RUNTIME_FUNCTION размером в 12-байт, и привязанная к ней UNWIND_INFO такого-же размера. Итого 24-байт вместо восьми. Чтобы не было путаницы, блок из этих\двух структур условимся называть «SEH-кадром», хотя кадр это и есть frame на англ.манер.
Как и в случае цепочки фреймов Win32, SEH-кадров может быть несколько – каждый из них отслеживает исключения в своей зоне программного кода. На рис.ниже представлены отличительные особенности обработки исключений на различных системах:
Из этой схемы видно, что инженеры не просто обновили старый механизм обработки исключений (как это было, например, с ключом /safeseh в Win32), а полностью переработали его до неузнаваемости. Как показала практика, старая схема изжила себя, безотлагательно требуя новых решений. Отметим, идея не нова и тестировалась ещё на 64-битной ХРюше, и видимо теперь пришло её время – как говорится: «Наши топоры, лежали до поры..»
Алгоритм работы диспетчера такой, что в момент исключения он получает от ядра адрес глючной инструкции (в виде текущего значения регистра RIP), и должен найти соответствие этому значению в структурах каждого из четырёх представленных кадров. В литературе этот процесс назвали «раскруткой кадров в стеке», чем собственно и занимается функция диспетчера RtlVirtualUnwind(). Обнаружив нужный кадр, диспетчер сможет изъять из его структуры UNWIND_INFO адрес обработчика возникшего исключения.
2. Проблемы отладчика WinDbg на 64-бит системах Win7
Всем, кто увлекается кодесами, часто требуются описания различных структур. Искать их каждый раз на MSDN не удобно от слова совсем, а если учесть, что представленные там прототипы в большинстве случаях для х32, имеет смысл установить на своём узле отладчик WinDbg, и командой
Однако на 64-бит системах Win7, без танцев с бубном локальная отладка ядра в WinDbg не поддерживается. Решать проблему энтузиасты предлагают при помощи ключа конфигуратора загрузки:
Cвязано это с тем, что с переходом семёрки на 64-бит, в поддерживающих дебаг ядра библиотеках WinDbg появились ещё две дополнительные проверки. Смысл их в том, чтобы полностью отключить обработку исключений пользовательского режима, при отладке ядра. Обойти данный тест можно подкрутив бут-менеджер следующим образом (см.справку: bcdedit /? /dbgsettings):
Если повезёт, с выставленными в такую позицию шестерёнками мы сможем активировать режим «Local-Kernel-Debug» на системе Win7-64. Но здесь нужно учитывать, что с включенной опцией
Можно-ли обойтись малой кровью и найти другое решение? Оказалось, что да и заключается оно в следующем..
Скачиваем с репозитория Microsoft веб-установщик WinDbg (для ленивых я положил его в скрепку), в окне установщика выбираем SDK и DebugingTools, после чего ОК и ждём окончания процесса (установщик должен скачать порядка 250 МБ). По завершению, конфигуратор
Запустив её на исполнение обнаруживаем, что она магическим образом открыла все замки в Kernel, и нам стала доступна локальная отладка ядра. Ради интереса я проверил настройки загрузчика ОС (bcdedit без параметров), и ключа
3. Программная реализация SEH64
Выше упоминалось, что в базовой форме, каждый SEH-кадр включает в себя две структуры.
Обязательная IMAGE_RUNTIME_FUNCTION_ENTRY должна присутствовать во-всех кадрах, и описывает один программный блок кода, внутри которого диспетчер будет отлавливать исключения. Если в отладчике выше ввести команду
Обратите внимание, что в этой структуре нужно указывать адреса относительно базы загрузки образа в памяти, или «Relative-Virtual-Address» (относительный). Причиной тому системный механизм ASLR (Advanced-Space-Layot-Randomization), который призван при каждой перезагрузке системы рандомно менять базу исполняемых файлов в вирт.памяти (в дефолте она выставляется компиляторами на значение 0х00400000). Поэтому в структуре выше адреса указываются без базы, а диспетчер потом добавляет к ним текущую, от ASLR.
Последнее поле «UnwindInfoAddress» хранит указатель на вторую структуру кадра UNWIND_INFO, и она требует некоторых пояснений. Основное условие – это варавнивание её в памяти на 16-байтную границу (хотя в доках указано 8), иначе диспетчер посчитает содержимое не валидным. Структура имеет битовые поля, а потому асматикам придётся заполнять их инструкцией сдвига влево
Не пугайтесь такому зоопарку опций – байты по смещению 1,2,3 мы просто выставим в нуль, что означает дефолт. Версию и флаги ставим в 1. Двоичная маска флагов может принимать значения ниже, и нашим требованиям удовлетворяет «Execute» (обрабатывать исключения). Флаг со-значением нуль (Next) даёт постановку диспетчеру пропустить данный кадр при раскрутке стека-исключений, т.е. делает привязанный к кадру обработчик полностью недееспособным. Этим поспешила воспользоваться малварь и прочая нечисть.
Теперь рассмотрим формат вложенной структуры UNWIND_CODE, которая непостоянна, а содержимое её напрямую зависит от флагов E,U,C. Поскольку расположившись в хвосте она фактически является продолжением UNWIND_INFO, то объединив все битовые поля, последнюю можно представить так:
Глядя на неё можно сделать вывод, что содержимое этой структуры диспетчер интерпретирует в одном из трёх вариантов, а мы будем использовать только первый
А вот как выглядит вся эта болтология в графическом виде.
На этой схеме имеем один обработчик на два подконтрольных блока в приложении. По сути можно было к каждому из них привязать свой кадр с отдельными парами структур RUNTIME_FUNCTION + UNWIND_INFO, но в своей демке я внутри обработчика проверяю код-исключения, и по его значению возвращаюсь обратно на нужный участок кода. В реальных программах нужно избегать таких\ленивых алгоритмов, а в качестве демонстрации и так сойдёт:
Ассемблер FASM выделяется на фоне остальных своими макросами. Так, чтобы вычислить относительный адрес RVA, нам не нужно знать базу образа в памяти (тем более, что ASLR меняет её по своему усмотрению) – достаточно вставить в нужное поле одноимённый макрос «RVA», и транслятор сам рассчитает относительный адрес. Не знаю, есть-ли подобная фишка в других ассемблерах типа MASM, NASM и в зоопарке остальных, но данный макрос несомненно избавляет нас от скучной рутины.
4. Формат обработчиков исключений
В момент, когда наш обработчик исключения получит штурвал, диспетчер должен передать ему техническую инфу о возникшем исключении. На системах х32 это был фрейм в стеке приложения, где лежали 4-указателя на служебные структуры. Но в системах Win64 аргументы передаются функциям уже через регистры RCX,RDX,R8,R9, а потому нужно искать информацию именно в них. Таким образом, на входе в обработчик EXCEPTION_ROUTINE, в перечисленных регистрах можно будет найти следующие указатели:
Как видно из этой справки, актуальная для нас информация передаётся диспетчером только в регистрах RCX и R8. Для удобного доступа к структурам RECORD64 и CONTEXT, имеет смысл виртуализировать их по адресу. Воплотить эту идею в реальность способен оператор
Теперь внутри обработчика, мы можем в любой момент обращаться к этим структурам, например:
Когда мы обработаем исключение, нужно будет вернуть управление диспетчеру с флагом
Обработчики исключений (а в целом и все пользовательские функции) не должны изменять содержимое регистров RBX,RBP,RSI,RDI,R12-R15,XMM6-XMM15. В оригинальных доках их так и называют «Non-Volatile Registers». Не соблюдение этого правила приведёт к большим неприятностям, особенно если речь идёт об обработчиках исключений. Зациклившись, система может арестовать нас внутри своего-же обработчика, что будет выглядеть весьма глупо. Из режима Long-mode была напрочь удалена инструкция
5. Секция исключений «.pdata»
Microsoft утверждает, что структуры механизма исключений RUNTIME_FUNCTION и UNWIND_INFO должны находиться в специальной секции РЕ-файла, которая носит название «.pdata». Однако при тестах оказалось, что имя секции может быть произвольным, но зарегистрировать её в каталоге РЕ-файла, нужно обязательно под номером(3). Когда система загружает образ приложения в память, лоадер просматривает записи в этом каталоге, и передаёт информацию соответствующим инстанциям, в том числе и диспетчеру исключений. Вот как отображает записи данного каталога двоичный редактор Hiew (жми комбинацию: Enter-->F8-->F10):
С именем секции «.pdata» обнаружился неприятный один нюанс..
Оказывается весь софт по сбору информации о РЕ-файлах, ищет в файлах исключительно секцию «.pdata», и если она называется как-то иначе, то просто не отображает инфу об исключениях в своём окне. Это никак не добавляет скила разработчикам софта. Если ты видишь запись в каталоге секций, значит секция активна, и нужно просто по какой-нибудь сигнатуре найти её содержимое в дампе. На скрине ниже одна из таких программ «PE File Browser x64». Тулза конечно мощная и способна дать фору всем остальным, но и она игнорирует все имена, кроме «.pdata»:
Посмотрите на заголовок окна - я загрузил в неё «Autoruns64.ехе» Марка Руссиновича. Удивляет то, что автор не поленился установить аж 1127 обработчиков исключений, детальную инфу о которых и показывает софт. Ясно, что перед нами программа на плюсах, лексикон которой поддерживает
6. Пример программы обработки исключений
Под занавес закрепим теорию практикой, где я контролирую два потенциально опасных участка кода.
• Первый блок будет запрашивать у юзера адрес для чтения памяти. Если область не доступна, то система должна оповестить нас исключением «AccessViolation» с кодом 0xC0000005. При обычных обстоятельствах, после этого прожка отправилась-бы к праотцам, но я перехватываю экспепшен, и обработав возвращаю управление опять коду.
• Второй блок будет вести контроль над целочисленным делением. Любому младенцу известно, что при операциях с целыми числами делить на нуль нельзя, но у нас получится «льзя». Здесь я так-же ставлю свой обработчик, и вытягиваю приложение буквально с того света.
7. Заключение
Очередное словоблудие подошло к концу..
В скрепке лежат 4 файла:
• Веб-установщик отладчика WinDbg для Win7-x64 (win7sdk_web.exe);
• Исполняемый файл для тестов отлова исключений (SEH64.exe);
• Инклуд с константами и форматом структур Except64.inc (положить в папку: fasm\include\equates);
• Инклуд с перечислением функций из библиотеки msvcrt.dll (положить в папку: fasm\include\api). Он избавит вас от ручного перечисления функций в секции-импорта.
Всем удачи, пока!
Оглавление:
1. Введение;
2. Проблемы отладчика WinDbg на 64-бит системах Win7;
3. Программная реализация обработки исключений;
4. Структуры обработчиков;
5. Секция исключений «.pdata»;
6. Практика – пример программы;
7. Заключение.
1. Введение и терминология
Интеллект центрального процессора заключается в способности отлавливать им допущенные как пользователем, так и программистом ошибки. Если глюк глобальный и может отрицательно повлиять на дальнейший ход событий, при помощи ловушек Trap система пытается сама устранить его, иначе проц забивается в угол и от безысходности генерит исключение Еxception. В сишном хидере Ntstatus.h можно найти коды 37 эксепшенов, которые предоставляет диспетчер в надежде на то, что в системе найдётся заинтересованное лицо и решит всё-таки возникшую проблему. В противном случае ОС выстрелит в нас своим окном «Ошибка приложения», с бесполезным предложением найти ответ в интернете.
Чтобы получить от диспетчера код-исключения и повесить на него свой обработчик, все системы класса Win предлагают нам два механизма – это векторный и структурный, причём в силу своей простоты последний используется чаше собрата, и звучит как «Structured Exception Handler» SEH. В классическом виде, он держится всего на двух указателях – первый хранит адрес сл.фрейма в цепочке, второй – адрес обработчика. Связывание SEH-фреймов в цепочки Chain дают возможность обрабатывать исключения разного типа, в зависимости от их кодов.
Такая схема верой и правдой прослужила нам без малого пол-века, но за такой период нажила себе и врагов. Как и следовало ожидать, уязвимым местом стало использование ею системного стека. Переполняющая буфера малварь могла запросто затереть стековый SEH-фрейм, в результате чего вся конструкция по обработке исключений с треском падала, в очередной раз доказывая свою несостоятельность.
Учитывая сей факт, на системах Win64 инженеры выбрали другой подход. Теперь в SEH-фреймах прописываются не указатели, а сразу несколько связанных между собой структур. В базовом варианте обязательны лишь две из них – это основная RUNTIME_FUNCTION размером в 12-байт, и привязанная к ней UNWIND_INFO такого-же размера. Итого 24-байт вместо восьми. Чтобы не было путаницы, блок из этих\двух структур условимся называть «SEH-кадром», хотя кадр это и есть frame на англ.манер.
Как и в случае цепочки фреймов Win32, SEH-кадров может быть несколько – каждый из них отслеживает исключения в своей зоне программного кода. На рис.ниже представлены отличительные особенности обработки исключений на различных системах:
Из этой схемы видно, что инженеры не просто обновили старый механизм обработки исключений (как это было, например, с ключом /safeseh в Win32), а полностью переработали его до неузнаваемости. Как показала практика, старая схема изжила себя, безотлагательно требуя новых решений. Отметим, идея не нова и тестировалась ещё на 64-битной ХРюше, и видимо теперь пришло её время – как говорится: «Наши топоры, лежали до поры..»
• Во-первых, стек приложения освободили от всего хлама, и перенесли новые структуры в специально предназначенную для этих целей, секцию РЕ-файла «.pdata». Благодаря такому подходу, затирающая стек малварь теперь не сможет нарушить работу механизма, пока в корень не сменит стратегию.
• Во-вторых, обработчики исключений и вся их поддержка устанавливаются уже не динамически (после загрузки образа в память), а статически, на этапе компиляции кода. Ясно, что это требует от нас некоторой квалификации и «правильных» телодвижений, но взамен получаем невероятно устойчивый к сбоям механизм.
• Если в приложении много контролируемых участков кода (в сишке они оборачиваются в блоки _try\_except), нам нужно организовать массив кадров в секции .pdata (на рис.выше их всего 4). В своём коде мы можем располагать кадры последовательно в произвольном порядке, однако на этапе загрузки образа в память, системный загрузчик сортирует их по возрастанию адресов подконтрольных им блоков, предоставляя диспетчеру более удобную схему для поиска.
Алгоритм работы диспетчера такой, что в момент исключения он получает от ядра адрес глючной инструкции (в виде текущего значения регистра RIP), и должен найти соответствие этому значению в структурах каждого из четырёх представленных кадров. В литературе этот процесс назвали «раскруткой кадров в стеке», чем собственно и занимается функция диспетчера RtlVirtualUnwind(). Обнаружив нужный кадр, диспетчер сможет изъять из его структуры UNWIND_INFO адрес обработчика возникшего исключения.
2. Проблемы отладчика WinDbg на 64-бит системах Win7
Всем, кто увлекается кодесами, часто требуются описания различных структур. Искать их каждый раз на MSDN не удобно от слова совсем, а если учесть, что представленные там прототипы в большинстве случаях для х32, имеет смысл установить на своём узле отладчик WinDbg, и командой
dt
запрашивать экстерны у него. Так мы убиваем сразу двух зайцев – получаем самые актуальные версии структур, и всё-это не отходя от кассы.Однако на 64-бит системах Win7, без танцев с бубном локальная отладка ядра в WinDbg не поддерживается. Решать проблему энтузиасты предлагают при помощи ключа конфигуратора загрузки:
bcdedit /debug on
. По личному опыту могу сказать, что этот совет не только бесполезный для этих систем, но после него в лучшем случае мастдай вообще перестанет грузиться (на старте виснет, ожидая отлаживаемую ОС), а в худшем – роняет систему в BSOD.Cвязано это с тем, что с переходом семёрки на 64-бит, в поддерживающих дебаг ядра библиотеках WinDbg появились ещё две дополнительные проверки. Смысл их в том, чтобы полностью отключить обработку исключений пользовательского режима, при отладке ядра. Обойти данный тест можно подкрутив бут-менеджер следующим образом (см.справку: bcdedit /? /dbgsettings):
1. bcdedit /debug on
2. bcdedit /dbgsettings local /start disable /noumex
Если повезёт, с выставленными в такую позицию шестерёнками мы сможем активировать режим «Local-Kernel-Debug» на системе Win7-64. Но здесь нужно учитывать, что с включенной опцией
/debug
воспроизведение DVD/MPEG2 не будет работать в системе – это не ошибка, а изначально так задумано разработчиком. Мда.. картина создаёт удручающее впечатление. Чтобы подобраться к структурам ядра, мы должны пожертвовать юзермодными исключениями, и кодеками MPEG2. Имхо обмен не рентабельный..Можно-ли обойтись малой кровью и найти другое решение? Оказалось, что да и заключается оно в следующем..
Скачиваем с репозитория Microsoft веб-установщик WinDbg (для ленивых я положил его в скрепку), в окне установщика выбираем SDK и DebugingTools, после чего ОК и ждём окончания процесса (установщик должен скачать порядка 250 МБ). По завершению, конфигуратор
bcdedit
вообще не трогаем, а скачиваем пакет Руссиновича
Ссылка скрыта от гостей
, в составе которого имеется консольная утилита «LiveKD».Запустив её на исполнение обнаруживаем, что она магическим образом открыла все замки в Kernel, и нам стала доступна локальная отладка ядра. Ради интереса я проверил настройки загрузчика ОС (bcdedit без параметров), и ключа
/debug
в нём не нашёл. Можно-же оказывается не доставать бубен, но видимо Microsoft это не устраивает, и нужно обязательно создать проблемы юзеру. В результате получаем окно ниже, которое отзывается на все стандартные команды WinDbg:3. Программная реализация SEH64
Выше упоминалось, что в базовой форме, каждый SEH-кадр включает в себя две структуры.
Обязательная IMAGE_RUNTIME_FUNCTION_ENTRY должна присутствовать во-всех кадрах, и описывает один программный блок кода, внутри которого диспетчер будет отлавливать исключения. Если в отладчике выше ввести команду
dt
и аргументом передать имя этой структуры, он вернёт нам следующее:
C-подобный:
struct IMAGE_RUNTIME_FUNCTION_ENTRY
BeginAddress dd 0 ;// RVA-адрес начала блока
EndAddress dd 0 ;// RVA-адрес конца блока
UnwindInfoAddress dd 0 ;// RVA-линк на дочернюю структуру "UNWIND_INFO"
ends
Обратите внимание, что в этой структуре нужно указывать адреса относительно базы загрузки образа в памяти, или «Relative-Virtual-Address» (относительный). Причиной тому системный механизм ASLR (Advanced-Space-Layot-Randomization), который призван при каждой перезагрузке системы рандомно менять базу исполняемых файлов в вирт.памяти (в дефолте она выставляется компиляторами на значение 0х00400000). Поэтому в структуре выше адреса указываются без базы, а диспетчер потом добавляет к ним текущую, от ASLR.
Последнее поле «UnwindInfoAddress» хранит указатель на вторую структуру кадра UNWIND_INFO, и она требует некоторых пояснений. Основное условие – это варавнивание её в памяти на 16-байтную границу (хотя в доках указано 8), иначе диспетчер посчитает содержимое не валидным. Структура имеет битовые поля, а потому асматикам придётся заполнять их инструкцией сдвига влево
SHL
, ну или использовать сразу предопределённые константы 9 и 19h.
Код:
0: kd> dt _unwind_info
WDFLDR!_UNWIND_INFO
+0x000 Version : Pos 0. 3-Bits ;// 3-бита под версию (1 или 2)
+0x000 Flags : Pos 3. 5-Bits ;// 5-бит под флаги (см.ниже)
+0x001 SizeOfProlog : UChar ;// байт с размером прогола функции
+0x002 CountOfCodes : UChar ;// байт с кол-вом вложенных структур UNWIND_CODE
+0x003 FrameRegister : Pos 0. 4-Bits ;// 4-бита под регистр-кадра (0=RAX и т.д)
+0x003 FrameOffset : Pos 4. 4-Bits ;// 4-бита под смещение кадра
+0x004 UnwindCode : _UNWIND_CODE ;// массив кодов раскрутки (зависит от флагов)
0: kd>
Не пугайтесь такому зоопарку опций – байты по смещению 1,2,3 мы просто выставим в нуль, что означает дефолт. Версию и флаги ставим в 1. Двоичная маска флагов может принимать значения ниже, и нашим требованиям удовлетворяет «Execute» (обрабатывать исключения). Флаг со-значением нуль (Next) даёт постановку диспетчеру пропустить данный кадр при раскрутке стека-исключений, т.е. делает привязанный к кадру обработчик полностью недееспособным. Этим поспешила воспользоваться малварь и прочая нечисть.
C-подобный:
#define UNW_FLAG_NHANDLER = 00000 = 0 ;// Next (пропустить)
#define UNW_FLAG_EHANDLER = 00001 = 1 ;// Execute (обработать)
#define UNW_FLAG_UHANDLER = 00010 = 2 ;// Unwind (раскрутить)
#define UNW_FLAG_CHAININFO = 00100 = 4 ;// Chain (это цепочка)
Теперь рассмотрим формат вложенной структуры UNWIND_CODE, которая непостоянна, а содержимое её напрямую зависит от флагов E,U,C. Поскольку расположившись в хвосте она фактически является продолжением UNWIND_INFO, то объединив все битовые поля, последнюю можно представить так:
C-подобный:
struct UNWIND_INFO
VersionFlags db 9 ;// флаг и версия = по 1
SizeOfProlog db 0 ;// дефолт 16-байт
CountOfCodes db 0 ;// отсчёт с нуля = 1
FrameRegOffset db 0 ;// 0=RAX регистр фрейма
;//---
;//--- if Flags = 001b (eHandler, обработка)
ExceptHandlerAddr dd 0 ;// RVA-адрес обработчика исключения
;//--- if Flags = 010b (uHandler, раскрутить кадры в стеке)
UnwindCode UNWIND_CODE ;// вложенная структура
;//--- if Flags = 100b (cHandler, это цепочка, так-что опять вставляем первую структуру кадра)
FunctionEntry IMAGE_RUNTIME_FUNCTION_ENTRY
LangSpecificData dd 0 ;// нет специфичных для языка-программирования данных
ends
Глядя на неё можно сделать вывод, что содержимое этой структуры диспетчер интерпретирует в одном из трёх вариантов, а мы будем использовать только первый
ExceptionHandler
, передав диспетчеру адрес своего обработчика исключений. Второй варик предусмотрен на случай, когда исключение возникает не в основной функции программного кода, а во-вложенной в неё. Тогда диспетчеру придётся раскручивать стек, двигаясь назад к параметрам основной функции, при этом черпая информацию из структуры UNWIND_CODE. Третий вариант – это схема обработки исключений с цепочкой структур IMAGE_RUNTIME_FUNCTION_ENTRY, и соответственно первая связывается со-следующей своей ксерокопией. Поле с языком заполняют компиляторы си, поэтому в ассме оно нам не интересно и ставим его в нуль.А вот как выглядит вся эта болтология в графическом виде.
На этой схеме имеем один обработчик на два подконтрольных блока в приложении. По сути можно было к каждому из них привязать свой кадр с отдельными парами структур RUNTIME_FUNCTION + UNWIND_INFO, но в своей демке я внутри обработчика проверяю код-исключения, и по его значению возвращаюсь обратно на нужный участок кода. В реальных программах нужно избегать таких\ленивых алгоритмов, а в качестве демонстрации и так сойдёт:
Ассемблер FASM выделяется на фоне остальных своими макросами. Так, чтобы вычислить относительный адрес RVA, нам не нужно знать базу образа в памяти (тем более, что ASLR меняет её по своему усмотрению) – достаточно вставить в нужное поле одноимённый макрос «RVA», и транслятор сам рассчитает относительный адрес. Не знаю, есть-ли подобная фишка в других ассемблерах типа MASM, NASM и в зоопарке остальных, но данный макрос несомненно избавляет нас от скучной рутины.
4. Формат обработчиков исключений
В момент, когда наш обработчик исключения получит штурвал, диспетчер должен передать ему техническую инфу о возникшем исключении. На системах х32 это был фрейм в стеке приложения, где лежали 4-указателя на служебные структуры. Но в системах Win64 аргументы передаются функциям уже через регистры RCX,RDX,R8,R9, а потому нужно искать информацию именно в них. Таким образом, на входе в обработчик EXCEPTION_ROUTINE, в перечисленных регистрах можно будет найти следующие указатели:
Как видно из этой справки, актуальная для нас информация передаётся диспетчером только в регистрах RCX и R8. Для удобного доступа к структурам RECORD64 и CONTEXT, имеет смысл виртуализировать их по адресу. Воплотить эту идею в реальность способен оператор
virtual
ассемблера FASM. Вот как данная фишка будет выглядеть в коде исходника:
C-подобный:
section '.xdata' code readable executable
proc Handler ;//<----- обработчик исключения!
;//-------------
virtual at rcx
record EXCEPTION_RECORD64
end virtual
;//-------------
virtual at r8
context CONTEXT64
end virtual
. . . . . .
Теперь внутри обработчика, мы можем в любой момент обращаться к этим структурам, например:
mov rax,[record.ExceptionCode]
. По названию структуры CONTEXT легко догадаться, что диспетчер сбрасывает в неё содержимое всех регистров процессора на момент исключения, включая MMX и XMM. На 64-бит системах она имеет размер 1224-байт, поэтому я зарыл её в спойлер ниже.Когда мы обработаем исключение, нужно будет вернуть управление диспетчеру с флагом
ContinueExecute=0
, на что диспетчер отреагирует восстановлением контекста регистров. Если в этот момент мы не обновим регистр-указателя на сл.инструкцию RIP, то получим мёртвый цикл, т.к. процессор будет генерить предыдущее исключение снова и снова. Чтобы не попасть в такую ловушку, на выходе из обработчика нам нужно перезаписать RIP меткой в коде, чтобы пропустить глючную инструкцию.
C-подобный:
......
mov [context.Rip],@next ;// обновить RIP в структуре “CONTEXT”
xor eax,eax ;// флаг “ContinueExecution=0”
ret
endp
Обработчики исключений (а в целом и все пользовательские функции) не должны изменять содержимое регистров RBX,RBP,RSI,RDI,R12-R15,XMM6-XMM15. В оригинальных доках их так и называют «Non-Volatile Registers». Не соблюдение этого правила приведёт к большим неприятностям, особенно если речь идёт об обработчиках исключений. Зациклившись, система может арестовать нас внутри своего-же обработчика, что будет выглядеть весьма глупо. Из режима Long-mode была напрочь удалена инструкция
pushaq
, при помощи которой можно было запихать в стек сразу все регистры. Поэтому придётся перечислять их в ручную, ну или написать соответствующий макрос.
Код:
0: kd> dt _context
nt!_CONTEXT
+0x000 P1Home : Uint8B
+0x008 P2Home : Uint8B
+0x010 P3Home : Uint8B
+0x018 P4Home : Uint8B
+0x020 P5Home : Uint8B
+0x028 P6Home : Uint8B
+0x030 ContextFlags : Uint4B
+0x034 MxCsr : Uint4B
+0x038 SegCs : Uint2B
+0x03a SegDs : Uint2B
+0x03c SegEs : Uint2B
+0x03e SegFs : Uint2B
+0x040 SegGs : Uint2B
+0x042 SegSs : Uint2B
+0x044 EFlags : Uint4B
+0x048 Dr0 : Uint8B
+0x050 Dr1 : Uint8B
+0x058 Dr2 : Uint8B
+0x060 Dr3 : Uint8B
+0x068 Dr6 : Uint8B
+0x070 Dr7 : Uint8B
+0x078 Rax : Uint8B
+0x080 Rcx : Uint8B
+0x088 Rdx : Uint8B
+0x090 Rbx : Uint8B
+0x098 Rsp : Uint8B
+0x0a0 Rbp : Uint8B
+0x0a8 Rsi : Uint8B
+0x0b0 Rdi : Uint8B
+0x0b8 R8 : Uint8B
+0x0c0 R9 : Uint8B
+0x0c8 R10 : Uint8B
+0x0d0 R11 : Uint8B
+0x0d8 R12 : Uint8B
+0x0e0 R13 : Uint8B
+0x0e8 R14 : Uint8B
+0x0f0 R15 : Uint8B
+0x0f8 Rip : Uint8B
+0x100 FltSave : _XSAVE_FORMAT
+0x100 Header : [2] _M128A
+0x120 Legacy : [8] _M128A
+0x1a0 Xmm0 : _M128A
+0x1b0 Xmm1 : _M128A
+0x1c0 Xmm2 : _M128A
+0x1d0 Xmm3 : _M128A
+0x1e0 Xmm4 : _M128A
+0x1f0 Xmm5 : _M128A
+0x200 Xmm6 : _M128A
+0x210 Xmm7 : _M128A
+0x220 Xmm8 : _M128A
+0x230 Xmm9 : _M128A
+0x240 Xmm10 : _M128A
+0x250 Xmm11 : _M128A
+0x260 Xmm12 : _M128A
+0x270 Xmm13 : _M128A
+0x280 Xmm14 : _M128A
+0x290 Xmm15 : _M128A
+0x300 VectorRegister : [26] _M128A
+0x4a0 VectorControl : Uint8B
+0x4a8 DebugControl : Uint8B
+0x4b0 LastBranchToRip : Uint8B
+0x4b8 LastBranchFromRip : Uint8B
+0x4c0 LastExceptionToRip : Uint8B
+0x4c8 LastExceptionFromRip : Uint8B
0: kd>
5. Секция исключений «.pdata»
Microsoft утверждает, что структуры механизма исключений RUNTIME_FUNCTION и UNWIND_INFO должны находиться в специальной секции РЕ-файла, которая носит название «.pdata». Однако при тестах оказалось, что имя секции может быть произвольным, но зарегистрировать её в каталоге РЕ-файла, нужно обязательно под номером(3). Когда система загружает образ приложения в память, лоадер просматривает записи в этом каталоге, и передаёт информацию соответствующим инстанциям, в том числе и диспетчеру исключений. Вот как отображает записи данного каталога двоичный редактор Hiew (жми комбинацию: Enter-->F8-->F10):
С именем секции «.pdata» обнаружился неприятный один нюанс..
Оказывается весь софт по сбору информации о РЕ-файлах, ищет в файлах исключительно секцию «.pdata», и если она называется как-то иначе, то просто не отображает инфу об исключениях в своём окне. Это никак не добавляет скила разработчикам софта. Если ты видишь запись в каталоге секций, значит секция активна, и нужно просто по какой-нибудь сигнатуре найти её содержимое в дампе. На скрине ниже одна из таких программ «PE File Browser x64». Тулза конечно мощная и способна дать фору всем остальным, но и она игнорирует все имена, кроме «.pdata»:
Посмотрите на заголовок окна - я загрузил в неё «Autoruns64.ехе» Марка Руссиновича. Удивляет то, что автор не поленился установить аж 1127 обработчиков исключений, детальную инфу о которых и показывает софт. Ясно, что перед нами программа на плюсах, лексикон которой поддерживает
_try\_except
, а остальное оформляет сам компилятор без участия программиста. Но чтобы кол-во обработчиков перевалило за тыщу, это вообще туши свет. По всей вероятности автор юзает для всех\своих приложений один и тот-же шаблон, т.к. у большинства обработчиков флаги выставлены в нуль, что означает UNW_FLAG_NHANDLER
, или пропустить. В любом случае «PE-Browser» круто исполняет своё дело, а это уже гуд.6. Пример программы обработки исключений
Под занавес закрепим теорию практикой, где я контролирую два потенциально опасных участка кода.
• Первый блок будет запрашивать у юзера адрес для чтения памяти. Если область не доступна, то система должна оповестить нас исключением «AccessViolation» с кодом 0xC0000005. При обычных обстоятельствах, после этого прожка отправилась-бы к праотцам, но я перехватываю экспепшен, и обработав возвращаю управление опять коду.
• Второй блок будет вести контроль над целочисленным делением. Любому младенцу известно, что при операциях с целыми числами делить на нуль нельзя, но у нас получится «льзя». Здесь я так-же ставлю свой обработчик, и вытягиваю приложение буквально с того света.
C-подобный:
format pe64 console
entry start
include 'win64ax.inc'
include 'equates\except64.inc'
;//-----------
section '.data' data readable writeable
a dq 0
b dq 0
c dq 0
aa dd 0
bb dd 0
rdAddr dq 0
szAccess db 'Access Violation!',0
szDivide db 'Divide by Zero!',0
buff dd 0
;//-----------
section '.code' code readable executable
start:
frame
invoke SetConsoleTitle,<'x64 SEH example',0>
invoke GetModuleHandle,0
xchg rdx,rax
;//----- Пытаемся прочитать адрес юзера ------------
cinvoke printf, <10,' Image base...: 0x%016I64x',\
10,' Read address.: 0x',0>,rdx
cinvoke scanf,<'%I64x',0>,rdAddr
or eax,eax
jnz @startSeh1
@err: cinvoke printf, <' Error!',0>
jmp @exit
;//**********************************************************************
;//********* БЛОК КОНТРОЛЯ ИСКЛЮЧЕНИЙ ***********************************
@startSeh1: mov rax,[rdAddr]
mov rdx,[rax]
@endSeh1: cinvoke printf, <' Value........: 0x%016I64x',10,0>,rdx
;//**********************************************************************
;//**********************************************************************
;//----- Умножение чисел с плавающей точкой -----------------------------
@next: cinvoke printf, <10,10,' MUL float value...',\
10,' ---------------------',\
10,' Input A float: ',0>
cinvoke scanf,<'%lf',0>,a
or eax,eax
jz @err
cinvoke printf, <' Input B float: ',0>
cinvoke scanf,<'%lf',0>,b
or eax,eax
jz @err
cinvoke printf, <' Result.......: ',0>
movsd xmm0,[a]
mulsd xmm0,[b]
movq rax,xmm0
cinvoke printf,<'%lf',0>,rax
;//----- Операция деления целых чисел -----------------------------------
cinvoke printf, <10,10,' DIV integer value...',\
10,' ---------------------',\
10,' Input A int..: ',0>
cinvoke scanf,<'%u',0>,aa
or eax,eax
jz @err
cinvoke printf, <' Input B int..: ',0>
cinvoke scanf,<'%u',0>,bb
or eax,eax
jz @err
cinvoke printf, <' Result.......: ',0>
;//**********************************************************************
;//********* БЛОК КОНТРОЛЯ ИСКЛЮЧЕНИЙ ***********************************
@startSeh2: xor rdx,rdx
mov eax,[aa]
div [bb]
xchg rbx,rdx
@endSeh2: cinvoke printf, <'%u.%u',0>,rax,rbx
;//**********************************************************************
;//**********************************************************************
@exit: cinvoke _getch
cinvoke exit,0
endf
;//--------------
section '.idata' import data readable
library kernel32,'kernel32.dll',msvcrt,'msvcrt.dll'
include 'api\kernel32.inc'
include 'api\msvcrt.inc'
;//----- Секция исключений --------------------------------
;//----- Регистрируем её в каталоге РЕ под номером(3) -----
section '.pdata' data readable
data 3
;//---- RUNTIME_FUNCTION(1)
dd RVA @startSeh1
dd RVA @endSeh1
dd RVA @UnwindInfo
;//---- RUNTIME_FUNCTION(2)
dd RVA @startSeh2
dd RVA @endSeh2
dd RVA @UnwindInfo
align 16 ;//<--- Обязательное выравнивание структуры “UNWIND_INFO”
@UnwindInfo: ;//v------------ Флаг
db 9,0,0,0 ;// 9 = 00001.001b <----- Версия
dd RVA Handler
dd 0
end data
;//----- Секция под обработчик исключений -----------------------
;//----- Microsoft советует помещать код обработчика именно сюда,
;//----- хотя это вовсе не принципиально ------------------------
section '.xdata' code readable writeable executable
proc Handler
;// RCX = адрес структуры "EXCEPTION_RECORD64"
;// R8 = адрес структуры "CONTEXT64"
virtual at rcx
record EXCEPTION_RECORD64
end virtual
virtual at r8
context CONTEXT64
end virtual
align 16
PushNonVolReg
mov [context.Rip],@next ;// обновить RIP
mov r15,szAccess
mov r10d,[record.ExceptionCode]
cmp r10d,EXCEPTION_ACCESS_VIOLATION ;// проверить код-исключения
jz @f
mov [context.Rip],@exit ;// меняем RIP в зависимости от кода
mov r15,szDivide
@@: mov r11d,[record.ExceptionFlags] ;// собираем инфу для лога
mov r12, [record.ExceptionAddress]
mov r13, [context.Rcx]
mov r14, [context.Rdx]
mov rsi, [context.R8]
mov rdi, [context.R9]
cinvoke printf,<10,10,' ************** Attention! Exception Handler **************',\
10,10,' Except code...: 0x%08X --> %s',\
10,' Except flag...: 0x%08x',\
10,' Except addr...: 0x%016I64x',10,\
10,' RCX: 0x%016I64x',\
' R8: 0x%016I64x',\
10,' RDX: 0x%016I64x',\
' R9: 0x%016I64x',10,\
10,' **********************************************************',0>,\
r10,r15,r11,r12,r13,rsi,r14,rdi
PopNonVolReg
mov eax,EXCEPTION_CONTINUE_EXECUTION ;// команда диспетчеру «Всё ОК! Продолжить»
ret
endp
7. Заключение
Очередное словоблудие подошло к концу..
В скрепке лежат 4 файла:
• Веб-установщик отладчика WinDbg для Win7-x64 (win7sdk_web.exe);
• Исполняемый файл для тестов отлова исключений (SEH64.exe);
• Инклуд с константами и форматом структур Except64.inc (положить в папку: fasm\include\equates);
• Инклуд с перечислением функций из библиотеки msvcrt.dll (положить в папку: fasm\include\api). Он избавит вас от ручного перечисления функций в секции-импорта.
Всем удачи, пока!