|
Эксперт Hardware
|
|||||||||||||||||||||
Пишем DLL на FASM'e25.10.2018, 00:16. Показов 36788. Ответов 17
Метки нет (Все метки)
Всё описанное здесь - это личное видение, если не оговорено обратное. Если вас мучает вопрос: "Как создать DLL-библиотеку?", то вы пришли по адресу. Небольшой ликбез для новичков позволит разобраться с такими вопросами как:
Dynamic Link Library в простонародье DLL - часть исполняемого файла в виде внешнего модуля. Модуль представляет из-себя библиотеку уникальных процедур и функций, которых нет в составе системных WinAPI, но необходимых нашей программе. Разделение исполняемого файла на подгружаемые модули позволяет эффективно использовать память, т.к. услугами одной библиотеки могут пользоваться сразу несколько приложений. По своей природе, DLL не отличается от EXE.. те-же секции кода/данных/импорта, только добавляется ещё одна - секция экспорта, благодаря которой приложения могут импортировать из библиотеки имеющиеся в ней функции. Есть и ещё одно/важное отличие *dll от *exe - они не способны сами исполнять код, поскольку каждый участок обёрнут в отдельную процедуру. Эти процедуры просто ждут своего часа, пока exe-модуль (или другая dll) не запросит какую-нибудь из них. Системная поддержка и проецирование в память Когда система запускает юзерский процесс, ядро проделывает огромную работу. Помимо прочего, это выделение памяти для юзерской кучи TLS - Thread Local Storage, и создание в этой куче служебных структур типа PEB - Process Environment Block (блок окружения процесса), и дочернюю к РЕВ структуру TEB - Thread Environment Block (блок окружения основного потока). Это т.н. "процесс инициализации приложения", подготавливающий юзерский контекст и окружение. Занимается этим "загрузчик образа", который находится в Ntdll.dll. Поскольку процедура инициализации стартует до нашего приложения, алгоритм её действий остаётся для нас прозрачным. Однако после загрузки exe, в стеке кое-что остаётся и если прокрутить его в отладчике назад (в сторону меньших адресов), то можно найти там много интересного. Инструкции POP и RET не затирают данные в стеке, а просто смещают указатель ESP, в результате чего всё тайны всплывают наружу. Проделав глобальную работу, загрузчик приступает к разбору таблицы импорта нашего EXE-модуля, чтобы подтянуть в юзерскую память внешние библиотеки, динамически скомпонованные во время компиляции EXE. Каждая импортируемая DLL может иметь свою/собственную таблицу импорта, поэтому эта операция будет продолжаться в рекурсивном режиме до тех пор, пока не будет найден весь импорт. По мере загрузки каждой DLL, загрузчик будет сохранять её информацию в виде базы, в структурах PEB и TEB. В поисках библиотек, загрузчик сначала открывает в реестре ветку: HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLsгде прописаны системные либы пользовательского режима. Если в таблице импорта нашего exe имеются и сторонние (по-факту самописные) dll'ки, то загрузчик ищет их в следующих директориях: 1. Текущий каталог, от куда запускается приложение; 2. C:\Windows\system32; 3. C:\Windows\system (для 16-бит приложений, но находит и 32-бит). Другими словами, мы можем располагать свои DLL в любую из указанных диров и загрузчик подхватит их на-лету, спроецировав на нашу память. Добавлено через 3 минуты База данных загруженных модулей Загрузчик ведёт список всех модулей, которые были загружены нашим процессом. Инфа хранится в структуре PEB, ..а именно в её подструктуре PEB_LDR_DATA. В ней загрузчик ведёт три списка 'list', в которых содержится одинаковые, но по разному выстроенные листы: по порядку загрузки(Load), по размещению в памяти(Memory) и в порядке инициализации(Init):
Посмотрим, чем занимается загрузчик на финальном этапе, после настройки окружения:
Добавлено через 1 минуту Точка входа в DLL-библиотеку Когда EXE-модуль вызывает какую-нибудь функцию из DLL, системный загрузчик прибегает к услугам API LdrLoadDLL() из либы ntdll. Эта-же API принимает управление от функций динамического вызова процедур LoadLibrary(), GetModuleFileName() etc, ожидающих получить дескрипторы модулей. Вот её прототип:
DllEntryPoint() (аналог DllMain), которая по-факту является точкой-входа в любую библиотеку. Необязательная она тем, что мы можем игнорировать её аргументы и это не приведёт к краху. Однако чего нельзя ингорить, так это возвращаемое ею значение TRUE в регистре EAX, поскольку LdrLoadDLL() рассчитывает его получить. Ldr передаёт в DllEntryPoint() три аргумента в таком порядке, а обрабатывать их или нет - дело хозяйское, ..главное вернуть EAX=1:
fdwReason, который передаёт нам LdrLoadDLL(). Благодаря ему мы можем провести какие-нибудь подготовительные операции init, перед вызовом конкретной функции из нашей тушки. Но повторюсь, что никто нас к этому не принуждает и мы можем просто вернуть единицу и всё. Нужно сказать и о том, что Ldr ожидает TRUE только при обращение к DDL'ке с флагом 'DLL_PROCESS_ATTACH', а в остальных случаях возврат не критичен.Кроме этого нужно учитывать, что на входе либа ещё не определена, и фактически мы находимся внутри обработчика LdrLoadDLL(). Такая картина накладывает определённые ограничения на вызов некоторых/вложенных Win-API внутри DllEntryPoint(), пока не выйдем из точки-входа по RET. Не сбрасывайте это со-счетов! С деталями можно ознакомиться в статье КК: "Динамические библиотеки для гурманов" - http://citforum.ru/book/cook/dll1.shtml
3
|
|||||||||||||||||||||
| 25.10.2018, 00:16 | |
|
Ответы с готовыми решениями:
17
Обсуждение статьи "Пишем DLL на FASM'e" Пишем DLL для работы с регистром.
|
|
Эксперт Hardware
|
|||||||||||
| 25.10.2018, 00:20 [ТС] | |||||||||||
Сообщение было отмечено Mikl___ как решение
Решение
Пример простейшей DLL на FASM
Думаю для теории достаточно и предлагаю подкрепить её практикой. Напишем небольшую тестовую DLL, чтобы проверить вышесказанное в отладчике. Из инструментов понадобятся: собственно FASM, отладчик 'OllyDbg', редактор 'HIEW' и наконец 'TotalComander', в котором используем только его плагин Lister для сбора инфы о модулях exe/dll (Ctrl+Q). Скелет типичной библиотеки DLL представлен ниже:
После компиляции исходника по F9, откроем полученную либу в Оле и посмотрим на состояние стека, где последовательно выстроились аргументы для 'DllEntryPoint'. Значит можно вообще не придерживаться правил и читать флаги fdwReason прямо из стека, что позволит проводить ручную инициализацию даже за пределами точки-входа: ![]() Намотав это на ус, теперь напишем EXE-модуль, который будет вызывать процедуру 'Hello' из нашей DLL. Если принять во-внимание толмут выше, проблем возникнуть не должно - компилим обычное гуй-приложение, которое затребует сначала внешнюю процедуру, а потом и сама покажет форточку:
Если всё сделали правильно, то функциональность либы должно подтвердиться окном 'Библиотека Dll удачно поключена!', иначе проблема в имени DLL. Практика показывает, что если не брать во-внимание парочку нюансов, то в программировании библиотек DLL нет ничего особенного. Другое дело как их вызывать и проецировать в память. Вот здесь и нужно набить руку и практику. Позже рассмотрим способы подключения библиотек, которых всего-то два - статический (как в примере выше) и динамический (требует некоторых пояснений). Кроме того, нужно уделить внимание и проблеме авто/перемещения загрузчиком базовых адресов загружаемых модулей (релоки) с технологией ALSR. Актуальными являются и способы обмена данными между DLL и EXE, аргументов и глобальных переменных. Было-бы желание, настрой и время..
5
|
|||||||||||
|
Эксперт Hardware
|
||||||
| 25.10.2018, 16:16 [ТС] | ||||||
|
Динамический вызов процедур и функций
Рассмотрим одну особенность работы с DLL, дающую нам полную власть и свободу. Описанный выше способ является "статическим" вызовом функций из библиотек. Это когда DLL загружается в адресное пространство процесса на этапе старта самого EXE. В момент, когда управление передаётся на точку-входа в приложение, все функции из импортируемых библиотек уже находятся в памяти вне зависимости от того, потребуются их функциональность, или нет. Мало того, что они отнимают выделенную процессу память, так ещё и болтаются там без дела, представляя собой уязвимость. Решить эту проблему позволяет вызов функций по-требованию, который назвали "динамическим". Здесь рулит минимализм, когда в память грузятся только те библиотеки, которые на самом деле нужны. После того-как функция отработает, библиотеку можно опять выгрузить из памяти. Ясно, что программировать динамику сложнее чем статику, однако в этом есть и свои плюсы. Поскольку ответственность за вызов тут лежит полностью на программисте, он сам проверяет передаваемые и возвращаемые значения. Например, если вызов вернёт ошибку, то статика сразу рухнет. А вот динамическое подключение позволяет обработать ошибку и продолжить исполнение, что немаловажно в случае миграции приложения на другую платформу. Динамический вызов держится на трёх API-китах: LoadLibrary, GetProcAddress и FreeLibrary.
У нас есть 'myDll.dll' с функций 'Hello' - оставим её без изменений, а вот EXE-модуль придётся модифицировать, причём с полной кастрацией нашей библиотеки из таблицы импорта экзешника:
4
|
||||||
|
Эксперт Hardware
|
|||||||||||
| 26.10.2018, 10:01 [ТС] | |||||||||||
|
DllEntryPoint, как обработчик событий EXE
Выше упоминалось, что при каждом обращении к DLL, управление принимает необязательная функция 'DllEntryPoint', возвращающая EAX=1. Более того, библиотеки реагируют на некоторые события внутри исполняемого файла, поскольку являются его логическим продолжением в адресном пространстве - их связывает невидимая нить, которая работает в фоне. Событий всего шесть, но их комбинацией можно значительно расширить поле деятельности. Замечу, что имя процедуры не обязательно должно быть 'DllEntryPoint' и может быть произвольным.. главное - передать на неё управление. Вот определение этих событий, которые отправляет приложение на точку-входа в библиотеку:
lpvReserved [esp+12] указывает на способ подключения DLL - если он равен нулю, библиотека загружена динамически чз LoadLibrary и, соответственно если аргумент =1, наоборот - статически.Теперь про fdwReason по адресу [esp+8]..1. DLL_PROCESS_DETACH информирует о том, что DLL выгружается из пространства процесса, потому что счётчик обращений 'LoadCount' в структуре 'PEB_LDR_MODULE' достиг нуля, или обнаружена FreeLibrary(). 2. DLL_PROCESS_ATTACH - это флаг загрузки DLL в пространство EXE. Он выставляется в трёх случаях: запуска процесса, первого обращения к функции, или в результате вызова LoadLibrary(). Если происходит повторный запуск какой-нибудь функции из DLL, этот флаг не выставляется, т.к. загрузчик берёт инфу уже из базы РЕВ. В этот момент 'lpReserved' указывает на способ загрузки - статический или динамический. 3. DLL_THREAD_ATTACH - в текущем процессе создаётся новый поток, при этом основной поток процесса этот флаг не выставляет. То-есть LdrLoadDLL() выставляет его только для потоков, созданных после загрузки DLL. 4. DLL_THREAD_DETACH - соответственно, завершение потока. Пример ниже демонстрирует обработку флагов в самописной библиотеки. Обратите внимание, что макрос .if определён только для аргумента 'fdwReason', а способ загрузки 'lpvReserved' нужно обрабатывать в ручную, через стек (по крайней мере у меня макрос не срабатывает). Видимо на это влияет то, что этот флаг определяется в манах как зарезервированный:
4
|
|||||||||||
|
Эксперт Hardware
|
||||||
| 26.10.2018, 15:07 [ТС] | ||||||
|
Проблемы загрузки DLL в адресное пространство EXE
Address Space Layout Randomization или коротко 'ASLR' - системный механизм рандомизации адресного пространства. Присутствует по-всех ОС и призван случайным образом подменять расположение в памяти критических данных: EXE/DLL-образов, хипа и т.д. Зачем это нужно и можно-ли обойтись без него? Проблема заключается в том, что по-умолчанию компиляторы зашивают в образы один и тот-же базовый адрес как для EXE, так и для DLL. Если не принять каких-либо мер, то при старте EXE, он попытается подгрузить библиотеку в аккурат поверх самого-себя, т.к. база в образах указана одинаковая - в дефолте 00400000h. Ясно, что ни к чему/хорошему это не приведёт, и приложение вылетит с ошибкой. Вот детали моих файлов в просмотрщике Тотала по Сtrl+Q:
section '.reloc' data readable fixups discardable Если в программе эта секция не определена, то EXE/DLL грузиться по фиксированному адресу 00400000h и операция перемещения образа не производится. Есть и другой способ решить проблему перемещаемых адресов - в ручную указать базовый адрес загрузки образа. Тогда секция релоков становится не обязательной, и уходит на скамейку запасных. Чтобы сменить дефолтную базу в FASM'e, достаточно в заголовке указать предпочтительный адрес, предварив его оператором at. Только учтите, что 'беззнаковые адреса' выше 80000000h принадлежат системе, и соваться туда не следует.На скрине - два варианта сборки образа.. На первом рисунке я скомпилировал DLL с настройками по-умолчанию и секцией-релоков. Здесь видно, что ASLR автоматически подменил базу DLL на адрес 00380000h вместо дефолтной 00400000h, хотя для EXE она осталась прежней. На нижнем-же рисунке, я зафиксил базу DLL на 10000000h, и отправил в топку секцию-релоков (закоментировал её) - приложение с такими настройками запускается без проблем. А теперь попробуйте запустить сборку DLL с настройками по-умолчанию и без секции-релоков. Увидите отборный мат системы, поскольку ASLR забьёт на наши проблемы - крах приложения обеспечен!
4
|
||||||
|
Эксперт Hardware
|
|||||||||||
| 30.10.2018, 08:35 [ТС] | |||||||||||
|
Что такое релоки и фиксапы?
В описании РЕ-формата упоминается таблица перемещения адресов Relocation Table. Но по сравнению с описанием таблиц импорта/экспорта и прочих, релокация не избалована вниманием, и остаётся как-то в стороне. Рассмотрим осново/полагающую этой фишки.. По своей природе, код PE-файлов позиционно-зависимый - он компилируется для базового адреса 00400000h. Чтобы получить виртуальные адреса процедур (переменных, меток и переходов), система складывает Base + Offset. Вроде всё ясно, но есть одна проблема - занятая ImageBase! EXE-модули редко сталкиваются с такой проблемой, т.к. они грузятся в свободное вирт.пространство памяти, до загрузки библиотек. Поэтому сказанное ниже относится только к образам DLL, которые подгружаются уже в адресное пространство EXE.Перебазировка включает в себя не только подмену базы. Программа должна создать таблицу и перечислить в ней все места в своей тушке, где есть обращения к памяти по абсолютным адресам. Только после этого загрузчик будет знать, к какому именно адресу применять поправки в виде дельта-смещений (разницу между текущим и скомпилированным адресом). Тут может возникнуть резонный вопрос: "Зачем нужна эта таблица, ведь смена одной только базы приведёт к загрузке образа по иному адресу?". Всё-правильно.. образ-то загрузится, только адреса внутри образа останутся прежними, поскольку линкер жёстко прошивает их на этапе компиляции. Посмотрим на дизассемблерный листинг моей DLL, которая проверяет тип запроса на DLL_PROCESS_ATTACH. Здесь видно, что условный переход JE 00402022 FASM скомпилировал в опкоды 74 16, т.е. вставил относительный адрес [+16h]. Однако во-втором случае, в качестве аргумента для MessageBox используется уже абсолютный адрес 00401000, о чём свидетельствует его опкод. Если система и поменяет ImageBase на лету, то опкод она уже не сможет изменить, пока мы не ткнём её носом в эту инструкцию.![]() Таким образом, таблица-релоков решает проблему перемещаемых адресов, а конкретные адреса в коде назвали 'FixUp': сколько подменяемых адресов - столько и фиксапов в RelocationTable. Вот как выглядит эта таблица в программе PE-Explorer (у меня всего 9-фиксапов в секции-кода), и результат её работы в отладчике OllyDbg (Оля подчёркивает абсолютные адреса). Как видим, после загрузки DLL в память, опкоды аргументов с абсолютными адресами подверглись изменению (база стала 36 вместо 40). По-сути, механизм релокации основан на само/модификации (сама Win легализовала её), чем поспешили воспользоваться хакеры: ![]() Рассмотрим формат таблицы-релоков и каждого из её фиксапов.. Для начала нужно найти таблицу всех секций файла ObjectTable, которая начинается по-смещению РЕ+F8h (указатель на РЕ-заголовок лежит в поле 3Ch от начала файла). В этой таблице плечом-к-плечу выстроены 28h-байтные описатели всех секций. Находим среди них нужный нам описатель .reloc, где по-смещению 14h от начала описателя будет прописан указатель на саму секцию - в данном случае 0х00000С00:
![]() На рисунке выше, 9 моих фиксапов имеют значения: 300F, 3014, 301C, ..etc Каждый фиксап размером 16-бит и представляет из себя 12-битное смещение от начала блока памяти, и 4-битный тип самого фиксапа. Например первый фикс(300F) кодируется как: тип=3, смещение=00F. Поскольку для смещения веделяется всего 12-бит, то макс.адрес до которого дотягивается фиксап получается 212=4Kb - т.е. одна страница виртуальной памяти. В терминологии релоков, эту страницу назвали "Перемещаемым блоком". Формат таблицы-релоков и его фиксапов представлен ниже:
4
|
|||||||||||
|
Эксперт Hardware
|
|||||||||||||||||||||||||||||||
| 31.10.2018, 10:15 [ТС] | |||||||||||||||||||||||||||||||
|
Раздаём атрибуты секциям программы
Рассмотрим определения, благодаря которым секции получают битовые флаги доступа и различные атрибуты. В глобальном масштабе, секции делятся только на код и данные, однако секция-кода (как-правило) присутствует в единственном экземпляре, зато секций-данных в природе - целый зоопарк. Идентификаторы типов могут принимать следующие значения, ..именно по ним выставляются 32-битные флаги атрибутов:
Нужно сказать, что FASM имеет зарезервированные имена, которые привязаны к встроенным макросам. Но можно создавать и свои, с произвольными именами макс.8-символов (вместе с точкой). Распространённый список имён выглядит так:
Полный список можно найти в файле определений "winnt.h" - вот вырезка из него: Section flags and characteristics
Комбинацию этих флагов показывают дизассемберы, в разделе информации. Например так они отображаются в "Hacker's Disassembler" - в самом быстром и удобном дизасме, с которым мне приходилось иметь дело:
3
|
|||||||||||||||||||||||||||||||
|
Эксперт Hardware
|
|||||||||||
| 31.10.2018, 19:22 [ТС] | |||||||||||
Сообщение было отмечено Mikl___ как решение
Решение
Обмен данными между DLL и EXE
Если библиотека не вещь-в-себе, то должна обмениваться данными с EXE. Судя по гуглу - проблема стоит остро, только на мой взгляд высосана она из пальца. FASM поддерживает транспорт данных в обе стороны, причём без каких-либо ухищрений и танцов с бубнами. Основное условие - чтобы атрибуты секций соответствовали требованиям, т.е. были открыты на R/W. Гонять инфу между модулями можно разными способами, и все они работают. Например: через регистры, через стек, через указатели, и наконец через MMF - MemoryMappedFile. Прямая передача через регистры - наиболее простой способ, только имеет ограничения в 32/64-бита. Когда нужно отправить какие-нибудь строки или блоки данных, лучше воспользоваться указателями на эти блоки и передавать их удобным программисту способом. В качестве примера рассмотрим такой код.. Имеем библиотеку, которая по запросу должна отправить строку из своей секции-данных, в секцию-данных приложения. Проблема в том, что на этапе компиляции, приложение ещё не знает адрес строки mes0, т.к. эта строка прописана в коде DLL. То-есть приложение не может обратиться к строке напрямую. Поэтому EXE должен запросить указатель на строку у самой DLL. Вот реализация такого алгоритма:
push ebp --> mov ebp,esp, поэтому аргумент/указатель оказывается по-смещению [ebp+8], оставив за бортом адрес-возврата и сам ebp. Эпилог на выходе восстанавливает стек через leave. Это то, что касается кода библиотеки..Теперь приложению всего-то нужно вызвать функцию "GetStr", как данные окажутся у него в кармане, и их можно вывести на экран:
В данном случае хотелось-бы отметить тот факт, что я не расшаривал секции-данных флагом shareable ни с той, ни с другой стороны - все настройки были дефолтные. По большому счёту, этот флаг вообще не имеет отношения к связыванию двух пространств во-едино. Шара подразумевает доступ к секции без копирования этой секции в память стороннего процесса, т.к. по-умолчанию система просто раздаёт дубликаты нерасшаренных секции всем/желающим.Здесь-же, приложение получило от системы базу 00400000h, а библиотека - базу 00180000h (по крайней мере у меня). При этом обмен происходит удачно, поскольку DLL расценивается как часть EXE и значит адресное пространство у них одно на двоих.
4
|
|||||||||||
|
Эксперт Hardware
|
|
| 01.11.2018, 18:52 [ТС] | |
|
Memory Mapping File - обмен через файл подкачки
..значит на данном этапе я выяснил, что мой процесс может пересылать данные моей-же библиотеке без каких-либо ограничений. Можно отправить "бандеролью" хоть всю секцию и принять ответ тем-же способом. Но рассмотрим другую ситуацию, когда в наш/внутренний диалог с библиотекой хочет вмешаться кто-то третий, например совсем посторонний процесс из другого адресного пространства. Для таких случаев Microsoft предусмотрела вполне легальный способ - это именованные файлы, которые создаются в пространстве файла-подкачки (pagefile.sys), что делает их общими для всех процессов. Поскольку подкачка есть ничто-иное как продолжение PhysicalMemory (физ.памяти), большой Билли назвал такие файлы "Memory Mapping File" - файлы, отображённые в память. Процессы могут получить доступ к MMF тремя способами: по имени, по дескриптору, и по его дубликату. Схема ниже демонстрирует работу этого механизма: ![]() Рассмотрим в общих чертах, откуда берутся и куда исчезают такие файлы.. Основа Win - объекты ядра, т.е. она построена на объектной модели. Диспетчер объектов позволяет создавать и оперировать несколькими типами объектов, в том числе и File-Mapping object. На самом деле каждый объект - это просто блок памяти, доступный только самому ядру. По известным причинам, юзеру не светит прямой доступ к ядерным объектам, для него выделена только директория "Global\", в которой он может создавать расшаренные (или нет) файлы. К каждому файлу привязывается счётчик обращений, и если этот счётчик сбрасывается в нуль, то файл автоматом удаляется. Для просмотра системных объектов можно воспользоваться утилитой WinObj от Sysinternal. Теперь разработаем план действий.. Поскольку файла ещё нет, значит его нужно создать функцией CreateFileMapping(). Не важно кто будет этим заниматься, главное что-бы он опубликовал всем имя этого файла, ..ну или его дескриптор. Суть в том, что "один" создаёт - а "общество" пользуется этим файлом. В общем случае, при публикации последовательность функций такая:
С этого момента любой процесс может прочитать этот файл по имени, или наоборот записать в него информацию, если при создании это было предусмотрено флагами защиты "CreateFileMapping()". В свою очередь тот-кто хочет прочитать этот файл, должен проделать эти-же операции, только в первом пункте не создать, а открыть его чз OpenFileMapping().
3
|
|
|
Эксперт Hardware
|
||||||||||||||||||||||||||
| 05.12.2018, 21:54 [ТС] | ||||||||||||||||||||||||||
Сообщение было отмечено Mikl___ как решение
Решение
TLS-CallBack, как обратный вызов загрузчика образов
Win имеет довольно интересный тип памяти с аббревиатурой TLS, которая плохо освящена в доках. В дословном переводе, это Thread Local Storage - локальная память отдельно взятого потока. Кстати в узких кругах, потоки называют "тредами" (thread), чтобы исключить путаницы с понятием "процесс". Процессы 64-битных систем могут порождать огромное кол-во тредов, в то время как в х32 их число не превышает 2000 единиц - потолок диктует размер юзерской кучи (heap). На системах х32 куча застыла на отметке 2Gb (ст.половину из 4-х система забирает себе). Если известно, что дефолтный стек процесса =1Mb, а всего памяти 2Gb, то тредов на процесс получаем ~2000 штук, и все/они выполняются в контексте родительского процесса. Напомню, что это только один процесс, а в реал-тайме - их десятки. Такая армия способна подмять под собой всё, в том числе и саму систему. Чтобы ситуация не ушла в разнос нужно предусмотреть защитные механизмы, которые усмиряли-бы разбушевавшиеся треды. В такой ситуации, глобальные переменные за пределами тредов будут скорее осинником, чем принесут профита - например поток(А) может записать значение в глобальную переменную, а ничего не подозревающий поток(В) может тут-же затереть её под себя, на что не рассчитывал поток(А). Проблемы именно такого характера призвана решать память TLS, когда у каждого из тредов (помимо стека) имеется и сугубо интимная область памяти, не доступная остальным тредам родительского процесса. Записав значения в TLS тред может не беспокоится, что переменную кто-то упрёт. То-есть получаем полное разграничение доступа к локальной памяти сожительствующих под одной крышей потоков. Мало того, TLS поддерживает фишку под названием Tls-Callback - это то, на чём нужно заострить своё внимание в первую очередь! Системный загрузчик образов вызывает функции обратного вызова (!)до точки входа в нашу программу, в результате чего отладчики с дефолтными опциями пропускают колбэки между ног - они дебажат именно с EntryPoint. Значит внутри callback-функции мы можем обнаружить платформу, и принять соответствующие меры. Нужно сказать, что Win сама предлагает нам это, а значит все/наши действия вполне легальны. Системная поддержка памяти TLS В арсенале системы имеется динамическая и статическая TLS. Это два абсолютно разных подхода. Со-стороны компиляторов и системы, поддержка имеется только для динамического выделения памяти, а для статического - нужно прибегать к народным средствам, т.е. строить крепость в ручную. Нужно отметить, что механизм колбэков основан на принципе именно статического выделения памяти и это логично, ведь динамически можно выделить память только после того-как программа загрузится в память, а обратные вызовы срабатывают до.. Обзор начнём с динамической TLS, которая физически расположена в структуре TEB треда. ТЕВ - это конспиративная квартира потока и представляет из себя его инфо-базу "Thread Environment Block". У каждого треда свой ТЕВ, а значит и своя TLS-память. 32-битная система хранит указатель на TEB в сегментном регистре(FS), а системы х64 - в регистре(GS). Посмотрим на поля этой структуры (в первом столбце указано смещение от начала):
fs:[0e10h] видим поле "TlsSlots" - это и есть локальная память треда, в которой он может хранить свои переменные и указатели размером dword. Так-же, по смещению fs:[0f94h] имеется поле "TlsExpansionSlots" - физ.расширение основной TLS, которое в данном случае находится под флагом null, т.е. не используется и находится в резерве. Динамическая TLS устроена таким образом, что оперировать с ней можно только 4-байтными словами. Каждое/такое слово представляет из себя TLS-ячейку, а место хранения ячеек назвали "слотами". Для адресации слотов применяют индексы, и если основной памяти в байтах всего 256, то получаем общее кол-во слотов: 256/4=64, и соответственно столько-же индексов (отсчёт с нуля). Когда тред забьёт все/64 слота основной памяти, система на автопилоте расширит память, и следующий индекс отправится уже в массив "TlsExpansionSlots". Согласно документации, в этом массиве имеется резерв ещё для 1024-слотов. От сюда следует, что каждому из тредов система выделяет: 64+1024=1088 слотов TLS, что чуть больше одной (4К-байтной) страницы виртуальной памяти. Используете-ли вы эту память в своих программах? - большой вопрос. Ладно.. будем считать, что с локальной памятью треда разобрались.. Но как на счёт её контроля со-стороны системы и родительского процесса? Ясно, что если треды принадлежат процессу, то и управлять их памятью должен тоже процесс, для чего в его структуре PEB имеются соответствующие поля. Точно так-же как и ТЕВ, PEB является структурой, только не треда, а родительского процесса - Process Environment Block. Это довольно содержательная база, из которой можно почерпнуть не мало интересного. Как ни странно, но указатель на РЕВ находится в ТЕВ, а точнее в его поле fs:[30h] под ником "РЕВ" (см.фрагмент teb выше). Здесь рулит оригинальное решение с определением "BitMap" - битовая карта TLS-индексов. Посмотрим на структуру PEB ниже.. Суть в том, что к каждому слоту в структуре TEB, привязан соответствующий бит в битовой карте РЕВ. Состояние слотов основной памяти TLS отражает 8-байтное поле TlsBitmapBits по смещению [fs:30h]+44h. 8-байт это в аккурат 64-бита, чего хватает для определения флагов состояния 64 слотов: (0)=чисто, (1)=используется.
Для динамического выделения локальной памяти, система предоставляет тредам всего 4 функции. Поскольку они оперируют только индексами, тред не может работать с отдельными байтами TLS - мин.порцией является DWORD'ный слот. Вот эти функции:
Кого заинтересует эта фишка, советую почитать Д.Рихтера "Win для профессионалов", где этой теме посвящена целая глава. Таким образом, мелкими шагами я подошёл к более интересной части - это статическое выделение памяти треду, на что система тут-же реагирует вызовом цепочки обработчиков TLS-Callback из нашего приложения. При динамической резервации этот механизм не доступен, поэтому динамика - на любителя, и в кругах программистов не представляет особого интереса. Зато статика раскрывает все прелести TLS, а использовать их или нет - дело хозяйское. Добавлено через 3 минуты Полное описание структуры РЕВ процесса
Полное описание структуры ТЕВ отдельного треда
2
|
||||||||||||||||||||||||||
|
Эксперт Hardware
|
||||||
| 06.12.2018, 20:43 [ТС] | ||||||
|
Статическая TLS - вид сверху
Для разбора статический памяти тредов нужно сменить тактику и переключиться на другую волну. Ключ к пониманию её принципов лежит в спецификации на РЕ-формат, а точнее лишь в описании её раздела Image_Data_Directory. Этот дир смещён на 78h от начала РЕ-заголовка и хранит RVA имеющихся в файле секций, с указанием размеров каждой из них. Размер - игнорируемый всеми пережиток прошлого. Для наглядности, возьмём обычный "Hello World" и открыв его в HIEW посмотрим на эту "DataDirectory" (меню F8->F10). Я чуть модифицировал скрин, добавив смещения записей от начала PE-Header (слева), и их порядковые номера (справа). Структура до неприличия проста: первый dword - это RVA на таблицу соответствующей секции в памяти, второй - её размер: ![]() Загрузчик, начиная с нуля по-очереди сканирует поле RVA всех записей и если оно не нуль, подгружает указанную секцию в виртуальную память VA (вычисляется по формуле: VA=ImageBase+RVA). Таким образом проверка начинается с секции экспорта(0), потом импорт(1), и.. программа фактически может исполнять уже любой код - ей выделена память и импортированы все функции из внешних модулей DLL. Однако на этом этапе, загрузчик не сразу передаёт управление на EntryPoint в программу, а продолжает сканирование остальных записей в списке. Когда очередь доходит до секции TLS под номером(9) происходит то, ради чего я вообще начинал этот разговор. Здесь, загрузчик приступает к вызову цепочки Tls-Callback'ов, которые (судя по названию) должны что-то возвращать загрузчику (back), но на самом-деле ему глубоко безразлично, чем будет заниматься вызванная им функция - он даже не проверяет возвращаемые ею параметры, и вызывает следующий колбэк в цепочке (если таковой имеется). Только после этого управление передаётся на ЕР в программу, с которой начинают анализ все отладчики, а значит мы его в фоне сможем обнаружить. От сюда следует, что пока мы не заполним поле под номером(9) в "ImageDataDirectory", не видать нам долгожданных Callback'ов как своих ушей. Сделать это просто - достаточно явно указать номер секции в коде, и компилятор сам позаботится об остальном. Кроме того нужно и секцию оформить должным образом (она имеет свой прототип), иначе загрузчик тупо проигнорирует наш план. Вот пример готовой секции TLS с описанием прототипа:
Всё-что находится внутри блока - это и есть статическая TLS тредов. Она является общей для всех потоков и не может расширяться динамически, как это было с полем "TlsExpansionSlots" в структуре ТЕВ. Этому есть простое объяснение - статическая TLS выделяется явно, на этапе компиляции программы. Данный блок можно переместить в любую секцию где есть доступ на запись, например в секцию-данных или специально предназначенную для этого - секцию '.tls'. Если ваш код многопоточный и создаёт ещё тредов (при этом вы хотите использовать для них статическую TLS), то должны зарезервировать TLS отдельно для каждого треда, внутри общей памяти. Когда загрузчик обнаруживает тред со-статической памятью, он назначает для него уникальный индекс, и сохраняет его первом двойном-слове выделенной памяти. В примере выше - это переменная @tls. По этому индексу система отличает память одного треда, от памяти другого. В структуре ТЕВ потока по адресу fs:[2Ch] имеется поле TlsPointer, которое хранит указатель на TLS-массив, содержащий данные его локальной памяти.Осталось рассмотреть, что находится снаружи блока.. Там 4 двойных-слова, и все/они указатели. Первые два - это линки на голову и хвост локальной памяти. Хвост никто не проверяет, поэтому туда можно пихать что-угодно (в данном случае я их сравнял). Эти указатели не используют переменную(@tls), а линки нужны лишь для того, чтобы компилятор сгенерил адрес. Кто использует эту переменную, так это загрузчик, записывая туда TLS-индекс обнаруженного треда. Ну и четвёртый dword - это указатель на колбэк-цепочку. Загрузчик будет вызывать по-очереди каждую ссылку из цепочки, пока не встретит нуль-терминальный dword.
2
|
||||||
|
Эксперт Hardware
|
||||||||||||||||
| 08.12.2018, 11:47 [ТС] | ||||||||||||||||
|
Обратные вызовы TLS-Callbacks
Колбэки – это цепочка функций, которые загрузчик вызывает до точки-входа в программу. Девятая запись на скрине выше, указывает на набор этих функций. Загрузчик вызывает второй колбэк в цепочке только когда получит ret от первого. В сети встречаются предположения, что цепочку можно расширять бесконечно, но в доках имеется ограничение на 8-штук. Код колбэков выполняются внутри кода загрузчика, а лоадер подменяет SEH заглушкой, поэтому любые исключения на этом этапе игнорируются системой. Функция "TlsCallback" имеет свой прототип, который ни к чему нас не обязывает. Это точная ксерокопия функции DllEntryPoint(), которая проверяет флаг "reason" на ATTACH/DETACH процессов и тредов - её мы уже подвергли трепанации, поэтому повторяться не будем: Tls_Callback (handle, reason, reserved).Пришла пора по-практиковаться.. Модифицируем обычный "HelloWorld", подключив к нему колбеки.. По-сути это приём антиотладки. Второй колбек - чисто для демонстрации вызова цепочки. Для обозначения своего присутствия, он тупо боксит мессагу и всё. Замечу, что первый не возвращает EAX=1 загрузчику, на чём так настаивает мелкософт. При инициализации DLL этот выхлоп может-быть и нужен, но в случае с EXE - им можно пренебречь. На функцию первого колбека возложена задача по разоблачению присутствия отладчика. Обнаружить Олю и её братию можно разными способами. Нам даже не нужно напрягаться, т.к. Win вычисляет отладку сама, выставляя флаги в структуре PEB - просто читаем их и делаем вывод. Первое поле: PEB.02.BeingDebugged выставляется системой в 1, а второе PEB.68h.NtGlobalFlag - это OR трёх/глобальных флагов системы, которые под отладчиком будут иметь значение 70h. Я выбрал первый вариант:
Если запустить его в обычном режиме, то увидим две мессаги. Но если приложение распознает агрессивную среду отладки, то выдаст мессагу и тупо встанет как-вкопанная, не отобразив в окне дебагера ни одного тушканчика. "Достучаться до небес" конечно-же можно, но начинающих исследователей это сбивает с толку не по-детски. Добавлено через 40 минут Кстати вот так можно вынести TLS-память за пределы таблицы, в спец-секцию .tls
Вот логи дизасма для обоих случаев:
2
|
||||||||||||||||
|
Эксперт Hardware
|
|
| 23.01.2019, 15:27 [ТС] | |
|
Hook виндовых API
Рассмотрим тему, за которую недавно сожгли-бы на костре, а сегодня - это легальный способ получить управление при вызовах API, который, кстати, в красках расписывается на RSDN например здесь: http://rsdn.ru/article/qna/baseserv/hookapi.xml. Речь пойдёт о перехвате API-функций, а всю/черновую работу можно возложить на TLS-callback, который проделает это в фоне отладчика. Вспомним алгоритм перехвата прерываний в среде MS-DOS.. Имеется таблица векторов прерываний(IVT) Interrupt Vector Table, эдакая штаб-квартира адресов в обработчики всех сервисов DOS. Если подменить вектор на адрес своей процедуры, то получаем управление до оригинального входа, что и представляет из-себя собственно перехват. С приходом Win алгоритм не изменился, зато изменился способ его реализации. Тут уже нет глобальной таблицы векторов, а обработчики в виде API-функции хранятся внутри системных библиотек, например kernel32.dll и прочие. Если в двух словах, то процесс отображения функций в пространство юзера происходит примерно так.. Когда мы вызываем API из своего кода, по-идее компилятор должен "зашить" в выходной файл конкретный адрес точки-входа в нужную нам функцию, из нужной библиотеки, например: • name = User32.dll; • offs = 0x77665544; • func = MessageBox(). Однако компилятор этого не делает, и оставляет (в исполняемом файле) поле с адресом пустым. В спецификации на РЕ-формат это поле называют IAT - Import Address Table. Это вынужденная мера, чтобы файл можно было запускать на любой ОС, т.к. библиотеки у них загружаются по-разным адресам ядерного пространства, и если сделать адрес статичным, то можно промахнуться мимо функции. Поэтому IAT экзешника заполняется загрузчиком РЕ-файлов уже после того, как он загрузит файл в память. Загрузчик сканирует таблицу-импорта файла, и для каждой из прописанных в ней функций вызывает двойку: LoadLibrary() и GetProcAddress(), на основании выхлопа которых и заполняет IAT. Первая fn.запрашивает у системы адрес библиотеки, а вторая - смещение нужной API-функции внутри этой "матки". Если-же на момент вызова LoadLibrary() нужной библиотеки в системной памяти не окажется, то либа динамически подгружается в память только вызывающего процесса, а не системы в целом. Но большинство из библиотек сидят в автозагрузке и уже болтаются в пространстве ядра. Если память библиотеки не расшарена флагом "shareable", то система копирует в пространство юзера код импортируемой API из своей области. Для этих целей выделена верхняя часть адресов юзера, ниже 0x7FFFFFFF (регион 32-битной памяти выше 0х80000000 принадлежит ОС). В противном случае и при расшаренной секции-экспорта библиотеки DLL, система уже не копирует код-функции юзеру, а выдаёт ему прямую ссылку на свою/ядерную память, что является огромной брешью в системе безопасности. В многопроцессорных системах планировщик если и ставит все потоки в кольцевую очередь, то только на одном из процессоров - второе его ядро абсолютно самостоятельно со-своим планировщиком, и работает параллельно первому. То-есть многопоточность не эмулируется, и в любой момент оба ядра могут затребовать одну API-функцию. Значит нужны аппаратные механизмы синхронизации, поддерживать которые дороже, чем раздавать каждому процессу по своей копии API.
2
|
|
|
Эксперт Hardware
|
||||||||||||||||
| 23.01.2019, 15:31 [ТС] | ||||||||||||||||
|
Таким образом, алгоритмы перехвата API в среде Windows можно разделить на два/основных типа: первый - это подмена адреса в таблице IAT исполняемого файла, а второй - это вставка перехода непосредственно в начало функции-обработчика, что известно как "сплайсинг".
К недостатку первого метода можно отнести тот факт, что если API вызывается из кода динамически чз LoadLibrary(), то адрес функции запрашивается напрямую у системы, и его подмена в таблице IAT нашего контекста профита не принесёт. Тут нужно перехватывать уже (стоящую ниже уровнем) недокументированную функцию LdrLoadDll(), что сделать не так-уж просто, т.к. память ядра не доступна на запись из юзера. Эту проблему решает сплайсинг - тупая врезка перехода в начало обработчика. Чтобы закодировать переход на функцию-двойник, достаточно всего 5-ти байт опкодов: 1-байт для инструкции(jmp), и 4-байта на сам адрес перехода (оригинальные 5-байт нужно запомнить в своём теле, и восстанавливать их после перехвата). Остановимся на первом варианте с IAT, и попробуем перезаписать адрес именно в контексте нашего процесса. Отлавливать будем MessageBox(), до передачи управления которой расшифруем выводимую строку (предварительно зашифровав её). Это позволит нам скрыть в секции-данных все строки от жадных взоров кодокопателей. Возможный план действий описан ниже: 1. Получить через GetProcAddress(), оригинальный адрес функции MessageBox(); 2. Сохранить его в своей переменной для возврата управления; 3. Найти IAT в памяти, и подменить в нём адрес MessageBox() на свой адрес; 4. Как-только получим управление, снять со-стека аргумент "адрес-строки" и расшифровать её до терминального нуля. Первые/два пункта не представляют из-себя проблемы, а вот третий - нужно рассмотреть подробней, для чего откроем спеку на РЕ-формат, и перейдём в её раздел ".idata". Дела с таблицей-импорта в РЕ-заголовке обстоят так.. • Поскольку мы подряжаемся иметь дело с образом файла в памяти (а не на диске), для начала нам нужно найти РЕ-заголовок в своём пространстве. Авансом скажу, что загрузчик копирует его с диска в область, начиная с "ImageBase" (в дефолте, это адрес 0х00400000). Тогда указатель на РЕ-Header можно будет найти по смещению Base.3Ch.• Теперь ищем секцию-импорта в памяти.. Как мы помним, её RVA хранится в первой записи Image_Directory_Table, а указывает на эту запись - поле РЕ.80h (см.скрин HIEW'a двумя постами выше). Получив содержимое данного поля прибавляем его к базе, и в лапках у нас оказывается прямой линк на начало секции-импорта в памяти - в моём случае это адрес 0x00403000:
Если перейти по-смещению 3056h, то попадаем в список указателей на имена импортируемых функций 0000306E и 0000307С (нулевой указатель является последним в списке). В данном случае, по-смещению 306Eh лежит word со-значением нуль. Если-бы по смещению(02) старший бит был-бы взведён, то данный word расценивался-бы как ординал/номер функции в библиотеке, за которым сразу следует ASCIIZ-имя этой функции. Таким образом, просканировав список указателей в "IMAGE_THUNK_DATA" можно получить полный лист функций из текущей библиотеки, вместе с их ординалами. Вот как выглядит лог просмотрщика РЕ-файлов в "Тотале" по Ctrl+Q.. Напомню, что это образ файла на диске, с незаполненным IAT:
2
|
||||||||||||||||
|
Эксперт Hardware
|
||||||
| 23.01.2019, 15:31 [ТС] | ||||||
|
Думаю для теории - более чем достаточно..
Теперь попробуем реализовать что-то подобное на практике. Код ниже перехватывает функцию MessageBox(), и декодирует для неё зашифрованную ключом(DCh) строку. После этого, код передаёт управление на оригинальную точку-входа, чтобы функция сбоксила уже готовую мессагу:
В идеале, нужно расшифровывать строку перед выводом её на экран, и криптовать обратно - после. Организовать это просто. Достаточно вместо jmp eax использовать call eax, и получив управление от оригинальной функции, повторить ксор строки. Кстати, аверки типа KAV и NOD относятся к нему вполне лояльно. Можно online-вскормить его Касперскому на: https://virusdesk.kaspersky.ru/ и убедится в этом. Это доказывает, что в перехвате API под виндой нет ничего криминального, и системный протекторат никак не препятствует этому, а только поощряет.
2
|
||||||
|
Эксперт Hardware
|
||||||||||||||||||||||||||
| 23.03.2019, 23:54 [ТС] | ||||||||||||||||||||||||||
|
SEH - Structured Exception Handling
В Win-системах, решать проблемы внештатных ситуаций выпало на долю механизма SEH - "Структурной обработки исключений". Структурной её назвали из-за цепочки 8-байтных структур "Exception_Registration", которая помещается исключительно в стек. Первый dword в этой структуре служит указателем на предыдущую структуру в цепочке, а второй - это указатель на обработчик данного исключения.
ОС не утруждает себя обработкой всех/юзерских ошибок, поскольку физически не может все/их предугадать. Поэтому мелкософт предоставил программистам возможность самим решать свои проблемы. Прописав начало цепочки в ТЕВ, нам остаётся только залечь в окопы и ждать исключение, после чего новоиспечённая ловушка украдёт сцену и мы можем играть соло на свой вкус и цвет - например, указать на схему пожарного выхода (безопасное место "safe place", заранее предусмотрев его в коде), сбалансировать стек, или тупо пропустить ошибку, если она не критична. В стерильных программах, в стеке всегда присутствует только один/дефолтный SEH-фрейм системы, с маркером -1. Скрин ниже демонстрирует устройство этого механизма: ![]() На этом рисунке, красно-жёлтые блоки выступают в роли структур "Exception_Registration" или SEH-фреймов. Их можно "размазывать" по всему стеку, т.е. фреймы необязательно должны следовать в строгом порядке один-за-другим, и это радует. SEH нас вдохновляет тем, что даёт возможность обрабатывать исключения каждого из программных потоков в отдельности, т.к. у каждого потока свой ТЕВ, стек и свои регистры. Этим не может похвастаться функция SetUnhandledExceptionFilter() (установить фильтр необрабатываемых исключений). У неё только один аргумент - указатель на обработчик, который будет глобальным внутри всего программного кода. Обработчик этой функции называют ещё "Финальным обработчиком" и из них нельзя выстраивать цепочку - она может быть только в единственном экземпляре (повторный вызов игнорируется). Обработчик такого типа не привязывается к конкретным потокам и будет всплывать при каждом чихе программы.Таким образом, в нашем распоряжении оказываются два типа обработчиков: финальный и внутрипоточный. Здесь и далее, мы будем рассматривать только внутрипоточный SEH (структуры и прочее), т.к. он представляет более гибкие возможности по сравнению с финальным. Пример установки до неприличия прост: ![]() Поскольку механизм SEH основан на процедурах обратного вызова (CallBack_Routine), обработчики должны возвращать диспетчеру один из двух идентификаторов в регистре EAX - от этого выхлопа зависят дальнейшие действия диспетчера: • Exception_Execute_Handler(0) - перезагрузить контекст и продолжить выполнение программы. • Exception_Continue_Search(1) - передать управление следующему SEH-фрейму в цепочке. Если ни один из наших фильтров в цепочке не вернул(0), то диспетчер зовёт на помощь функцию UnhandledExceptionFilter() (фильтр необрабатываемых исключений) - дефолтный фрейм системы с маркером 0хffffffff, который присутствует всегда и регистрируется загрузчиком образов при подготовке треда к запуску.Добавлено через 14 минут SEH - системный диспетчер исключений Весь функционал диспетчера пользовательского режима находится в kernel-API KiUserExceptionDispatcher(). Эта неэкспортируемая из модуля ntdll.dll функция занимается тем, что ищет подходящий обработчик и если такового в списке нет, просто прихлопывает процесс по Exit, не дав ему возможности даже попрощаться. По большому счёту, диспетчер - это бокс для нескольких API, работой которых он и управляет. Краткий перечень находящихся в подчинении диспетчера функций, приведён ниже:• RtlDispatchException() - ядро логики диспетчера прикладного уровня; • UnhandledExceptionFilter() - системная процедура финального обработчика исключений; • SetUnhandledExceptionFilter() - установить пользовательский финальный обработчик; • NtRaiseException() - сгенерить программное исключение (код = Е0000100h); • RtlUnwind() - обратная раскрутка стека после обработки исключения; • RtlRestoreContext() - восстанавливает регистры в прежнее состояние (x64); • NtContinue() - восстанавливает регистры в прежнее состояние (x32); При возникновении исключения, первым принимает его на грудь притаившийся в засаде диспетчер. Сначала в стек породившего исключение потока, диспетчер дампит текущее состояние всех регистров CPU - это важная для нас структура, которую назвали "Context". Среди членов этой структуры есть доступный для модификации EIP, что позволит нашему коду вернуться в прошлое (или будущее), если мы возвратим диспетчеру EAX=0 перезагрузить контекст! На сл.этапе, диспетчер отправляет в стек ещё одну структуру "Exception_Record" - технические детали подвергшегося трепанации исключения. На финишной прямой, диспетчер кладёт в стек указатели на оба/указанных инфо-блока, и возвращает управление опять нашей программе, чтобы она попыталась сама обработать исключение посредством своего SEH. Вышеописанный алгоритм приводит к тому, что когда наша/токсичная программа принимает управление от диспетчера, на макушке стека оказывается фрейм "Exception_Pointers" из четырёх указателей. Через эти указатели мы можем подобраться к структурам диспетчера и получить полную голограмму критической ошибки.
В блоке "Context" нет ничего особенного - это просто упорядоченный список состояния всех регистров процессора на момент исключения, последовательность которых приводится в инклуде "Except.inc" (оформим позже). Пристального внимания требует вторая структура "Exception_Record":
0 – обрабатываемое исключение, допускающее перезагрузки контекста CPU (EAX=0); 1 – необрабатываемое исключение (обработчик должен вернуть EAX=1); 2 – процедура обратной раскрутки стека (вообще нельзя обрабатывать). • NestedRecord - если обработчик вызван для обработки исключения, которое произошло во время обработки другого исключения, то указывает на "Exception_Record", которая относится к тому исключению, обработка которого была аварийно прервана; • ExceptionInfo – доп/информация. Здесь могут быть параметры функции RaiseException(), или (если код исключения = C0000005h = AccessViolation), здесь будет находиться сл.информация: 1st dword - 0= нарушение прав при чтении, 1= при попытке записи; 2nd dword – адрес, при попытке доступа к которому, произошло исключение. -------------------------------------------------- По-умолчанию, в своей тушке fasm'a не имеет инклуд с описанием структур SEH, поэтому есть резон оформить его самим. Это позволит обращаться к членам структур по их именам, а не использовать безликие смещения относительно начала инфо-блока. EXCEPT.INC - инклуд fasm'a,
Добавлено через 20 минут SEH - программная реализация В качестве демонстрации, напишем простой код для обработки исключений и вскормим этот код отладчику. Смысл в том, чтобы перехватить исключение и вытащить приложение "с того света", куда ему светит дорога при отсутствии SEH. Внутри своего обработчика я вывожу в форточку код-ошибки, и подменяю значение регистра EIP в структуре "Context". Дальше, прошу диспетчер перезагрузить контекст, в результате чего приложение не терпит крах, а благополучно отправляется на выход по "ExitProcess()":
![]() На этом скрине видно, что структура "Exception_Record" заполнена следующим образом, и мы можем собрать из этих данных достаточно информации: С0000005 = код ошибки "Access Violation" (нет прав на RW); 00000000 = флаг "Обрабатываемое исключение"; 00000000 = исключение не вложенное (не от другого SEH-фрейма в цепочке); 00402015 = адрес глючной инструкции; 00000002 = два дополнительных параметра; 00000000 = параметр(1) =0 (нарушение прав при чтении); 00000000 = параметр(2) =0 (адрес попытки доступа).
3
|
||||||||||||||||||||||||||
|
1 / 1 / 0
Регистрация: 21.09.2017
Сообщений: 31
|
||||||
| 11.02.2021, 11:29 | ||||||
|
Отличная статья, еще не все осилил, тем не менее, спасибо автору.
Возможно у кого то не будет работать 1 пример. При вызове функции приложение будет вылетать с исключением (0xc0000005). Чтобы устранить, нужно секцию '.reloc' сделать доступной для чтения:
1
|
||||||
|
Модератор
|
|
| 12.02.2021, 12:51 | |
|
R71MT, если у вас нет возможности править эту тему - могу внести исправление, предложенноеboolc, а его сообщение переместить в тему, для обсуждения данной статьи.
Исправить и перенести?
0
|
|
| 12.02.2021, 12:51 | |
|
Помогаю со студенческими работами здесь
18
Кто может поделиться файлами ogg.dll, vorbis.dll и vorbisfile.dll - 32-х и 64-битными версиями? Затупил, скажите где взять libmysqld.dll, libmysqld50.dll, libmysqld51.dll Сборка Qt: отучение Qt от mingw10.dll, libgcc_s_dw2-1.dll и других Qt***.dll Пишем патч на C++ Пишем брут Искать еще темы с ответами Или воспользуйтесь поиском по форуму: |
|
Новые блоги и статьи
|
|||
|
SDL3 для Web (WebAssembly): Реализация движения на Box2D v3 - трение и коллизии с повёрнутыми стенами
8Observer8 20.02.2026
Содержание блога
Box2D позволяет легко создать главного героя, который не проходит сквозь стены и перемещается с заданным трением о препятствия, которые можно располагать под углом, как верхнее. . .
|
Конвертировать закладки radiotray-ng в m3u-плейлист
damix 19.02.2026
Это можно сделать скриптом для PowerShell. Использование
. \СonvertRadiotrayToM3U. ps1 <path_to_bookmarks. json>
Рядом с файлом bookmarks. json появится файл bookmarks. m3u с результатом.
# Check if. . .
|
Семь CDC на одном интерфейсе: 5 U[S]ARTов, 1 CAN и 1 SSI
Eddy_Em 18.02.2026
Постепенно допиливаю свою "многоинтерфейсную плату". Выглядит вот так:
https:/ / www. cyberforum. ru/ blog_attachment. php?attachmentid=11617&stc=1&d=1771445347
Основана на STM32F303RBT6.
На борту пять. . .
|
Камера Toupcam IUA500KMA
Eddy_Em 12.02.2026
Т. к. у всяких "хикроботов" слишком уж мелкий пиксель, для подсмотра в ESPriF они вообще плохо годятся: уже 14 величину можно рассмотреть еле-еле лишь на экспозициях под 3 секунды (а то и больше),. . .
|
|
И ясному Солнцу
zbw 12.02.2026
И ясному Солнцу,
и светлой Луне.
В мире
покоя нет
и люди
не могут жить в тишине.
А жить им немного лет.
|
«Знание-Сила»
zbw 12.02.2026
«Знание-Сила»
«Время-Деньги»
«Деньги -Пуля»
|
SDL3 для Web (WebAssembly): Подключение Box2D v3, физика и отрисовка коллайдеров
8Observer8 12.02.2026
Содержание блога
Box2D - это библиотека для 2D физики для анимаций и игр. С её помощью можно определять были ли коллизии между конкретными объектами и вызывать обработчики событий столкновения. . . .
|
SDL3 для Web (WebAssembly): Загрузка PNG с прозрачным фоном с помощью SDL_LoadPNG (без SDL3_image)
8Observer8 11.02.2026
Содержание блога
Библиотека SDL3 содержит встроенные инструменты для базовой работы с изображениями - без использования библиотеки SDL3_image. Пошагово создадим проект для загрузки изображения. . .
|