Что такое переполнение буфера и как его вызвать?

Я слышал о переполнении буфера, и я хотел бы знать, как его вызвать.

Может кто-нибудь показать мне небольшой пример переполнения буфера? Новый (А для чего они используются?)

11 ответов

Решение

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

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

С точки зрения того, как вы могли бы запрограммировать его самостоятельно, это будет простой вопрос:

char a[4];
strcpy(a,"a string longer than 4 characters"); // write past end of buffer (buffer overflow)
printf("%s\n",a[6]); // read past end of buffer (also not a good idea)

Скомпилируется ли это и что произойдет при его запуске, вероятно, будет зависеть от вашей операционной системы и компилятора.

Классический пример переполнения буфера:

// noone will ever have the time to type more than 64 characters...
char buf[64];
gets(buf); // let user put his name

Переполнение буфера чаще всего не происходит преднамеренно. Это происходит чаще всего из-за так называемой ошибки "off-by-one". Это означает, что вы неправильно рассчитали размер массива на единицу - возможно, потому, что вы забыли учесть завершающий нулевой символ, или потому, что некоторые другие вещи.

Но это также может быть использовано для некоторых злых вещей. Действительно, пользователь долго знал эту дыру, а затем вставляет, скажем, 70 символов, причем последние содержат несколько специальных байтов, которые перезаписывают некоторый слот стека - если пользователь действительно хитрый, он / она попадет в слот обратного адреса в стеке и перезаписывает его так, что он переходит вперед в только что вставленный буфер: потому что пользователь ввел не его имя, а его шелл-код, который он ранее скомпилировал и выгрузил. Тот будет просто казнен. Есть некоторые проблемы. Например, вы должны договориться, чтобы в этом двоичном коде не было "\n" (потому что get прекратит чтение). Для других способов, которые связываются с опасными строковыми функциями, двоичный ноль проблематичен, потому что строковые функции перестают копировать туда в буфер. Люди использовали xor с двойным значением, равным нулю, без явной записи нулевого байта.

Это классический способ сделать это. Но есть некоторые блоки безопасности, которые могут сказать, что такие вещи произошли, и другие вещи, которые делают стек неисполнимым. Но я предполагаю, что есть намного лучшие трюки, чем я только что объяснил. Какой-то ассемблер, вероятно, мог бы сейчас рассказать вам длинные истории об этом:)

Как этого избежать

Всегда используйте функции, которые также принимают аргумент максимальной длины, если вы не уверены на 100%, что буфер действительно достаточно большой. Не играйте в такие игры, как "о, число не будет превышать 5 символов" - однажды это провалится. Помните, что одна ракета, где ученые говорили, что число не будет превышать какую-то величину, потому что ракета никогда не будет такой быстрой. Но однажды это было на самом деле быстрее, и в результате возникло целочисленное переполнение и сбой ракеты (речь идет об ошибке в Ariane 5, одной из самых дорогих компьютерных ошибок в истории).

Например, вместо использования получает fgets, И вместо sprintf использование snprintf где это уместно и доступно (или просто такие вещи в стиле C++, как istream и прочее)

В современной ОС Linux вы не можете использовать переполнение буфера без какого-либо дополнительного эксперимента. Зачем? потому что вы будете заблокированы ASLR (рандомизация уровня стека адресов) и защитником стека в этом современном компиляторе GNU C. Вы не сможете легко найти память, потому что память попадет в случайную память, вызванную ASLR. и вы будете заблокированы защитником стека, если попытаетесь переполнить программу.

Для начала вам нужно установить ASLR равным 0, значение по умолчанию равно 2

root@bt:~# cat /proc/sys/kernel/randomize_va_space
2
root@bt:~# echo 0 > /proc/sys/kernel/randomize_va_space
root@bt:~# cat /proc/sys/kernel/randomize_va_space
0
root@bt:~#

в данном случае это не учебник по переполнению буфера в старом стиле, который вы можете получить из интернета. или aleph one tutorial больше не будет работать в вашей системе.

Теперь давайте сделаем уязвимость программы в сценарии переполнения буфера

---------------------bof.c--------------------------
#include <stdio.h>
#include <string.h>

int main(int argc, char** argv)
{
        char buffer[400];
        strcpy(buffer, argv[1]);

        return 0;
}
---------------------EOF-----------------------------

выглядит как функция strcpy опасна без защитника стека, потому что функция без проверки, сколько байтов мы будем вводить. скомпилировать с дополнительной опцией -fno-stack-protector dan -mpreferred-stack-border =2 для снятия защиты стека в вашей C-программе

root@bt:~# gcc -g -o bof -fno-stack-protector -mpreferred-stack-boundary=2 bof.c
root@bt:~# chown root:root bof
root@bt:~# chmod 4755 bof

Переполнение буфера в C-программе со сценарием корневого доступа SUID. Теперь давайте посмотрим, сколько байтов нам нужно поместить в буфер, чтобы сделать ошибку сегментации программы.

root@bt:~# ./bof `perl -e 'print "A" x 400'`
root@bt:~# ./bof `perl -e 'print "A" x 403'`
root@bt:~# ./bof `perl -e 'print "A" x 404'`
Segmentation fault
root@bt:~#

Вы видите, что нам нужно 404 байта, чтобы сделать ошибку сегментации программы (сбой) теперь, сколько байтов нам нужно, чтобы перезаписать EIP? EIP инструкция будет выполнена после. поэтому хакеры перезаписывают EIP на злую инструкцию, что они хотят в двоичном SUID программы. если программа в корне SUID, инструкция будет запущена в режиме root.

