Стек – неотъемлемая часть любого, работающего под управлением процессора х86, приложения. Он организован на аппаратном уровне и магнитом притягивает к себе как системных программистов, так и различного рода малварь. Переполнение стека – наиболее часто встречающейся программный баг, который влечёт за собой катастрофические для приложения последствия. Стек активно используют все WinAPI, так-что вызов любой системной функции при исчерпавшем себя стеке, порождает необрабатываемое исключение STACK_OVERFLOW с кодом
Для адресации и работы с сегментом стека, в процессоры х86 была включена специальная регистровая пара
В данной статье стек рассматривается с технической стороны. Мы проведём некоторые эксперименты с ним (не путать с экскрементами), попытаемся расширить его под свои нужды, напишем несколько приложений и многое другое. Информацию приходилось собирать с различных источников, проверяя её на личном опыте. В качестве платформы будет выступать 32-битная Win-7.
------------------------------------
К истокам виртуальной памяти
Начнём с того, что стек – это такая-же программная секция, как и секция-данных. Основное её отличие заключается в том, что счётчик-адресов в этой секции инверсный, т.е. стек растёт в сторону меньших адресов. Инструкция PUSH кладёт значение в стек (при этом указатель ESP аппаратно уменьшается на четыре), а инструкция POP снимает 4-байта, на которые указывает регистр ESP (соответственно увеличивая его на 4).
Посмотрим на рисунок ниже, где представлена модель виртуальной памяти приложения. Здесь видно, что процессор имеет 6 сегментных регистров CS…GS, и к каждому регистру привязан свой 8-байтный дескриптор. На самом деле, описывающие секцию дескрипторы копируются системой прямо внутрь сегментных регистров, только программисту эта их часть не доступна – мы видим в сегментных регистрах только 2-байтные селекторы, в которых хранится текущая привелегия RPL – Request Privelege Level. Например, у пользовательских приложений, селектор секции-кода равен
Этот (позаимствованный из мануалов интела том.3) рисунок прекрасно демонстрирует скелет программы. Как видим стек – это тоже секция, которая адресуется сегментным регистром
Зачем нужно было делать такой винегрет, размазывая биты одного адреса по всему дескриптору, ведь можно-же было собрать их вместе? Дело в том, что изначально на процессорах i286 дескрипторы были 6-байтные, а 8-байтными они стали только с приходом i386. Инженеры тупо добавили биты 16-31 в старший дворд, что позволило сохранить совместимость со-старыми программами.
Система хранит шаблоны всех дескрипторов в двух таблицах – глобальной GDT (Global Descriptor Table), и локальной LDT (используется редко). Адреса этих таблиц хранятся в регистрах управления памятью
Поскольку в данный момент речь идёт о секции-стека, то из всего дескриптора нам интересно лишь выделенное красным поле TYPE – тип секции. В этом 4-битном поле (биты 11-8 старшего дворда) инженеры закодировали зоопарк из 16-ти типов секций, которые представлены на рисунке ниже. Эта таблица действительна только, когда бит(S) в дескрипторе =1:
Для начала, посмотрим в таблице на бит(11) – если он нуль, то это секция-данных, иначе секция-кода. Для секции-данных, остальные биты 10-9-8 переводятся как: Expand (направление адреса), Write и Access.
Выше упоминалось, что секция-стека является разновидностью секции-данных. Посмотрите на выделенный красным блок единиц в столбце бита(10) – это и есть четыре типа секций-стека с флагом Expand-Down, что означает "обратное расширение адреса". Интересным фактом является то, что при отключённом механизме DEP (Data Execute Protect, защита от исполнения данных), бит записи под номером(9) неявно подразумевает и исполнение Execute. То-есть задав в поле TYPE значение 6 или 7, мы получим исполняемый стек по аналогии с секцией-кода!
Нужно сказать, что на всех линейках Win, стек по-умолчанию является исполняемым, т.к. всегда доступен на запись
Организация стека прикладных программ
Все эти таблицы и дескрипторы обитают на ядерном уровне, куда без драйвера дорога закрыта. Однако и на уровне пользователя прописано достаточно информации о стеке, чтобы мы могли оперировать им программно – в частности это касается структуры TЕВ (Thread Environment Block). Отметим, что у каждого потока Thread свой стек, т.к. это самостоятельная исполняемая единица – на многоядерных CPU каждый из тредов одного процесса выполняется параллельно во-времени, что порождает проблемы их синхронизации (исключение ситуаций, когда один поток что-то записал в глобальную переменную, а второй поток затёр её под себя).
Когда система создаёт окружение нового процесса, в поле ТЕВ.04 она заносит базовый адрес секции-стека, а в поле ТЕВ.08 прописывает его лимит. Согласно документации, программа может явно указать системе, сколько стека ей требуется для нормальной работы – для этого в РЕ-заголовке файла имеются два поля:
• РЕ.60h – Stack Reserve предписывает системе зарезервировать указанный объём стековой памяти.
• PE.64h - Stack Commit является объёмом памяти, отводимой в стеке немедленно после загрузки программы.
Напишем небольшое приложение, которое покажет нам эти данные (на ТЕВ всегда указывает сегментный регистр fs на х32, и gs на х64)..
Обратите внимание, что загрузчик образов Win игнорирует поля РЕ.60/64h и забив на них следует своему алгоритму. Не смотря на то, что в заголовке РЕ.60h мы затребуем резерв стека, например 10-страниц памяти (в данном случае всего одна, которую выставил сам компилятор fasm), по-умолчанию все системы класса Win выделяют под стек лишь 2-страницы или 8 Кбайт, что демонстрирует отладчик значением
Поскольку стек растёт в обратном направлении, то следующая\третья страница будет сторожевой с атрибутами GUARD_PAGE. Когда мы заполним выделенные 2-страницы и упрёмся в сторожевую, система сгенерит исключение Guard_Page_Violation, и обработчик этого исключения снимет со-сторожевой атрибут GUARD_PAGE превратив её в обычную страницу стека с атрибутами RW.
Теперь этот-же обработчик системным аллокатором памяти выделяет ещё одну (примыкающую к предыдущей) страницу и делает из неё сторожевую – в данном случае её адрес будет
Приведём некоторые пояснения к этому рисунку..
Значит имеем две сторожевые страницы – жёлтая выделяет дополнительные страницы стека (если есть что выделять commit), а красная предвещает нам полные кранты и следит за тем, чтобы стек не вышел за границы резервного предела. В момент, когда жёлтая сторожевая страница с разгону врезается в красную, в запасе у нас имеется ещё три пейджа или 12 Кбайт стековой памяти, которые мы могли-бы использовать в военное время – это красная, белая с атрибутами PAGE_NOACCESS, и синяя.
Вообще-то синюю страницу трогать нельзя, иначе в критической ситуации мы останемся без API-функций и не сможем ничего за.PUSH.ить – как минимум одна страница всегда должна быть в резерве. Однако мастдай придерживается иного мнения на этот счёт, и при исключении STACK_OVERFLOW управление сразу передаётся в кернел, где происходит тихая смерть процесса на ядерном уровне – юзер может даже не понять, в чём именно причина.
Лабораторные опыты со стеком
На счёт максимального размера выделяемой стековой памяти в разных источниках пишут по-разному. Кто-то утверждает, что система выделяет под стек 1 Мбайт виртуальной памяти, что в сумме даёт 256 страниц размером по 4 Кбайт каждая. Как говорилось выше, полю из заголовка РЕ.60h с именем Stack Reserve доверять нельзя и разные версии Windows выделяют под стек кол-во виртуальных страниц на своё усмотрение. Например у XP и Win7 ручеёк сильно истощился и они резервирует всего 64-пейджа вместо 256 -
Из рисунка выше мы уже знаем, что стек лопается как пузырь в случае столкновения желтой и красной сторожевых страниц. При этом система генерит исключение STACK_OVERFLOW с кодом
Значит чтобы вычислить размер реально зарезервированного системой стека, нужно установить свой обработчик на исключение Stack_Overflow, что на практике реализуется пользовательским SEH-фреймом. В этот момент, система снимает с красной страницы атрибут PAGE_GUARD и делает её обычной стековой страницей, чтобы попытаться корректно обработать форс-мажорную ситуацию. Так-что внутри своего обработчика мы можем спокойно вызывать API-функции (для работы которым требуется стек), зато в следующий раз исключения Stack_Overflow мы уже не получим и система может уйти в разнос. Другими словами, этот обработчик даёт нам возможность только сохранить какие-нибудь данные и сразу выйти из приложения по ExitProcess().
В то-же время, алгоритм работы промышленного обработчика жёлтой\сторожевой страницы PAGE_GUARD такой, что при каждом выделении дополнительных страниц он уменьшает значение в поле ТЕВ.08 (stack_limit) до тех пор, пока не наступит взрыв при столкновении желтой и красной страниц. Значит если насилуя стек мы будем следить за полем лимита ТЕВ.08, то сможем фиксить момент выделения стековых страниц системой. Вот как это звучит на ассемблере:
Логи показывают, что обе системы резервируют под стек одинаковое кол-во страниц 59, плюс две выделенные вначале страницы Stack_Commit, итого 61. Здесь может возникнуть резонный вопрос: -"Почему памяти выделяется не 256, а 244 Кбайт – не кратное кол-во?". Именно этот факт подтверждает теорию о том, что в самом конце стека остаются ещё три страницы – красная\сторожевая, обычная с атрибутами PAGE_NOACCES, и в самом конце ещё и синяя страница. В этом коде, исключение Stack_Overflow мы получили в аккурат при обращении к красной, а значит прожка её и две последующие не посчитала. Три страницы это 12 Кбайт, которые добавляем к 244 и получаем 256 – теория доказана.
Создаём рукотворный стек
Таким образом, при программировании мы вполне можем получить ошибку, в результате которой наша программа будет тупо проваливаться в чёрную дыру, не выводя никаких сообщений. Отлавливать такие глюки трудно, поскольку всё как-бы функционирует. Например если мы работаем с графикой, то нам потребуется стек намного больших размеров, чем выделяемый системой по-умолчанию 256 Кбайт. В основном это касается динамичных игр, или больших математических вычислений. Как быть в такой ситуации и объяснить системе, что нам нужен стек гигантских размеров, ведь поле РЕ.60h.StackReserve в заголовке файла система игнорирует?
Именно для таких случаев необходимо приобрести навыки борьбы со-стеком в рукопашную. Теперь, когда мы знаем его структуру, нам не составит труда по кирпичикам соорудить свой стек любого (в разумных пределах) размера. Для этого всего-то нужно:
1. Функцией VirtualAlloc() выделить необходимый объём непрерывной памяти, которую будем использовать как стек. После того-как установим указатель ESP в конец этого блока памяти, на том конце кто-то включит рубильник и сегментный регистр SS сам присвоит ему атрибут Expand-Down, что означает "обратный шаг адреса". Функция имеет такой прототип, в аргументе "Protect" которой можно задавать атрибуты:
2. Сразу выделить последние несколько страниц Stack_Commit с атрибутами RW (система выделяет две).
3. Следующей за Commit странице присвоить атрибут PAGE_GUARD – это будет жёлтая\сторожевая страница стека.
4. Изменить поле в структуре TЕB.04.StackBase на последний адрес выделенного блока памяти.
5. Изменить поле в структуре TЕB.08.StackLimit, задав ему размер выделенного блока.
6. Выставить регистр-указатель стека ESP на StackBase.
Если хотите идеальный стек, то можно самым верхним страницам присвоить соответствующие атрибуты (см.рис.выше). Однако с технической стороны проще выделить доп.страницы, чтобы до исключения StackOverflow дело вообще не дошло – это уже на личное усмотрение.
Заключение
Археологические раскопки стека – довольно интересное занятие, особенно если знаешь что и где искать. Это излюбленное место обитания разного рода червей и прочей нечисти – дичь может гнездиться там, где никогда-бы и не подумал её искать. А всё из-за того, что стек довольно хрупкое сооружение и при малейшем чихе может запросто положить приложение на лопатки.
Например обычная инструкция обнуления указателя стека
0xC00000FD
.Для адресации и работы с сегментом стека, в процессоры х86 была включена специальная регистровая пара
SS:ESP
(StackSegment –> StackPointer). Microsoft неохотно делится с тонкостями реализации, поскольку сама "слизала" его ещё с допотопной машины DEC-PDP и в своей документации нигде это даже не обозначила. Но оставим сей факт на их совести, ведь в чём-то мелкомягкие и правы – зачем изобретать велосипед, когда есть готовое и удовлетворяющее требованиям решение?В данной статье стек рассматривается с технической стороны. Мы проведём некоторые эксперименты с ним (не путать с экскрементами), попытаемся расширить его под свои нужды, напишем несколько приложений и многое другое. Информацию приходилось собирать с различных источников, проверяя её на личном опыте. В качестве платформы будет выступать 32-битная Win-7.
------------------------------------
К истокам виртуальной памяти
Начнём с того, что стек – это такая-же программная секция, как и секция-данных. Основное её отличие заключается в том, что счётчик-адресов в этой секции инверсный, т.е. стек растёт в сторону меньших адресов. Инструкция PUSH кладёт значение в стек (при этом указатель ESP аппаратно уменьшается на четыре), а инструкция POP снимает 4-байта, на которые указывает регистр ESP (соответственно увеличивая его на 4).
Посмотрим на рисунок ниже, где представлена модель виртуальной памяти приложения. Здесь видно, что процессор имеет 6 сегментных регистров CS…GS, и к каждому регистру привязан свой 8-байтный дескриптор. На самом деле, описывающие секцию дескрипторы копируются системой прямо внутрь сегментных регистров, только программисту эта их часть не доступна – мы видим в сегментных регистрах только 2-байтные селекторы, в которых хранится текущая привелегия RPL – Request Privelege Level. Например, у пользовательских приложений, селектор секции-кода равен
0x1B
(см.значение CS в отладчике), а селектор кода ядра всегда равен 0х08
. По этим селекторам система определяет, от куда именно поступает запрос – от юзера или кернела:Этот (позаимствованный из мануалов интела том.3) рисунок прекрасно демонстрирует скелет программы. Как видим стек – это тоже секция, которая адресуется сегментным регистром
SS
со-своим дескриптором. Как говорилось выше, размер дескриптора 8-байт и в нём зашиты база, размер (лимит) и атрибуты Access соответствующей секции. Вот как представляет нам битовую карту дескриптора тот-же INTEL:Зачем нужно было делать такой винегрет, размазывая биты одного адреса по всему дескриптору, ведь можно-же было собрать их вместе? Дело в том, что изначально на процессорах i286 дескрипторы были 6-байтные, а 8-байтными они стали только с приходом i386. Инженеры тупо добавили биты 16-31 в старший дворд, что позволило сохранить совместимость со-старыми программами.
Система хранит шаблоны всех дескрипторов в двух таблицах – глобальной GDT (Global Descriptor Table), и локальной LDT (используется редко). Адреса этих таблиц хранятся в регистрах управления памятью
GDTR
и LDTR
соответственно. Некоторые поля дескриптора требуют пояснений – рассмотрим вкратце..• Значит бит(G) определяет гранулярность лимита. Сам лимит – это максимальный адрес секции, т.е. её размер относительно базы. Когда флаг гранулярности сброшен, лимит интерпретируется в байтах, а когда установлен – в 4 Кбайтных страницах. Как показывает скрин, под лимит выделяется 20-бит, тогда при G=1 получаем:
2^20=1.048.576
и применяем к нему множитель 4К, что в произведении даёт макс.размер одной секции =4Gb. Соответственно если G=0 лимит исчисляется в байтах, и получаем 1Мб в реальном (редко в защищённом) режиме работы процессора.
• Следующий бит(Р) является флагом того, присутствует-ли в текущий момент секция в памяти P=1, или она вытолкнута системой в файл-подкачки P=0. Когда системе не хватает физической памяти, содержимое редко используемых секций она сбрасывает на диск, и в их дескрипторах бит(Р) выставляется в нуль, т.е. секция виртуальной памяти отсутствует (см.первый скрин).
• Сброшенный в нуль бит(S) сигнал к тому, что перед нами системная секция, например типа TSS – Task State Segment (сегмент состояния задачи), таблица GDT или шлюз обработки прерываний. У всех прикладных программ бит S=1, что характеризует секцию как программную.
Поскольку в данный момент речь идёт о секции-стека, то из всего дескриптора нам интересно лишь выделенное красным поле TYPE – тип секции. В этом 4-битном поле (биты 11-8 старшего дворда) инженеры закодировали зоопарк из 16-ти типов секций, которые представлены на рисунке ниже. Эта таблица действительна только, когда бит(S) в дескрипторе =1:
Для начала, посмотрим в таблице на бит(11) – если он нуль, то это секция-данных, иначе секция-кода. Для секции-данных, остальные биты 10-9-8 переводятся как: Expand (направление адреса), Write и Access.
Выше упоминалось, что секция-стека является разновидностью секции-данных. Посмотрите на выделенный красным блок единиц в столбце бита(10) – это и есть четыре типа секций-стека с флагом Expand-Down, что означает "обратное расширение адреса". Интересным фактом является то, что при отключённом механизме DEP (Data Execute Protect, защита от исполнения данных), бит записи под номером(9) неявно подразумевает и исполнение Execute. То-есть задав в поле TYPE значение 6 или 7, мы получим исполняемый стек по аналогии с секцией-кода!
Нужно сказать, что на всех линейках Win, стек по-умолчанию является исполняемым, т.к. всегда доступен на запись
PUSH
– неисполняемым его может сделать только механизм DEP, который рассматривался в предыдущей статье. Более того, даже помещённый в обычную секцию-данных код – прекрасно отрабатывает, хотя секция не имеет атрибута Ех, а только RW – вот пример:
C-подобный:
include 'win32ax.inc'
.data
;//=== Код в секции-данных ===
proc Hello
invoke MessageBox,0,msg,0,0
ret
endp
msg db 'Код из секции-данных!',0
;//-------
.code
start: call Hello ;// вызов процедуры!
invoke ExitProcess,0
.end start
Организация стека прикладных программ
Все эти таблицы и дескрипторы обитают на ядерном уровне, куда без драйвера дорога закрыта. Однако и на уровне пользователя прописано достаточно информации о стеке, чтобы мы могли оперировать им программно – в частности это касается структуры TЕВ (Thread Environment Block). Отметим, что у каждого потока Thread свой стек, т.к. это самостоятельная исполняемая единица – на многоядерных CPU каждый из тредов одного процесса выполняется параллельно во-времени, что порождает проблемы их синхронизации (исключение ситуаций, когда один поток что-то записал в глобальную переменную, а второй поток затёр её под себя).
Когда система создаёт окружение нового процесса, в поле ТЕВ.04 она заносит базовый адрес секции-стека, а в поле ТЕВ.08 прописывает его лимит. Согласно документации, программа может явно указать системе, сколько стека ей требуется для нормальной работы – для этого в РЕ-заголовке файла имеются два поля:
• РЕ.60h – Stack Reserve предписывает системе зарезервировать указанный объём стековой памяти.
• PE.64h - Stack Commit является объёмом памяти, отводимой в стеке немедленно после загрузки программы.
Напишем небольшое приложение, которое покажет нам эти данные (на ТЕВ всегда указывает сегментный регистр fs на х32, и gs на х64)..
C-подобный:
;// fasm-code
;//------------------
format pe console
include 'win32ax.inc'
entry start
;//-------
.data
capt db 13,10,' STACK INFO v0.1'
db 13,10,' ******************************************'
db 13,10,' TEB.04 stack Base....: 0x%08X'
db 13,10,' TEB.08 stack Limit...: 0x%08X'
db 13,10,' PE.64 stack Commit..: 0x%08X'
db 13,10,' PE.60 stack Reserve.: 0x%08X'
db 13,10,' ------------------------------------------'
db 13,10,' Current stack: TEB.04 - TEB.08 = %d byte',0
frmt db '%s',0
buff db 0
;//-------
.code
start: mov eax,[fs:04] ;// EAX = база стека из TEB
mov ebx,[fs:08] ;// EBX = лимит из TEB
mov edx,eax ;// .....
sub edx,ebx ;// EDX = размер текущего стека в байтах
mov esi,[fs:0x30] ;// ESI = адрес РЕВ
mov esi,[esi+08] ;// ESI = база нашей программы в памяти
mov edi,esi ;// EDI = ESI
mov esi,[esi+0x3c] ;// ESI = RVA на РЕ заголовок
add esi,edi ;// ESI = VA-адрес РЕ заголовка
mov ecx,[esi+0x64] ;// ECX = PE.64h (stack commit)
mov esi,[esi+0x60] ;// ESI = PE.60h (stack reserve)
cinvoke printf,capt,eax,ebx,ecx,esi,edx
@exit: cinvoke scanf,frmt,buff
cinvoke exit,0
;//-------
section '.idata' import data readable
library msvcrt,'msvcrt.dll'
import msvcrt, printf,'printf',scanf,'scanf',exit,'exit'
Обратите внимание, что загрузчик образов Win игнорирует поля РЕ.60/64h и забив на них следует своему алгоритму. Не смотря на то, что в заголовке РЕ.60h мы затребуем резерв стека, например 10-страниц памяти (в данном случае всего одна, которую выставил сам компилятор fasm), по-умолчанию все системы класса Win выделяют под стек лишь 2-страницы или 8 Кбайт, что демонстрирует отладчик значением
0x2000
на рисунке выше (см.зелёную строку) – это т.н. Stack Commit или изначально выделенный программе стек.Поскольку стек растёт в обратном направлении, то следующая\третья страница будет сторожевой с атрибутами GUARD_PAGE. Когда мы заполним выделенные 2-страницы и упрёмся в сторожевую, система сгенерит исключение Guard_Page_Violation, и обработчик этого исключения снимет со-сторожевой атрибут GUARD_PAGE превратив её в обычную страницу стека с атрибутами RW.
Теперь этот-же обработчик системным аллокатором памяти выделяет ещё одну (примыкающую к предыдущей) страницу и делает из неё сторожевую – в данном случае её адрес будет
0х6С000
. Так сторожевая страница будет передвигаться всё выше и выше, пока стек не исчерпает себя. Главное требование к нему – непрерывная область памяти! В результате получаем такую картину, где Stack Reserve определяет сама система, а не РЕ.60h:Приведём некоторые пояснения к этому рисунку..
Значит имеем две сторожевые страницы – жёлтая выделяет дополнительные страницы стека (если есть что выделять commit), а красная предвещает нам полные кранты и следит за тем, чтобы стек не вышел за границы резервного предела. В момент, когда жёлтая сторожевая страница с разгону врезается в красную, в запасе у нас имеется ещё три пейджа или 12 Кбайт стековой памяти, которые мы могли-бы использовать в военное время – это красная, белая с атрибутами PAGE_NOACCESS, и синяя.
Вообще-то синюю страницу трогать нельзя, иначе в критической ситуации мы останемся без API-функций и не сможем ничего за.PUSH.ить – как минимум одна страница всегда должна быть в резерве. Однако мастдай придерживается иного мнения на этот счёт, и при исключении STACK_OVERFLOW управление сразу передаётся в кернел, где происходит тихая смерть процесса на ядерном уровне – юзер может даже не понять, в чём именно причина.
Лабораторные опыты со стеком
На счёт максимального размера выделяемой стековой памяти в разных источниках пишут по-разному. Кто-то утверждает, что система выделяет под стек 1 Мбайт виртуальной памяти, что в сумме даёт 256 страниц размером по 4 Кбайт каждая. Как говорилось выше, полю из заголовка РЕ.60h с именем Stack Reserve доверять нельзя и разные версии Windows выделяют под стек кол-во виртуальных страниц на своё усмотрение. Например у XP и Win7 ручеёк сильно истощился и они резервирует всего 64-пейджа вместо 256 -
64*4096=256 Кбайт
. Как это можно проверить на практике? Да очень просто..Из рисунка выше мы уже знаем, что стек лопается как пузырь в случае столкновения желтой и красной сторожевых страниц. При этом система генерит исключение STACK_OVERFLOW с кодом
0xC00000FD
(см.сишный хидер ntstatus.h):// MessageId: STATUS_STACK_OVERFLOW
// MessageText:
// A new guard page for the stack cannot be created. (не могу создать Stack_Page_Guard)
//
#define STATUS_STACK_OVERFLOW (0xC00000FD)
Значит чтобы вычислить размер реально зарезервированного системой стека, нужно установить свой обработчик на исключение Stack_Overflow, что на практике реализуется пользовательским SEH-фреймом. В этот момент, система снимает с красной страницы атрибут PAGE_GUARD и делает её обычной стековой страницей, чтобы попытаться корректно обработать форс-мажорную ситуацию. Так-что внутри своего обработчика мы можем спокойно вызывать API-функции (для работы которым требуется стек), зато в следующий раз исключения Stack_Overflow мы уже не получим и система может уйти в разнос. Другими словами, этот обработчик даёт нам возможность только сохранить какие-нибудь данные и сразу выйти из приложения по ExitProcess().
В то-же время, алгоритм работы промышленного обработчика жёлтой\сторожевой страницы PAGE_GUARD такой, что при каждом выделении дополнительных страниц он уменьшает значение в поле ТЕВ.08 (stack_limit) до тех пор, пока не наступит взрыв при столкновении желтой и красной страниц. Значит если насилуя стек мы будем следить за полем лимита ТЕВ.08, то сможем фиксить момент выделения стековых страниц системой. Вот как это звучит на ассемблере:
C-подобный:
format pe console
include 'win32ax.inc'
entry start
;//-------
.data
capt db 13,10,' STACK OVER-FLOW v0.1'
db 13,10,' ********************'
db 13,10,' Stack Base: 0x%X' ,10
db 13,10,' Allocate Address'
db 13,10,' -------------------',0
limit db 13,10,' Page: %02d 0x%X' ,0
over db 13,10,' ------------------------------'
db 13,10,' == Exception Stack_OverFlow =='
db 13,10,' System stack reserve = %d Kb',0
count dd 0 ;// счётчик выделенных страниц
frmt db '%s',0
buff db 0
;//-------
.code
start:
;// ставим SEH на 'StackOverflow'
push @trap ;// линк на функцию-ловушку
push dword[fs:0] ;// привязать цепочку..
mov [fs:0],esp ;// подмена 'ExceptionList' в ТЕВ
mov eax,[fs:04] ;// EAX = база стека из TEB
cinvoke printf,capt,eax ;// показать!
;// Основная программа ========
mov eax,[fs:08] ;// EAX = лимит из TEB
@cycle: push 'DEAD' ;// забиваем стек двойными словами!!!
cmp [fs:08],eax ;// проверить смену лимита в ТЕВ
je @cycle ;// повторить, если лимит не изменился..
mov ebp,esp ;// иначе: взять в EBP указать на стек
add ebp,4 ;// коррекция..
inc [count] ;// счётчик выделенных страниц +1
cinvoke printf,limit,[count],ebp ;// вывести данные на консоль!
mov eax,[fs:08] ;// изменить условие (EAX = новый лимит)
jmp @cycle ;// промотать цикл до исключения 'StackOver'
;//============================
@exit: cinvoke scanf,frmt,buff ;// SEH передаст управление сюда
cinvoke exit,0 ;// на выход!
;//=== Обработчик исключения 'StackOverflow'
@trap: mov esi,[esp+12] ;// ESI = указатель на CONTEXT регистров
mov eax,@exit ;// EAX = адрес метки для выхода
mov [esi+0xb8],eax ;// 0xb8 = заменить EIP в контексте!
mov eax,[count] ;// EAX = счётчик выделенных
mov ebx,4096 ;// EBX = размер одной страницы вирт.памяти
mul ebx ;// EAX = размер выделенных страниц в байтах
add eax,4096*2 ;// плюс две выделенные в начале страницы COMMIT
shr eax,10 ;// перевести в Кбайты (разделить на 1024)
cinvoke printf,over,eax ;// мессага!
xor eax,eax ;// команда "ребут" диспетчеру-исключений
ret ;// return на выход..
;//-------
section '.idata' import data readable
library msvcrt,'msvcrt.dll'
import msvcrt, printf,'printf',scanf,'scanf',exit,'exit'
Логи показывают, что обе системы резервируют под стек одинаковое кол-во страниц 59, плюс две выделенные вначале страницы Stack_Commit, итого 61. Здесь может возникнуть резонный вопрос: -"Почему памяти выделяется не 256, а 244 Кбайт – не кратное кол-во?". Именно этот факт подтверждает теорию о том, что в самом конце стека остаются ещё три страницы – красная\сторожевая, обычная с атрибутами PAGE_NOACCES, и в самом конце ещё и синяя страница. В этом коде, исключение Stack_Overflow мы получили в аккурат при обращении к красной, а значит прожка её и две последующие не посчитала. Три страницы это 12 Кбайт, которые добавляем к 244 и получаем 256 – теория доказана.
Создаём рукотворный стек
Таким образом, при программировании мы вполне можем получить ошибку, в результате которой наша программа будет тупо проваливаться в чёрную дыру, не выводя никаких сообщений. Отлавливать такие глюки трудно, поскольку всё как-бы функционирует. Например если мы работаем с графикой, то нам потребуется стек намного больших размеров, чем выделяемый системой по-умолчанию 256 Кбайт. В основном это касается динамичных игр, или больших математических вычислений. Как быть в такой ситуации и объяснить системе, что нам нужен стек гигантских размеров, ведь поле РЕ.60h.StackReserve в заголовке файла система игнорирует?
Именно для таких случаев необходимо приобрести навыки борьбы со-стеком в рукопашную. Теперь, когда мы знаем его структуру, нам не составит труда по кирпичикам соорудить свой стек любого (в разумных пределах) размера. Для этого всего-то нужно:
1. Функцией VirtualAlloc() выделить необходимый объём непрерывной памяти, которую будем использовать как стек. После того-как установим указатель ESP в конец этого блока памяти, на том конце кто-то включит рубильник и сегментный регистр SS сам присвоит ему атрибут Expand-Down, что означает "обратный шаг адреса". Функция имеет такой прототип, в аргументе "Protect" которой можно задавать атрибуты:
C-подобный:
VirtualAlloc (
lpvAddress // адрес нового блока (если нуль, система найдёт сама)
dwSize // размер выделяемого блока памяти
fdwAllocationType // резерв или закрепить физ.память?
fdwProtect ); // атрибуты..
;//-----------------
;// Флаги Protect
;//-----------------
;// PAGE_READONLY
;// PAGE_READWRITE
;// PAGE_EXECUTE
;// PAGE_EXECUTE_READ
;// PAGE_EXECUTE_READWRITE
;// PAGE_GUARD
;// PAGE_NOCACHE
2. Сразу выделить последние несколько страниц Stack_Commit с атрибутами RW (система выделяет две).
3. Следующей за Commit странице присвоить атрибут PAGE_GUARD – это будет жёлтая\сторожевая страница стека.
4. Изменить поле в структуре TЕB.04.StackBase на последний адрес выделенного блока памяти.
5. Изменить поле в структуре TЕB.08.StackLimit, задав ему размер выделенного блока.
6. Выставить регистр-указатель стека ESP на StackBase.
Если хотите идеальный стек, то можно самым верхним страницам присвоить соответствующие атрибуты (см.рис.выше). Однако с технической стороны проще выделить доп.страницы, чтобы до исключения StackOverflow дело вообще не дошло – это уже на личное усмотрение.
Заключение
Археологические раскопки стека – довольно интересное занятие, особенно если знаешь что и где искать. Это излюбленное место обитания разного рода червей и прочей нечисти – дичь может гнездиться там, где никогда-бы и не подумал её искать. А всё из-за того, что стек довольно хрупкое сооружение и при малейшем чихе может запросто положить приложение на лопатки.
Например обычная инструкция обнуления указателя стека
xor esp,esp
приводит к полному краху приложения и оно тупо исчезает, даже не попав в руки выводящего сообщения модуля WER – Windows Error Reporting. Об атаках типа "переполнение стека" и говорить не стоит, хотя отличной панацеей от них является всё тот-же обработчик структурных исключений SEH. Позже мы ещё не раз вернёмся к этой теме, а пока поставим здесь точку.
Последнее редактирование: