Традиционные эксплойты переполнения буфера did требуют выполнения кода в стеке, но ваша программа этого не делает. Ваш массив shellcode
не находится в стеке, и конструкция, которую вы использовали, чтобы замкнуть адрес возврата main
для указания на массив shellcode
, не требует выполнения кода в стеке. Когда я запускаю вашу программу на моем Linux-компьютере (также работающем на процессоре x86), скомпилированном с gcc -O0 -m32
, она устанавливает регистр EIP так, чтобы он указывал на машинный код в shellcode
. Но затем, как и для вас, он падает с ошибкой сегментации.
Причина сбоя в том, что shellcode
загружен в область памяти, помеченную как , а не исполняемая . (Имя этой области памяти - «сегмент данных».) Процессор отказывается выполнять машинные инструкции из этой области, вместо этого генерируя «исключение» (это аппаратная концепция, а не то же самое, что исключение C ++), которое ядро переводится в сигнал SIGSEGV.
Старые учебники по написанию шелл-кода и эксплойтов переполнения буфера не предупреждают вас об этой возможности, потому что старшие поколения архитектуры x86 не могли пометить память как неисполняемую для каждой страницы. В «плоской» конфигурации сегментного регистра, используемой большинством 32-разрядных операционных систем на базе x86, любая страница, которая была читаема, также была исполняемой. Тем не менее, последние несколько поколений архитектуры смогли пометить отдельные страницы как неисполняемые, и вы должны обойти это. (Если я правильно помню, постраничная исполнимость была добавлена в архитектуру x86 около 2003 года одновременно с 64-битным режимом, но для того, чтобы поддержка операционной системы стала универсальной, потребовалось немного больше времени.)
На моем компьютере с Linux, как указано выше, эта измененная версия вашей программы успешно передает управление и выполняет машинный код в shellcode
. Он использует системный вызов mprotect
для создания области памяти, содержащей shellcode
исполняемый файл.
#include <stdint.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
const char shellcode[] =
"\xbb\x16\x00\x00\x00"
"\xb8\x01\x00\x00\x00"
"\xcd\x80";
int main(void)
{
uintptr_t pagesize = sysconf(_SC_PAGESIZE);
if (mprotect((void *)(((uintptr_t)shellcode) & ~(pagesize - 1)),
pagesize, PROT_READ|PROT_EXEC)) {
perror("mprotect");
return 1;
}
void **ret;
ret = (void **) &ret;
ret[9] = (void *)shellcode;
return 0;
}
Как и сама операция mprotect
, обратите внимание, как добавление этого фрагмента кода изменило структуру стека и поместило адрес возврата в другое место. Если я скомпилирую с включенной оптимизацией, компоновка стека снова изменится, и адрес возврата не будет перезаписан. Также обратите внимание, как я сделал shellcode
be const char
. Если бы я этого не сделал, мне нужно было бы использовать PROT_READ|PROT_WRITE|PROT_EXEC
в вызове mprotect
, чтобы избежать преждевременного сбоя, потому что какая-то случайная глобальная переменная внезапно перестала быть доступной для записи, когда библиотека C ожидала этого, и ядро возможно, произошел сбой вызова mprotect
из-за политики безопасности W ^ X .
В зависимости от возраста вашего ядра и библиотеки C, сделать shellcode
be const char
может быть достаточно само по себе, но с ядром 4.19 и glibc 2.28, что у меня есть, данные только для чтения не исполняемый либо.