root@bt:~# gdb -q bof
(gdb) list
1       #include <stdio.h>
2       #include <string.h>
3
4       int main(int argc, char** argv)
5       {
6               char buffer[400];
7               strcpy(buffer, argv[1]);
8
9               return 0;
10      }
(gdb) run `perl -e 'print "A" x 404'`
Starting program: /root/bof `perl -e 'print "A" x 404'`

Program received signal SIGSEGV, Segmentation fault.
0xb7e86606 in __libc_start_main () from /lib/tls/i686/cmov/libc.so.6
(gdb) run `perl -e 'print "A" x 405'`
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /root/bof `perl -e 'print "A" x 405'`

Program received signal SIGSEGV, Segmentation fault.
0xb7e800a9 in ?? () from /lib/tls/i686/cmov/libc.so.6
(gdb)

программа ПОЛУЧИЛА код возврата ошибки сегментации. давайте введем больше байтов и посмотрим на регистр EIP.

(gdb) run `perl -e 'print "A" x 406'`
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /root/bof `perl -e 'print "A" x 406'`

Program received signal SIGSEGV, Segmentation fault.
0xb7004141 in ?? ()
(gdb)

(gdb) run `perl -e 'print "A" x 407'`
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /root/bof `perl -e 'print "A" x 407'`

Program received signal SIGSEGV, Segmentation fault.
0x00414141 in ?? ()
(gdb)

еще немного

(gdb) run `perl -e 'print "A" x 408'`
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /root/bof `perl -e 'print "A" x 408'`

Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()
(gdb)

(gdb) i r
eax            0x0      0
ecx            0xbffff0b7       -1073745737
edx            0x199    409
ebx            0xb7fc9ff4       -1208180748
esp            0xbffff250       0xbffff250
ebp            0x41414141       0x41414141
esi            0x8048400        134513664
edi            0x8048310        134513424
eip            0x41414141       0x41414141 <-- overwriten !!
eflags         0x210246 [ PF ZF IF RF ID ]
cs             0x73     115
ss             0x7b     123
ds             0x7b     123
es             0x7b     123
fs             0x0      0
gs             0x33     51
(gdb)

Теперь вы можете сделать свой следующий шаг...

Переполнение буфера просто записывает после конца буфера:

int main(int argc, const char* argv[])
{
    char buf[10];
    memset(buf, 0, 11);
    return 0;
}

В дополнение к тому, что уже было сказано, имейте в виду, что ваша программа может или не может "зависнуть" при переполнении буфера. Это должно произойти сбой, и вы должны надеяться, что это произойдет - но если переполнение буфера "переполнится" в другой адрес, который также выделено вашим приложением - ваше приложение может работать нормально в течение более длительного периода времени.

Если вы используете более позднюю версию Microsoft Visual Studio - я бы предложил использовать новые безопасные аналоги в stdlib, такие как sprintf_s insted из sprintf, ect...

"Классический" пример переполнения буфера:

int main(int argc, char *argv[])
{
    char buffer[10];
    strcpy(buffer, argv[1]);
}

Это позволяет вам играть с параметрами переполнения буфера и подстраивать их под ваши сердца. В книге " Взлом - искусство эксплуатации" (ссылка идет на Amazon) подробно рассказывается о том, как поиграть с переполнением буфера (очевидно, чисто как интеллектуальное упражнение).

Это должно быть достаточно, чтобы воспроизвести это:

void buffer_overflow() 
{
    char * foo = "foo";
    char buffer[10];

    for(int it = 0; it < 1000; it++) {
        buffer[it] = '*';
    }

    char accessViolation = foo[0];
}

Это общий комментарий об ответах, которые вы получили. Например:

int main(int argc, char *argv[])
{
    char buffer[10];
    strcpy(buffer, argv[1]);
}

А также:

int main(int argc, const char* argv[])
{
    char buf[10];
    memset(buf, 0, 11);
    return 0;
}

На современных платформах Linux это может работать не так, как ожидалось или предполагалось. Может не работать из-за функции безопасности FORTIFY_SOURCE.

FORTIFY_SOURCE использует "более безопасные" варианты функций высокого риска, такие как memcpy а также strcpy, Компилятор использует более безопасные варианты, когда он может определить размер буфера назначения. Если копия превысит размер буфера назначения, то программа вызывает abort(),

Чтобы отключить FORTIFY_SOURCE для вашего тестирования, вы должны скомпилировать программу с -U_FORTIFY_SOURCE или же -D_FORTIFY_SOURCE=0,

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

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

Атака переполнения буфера - это та, которая использует эту ошибку для выполнения чего-то, что автор программы не собирался сделать возможным.

Переполнение буфера - это вставка символов, превышающая то, что может содержать выделенная память.

С правильными ответами: Чтобы получить больше информации в этой теме, вы можете прослушать Podcast Security Now. В Эпизоде ​​39 (некоторое время назад) они подробно обсуждали это. Это быстрый способ получить более глубокое понимание без необходимости переваривать целую книгу.

(По ссылке вы найдете архив с версиями нескольких размеров, а также стенограмму, если вы достаточно визуально ориентированы). Аудио не идеальная среда для этой темы, но Стив творит чудеса, чтобы справиться с этим.

Другие вопросы по тегам