Техника защиты компакт-дисков от копирования


"Раскрутка" стека


Далеко не во всех случаях принудительный выход из функции оказывается возможным. Ряд критических сбоев затрагивает не одну, а сразу несколько вложенных функций, и тогда для реанимации программы мы должны совершить глубокий откат назад, продолжив выполнение программы с того места, где бы ее работоспособности ничто не угрожало. Точная глубина отката подбирается экспериментально и обычно составляет три–— пять ступеней. Имейте ввиду, что если вложенные функции модифицируют глобальные данные (например данные кучи), то попытка отката может привести к полному краху отлаживаемой программы, поэтому требуемую глубину отката желательно угадать с первого раза, придерживаясь правила: "лучше перебрать, чем недобрать". С другой стороны, чрезмерно глубокий откат ведет к потере всех не сохраненных данных…

Процедура отката состоит из трех шагов: а)

q       построения дерева вызовов;

q      б) определения координат стекового фрейма для каждого из них; в)

q       восстановления регистрового контекста материнской функции.

Хороший отладчик все это сделает за нас, и вам останется лишь записать в регистры EIP и ESP соответствующие значения. К сожалению, отладчик Microsoft Visual Studio Debugger к хорошим отладчикам не относится. Он довольно посредственно трассирует стек, пропуская FPO-функции (Frame Point Omission —  – функции с оптимизированным фреймом) и не сообщает координат стекового фрейма, "благодаря" чему самую трудоемкую часть работы нам приходится выполнять самостоятельно.

Впрочем, даже такой стек вызовов все же лучше, чем совсем ничего. Раскручивая его вручную мы будем отталкиваться от того, что координаты фрейма естественным образом определяются по адресу возврата. Допустим, содержимое окна "Call Stacks"

выглядит так как это показано в листинге 3.8.:

Листинг 3.8. Содержимое окна Call Stacks отладчика Microsoft Visual Studio Debugger


TESTCEDIT! 00401362()

MFC42! 6c2922ae()

MFC42! 6c298fc5()



MFC42! 6c292976()

MFC42! 6c291dcc()

MFC42! 6c291cea()

MFC42! 6c291c73()

MFC42! 6c291bfb()

MFC42! 6c291bba()

Листинг 8  содержимое окна Call Stacks отладчика Microsoft Visual Studio Debugger

Попробуем найти в стеке адреса 6C2922AEh и 6C298FC5h, соответствующие двум последним ступеням исполнения. Нажимаем <ATL>+<-6> для перехода в окно дампа и, воспользовавшись "горячей" комбинацией клавиш <Ctr> +<l-G> в качестве базового адреса отображения, выбираем "ESP". Прокручивая окно дампа вниз, мы обнаруживаем оба адреса возврата (в приведенном далеениже листинге 3.9 они выделены рамкой).:

Листинг 3.9. Содержимое стека после "раскрутки"

0012F488  0012FA64  0012FA64  004012FF ß 0040136F:ret 8 первый адрес возврата

0012F494  00000000  00000064  00403458 ß 00401328:pop esi

0012F4A0  FFFFFFFF  0012F4C4  6C291CEA

0012F4AC  00000019  00000000  6C32FAF0

0012F4B8  0012F4C0  0012FA64  01100059

0012F4C4  00320774  002F5788  00000000

0012F4D0  00320701  77E16383  004C1E20

0012F4DC  00320774  002F5788  00000000

0012F4E8  000003E8  0012FA64  004F8CD8

0012F4F4  0012F4DC  002F5788  0012F560

0012F500  77E61D49  6C2923D8  00403458 ß 0040132C:ret;

0012F50C  00000111  0012F540  6C2922AE ß6C29237E:pop ebx/pop ebp/ret 1Ch

0012F518  0012FA64  000003E8  00000000

0012F518  0012FA64  000003E8  00000000

0012F524  004012F0  00000000  0000000C

0012F530  00000000  00000000  0012FA64

0012F53C  000003E8  0012F564  6C298FC5

0012F548  000003E8  00000000  00000000

0012F554  00000000  000003E8  0012FA64

