Различия

Здесь показаны различия между двумя версиями данной страницы.

Ссылка на это сравнение

articles:asm-stack-hck [2017/09/05 02:55] (текущий)
Строка 1: Строка 1:
 +====== asm-stack-hck ======
 +<​sub>​{{asm-stack-hck.odt|Original file}}</​sub>​
 +
 +====== ассемблерные извращения — натягиваем стек\\ (черновик) ======
 +
 +крис касперски ака мыщъх, no-email
 +
 +**ассемблер представляет практически неограниченную свободу для самовыражения и всевозможных извращений,​ что выгодно отличает его от языков высокого уровня. вот мы и воспользуемся этой возможностью,​ извратившись не по детски и сотворив со стеком то, о чем приплюснутый си только мечтает.**
 +
 +===== турбопередача стековых аргументов =====
 +
 +Передачу аргументов через стек можно существенно ускорить,​ особенно если аргументы представляют собой константу,​ известную еще на стадии трансляции. Классический способ передачи выглядит так:
 +
 +00000000: 6869060000push000000669
 +
 +00000005: 6899090000push000000999
 +
 +0000000A: 6896060000push000000696
 +
 +0000000F: E852060000call000000666
 +
 +Листинг 1 классический способ передачи стековых аргументов
 +
 +Довольно расточительное (в плане процессорных тактов) решение,​ особенно если функция вызывается многократно. При этом операнды команды PUSH перегоняются из секции .text (находящейся в кодовой кэш-памяти первого уровня) в область стека, находящейся в кэш-памяти данных. Ну и на хрена гонять их туда и обратно,​ когда аргументы можно использовать непосредственно по месту хранения.
 +
 +Усовершенствованный пример выглядит так:
 +
 +.code
 +
 +MOV EBP, ESP
 +
 +MOV ESP, offset func_arg + 4
 +
 +CALL my_func
 +
 +MOV ESP, EBP
 +
 +
 +
 +.data
 +
 +func_argDD 00h, 696h, 999h, 669h
 +
 +Листинг 2 оптимизированный способ передачи аргументов
 +
 +И хотя размер кода после оптимизации не только не сократился,​ но даже увеличился (14h байт до оптимизации и 1Eh) зато мы сохранили немного стековой памяти и сократили время выполнения. Причем,​ чем больше аргументов передается функции,​ тем в более выигрышном положении оказывается оптимизированный вариант,​ поскольку неоптимизированный вынужден тратить на каждый аргумент один дополнительный байт!
 +
 +00000000: 8BECmovebp, esp
 +
 +00000002: BC66000000movesp,​ 000000013
 +
 +00000007: E80E000000call000000666
 +
 +0000000C: 8BE5movesp, ebp
 +
 +
 +
 +0000000E: 00 00 00 00 96 06 00 00 │ 99 09 00 00 69 06 00 00
 +
 +0000001E: ​
 +
 +Листинг 3 дизассемблерный листинг оптимизированного варианта передачи аргументов
 +
 +Несколько замечаний по поводу. Операционные системы семейства Windows NT (к которым принадлежит Windows 2000,​ Windows XP,​ Windows Vista,​ Windows Server 2003 и Windows Server Longhorn) гарантируют целостность содержимого стека выше его вершины (т. е. для адресов меньших,​ чем ESP), поэтому свободно переносят такие извращения безо всякого ущерба для работоспособности программы. Операционные системы семейства Windows 9x ведут себя иначе, бесцеремонно используя все, что находится выше ESP в целях "​производственной необходимости",​ что ведет к искажению секции данных и последующему краху программы,​ поэтому,​ все, сказанное здесь, распространяется только на NT.
 +
 +{{asm-stack-hck_Image_0.png?​552}}
 +
 +Рисунок 1 передача стековых аргументов напрямую без их фактической засылки в стек
 +
 +Замечание номер два. Перед аргументами необходимо оставить двойное слово (а в 64-битном режиме — четвертное) для сохранения адреса возврата,​ при этом, секция данных,​ где находится это слово должна быть доступна на запись. Если же функция вызывается из одного единственного места и адрес возврата известен заранее,​ ничего не мешает положить его рядом с аргументами,​ но тогда функцию придется вызывать командой jump, а не call, что еще больше увеличивает производительность:​
 +
 +.code
 +
 +MOV EBP, ESP
 +
 +MOV ESP, offset func_arg + 4
 +
 +JMP my_func
 +
 +here:
 +
 +MOV ESP, EBP
 +
 +
 +
 +.data
 +
 +func_argDD offset here, 696h, 999h, 669h
 +
 +Листинг 4 вызов функции с предопределенным адресом возврата командой JMP
 +
 +Кстати говоря,​ ни адрес возврата,​ ни аргументы функции вовсе не обязаны быть константой,​ известной на стадии компиляции и они могут свободно модифицироваться в любой момент командами MOV/STOS. Аналогичным образом,​ если аргументы хранятся в локальных переменных,​ то засылать их в стек необязательно! Достаточно лишь скорректировать регистр ESP таким образом,​ чтобы переменные-аргументы оказались на вершине (естественно,​ порядок размещения аргументов в памяти должен совпадать с порядком передачи аргументов,​ но на ассемблере,​ в отличии от языков высокого уровня мы можем самостоятельно выбирать нужную схему размещения переменных,​ так что это не проблема).
 +
 +Еще одна тонкость — "​оптимизированный"​ вариант обладает всеми формальными атрибутами "​передачи по значению",​ но де-факто,​ аргументы передаются по ссылке. То есть нет! Совсем наоборот! Аргументы передаются по _значению_ но это значение после выхода из функции сохраняет свое состояние,​ то есть ведет себя так, как будто бы ото было передано по ссылке. Иногда это экономит такты процессора и сокращает потребности в памяти,​ но иногда ведет к трудноуловимым ошибкам,​ лишний раз подтверждая тезис, что совершенства в мире не бывает.
 +
 +И последнее:​ при всех этих играх со стеком следует помнить,​ что целый ряд API-функций требует,​ чтобы указатель стека был выровнен на границу 4х байт. Нарушение этого правила ведет к непредсказуемым последствиям.
 +
 +===== повторное использование кадра стека =====
 +
 +При входе внутрь функции,​ большое количество локальных переменных инициализируется константами или значениями,​ инвариантными по отношению к самой функции (т. е. другими переменными,​ как правило,​ глобальными). Причем инициализация обычно осуществляется командой MOV, а для обслуживания строковых переменных приходится прибегать к REP MOVSB. Все это медленно,​ громоздко и непроизводительно.
 +
 +А почему бы не подготовить кадр стека еще на стадии трансляции?​! В грубом приближении это будет выглядеть так:
 +
 +.code
 +
 +MOV EBP, ESP
 +
 +MOV ESP, offset func_arg
 +
 +JMP my_func
 +
 +MOV ESP, EBP
 +
 +
 +
 +my_func:
 +
 +MOV EBP,ESP
 +
 +SUB ESP, offset func_locals - offset return_address
 +
 +
 +
 +
 +
 +
 +
 +MOV ESP,EBP
 +
 +RETN
 +
 +.data
 +
 +func_locals:​
 +
 +var_1DB66h
 +
 +var_2DDoffset globalFlag
 +
 +var_sDB"​hello",​0
 +
 +var_xDD0
 +
 +var_yDD0
 +
 +return_address:​
 +
 +DD 00h
 +
 +func_args:
 +
 +DD 696h, 999h, 669h
 +
 +Листинг 5 вызов функции с заранее подготовленными аргументами и локальными переменными
 +
 +В некоторых случаях достигается просто колоссальное ускорение,​ однако… тут есть один подводный камень — при повторном вызове функции все "​инициализированные"​ переменные сохраняет свои _текущие_ значения и наступит полный облом. Фактически,​ мы добились того, что превратили локальные стековые переменные в статические! Бесспорно,​ _иногда_ это очень хорошо,​ но в 90% случав нам нужно совсем другое. Вот и устроим себе это другое с помощью REP MOVS! Подготавливаем инициализированные локальные переменные на стадии создания ассемблерной программы,​ а затем копируем их в кадр функции при его открытии. Это _намного_ быстрее,​ чем инициализировать каждую локальную переменную по отдельности командой MOV.
 +
 +К тому же, кадры некоторых функций достаточно схожи между собой, что позволяет объединить несколько кадров в один! Достаточно сказать,​ что каждая функция нуждается в переменных,​ инициализированных нулями. Чтобы не делать много раз один и тот же MOV [EBP+XXh],​0 лучше (и быстрее) выполнить REP STOS!
 +
 +Вот в чем истинная сила ассемблера! Вот извращения,​ недоступные языкам высокого уровня,​ но… самые зверские издевательства следуют впереди!!!
 +
 +===== защита адреса возврата от переполнения =====
 +
 +Проблема переполняющихся буферов породило огромное количество червей,​ открыв безграничный простор для хакерских атак, но, несмотря на все ухищрения,​ предпринятые как со стороны производителей компиляторов,​ так и со стороны разработчиков операционных систем,​ проблем остается нерешенной и посей день.
 +
 +Ассемблер предоставляет по меньшей мере два надежных механизма,​ до которых еще не компиляторы "​додумались"​. Первое и самое простое — это _два_ стека: один для хранения адресов возврата,​ другой:​ для передачи аргументов и локальных переменных. Кстати говоря,​ существуют процессорные архитектуры,​ в которых этот механизм реализован изначально,​ но x86 семейство к ним увы не относятся,​ поэтому приходится брать в лапы напильник и точить. Или торчать?​ Неееет,​ торчать мы будем потом, когда забьем косяк, а пока лучше поточим.
 +
 +Собственно говоря,​ для организации двух раздельных стеков нам требуется всего лишь один дополнительный регистр (который можно выделить из пула регистров общего назначения). Пусть это будет регистр EBP, указывающий на стек с локальными переменными. Собственно говоря,​ неправильно будет называть его стеком,​ поскольку в операционных системах семейства Windows стек представляет собой _особый_ регион памяти,​ подпираемый сверху сторожевой страницей page-guard. Мы же разместим свой стек в памяти,​ выделенной функцией VirtualAlloc или (если хочется оптимизации) в .BSS сеции PE-файла,​ выделение которой обходится очень дешевого (в плане машинного времени). Но это все детали реализации. Будем считать,​ что ESP указывает на нормальный стек, а EBP — на "​рукотворный"​. Как тогда будет происходить вызов функций и передача аргументов?​
 +
 +А вот так:
 +
 +; // подготовительные операции
 +
 +MOVEBP, [XXX]; XXX - указатель на "​рукотворный"​ стек
 +
 +MOV ESP, ESP; ;-)
 +
 +
 +
 +; // передача аргументов функции
 +
 +MOV [EBP+00h], arg_a
 +
 +MOV [EBP+04h], arg_b
 +
 +MOV [EBP+08h], arg_c
 +
 +// вызов самой функции
 +
 +CALLfunc
 +
 +
 +
 +// ================================================================================
 +
 +; // реализация самой функции
 +
 +func:
 +
 +ADDEBP, local_var_size;​ резервируем память под локальные переменные
 +
 +MOVECX, [EBP-local_var_size+04h];​ загрузка аргумента arg_b в регистр ECX
 +
 +MOV ESI, [EBP-local_var_size+08h];​ загрузкааргумента arg_c врегистр ESI
 +
 +MOVEDI, EBP; грузим в EDI указатель конец области лок. пер.
 +
 +SUBEDI, local_var_size;​ вычисляем указатель на локальный буфер
 +
 +; (в данном случае он расположен по смещению 00h
 +
 +;  относительно фрейма)
 +
 +REPMOVSB; копируем arg_b байт из arg_c в лок. буффер
 +
 +; // делаем еще что-то полезное
 +
 +RET; выходим из функции
 +
 +Листинг 6 передача и использование аргументов при раздельных стеках
 +
 +"​Рукотворный"​ стек с локальными переменными и аргументами растер сверху вниз (т.е. в направлении противоположном росту обычного стека) и это неспроста. Во-первых,​ подсистема памяти IBMPC и операционная система Windows оптимизированы именно под такое выделение памяти и мы получаем выигрыш в производительности. Во-вторых,​ внизу рукотворного стека находится неинициализированная область памяти,​ что делает ошибки переполнения неактуальными. Затираются лишь локальные переменные текущей функции,​ да и то лишь те, которые лежат ниже переполняющегося буфера.
 +
 +Адреса возврата хранятся вообще в другом месте и на них эти переполнения вообще не распространяются (если, конечно,​ "​натуральный"​ стек расположен выше рукотворного,​ т.е. лежит в более младших адресах).
 +
 +Основную трудность,​ конечно,​ представляет засылка аргументов в рукотворный стек. Это под MS-DOS мы могли выделить отдельный сегмент и использовать PUSH с префиксом "​GS:",​ а под Windows приходится использовать MOV [EBP+XXh],​ YYYY и это при том, что адресации типа "​память - память"​ в x86 процессорах не было и нет. В практическом плане это означает,​ что нам придется использовать промежуточные регистры:​ MOV EAX, [YYYY]/​MOV [EBP+XXh],​ EAX. Впрочем,​ это можно оптимизировать,​ если использовать команду STOSD, занимающую в машинном представлении всего один байт и копирующую содержимое EAX в ячейку на которую указывает EDI одновременно с увеличением последнего на размер двойного слова. Стаскивать аргументы с рукотворного стека можно командой LODSD.
 +
 +Окончательно расхулиганившись,​ можно создать целых три стека — один, "​стандартный"​ для хранения адресов возврата,​ другой — для аргументов и третий для локальных переменных. Чтобы не расходовать регистры понапрасну,​ можно хранить указатели на вершины двух "​рукотворных"​ стеков в оперативной памяти,​ загружая их то в регистр EBP, то в ESI/EDI в зависимости от того, какой из них окажется удобнее в данный конкретный момент. Падения производительности можно не опасаться. Большую часть своего времени указатели будут проводить в кэш-памяти,​ извлекаясь всего за один-два такта.
 +
 +Естественно,​ все, сказанное выше, относится _только_ к нашим собственным функциям,​ а API-функции операционной системы таких извращений не понимают и ожидают аргументов в "​стандартном"​ стеке. Ну… что тут можно сказать… "​Персонально"​ для API-функций аргументы можно передать и в стандартном стеке, предварительно убедившись,​ что при данных аргументах функция гарантированно не вызовет переполнения (что вовсе не факт, особенно при работе с функциями из библиотеки mshtml.dll). К тому же, в 64-битной редакции Windows аргументы API-функциями в большинстве случаев передаются не через стек, а через регистры,​ поэтому описанная методика к ним вполне применима.
 +
 +А вот как защитить от переполнения функции обычных библиотек?​ Самое простое решение — вызвать функции не по CALL, а по JMP, разместив адрес возврата на вершине страницы памяти,​ доступной только на чтение. Ниже ее будут только аргументы (доступные так же только на чтение),​ а вот локальные переменные,​ создаваемые функцией будут доступны и на чтение и на запись. Естественно,​ этот трюк будет работать только с теми функциями,​ которые не изменяют своих аргументов (а многие из них изменяют их только так), но по другому просто не получается!
 +
 +===== локальные переменные в куче =====
 +
 +{{asm-stack-hck_Image_1.png?​553}}
 +
 +Рисунок 2 реакция soft-ice на исчерпание стека (windows, к слову сказать,​ на это не реагирует вообще!)
 +
 +__вызов функций,​ уже написанных на языках высокого уровня
 +
 +===== >>>​ врезка XORESP,ESP в NT =====
 +
 +{{asm-stack-hck_Image_2.png?​553}}
 +
 +Рисунок 3 даже такой простой отладчик как MicrosoftVisualStudioDebugger,​ запущенный под NT, уверенно продолжает трассировку и при нулевом значении регистра ESP
 +
 +