|
Переполнение буфера в стеке. Часть 1 kas1e Xakep, номер #050, стр. 050-062-5 main() { int lala; // локальная переменная. } Сделаем из нее исполнимый файл и посмотрим в отладчике, что же происходит на самом деле: # gcc test.c -o test # gdb -q ./test (gdb) disas main Dump of assembler code for function main: 0x8048420 <main>: push %ebp 0x8048421 <main+1>: mov %esp,%ebp 0x8048423 <main+3>: sub $0x4,%esp 0x8048426 <main+6>: mov %ebp,%esp 0x8048428 <main+8>: pop %ebp 0x8048429 <main+9>: ret End of assembler dump. Разберем, что здесь написано. Первые 2 инструкции (push %ebp и mov %esp,%ebp) стандартны для начала любой С-подпрограммы/функции (сделано опять же для простоты адресации по переменным). Чуть ниже я их объясню. Далее идет именно то, что нам нужно - мы резервируем в стеке место под нашу локальную переменную 'lala'. Это важный момент. Далее идут также 2 стандартные инструкции (mov %ebp,%esp и pop %ebp), которые стандартны для конца любой С-подпрограммы/функции. Их я тоже чуть ниже объясню. Ну и ret инструкция - выход из программы. Итак, мы входим в функцию. Сразу в стек заносится адрес возврата (т.е. адрес, который RET положит в EIP и скажет процессору, что выполнять дальше). Если мы говорим про функцию main(), то адрес возврата заносится в самое начало стека (т.е. в самый максимальный адрес). Это значит, что в первых четырех байтах всей доступной области стека у нас адрес, на который мы вернемся при выходе из функции main(). Для всех других функций - адрес возврата ложится в то место стека, которое считается вершиной для этих самых других функций. Допустим, что мы в main() проделали какие-то вещи со стеком и вызвали функцию. Сразу же ее адрес возврата будет положен в первые 4 стековых байта, которые стали вершиной для вызванной функции, но для main - это просто очередные запары со стеком. Переполнение стека Для примера, опять-таки, возьмем main(). Итак, что у нас получается? А получается то, что адрес возврата из main() лежит в самом верху стека. Т.е. в main() выполняются инструкции до тех пор, пока не встретится RET. Тут же считывается адрес возврата из стека и кладется в EIP, чтобы процессор знал, куда ему возвращаться. И вот ключевой момент нашего разговора. Локальные переменные располагаются в стеке. Адрес возврата тоже располагается в стеке. И если каким-то образом локальными переменными перезаписать этот адрес возврата, то возврат из main() произойдет именно туда, что будет в EIP (а там будут наши переменные, которыми мы перезаписали адрес возврата). И вместо выхода из main() процессор пойдет далеко ;). Главное, вникни в последние абзацы. Следующие статьи невозможно будет понять, если ты не переваришь эту, так сказать, вводную. Но я отвлекся... |