Стековая память — это раздел памяти, используемый функциями c целью хранения таких данных, как локальные переменные и параметры, которые будут использоваться вредоносным ПО для осуществления своей злонамеренной деятельности на взломанном устройстве.
Эта статья является третьей в серии из четырех частей, посвященных x64dbg:
Часть 3. Обзор Stack Memory (мы здесь)
Часть 4. Анализ вредоносного ПО с помощью x64dbg
Что такое стековая память?
Стековую память часто называют LIFO (last in, first out; «последним пришёл — первым вышел»). Представьте ее как стопку строительных кирпичей, уложенных друг на друга: вы не можете взять кирпич из середины, так как стопка упадет, поэтому сначала нужно брать самый верхний кирпич. Так работает стек.
В предыдущей статье мы объяснили, что такое регистры в x64dbg, а также разобрали некоторые основные ассемблерные инструкции. Эта информация необходима для понимания работы стека. Когда в стек добавляются новые данные, вредоносная программа использует команду PUSH. Чтобы удалить элемент из стека, вирус использует команду POP. Данные также могут быть выведены из стека в регистр.
Регистр "ESP" используется для обозначения следующего элемента в стеке и называется "указателем стека".
EBP (он же «указатель кадра») служит неизменной точкой отсчета для данных в стеке. Этот регистр позволяет программе определить, насколько далеко от этой точки находится элемент стека. Так, если переменная находится на расстоянии двух «строительных блоков», то это [EBP+8], так как каждый «блок» в стеке равен 4 байтам.
Каждая функция в программе будет генерировать собственный стековый кадр для ссылки на свои переменные и параметры с использованием этого метода.
Архитектура стековой памяти
Следующая схема поможет проиллюстрировать, как стек формируется подобно набору строительных блоков:
Низкие адреса памяти расположены вверху, а высокие адреса — внизу.
Каждая функция генеририрует собственный стековый кадр, поэтому стековый кадр в приведенном выше примере может накладываться на другой кадр, который используется для другой функции.
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
Первые три строки — это так называемый пролог функции, именно здесь в стеке создается пространство для функции.
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 («Выполнить»).
Сейчас мы находимся в точке входа вредоносной программы, и я выделил два окна, которые содержат информацию о стековой памяти:
В первом окне показаны параметры, которые были перенесены в стек. Мы знаем, что это параметры, а не переменные, так как это esp+, а не esp-, как объяснялось ранее.
Второе окно — это собственно стековая память:
Первый столбец — это список адресов в стековой памяти. Как упоминалось ранее, высокие адреса находятся внизу, а низкие — вверху.
Второй столбец содержит данные, которые были помещены в стек, а синие скобки представляют отдельные стековые кадры. Помните, что каждая функция будет иметь собственный стековый кадр для хранения своих параметров.
В третьем столбце содержится информация о том, что он автоматически заполняется утилитой x64dbg. В этом примере мы видим адреса, куда x64dbg вернется после выполнения функции.
На изображении ниже первой командой, на которую указывает EIP, является push ebp; текущее значение в EBP, которое выделено на изображении ниже, — 0038FDE8.
Посмотрев на окно стека, мы выделили этот адрес, который является текущим базовым указателем стекового кадра.
При нажатии кнопки Step over («Обойти») EBP помещается в стек, и после завершения этой функции вредоносная программа может вернуться к этому адресу.
Теперь нам нужно переместить наш текущий указатель стека в ESP — это адрес 0038FDDC, выделенный ниже.
Выполнение этой команды перемещает ESP в регистр EBP, который выделен ниже.
Затем вредоносная программа освобождает место в стеке путем вычитания 420 из ESP. Она использует вычитание для создания области в нижнем адресном пространстве, которое находится выше по стеку (на следующем изображении показано нижнее адресное пространство над текущим стековым кадром).
Затем выполнение команды sub esp, 420 обновляет стек.
Обратите внимание, что теперь мы находимся в более низком адресном пространстве, которое находится выше по стеку, а ESP обновлен для отображения нового местоположения на вершине стека.
Это общая схема, которую вы увидите при запуске функций вредоносных программ и с которой вам предстоит познакомиться.
Далее идут три инструкции push, которые перемещают значения трех регистров в стек. Как и ожидалось, пошаговое выполнение данных инструкций обновляет стек и окно параметров:
На следующем этапе рассмотрим одну из функций, написанных хакером, и разберемся, что выполняет функция и как задействуется стек.
На изображении ниже мышь наведена на функцию 267_unpacked.101AEC9, при выполнении которой в x64dbg появляется всплывающее окно с предварительным просмотром данной функции. Это позволяет пользователю увидеть часть ассемблерного кода вызываемой функции. В этом всплывающем окне мы видим, что большое количество строк перемещается в переменные, и мы знаем, что это переменные, благодаря синтаксису ebp-. Такие строки представляют собой обфусцированные API вызовов Windows, которые будут использоваться вредоносным ПО для выполнения различных действий, таких как создание процессов и файлов на диске.
Перейдя к этой функции, мы можем более детально рассмотреть, что происходит, а также увидеть, как стек используется в x64dbg.
В правом нижнем углу выделен созданный стековый кадр, и снова, как и предполагалось, у нас есть пролог функции.
Вход в эту функцию теперь обновляет ESP, который является адресом 0038F9AC в стековой памяти, содержащей адрес возврата к функции main, а также создает пространство в стеке путем вычитания 630 из ESP. Инструкции, начинающиеся с mov, перемещают имена хешированных функций в свои собственные переменные.
Прокручивая вниз ассемблерный код, мы доходим до конца функции и видим несколько вызовов функций, которые используются для деобфускации хешей, только что перенесенных в переменные.
Выделенные команды — это так называемый "эпилог функции", который очищает стек после завершения функции. Мы переходим к этим инструкциям, выбрав интересующую нас инструкцию, "add esp, C", а затем выбрав "Debug" на панели инструментов и "Run until selection".
Это обновляет EIP согласно инструкции, которую мы выделили, а также показывает стек перед очисткой.
В прологе функции для создания пространства в стеке вредоносная программа должна была произвести вычитание из ESP, чтобы иметь возможность выделить место в стеке, которое находилось в нижнем адресном пространстве. Теперь нам нужно удалить выделенное пространство, поэтому, выполнив команду add esp, C, мы добавляем шестнадцатеричное значение «C» в стек, чтобы переместиться вниз в более высокое адресное пространство.
На изображении ниже показан обновленный стек после выполнения команды add esp, C.
Следующая команда — mov esp, ebp, которая переместит значение в EBP в ESP. Наш текущий EBP — 0042F3EC. Прокручивая вниз данные в окне стека, мы видим, что этот адрес содержит наш старый ESP, который является указателем стека.
Выполнение этой команды очищает стек.
Команда pop ebp открывает адрес 00E0CDA8, который хранился в верхней части стека, и перемещает его в EBP.
Это означает, что при выполнении следующей инструкции ret мы вернемся к адресу 00E0CDA8.
На изображении выше показано, что мы вернулись к «основной» функции вредоносного кода и находимся по адресу 00E0CDA8, сразу после функции, которую мы только что проанализировали в x64dbg.
Теперь вы готовы приступить к реверс-инжинирингу вредоносного ПО с помощью x64dbg! В следующей статье мы разберем, как применить знания, полученные в предыдущих статьях блога, чтобы выполнить обратную разработку на практике.