Статья Стек – общая философия

Marylin

Marylin

Mod.Assembler
Red Team
05.06.2019
148
455
Стек – неотъемлемая часть любого, работающего под управлением процессора х86, приложения. Он организован на аппаратном уровне и магнитом притягивает к себе как системных программистов, так и различного рода малварь. Переполнение стека – наиболее часто встречающейся программный баг, который влечёт за собой катастрофические для приложения последствия. Стек активно используют все WinAPI, так-что вызов любой системной функции при исчерпавшем себя стеке, порождает необрабатываемое исключение STACK_OVERFLOW с кодом 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. По этим селекторам система определяет, от куда именно поступает запрос – от юзера или кернела:

prog_struct.png


Этот (позаимствованный из мануалов интела том.3) рисунок прекрасно демонстрирует скелет программы. Как видим стек – это тоже секция, которая адресуется сегментным регистром SS со-своим дескриптором. Как говорилось выше, размер дескриптора 8-байт и в нём зашиты база, размер (лимит) и атрибуты Access соответствующей секции. Вот как представляет нам битовую карту дескриптора тот-же INTEL:

Descriptor.png


Зачем нужно было делать такой винегрет, размазывая биты одного адреса по всему дескриптору, ведь можно-же было собрать их вместе? Дело в том, что изначально на процессорах 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:

seg_type.png


Для начала, посмотрим в таблице на бит(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'
StackInfo.png


Обратите внимание, что загрузчик образов 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:

Stack_scheme.png


Приведём некоторые пояснения к этому рисунку..
Значит имеем две сторожевые страницы – жёлтая выделяет дополнительные страницы стека (если есть что выделять 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'
StackOver.png


Логи показывают, что обе системы резервируют под стек одинаковое кол-во страниц 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. Позже мы ещё не раз вернёмся к этой теме, а пока поставим здесь точку.
 
Последнее редактирование:
Roman Kazarov

Roman Kazarov

New member
02.02.2020
1
0
Тяжело для понимания. Может кто то дать ссылки и рекомендации что почитать что бы больше вникнуть в тему.
 
Mikl___

Mikl___

New member
01.09.2019
4
7
Тяжело для понимания. Может кто то дать ссылки и рекомендации что почитать что бы больше вникнуть в тему
  1. Д.Соломон, М.Русинович "Внутреннее устройство Microsoft Windows 2000. Структура и алгоритмы работы компонентов Windows 2000 и NTFS 5"
  2. Д.Рихтер "Создание эффективных Win32-приложений с учетом специфики 64-разрядной версии Windows"
  3. "Intel Architecture Software Developer’s Manual Volume #3: System Programming"
  4. Tarik Soulami "Inside Windows Debugging. Practical Debbugging and Tracing Strategies"
 
Последнее редактирование:
  • Нравится
Реакции: Cytrus и Marylin
Marylin

Marylin

Mod.Assembler
Red Team
05.06.2019
148
455
Тяжело для понимания.
Согласен, материал рассчитан на тех, у кого имеется база. Однако можете задавать непонятные моменты здесь, или создать отдельную тему (если это не относится именно к стеку). Ну и ссылки от @
Mikl___ отлично раскрывают общие понятия от архитектуре.
 
  • Нравится
Реакции: Mikl___
SearcherSlava

SearcherSlava

Red Team
10.06.2017
816
1 087
Согласен, материал рассчитан на тех, у кого имеется база. Однако можете задавать непонятные моменты здесь, или создать отдельную тему (если это не относится именно к стеку). Ну и ссылки от @
Mikl___ отлично раскрывают общие понятия от архитектуре.
Здрав будь! Материал рассчитан на тех, у кого база знаний в данной области приближенно равна твоей, а таких или немного, или они не светятся ни днем, ни ночью, отсюда и непонятки в понимании. Сам отстаю на много световых лет, единственное, форумчане могут набрать в поисковичке Стек, или Путешествие туда и обратно, посмотреть букварик, или же, открыв гуглоуточку, набрать и ознакомиться:

Stack LIFO pdf
Register Stack pdf
Stack Assembler pdf
Stack Operations pdf
Stack Data Structure pdf
Procedures and the Stack pdf
The stack and the stack pointer pdf

Stack to me, tell me what you're thinking
Stack to me, say the words I want to hear
......
Stack the talk, start up a conversation
Stack the talk, put me inside your mind
Stack the talk, it could be a revelation
Stack the talk, we're talkin' the talk this time
 
  • Нравится
Реакции: Marylin
Marylin

Marylin

Mod.Assembler
Red Team
05.06.2019
148
455
Материал рассчитан на тех, у кого база знаний в данной области приближенно равна твоей, а таких или немного, или они не светятся ни днем, ни ночью, отсюда и непонятки в понимании.
Просто я изначально не хотел вверенный мне здесь раздел заполнять основами ассемблера, чтобы поддержать марку форума. А так согласен - гугл рулит по всем созданным мной темам.
 
SearcherSlava

SearcherSlava

Red Team
10.06.2017
816
1 087
Просто я изначально не хотел вверенный мне здесь раздел заполнять основами ассемблера, чтобы поддержать марку форума. А так согласен - гугл рулит по всем созданным мной темам.
Гугл рулит и миру говорит: аксесс дистрибушн не нарушен, см. Google Data Collection pdf. Какие перспективы возможны на основе понимания теории графов и предпочтительного присоединения по отношению ко всем пользователям? Безусловно, наданалитические.
 
Последнее редактирование:
Мы в соцсетях: