Ассемблерные извращения. Натягиваем стек по-хакерски — «Хакер»

Найти:

- Взлом
- Приватность
- Трюки
- Кодинг
- Админ
- Geek
- Обучение
- Магазин

- Windows
- Linux
- Android
- Железо
- Мессенджеры
- Утечки
- Коронавирус
- Все выпуски «Хакера»

### Содержание статьи

- Турбопередача стековых аргументов
- Повторное использование кадра стека
- Защита адреса возврата от переполнения

Ассемблер предоставляет практически неограниченную свободу для самовыражения и всевозможных извращений, что выгодно отличает его от языков высокого уровня. Вот мы и воспользуемся этой возможностью, извратившись не по‑детски и сотворив со стеком то, о чем приплюснутый Си только мечтает.

## Турбопередача стековых аргументов

Передачу аргументов через стек можно существенно ускорить, в случае если аргументы представляют собой константу, известную еще на стадии трансляции. Классический способ передачи выглядит так:

##### Классический способ передачи стековых аргументов

`
00000000: 6869060000 push 000000669
00000005: 6899090000 push 000000999
0000000A: 6896060000 push 000000696
0000000F: E852060000 call 000000666
`

Довольно расточительное (в плане процессорных тактов) решение, особенно если функция вызывается многократно. При этом операнды команды `PUSH` перегоняются из секции `.text` (находящейся в кодовой кэш‑памяти первого уровня) в область стека, находящуюся в кэш‑памяти данных. Ну и зачем гонять их туда и обратно, когда аргументы можно использовать непосредственно по месту хранения?

Усовершенствованный пример выглядит так:

`
.code
MOV EBP, ESP
MOV ESP, offset func_arg + 4
CALL my_func
MOV ESP, EBP
.data
func_arg DD 00h, 696h, 999h, 669h
`

И хотя размер кода после оптимизации не только не сократился, но даже увеличился (14h байт до оптимизации и 1Eh - после), мы сохранили немного стековой памяти и сократили время выполнения. Причем чем больше аргументов передается функции, тем в более выигрышном положении оказывается оптимизированный вариант, поскольку неоптимизированный вынужден тратить на каждый аргумент один дополнительный байт!

`
00000000: 8BEC mov ebp, esp
00000002: BC66000000 mov esp, 000000013
00000007: E80E000000 call 000000666
0000000C: 8BE5 mov esp, ebp
…
0000000E: 00 00 00 00 96 06 00 00 ? 99 09 00 00 69 06 00 00
0000001E:
`

Несколько замечаний по поводу. Первое. Операционные системы семейства Windows NT (к которым принадлежат Windows 2000, Windows XP, Windows Vista, Windows Server 2003 и Windows Server Longhorn) гарантируют целостность содержимого стека выше его вершины (для адресов меньших, чем ESP), поэтому переносят такие извращения безо всякого ущерба для работоспособности программы. Операционные системы семейства Windows 9x ведут себя иначе, бесцеремонно используя все, что находится выше ESP в целях «производственной необходимости», что ведет к искажению секции данных и последующему краху программы. Поэтому все, что было сказано здесь, распространяется только на NT.

Передача стековых аргументов напрямую, без их фактической засылки в стек

Замечание номер два. Перед аргументами необходимо оставить двойное слово (а в 64-битном режиме — четвертное) для сохранения адреса возврата. При этом секция данных, где находится это слово, должна быть доступна на запись. Если же функция вызывается из одного единственного места и адрес возврата известен заранее, ничего не мешает положить его рядом с аргументами. Но тогда функцию придется пускать командой jump, а не call, что еще больше увеличивает производительность:

##### Вызов функции с предопределенным адресом возврата командой JMP

`
.code
MOV EBP, ESP
MOV ESP, offset func_arg + 4
JMP my_func
here:
MOV ESP, EBP
.data
func_arg DD offset here, 696h, 999h, 669h
`

