Як підмінити стек

Навіщо це потрібно

Підміняти стек має сенс коли:

  1. Ви викликаєте функцію, яка вимагає великого стека, і ви не хочете, щоб вона користувалася поточним.
  2. Ви викликаєте функцію, що вимагає великого стека, коли ваш власний стек практично заповнений (наприклад, в тому ж самому обробнику переповнення стека).

На 32-х розрядних Intel-сумісних процесорах для додатків призначеного для користувача режиму розмір сторінки дорівнює 4096 байт.

Після короткого розбору, ми з'ясували, що обраний метод (підхід) вірний, а от реалізація була помилковою. Я провів пару днів за отладчиком, і дещо з'ясував, щодо того, як сама система організовує стек. Результати представлені вашій увазі.

трохи теорії

команда PUSH

команда POP

команда RETN

Діє також як і pop, однак операндом для неї неявно служить регістр eip - extended instruction pointer (покажчик команд). За допомогою цієї команди ви можете змінювати вміст покажчика команд, хоча явних інструкцій для його зміни немає.

Розмір стека

Як і у всього хорошого в цьому світі, у стека є межа або розмір. Тобто ви не можете нескінченно поміщати в нього дані. Як тільки він досягне свого ліміту, система згенерує виняток EXCEPTION_STACK_OVERFLOW. Трохи пізніше ми розглянемо всі подробиці цієї операції. За замовчуванням, розмір стека дорівнює одному мегабайту. Це значення можна змінити за допомогою опції линкера «/ STACK». Для кожного потоку система організовує свій стек.

організація стека

У WindowsXP, за допомогою прапора STACK_SIZE_PARAM_IS_A_RESERVATION, ви можете вказати, що резервуються розмір стека.

Для останньої з переданих спочатку станиць встановлюється прапор PAGE_GUARD. У міру розростання дерева викликів система передає все більше сторінок стека фізичної пам'яті. Остання сторінка «звичайного» стека ніколи не передається і завжди залишається зарезервованої.

Робота зі стеком

Остання передана сторінка стека завжди має встановлений прапор PAGE_GUARD.

Для повністю заповненого стека це не так. Остання передана сторінка має такий же атрибут, як і всі інші передані сторінки стека, а саме PAGE_READWRITE.

Коли відбувається звернення до цієї сторінки, система генерує виняток EXCEPTION_GUARD_PAGE.

Якщо воно виникло при зверненні до сторінок стека, то система обробляє його сама. У всіх інших випадках виключення передається з додатком. Я не знаю, на якому рівні система визначає, що це виняток стека, але знаю, на основі яких даних вона це робить. Про це трохи пізніше.

Оброблювач виключення EXCEPTION_GUARD_PAGE знімає атрибут PAGE_GUARD зі сторінки, на якій сталася помилка, і намагається передати наступну сторінку. Якщо наступна сторінка є останньою, то вона не передається і обробник генерує EXCEPTION_STACK_OVERFLOW. Якщо ж вона не остання - сторінка передається з прапорами PAGE_GUARD і PAGE_READWRITE, і стає черговий сторожовий сторінкою стека потоку.

Таким чином, система повинна знати, як мінімум три речі про стек як про структуру:

Насправді система використовує наступну структуру для управління стеком:

Не знаю точно, але можу припустити, що перші два поля є застарілими і ігноруються. По крайней мере, в [2] і в kernel32.dll при створенні потоку вони обнуляються.

Інформаційний блок потоку

TIB - це структура, яка перебуває на самому початку іншої структури - TEB. TEB - Thread Environment Block (блок оточення потоку). Повний опис TEB ми розглядати не будемо, а ось структуру TIB привести можна. Вона документована в NTDDK. Також її можна знайти в заголовки winnt.h.

Дуже проста і важлива функція, яка повертає за посиланням TIB для поточного потоку.

Тепер, озброївшись теорією, спробуємо реалізувати свій стек, вірніше сказати, спробуємо підмінити поточної стек потоку на свій власний.

підміна стека

І так, стек - досить складна структура і обмежитися одним оновленням регістра esp ми не можемо. Що потрібно для заміни стека?

Для відновлення стека необхідно:

  1. Відновити старі значення полів StackBase і StackLimit структури NT_TIB.
  2. Відновити регістр esp.
  3. Звільнити зайняту під стек пам'ять.

Всю роботу по заміні та відновленню стека виконують дві функції:

Початковий розмір стека встановлюється рівним семи сторінках, остання з яких - сторожова.

Ліміт стека встановлюється на сторінку, що передує сторожовий.

Збереження попереднього стану і зміна полів TIB

Тут наводиться уривок з функції SetNewStack. Вся вона буде розглянута далі.

Перші два рядки зберігають попередні значення полів StackLimit і StackBase. Потім вони встановлюються новими значеннями. Зараз фактично структура TIB знаходиться не в узгодженому стані, так як сам регістр esp ще не оновлено. При виникненні будь-якої ситуації, де системі буде потрібно інформація про стек, може статися все що завгодно. Відзначимо, що перемикання контексту не є такою операцією.

Так як функція SetNewStack приймає два параметри, стек виглядає наступним чином:

Зміна регістра esp і вихід з функції SetNewStack

Якщо ви вже забули, щодо зміщення 0x4 в TIB знаходиться база стека (вже нова). Це значення ми і заносимо в регістри ebp і esp. Наступні дві команди призначені для повернення з процедури.

Повний текст функції SetNewStack

Так як доступ до вхідних параметрів здійснюється через регістр ebp, ми повинні відповідним чином його налаштувати. Першою командою в регістр ebx перший параметр. Потім ми зберігаємо значення регістра ebp і, потім, встановлюємо його значенням з регістра esp за вирахуванням чотирьох. Навіщо? Справа в тому, що компілятор генерує стандартні пролог і епілог для функції, який виглядає наступним чином:

Стек після прологу виглядає так:

Для того щоб отримати доступ до першого параметру, потрібно використовувати регістр ebp, збільшений на 8. Але ми в своїй функції не поміщали ebp в стек, так що [ebp + 8] звернеться до другого параметру. Для того щоб виправити цю ситуацію, ми і коригуємо ebp.

відновлення стека

відновлення TIB

Схожі статті