Ассемблерные извращения Крис Касперски Хакер, номер #096, стр. 096-140-4 Рукотворный стек с локальными переменными и аргументами растет сверху вниз, то есть в направлении, противоположном росту обычного стека, и это неспроста. Во-первых, подсистема памяти IBM PC и операционная система Windows оптимизированы именно под такое выделение памяти и мы получаем выигрыш в производительности. Во-вторых, внизу рукотворного стека находится неинициализированная область памяти, что делает ошибки переполнения неактуальными. Затираются лишь локальные переменные текущей функции, да и то лишь те, которые лежат ниже переполняющегося буфера. Адреса возврата хранятся в другом месте и на них эти переполнения не распространяются, если, конечно, натуральный стек расположен выше рукотворного. Основную трудность представляет засылка аргументов в рукотворный стек. Под MS-DOS мы могли выделить отдельный сегмент и использовать PUSH с префиксом «GS:», а под Windows приходится применять MOV [EBP+XXh], YYYY. При этом адресации типа «память – память» в x86-процессорах не было и нет. В практическом плане это означает, что нам придется использовать промежуточные регистры: MOV EAX, [YYYY]/MOV [EBP+XXh], EAX. Впрочем, это можно оптимизировать, если использовать команду STOSD, занимающую в «машинном представлении» всего один байт и копирующую содержимое EAX в ячейку, на которую указывает EDI, одновременно с увеличением последнего на размер двойного слова. Стаскивать аргументы с рукотворного стека можно командой LODSD. Окончательно расхулиганившись, можно создать целых 3 стека: один - стандартный, для хранения адресов возврата, другой — для аргументов и третий - для локальных переменных. Чтобы не расходовать регистры понапрасну, можно хранить указатели на вершины двух рукотворных стеков в оперативной памяти, загружая их то в регистр EBP, то в ESI/EDI, в зависимости от того, какой из них окажется удобнее в тот или иной момент. Падения производительности можно не опасаться. Большую часть своего времени указатели будут проводить в кэш-памяти, извлекаясь всего за 1-2 такта. Естественно, все сказанное выше, относится только к нашим собственным функциям, а API-функции операционной системы таких извращений не понимают и ожидают аргументов в стандартном стеке. Ну, что тут можно сказать… Персонально для API-функций аргументы можно передать и в стандартном стеке, предварительно убедившись, что при этих аргументах функция гарантированно не вызовет переполнения (что вовсе не факт, особенно при работе с функциями из библиотеки mshtml.dll). К тому же в 64-битной редакции Windows аргументы API-функциями в большинстве случаев передаются не через стек, а через регистры, поэтому описанная методика к ним вполне применима. А вот как защитить от переполнения функции обычных библиотек? Самое простое решение — вызвать функции не по CALL, а по JMP, разместив адрес возврата на вершине страницы памяти, доступной только на чтение. Ниже ее будут только аргументы, также доступные только на чтение, а вот локальные переменные, создаваемые функцией, будут доступны и на чтение, и на запись. Естественно, этот трюк будет работать только с теми функциями, которые не изменяют своих аргументов (а многие из них изменяют их только так), но по-другому просто не получается! |