Кстати говоря, ни адрес возврата, ни аргументы функции вовсе не обязаны быть константами, известными на стадии компиляции, и они могут свободно модифицироваться в любой момент командами `MOV` и `STOS`. Также если аргументы хранятся в локальных переменных, то засылать их в стек не обязательно! Достаточно лишь скорректировать регистр ESP таким образом, чтобы переменные‑аргументы оказались на вершине. Естественно, порядок размещения аргументов в памяти должен совпадать с порядком передачи аргументов, но на ассемблере, в отличие от языков высокого уровня, мы можем самостоятельно выбирать нужную схему размещения переменных, так что это не проблема.

Еще одна тонкость: «оптимизированный» вариант обладает всеми формальными атрибутами «передачи по значению», но де‑факто аргументы передаются по ссылке. То есть совсем наоборот! Аргументы передаются по значению, но это значение после выхода из функции сохраняет свое состояние, ведет себя так, как будто бы оно было передано по ссылке. Иногда это экономит такты процессора и сокращает потребности в памяти, но иногда ведет к трудноуловимым ошибкам, лишний раз подтверждая тезис, что нет в мире совершенства.

И последнее: при всех этих играх со стеком следует помнить, что целый ряд API-функций требует, чтобы указатель стека был выровнен на границу четырех байтов. Нарушение этого правила ведет к непредсказуемым последствиям.

## Повторное использование кадра стека

При входе внутрь функции большое количество локальных переменных инициализируется константами или значениями, инвариантными по отношению к самой функции (то есть другими переменными, чаще всего глобальными). Причем инициализация обычно осуществляется командой `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_1 DB 66h
var_2 DD offset globalFlag
var_s DB "hello",0
var_x DD 0
var_y DD 0
return_address:
DD 00h
func_args:
DD 696h, 999h, 669h
`

В некоторых случаях достигается просто колоссальное ускорение, однако тут есть один подводный камень — при повторном вызове функции все «инициализированные» переменные сохраняют свои текущие значения и наступает полный облом. Фактически мы добились того, что превратили локальные стековые переменные в статические! Бесспорно, иногда это очень хорошо, но в 90% случаев нам нужно совсем другое. Вот и устроим себе это другое с помощью `REP MOVS`! Подготавливаем инициализированные локальные переменные на стадии создания ассемблерной программы, а затем копируем их в кадр функции при его открытии. Это намного быстрее, чем инициализировать каждую локальную переменную по отдельности командой `MOV`.

К тому же кадры некоторых функций достаточно схожи между собой, что позволяет объединить несколько кадров в один! Достаточно сказать, что каждая функция нуждается в переменных, инициализированных нулями. Чтобы не делать много раз один и тот же `MOV [EBP+XXh],0` лучше (и быстрее) выполнить `REP STOS`!

Вот в чем истинная сила ассемблера! Вот извращения, недоступные языкам высокого уровня, но… самые зверские издевательства еще впереди!!!

## Защита адреса возврата от переполнения

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

Ассемблер предоставляет по меньшей мере 2 надежных механизма, до которых компиляторы еще не «додумались». Первый и самый простой — это 2 стека: один для хранения адресов возврата, другой - для передачи аргументов и локальных переменных. Кстати говоря, существуют процессорные архитектуры, в которых этот механизм реализован изначально. Но x86-семейство к ним, увы, не относится, поэтому приходится брать в лапы напильник и точить.

Для организации двух раздельных стеков нам требуется всего лишь 1 дополнительный регистр (который можно выделить из пула регистров общего назначения). Пусть это будет регистр EBP, указывающий на стек с локальными переменными. Собственно говоря, неправильно называть его стеком, поскольку в операционных системах семейства Windows стек представляет собой особый регион памяти, подпираемый сверху сторожевой страницей page-guard. Мы же разместим свой стек в памяти, полученной функцией VirtualAlloc или, если хочется оптимизации, в .BSS-секции PE-файла, выделение которой обходится очень дешевого (в плане машинного времени). Но это все детали реализации. Будем считать, что ESP указывает на нормальный стек, а EBP — на рукотворный. Как тогда будет происходить вызов функций и передача аргументов?

