![](https://habrastorage.org/getpro/habr/upload_files/547/2c9/ea4/5472c9ea4ba70e06c6c01c53eb4ce6a9.png)
Стековая память — это раздел памяти, используемый функциями c целью хранения таких данных, как локальные переменные и параметры, которые будут использоваться вредоносным ПО для осуществления своей злонамеренной деятельности на взломанном устройстве.
Эта статья является третьей в серии из четырех частей, посвященных x64dbg:
Часть 3. Обзор Stack Memory (мы здесь)
Часть 4. Анализ вредоносного ПО с помощью x64dbg
Что такое стековая память?
Стековую память часто называют LIFO (last in, first out; «последним пришёл — первым вышел»). Представьте ее как стопку строительных кирпичей, уложенных друг на друга: вы не можете взять кирпич из середины, так как стопка упадет, поэтому сначала нужно брать самый верхний кирпич. Так работает стек.
В предыдущей статье мы объяснили, что такое регистры в x64dbg, а также разобрали некоторые основные ассемблерные инструкции. Эта информация необходима для понимания работы стека. Когда в стек добавляются новые данные, вредоносная программа использует команду PUSH. Чтобы удалить элемент из стека, вирус использует команду POP. Данные также могут быть выведены из стека в регистр.
Регистр "ESP" используется для обозначения следующего элемента в стеке и называется "указателем стека".
EBP (он же «указатель кадра») служит неизменной точкой отсчета для данных в стеке. Этот регистр позволяет программе определить, насколько далеко от этой точки находится элемент стека. Так, если переменная находится на расстоянии двух «строительных блоков», то это [EBP+8], так как каждый «блок» в стеке равен 4 байтам.
Каждая функция в программе будет генерировать собственный стековый кадр для ссылки на свои переменные и параметры с использованием этого метода.
Архитектура стековой памяти
Следующая схема поможет проиллюстрировать, как стек формируется подобно набору строительных блоков:
![](https://habrastorage.org/getpro/habr/upload_files/bfb/8b6/fc1/bfb8b6fc13cd3edef99718273464b609.png)
Низкие адреса памяти расположены вверху, а высокие адреса — внизу.
Каждая функция генеририрует собственный стековый кадр, поэтому стековый кадр в приведенном выше примере может накладываться на другой кадр, который используется для другой функции.
EBP, как упоминалось ранее, хранится в качестве неизменной точки отсчета в стеке — это делается путем перемещения значения ESP (указателя стека) в EBP. Мы делаем это, поскольку ESP будет меняться, так как он всегда указывает на вершину стека. Хранение его в EBP дает нам неизменную точку отсчета в стеке, и теперь функция может ссылаться на свои переменные и параметры в стеке из этого места.
В этом примере переданные в функцию параметры хранятся в [EBP]+8, [EBP]+12 и [EBP]+16. Поэтому, когда мы видим [EBP]+8, это расстояние в стеке от EBP.
Переменные будут сохраняться после начала выполнения функции, поэтому они будут располагаться выше по стеку, но в более низком адресном пространстве, поэтому в данном примере они отображаются как [EBP]-4.
Пример стековой памяти
Чтобы проиллюстрировать информацию выше, приведем пример простой программы на языке C, которая вызывает функцию addFunc, складывает два числа (1+4) и выводит результат на экран.
#include "stdio.h"
int addFunc (int a, int b);
int main (void) {
int x = addFunc(1,4);
printf("%d\n", x);
return 0;
}
int addFunc(int a, int b) {
int c = a + b;
return c;
Если сосредоточиться на коде функции addFunc, то в качестве аргументов передаются два параметра (a и b) и локальная переменная c, в которой хранится результат. После компиляции программы ее можно загрузить в x64dbg. Ниже показано, как будет выглядеть ассемблерный код для этой программы:
push ebp
mov ebp,esp
sub esp,10
mov edx,dword ptr ss:[ebp+8]
mov eax,dword ptr ss:[ebp+C]
add eax,edx
mov dword ptr ss:[ebp-4],eax
mov eax,dword ptr ss:[ebp-4]
leave
ret
Первые три строки — это так называемый пролог функции, именно здесь в стеке создается пространство для функции.
![](https://habrastorage.org/getpro/habr/upload_files/b32/b07/295/b32b072959359674b261b71587fef7aa.png)
push ebp сохраняет ESP (предыдущий указатель стекового кадра), чтобы к нему можно было вернуться в конце функции. Стековый кадр используется для хранения локальных переменных, и каждая функция будет иметь собственный стековый кадр в памяти.
mov ebp, esp перемещает текущую позицию стека в EBP, которая является основанием стека. Теперь у нас есть точка отсчета, которая позволяет ссылаться на наши локальные переменные, хранящиеся в стеке. Значение EBP больше никогда не меняется.
sub esp, 10 увеличивает стек на 16 байт (10 в шестнадцатеричном формате), чтобы выделить место в стеке для любых переменных, на которые нам нужно ссылаться.
Ниже показано, как будет выглядеть стек для этой программы. Каждый используемый фрагмент данных располагается друг над другом на участке памяти в соответствии с приведенной ранее схемой.
EBP-10
EBP-C
EBP-8
EBP-4 (int c)
EBP = Помещается в стек в начале функции. Это начало нашего стекового кадра.
EBP+4 = Адрес возврата к предыдущей функции
EBP+8 = Параметр 1 (int a)
EBP+C = Параметр 2 (int b)
Данный пример иллюстрирует, что в этом стеке выделено место для четырех локальных переменных, однако у нас есть только одна переменная — int c.
mov edx,dword ptr ss:[ebp+8]: здесь мы перемещаем int a (со значением 1) в регистр EDX.
Важной частью здесь является [ebp+8]. Квадратные скобки означают, что вы напрямую обращаетесь к памяти в этом месте. Это ссылка на место в памяти, которое находится на 8 байт выше в стеке, чем то, что находится в EBP.
Ранее мы упоминали, что параметры, перемещаемые в функцию, всегда находятся в более высоком адресном пространстве, которое расположено ниже по стеку. Наши параметры int a и int b были переданы в функцию до создания стекового кадра, поэтому они находятся в ebp+8 и ebp+c.
mov eax,dword ptr ss:[ebp+C]: то же самое, что и выше, но теперь мы ссылаемся на ebp+C, то есть int b (значение 4), и перемещаем его в регистр EAX.
Команда add eax, edx добавляет и сохраняет результат в EAX.
mov dword ptr ss:[ebp-4],eax: здесь мы перемещаем результат, хранящийся в EAX, в локальную переменную int c.
Локальная переменная «c» определяется внутри функции, поэтому она находится в более низком адресе памяти, чем верхняя часть стека. Таким образом, поскольку она находится внутри стекового кадра и имеет размер 4 байта, мы можем просто использовать часть пространства, ранее выделенного для переменных, вычитая 10 из esp, и в этом случае воспользоваться EBP-4.
mov eax,dword ptr ss:[ebp-4]: большинство функций возвращают значение, хранящееся в EAX, поэтому если выше возвращаемое значение находилось в EAX и мы переместили его в переменную «c», то здесь оно просто помещается обратно в EAX, готовое к возврату.
Leave: это маска для операции, которая перемещает EBP обратно в ESP и выводит его из стека, т. е. подготавливает стековый кадр функции, которая вызвала эту функцию.
Команда ret перенаправляет к обратному адресу для возвращения к вызываемой функции, у которой есть хорошо сохранившийся стековый кадр, потому что мы запомнили его в начале этой функции.
Практический пример: стековая память и x64dbg
В предыдущей статье было показано, как распаковать вредоносные программы с помощью x64dbg. Теперь мы рассмотрим некоторые функции, используемые вредоносным ПО, и то, как используется стек.
Сначала откройте распакованную вредоносную программу в x64dbg; в данном примере программа называется просто 267_unpacked.bin.
Перейдите к точке входа вредоносной программы, выбрав Debug («Отладка»), а затем Run («Выполнить»).
![](https://habrastorage.org/getpro/habr/upload_files/063/849/d7b/063849d7b4b920179d8641a9be05bd1f.png)
Сейчас мы находимся в точке входа вредоносной программы, и я выделил два окна, которые содержат информацию о стековой памяти:
![](https://habrastorage.org/getpro/habr/upload_files/ee0/c96/280/ee0c96280749797d23a96a5fc3b3ce60.png)
В первом окне показаны параметры, которые были перенесены в стек. Мы знаем, что это параметры, а не переменные, так как это esp+, а не esp-, как объяснялось ранее.
![](https://habrastorage.org/getpro/habr/upload_files/0f5/107/2e0/0f51072e0e72488b25c35798b9fb3f2a.png)
Второе окно — это собственно стековая память:
![](https://habrastorage.org/getpro/habr/upload_files/361/43d/df7/36143ddf7240c6bb75f12de2db0f0a50.png)
Первый столбец — это список адресов в стековой памяти. Как упоминалось ранее, высокие адреса находятся внизу, а низкие — вверху.
Второй столбец содержит данные, которые были помещены в стек, а синие скобки представляют отдельные стековые кадры. Помните, что каждая функция будет иметь собственный стековый кадр для хранения своих параметров.
В третьем столбце содержится информация о том, что он автоматически заполняется утилитой x64dbg. В этом примере мы видим адреса, куда x64dbg вернется после выполнения функции.
На изображении ниже первой командой, на которую указывает EIP, является push ebp; текущее значение в EBP, которое выделено на изображении ниже, — 0038FDE8.
![](https://habrastorage.org/getpro/habr/upload_files/d0d/61f/a57/d0d61fa57eab1c6add79fcf2ae8e490e.png)
Посмотрев на окно стека, мы выделили этот адрес, который является текущим базовым указателем стекового кадра.
При нажатии кнопки Step over («Обойти») EBP помещается в стек, и после завершения этой функции вредоносная программа может вернуться к этому адресу.
Теперь нам нужно переместить наш текущий указатель стека в ESP — это адрес 0038FDDC, выделенный ниже.
![](https://habrastorage.org/getpro/habr/upload_files/cfc/e0e/ab3/cfce0eab384affaff93f37e243aa5a95.png)
Выполнение этой команды перемещает ESP в регистр EBP, который выделен ниже.
![](https://habrastorage.org/getpro/habr/upload_files/60d/077/ec6/60d077ec61a77959163b511ab6bcc212.png)
Затем вредоносная программа освобождает место в стеке путем вычитания 420 из ESP. Она использует вычитание для создания области в нижнем адресном пространстве, которое находится выше по стеку (на следующем изображении показано нижнее адресное пространство над текущим стековым кадром).
![](https://habrastorage.org/getpro/habr/upload_files/599/df3/a07/599df3a07fcfbb0364ae841646d6cef6.png)
Затем выполнение команды sub esp, 420 обновляет стек.
![](https://habrastorage.org/getpro/habr/upload_files/8c3/c74/8d5/8c3c748d54d1290042f24c44c137233d.png)
Обратите внимание, что теперь мы находимся в более низком адресном пространстве, которое находится выше по стеку, а ESP обновлен для отображения нового местоположения на вершине стека.
Это общая схема, которую вы увидите при запуске функций вредоносных программ и с которой вам предстоит познакомиться.
Далее идут три инструкции push, которые перемещают значения трех регистров в стек. Как и ожидалось, пошаговое выполнение данных инструкций обновляет стек и окно параметров:
![](https://habrastorage.org/getpro/habr/upload_files/c35/4a3/cd2/c354a3cd261835b1fec688a8c82eb930.png)
На следующем этапе рассмотрим одну из функций, написанных хакером, и разберемся, что выполняет функция и как задействуется стек.
На изображении ниже мышь наведена на функцию 267_unpacked.101AEC9, при выполнении которой в x64dbg появляется всплывающее окно с предварительным просмотром данной функции. Это позволяет пользователю увидеть часть ассемблерного кода вызываемой функции. В этом всплывающем окне мы видим, что большое количество строк перемещается в переменные, и мы знаем, что это переменные, благодаря синтаксису ebp-. Такие строки представляют собой обфусцированные API вызовов Windows, которые будут использоваться вредоносным ПО для выполнения различных действий, таких как создание процессов и файлов на диске.
![](https://habrastorage.org/getpro/habr/upload_files/dcc/343/e81/dcc343e81f58274feff0b35d46bdb321.png)
Перейдя к этой функции, мы можем более детально рассмотреть, что происходит, а также увидеть, как стек используется в x64dbg.
В правом нижнем углу выделен созданный стековый кадр, и снова, как и предполагалось, у нас есть пролог функции.
![](https://habrastorage.org/getpro/habr/upload_files/4c3/0cd/981/4c30cd9815950ad335be24d254664e20.png)
Вход в эту функцию теперь обновляет ESP, который является адресом 0038F9AC в стековой памяти, содержащей адрес возврата к функции main, а также создает пространство в стеке путем вычитания 630 из ESP. Инструкции, начинающиеся с mov, перемещают имена хешированных функций в свои собственные переменные.
![](https://habrastorage.org/getpro/habr/upload_files/674/a22/650/674a22650ea71c512ae0db712bdbbc96.png)
Прокручивая вниз ассемблерный код, мы доходим до конца функции и видим несколько вызовов функций, которые используются для деобфускации хешей, только что перенесенных в переменные.
Выделенные команды — это так называемый "эпилог функции", который очищает стек после завершения функции. Мы переходим к этим инструкциям, выбрав интересующую нас инструкцию, "add esp, C", а затем выбрав "Debug" на панели инструментов и "Run until selection".
![](https://habrastorage.org/getpro/habr/upload_files/eba/2a9/015/eba2a90155a2e523ac6b35f7f0317bc8.png)
Это обновляет EIP согласно инструкции, которую мы выделили, а также показывает стек перед очисткой.
![](https://habrastorage.org/getpro/habr/upload_files/c09/f2d/8ea/c09f2d8ea269f553890a5125f72a5ee4.png)
В прологе функции для создания пространства в стеке вредоносная программа должна была произвести вычитание из ESP, чтобы иметь возможность выделить место в стеке, которое находилось в нижнем адресном пространстве. Теперь нам нужно удалить выделенное пространство, поэтому, выполнив команду add esp, C, мы добавляем шестнадцатеричное значение «C» в стек, чтобы переместиться вниз в более высокое адресное пространство.
На изображении ниже показан обновленный стек после выполнения команды add esp, C.
![](https://habrastorage.org/getpro/habr/upload_files/9f9/6f3/530/9f96f3530fddc9f7a1114300b14b6192.png)
Следующая команда — mov esp, ebp, которая переместит значение в EBP в ESP. Наш текущий EBP — 0042F3EC. Прокручивая вниз данные в окне стека, мы видим, что этот адрес содержит наш старый ESP, который является указателем стека.
Выполнение этой команды очищает стек.
Команда pop ebp открывает адрес 00E0CDA8, который хранился в верхней части стека, и перемещает его в EBP.
![](https://habrastorage.org/getpro/habr/upload_files/41f/f25/d7c/41ff25d7c884e8862c7fafd153c4a27f.png)
Это означает, что при выполнении следующей инструкции ret мы вернемся к адресу 00E0CDA8.
![](https://habrastorage.org/getpro/habr/upload_files/010/8aa/f03/0108aaf0396eb52065a4536120e137e4.png)
На изображении выше показано, что мы вернулись к «основной» функции вредоносного кода и находимся по адресу 00E0CDA8, сразу после функции, которую мы только что проанализировали в x64dbg.
Теперь вы готовы приступить к реверс-инжинирингу вредоносного ПО с помощью x64dbg! В следующей статье мы разберем, как применить знания, полученные в предыдущих статьях блога, чтобы выполнить обратную разработку на практике.