Листинг 9 содержимое стека после раскрутки

Ячейки памяти, лежащие выше адресов возврата, представляют собой значения регистров, сохраненные в стеке при входе в функцию и восстанавливаемые при ее завершении. Ячейки памяти, лежащие ниже адресов возврата, "оккупированы" аргументами функции (если, конечно, у функции есть аргументы), или же принадлежат локальным переменным материнской функции, если дочерняя функция не принимает никаких аргументов.



Возвращаясь к листингу 3.5, отметим, что два двойных слова, лежащие на верхушке стека, соответствуют машинным командам POP EDI и POP ESI, а следующий за ними адрес –— 4012FFh –— это тот самый адрес, управление которому передается командой 40136Fh:RET 8. Для продолжения раскрутки стека мы должны дизассемблировать код по этому адресу (листинг 3.1:0).

Листинг 3.10. Дизассемблерный листинг праматеринской функции ("бабушки")

004012FA             call           00401350

004012FF             cmp            eax,0FFh

00401302             je       0040132D

00401304             push           eax

00401305             lea             eax,[esp+8]

00401309             push           405054h

0040130E             push           eax

0040130F             call           dword ptr ds:[4033B4h]

00401315             add             esp,0Ch

00401318             lea             ecx,[esp+4]

0040131C             push           0

0040131E             push           0

00401320             push           ecx

00401321             mov             ecx,esi

00401323             call           00401BC4

00401328             pop             esi

00401329             add             esp,64h        

0040132C             ret                                     ; SS:[ESP] = 6C2923D8

Листинг 10 дизассемблерный листинг праматеринской функции ("бабушки")

Прокручивая экран вниз, мы замечаем инструкцию ADD  ESP, 64, закрывающую текущий кадр стека. Еще восемь байт снимает инструкция 40136Fh:RET 8 и четыре байта оттягивает на себя 401328:POP ESI. Таким образом, позиция адреса возврата в стеке равна: current_ESP + 64h + 8 + 4 == 70h. Спускаемся на 70h байт ниже и видим адрес возврата из праматеринской функции (листинг3.11).:

Листинг 3.12. Адрес возврата из праматеринской функции

0012F500  77E61D49  6C2923D8  00403458 ß

00401328:POP ESI/ret;



Листинг 11 адрес возврата из праматеринской функции

Первое двойное слово –— это значение регистра ESI, который нам предстоит вручную восстановить; второе –— адрес возврата из функции. Нажатием <Ctrl>+<-G>, "0x6C2923D8" мы продолжаем "раскручивать" стек (листинг 3.12).:

Листинг 3.12. Дизассемблерный листинг прапраматеринской функции

6C2923D8             jmp             6C29237B



6C29237B             mov             eax,ebx

6C29237D             pop             esi

6C29237E             pop             ebx

6C29237F             pop             ebp

6C292380             ret             1Ch

Листинг 12 дизассемблерный листинг пра-праматеринской функции

Вот мы и добрались до восстановления регистров! Сместившись на одно двойное слово вправо (оно только что было вытолкнуто из стека командой RET), переходим в окно "Registers" и восстанавливаем регистры ESI, EBX, EBP, извлекая сохраненные значения из стека (листинг 3.13).:

Листинг 3.13. Содержимое регистров, ранее сохраненных в стеке вместе с адресом возврата

0012F500  77E61D49  6C2923D8  00403458

ß

6C29237D:pop esi

0012F50C  00000111  0012F540  6C2922AE ß6C29237E:pop ebx/pop ebp/ret 1Ch

Листинг 13 содержимое регистров, ранее сохраненных в стеке вместе с адресом возврата

Как вариант можно переместить регистр EIP на адрес 6C29237Dh, а регистр ESP на адрес 12F508h, после чего нажать на клавишу <F5> для продолжения выполнения программы. И этот прием действительно срабатывает! Причем, реанимированная программа уже "не ругается" на ошибку последней операции (как это было при восстановлении путем принудительного выхода из функции), а просто ее не выполняет. Красота!


Содержание раздела