А вот так:

`
; // подготовительные операции
MOV EBP, [XXX] ; XXX - указатель на рукотворный стек
MOV ESP, ESP ; ;-)
…
; // передача аргументов функции
MOV [EBP+00h], arg_a
MOV [EBP+04h], arg_b
MOV [EBP+08h], arg_c
// вызов самой функции
CALL func
…
// ================================================================================
; // реализация самой функции
func:
ADD EBP, local_var_size ; резервируем память под локальные переменные
MOV ECX, [EBP-local_var_size+04h] ; загрузка аргумента arg_b в регистр ECX
MOV ESI, [EBP-local_var_size+08h] ; загрузка аргумента arg_c в регистр ESI
MOV EDI, EBP ; грузим в EDI указатель на конец области локальных переменных
SUB EDI, local_var_size ; вычисляем указатель на локальный буфер
; (в данном случае он расположен по смещению 00h
; относительно фрейма)
REP MOVSB ; копируем arg_b байтов из arg_c в локальный буфер
; // делаем еще что-то полезное
RET ; выходим из функции
`

Рукотворный стек с локальными переменными и аргументами растет сверху вниз, то есть в направлении, противоположном росту обычного стека, и это неспроста. Во‑первых, подсистема памяти 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`, разместив адрес возврата на вершине страницы памяти, доступной только на чтение. Ниже ее будут только аргументы, также доступные только на чтение, а вот локальные переменные, создаваемые функцией, будут доступны и на чтение, и на запись. Естественно, этот трюк будет работать только с теми функциями, которые не изменяют своих аргументов (а многие из них изменяют их только так), но по‑другому просто не получается!

#### Оцени статью:

###### Что тебе понравилось больше всего?

Статья легкая и понятная Статья глубокая и подробная Интересуюсь этой темой Данные новые и актуальные Приятный стиль изложения

###### Что тебе не понравилось больше всего?

Статья поверхностная Статья слишком сложная Тема мне не интересна Данные устарели Неприятный стиль изложения
← Ранее LaCie Huby
Далее → Китайцы взломали Военно-морской колледж США
1 комментарий

#### Последние взломы

### Краденые миллиарды и плохой рэп. Как арестовали супругов, которые отмывали деньги Bitfinex и снимали тиктоки

### HTB EarlyAccess. Эксплуатируем веб-уязвимости и мисконфиги Docker

### HTB Horizontall. Захватываем веб-сервер через уязвимости в Strapi и Laravel

### Препарируем P-Code. Как реверсить старый софт на Visual Basic

#### Компьютерные трюки

### Терминальное счастье. Колонка главреда

### Анатомия терминала. Осваиваем текстуальную графику

### Змей-стражник. Сигнализация для велосипеда из старого смартфона на Android

### M1 для хакера. Тестируем Homebrew, виртуалки, Docker и Node на новой архитектуре Apple

#### Свежие новости

- 2 дня назад

  ### Для критической уязвимости в Magento появился эксплоит, а Adobe исправила второй аналогичный баг

- 2 дня назад

  ### Вредоносные письма могут вывести Cisco Email Security Appliance из строя

- 3 дня назад

  ### WordPress-плагин с 3 000 000 установок позволял всем желающим скачивать бэкапы сайтов

- 3 дня назад

  ### Француз случайно оставил без интернета целый город, пытаясь отключить интернет своим детям

- 3 дня назад

  ### Google представила «легкую» Chrome OS Flex, подходящую для старых ПК

Вопросы по материалам и подписке: support@glc.ru
Отдел рекламы и спецпроектов: yakovleva.a@glc.ru
Контент 18+
Сайт защищен Qrator —
самой забойной защитой от DDoS в мире
Подписка для физлиц
Подписка для юрлиц
Реклама на «Хакере»
Контакты
Posting....
wpDiscuz
FooBox
…