Buffer Overflow được đặc trưng bởi việc ghi đè lên các đoạn bộ nhớ của quá trình mà lẽ ra không bao giờ được sửa đổi một cách cố ý hoặc vô ý. Việc ghi đè các giá trị của IP (Instruction Pointer), BP (Base Pointer) và các register khác gây ra các ngoại lệ, lỗi phân đoạn và các lỗi khác xảy ra. Thông thường những lỗi này kết thúc việc thực thi ứng dụng theo cách không mong muốn. Buffer Overflow xảy ra khi chúng ta thao tác trên bộ đệm kiểu char.
Các bài viết liên quan:
Buffer Overflow có thể bao gồm làm tràn ngăn xếp [Tràn ngăn xếp] hoặc tràn đống [Tràn đống]. Chúng tôi không phân biệt giữa hai điều này trong bài viết này để tránh nhầm lẫn.
Các ví dụ dưới đây được viết bằng ngôn ngữ C trong hệ thống GNU / Linux trên kiến trúc x86.
Các ví dụ
ví dụ 1
#include <stdio.h> int main(int argc, char **argv) { char buf[8]; // buffer for eight characters gets(buf); // read from stdio (sensitive function!) printf("%s\n", buf); // print out data stored in buf return 0; // 0 as return value }
Ứng dụng rất đơn giản này đọc từ đầu vào chuẩn một mảng các ký tự và sao chép nó vào bộ đệm kiểu char. Kích thước của bộ đệm này là tám ký tự. Sau đó, nội dung của bộ đệm được hiển thị và ứng dụng sẽ thoát.
Biên dịch chương trình:
user@spin ~/ $ gcc bo-simple.c -o bo-simple /tmp/ccECXQAX.o: In function `main': bo-simple.c:(.text+0x17): warning: the `gets' function is dangerous and should not be used.
Ở giai đoạn này, ngay cả trình biên dịch cũng gợi ý rằng hàm get () không an toàn.
Ví dụ sử dụng:
user@spin ~/ $ ./bo-simple // program start 1234 // we eneter "1234" string from the keyboard 1234 // program prints out the conent of the buffer user@spin ~/ $ ./bo-simple // start 123456789012 // we eneter "123456789012" 123456789012 // content of the buffer "buf" ?!?! Segmentation fault // information about memory segmenatation fault
Chúng tôi quản lý (không) may mắn để thực hiện hoạt động bị lỗi bởi chương trình và khiến nó thoát ra bất thường.
Phân tích vấn đề:
Chương trình gọi một hàm, hoạt động trên bộ đệm kiểu char và không kiểm tra việc làm tràn kích thước được gán cho bộ đệm này. Do đó, có thể cố ý hoặc vô ý lưu trữ nhiều dữ liệu hơn trong bộ đệm, điều này sẽ gây ra lỗi. Câu hỏi sau được đặt ra: Bộ đệm chỉ lưu trữ tám ký tự, vậy tại sao hàm printf () lại hiển thị mười hai ?. Câu trả lời đến từ tổ chức bộ nhớ tiến trình. Bốn ký tự làm Buffer Overflow cũng ghi đè giá trị được lưu trữ trong một trong các thanh ghi, điều này cần thiết cho việc trả về hàm đúng. Tính liên tục của bộ nhớ dẫn đến việc in ra dữ liệu được lưu trữ trong vùng bộ nhớ này.
Ví dụ 2
#include <stdio.h> #include <string.h> void doit(void) { char buf[8]; gets(buf); printf("%s\n", buf); } int main(void) { printf("So... The End...\n"); doit(); printf("or... maybe not?\n"); return 0; }
Ví dụ này tương tự như ví dụ đầu tiên. Ngoài ra, trước và sau hàm doit (), chúng ta có hai lệnh gọi hàm printf ().
Tổng hợp:
Compilation: user@dojo-labs ~/buffer_overflow $ gcc example02.c -o example02 -ggdb /tmp/cccbMjcN.o: In function `doit': /home/user/owasp/buffer_overflow/example02.c:8: warning: the `gets' function is dangerous and should not be used. Usage example: user@dojo-labs ~/buffer_overflow $ ./example02 So... The End... TEST // user data on input TEST // print out stored user data or... maybe not?
Chương trình giữa hai lệnh gọi printf () được xác định hiển thị nội dung của bộ đệm, được lấp đầy bởi dữ liệu do người dùng nhập vào.
user@dojo-labs ~/buffer_overflow $ ./example02 So... The End... TEST123456789 TEST123456789 Segmentation fault
Vì kích thước của bộ đệm đã được xác định (char buf [8]) và nó được lấp đầy bằng mười ba ký tự kiểu char, bộ đệm đã bị tràn.
Nếu ứng dụng nhị phân của chúng tôi ở định dạng ELF, thì chúng tôi có thể sử dụng chương trình objdump để phân tích nó và tìm thông tin cần thiết để khai thác Buffer Overflow.
Dưới đây là kết quả được tạo ra bởi objdump. Từ đầu ra đó, chúng ta có thể tìm địa chỉ, nơi printf () được gọi là (0x80483d6 và 0x80483e7).
user@dojo-labs ~/owasp/buffer_overflow $ objdump -d ./example02 080483be <main>: 80483be: 8d 4c 24 04 lea 0x4(%esp),%ecx 80483c2: 83 e4 f0 and $0xfffffff0,%esp 80483c5: ff 71 fc pushl 0xfffffffc(%ecx) 80483c8: 55 push %ebp 80483c9: 89 e5 mov %esp,%ebp 80483cb: 51 push %ecx 80483cc: 83 ec 04 sub $0x4,%esp 80483cf: c7 04 24 bc 84 04 08 movl $0x80484bc,(%esp) 80483d6: e8 f5 fe ff ff call 80482d0 <puts@plt> 80483db: e8 c0 ff ff ff call 80483a0 <doit> 80483e0: c7 04 24 cd 84 04 08 movl $0x80484cd,(%esp) 80483e7: e8 e4 fe ff ff call 80482d0 <puts@plt> 80483ec: b8 00 00 00 00 mov $0x0,%eax 80483f1: 83 c4 04 add $0x4,%esp 80483f4: 59 pop %ecx 80483f5: 5d pop %ebp 80483f6: 8d 61 fc lea 0xfffffffc(%ecx),%esp 80483f9: c3 ret 80483fa: 90 nop 80483fb: 90 nop
Nếu lệnh gọi thứ hai tới printf () thông báo cho quản trị viên về việc đăng xuất của người dùng (ví dụ: phiên đã đóng), thì chúng ta có thể cố gắng bỏ qua bước này và kết thúc mà không cần gọi tới printf ().
user@dojo-labs ~/buffer_overflow $ perl -e 'print "A"x12 ."\xf9\x83\x04\x08"' | ./example02 So... The End... AAAAAAAAAAAAu*. Segmentation fault
Ứng dụng đã kết thúc quá trình thực thi với lỗi phân đoạn, nhưng lệnh gọi thứ hai tới printf () không có chỗ đứng.
Một vài lời giải thích:
perl -e ‘print“ A ”x12.” \ xf9 \ x83 \ x04 \ x08 ”’ – sẽ in ra mười hai ký tự “A” và sau đó là bốn ký tự, thực tế là địa chỉ của lệnh chúng ta muốn thực thi. Tại sao lại là mười hai?
8 // size of buf (char buf[8]) + 4 // four additional bytes for overwriting stack frame pointer ---- 12
Phân tích vấn đề:
Vấn đề giống như trong ví dụ đầu tiên. Không có quyền kiểm soát kích thước của bộ đệm được sao chép vào bộ đệm được khai báo trước đó. Trong ví dụ này, chúng tôi ghi đè lên thanh ghi EIP với địa chỉ 0x080483f9, thực tế là một lệnh gọi để ret vào giai đoạn cuối của quá trình thực thi chương trình.
Làm thế nào để sử dụng Buffer Overflow theo một cách khác?
Nói chung, việc khai thác các lỗi này có thể dẫn đến:
- DoS
- sắp xếp lại việc thực hiện các chức năng
- thực thi mã (nếu chúng tôi có thể đưa mã shellcode, được mô tả trong tài liệu riêng)
Buffer Overflow được thực hiện như thế nào?
Những loại lỗi này rất dễ mắc phải. Trong nhiều năm, chúng là cơn ác mộng của lập trình viên. Vấn đề nằm ở các hàm C gốc, không quan tâm đến việc kiểm tra độ dài bộ đệm thích hợp. Dưới đây là danh sách các chức năng như vậy và nếu chúng tồn tại, các chức năng tương đương an toàn của chúng:
- get () – \> fgets () – đọc ký tự
- strcpy () – \> strncpy () – sao chép nội dung của bộ đệm
- strcat () – \> strncat () – nối bộ đệm
- sprintf () – \> snprintf () – điền vào bộ đệm với dữ liệu thuộc các loại khác nhau
- (f) scanf () – đọc từ STDIN
- getwd () – trả về thư mục làm việc
- realpath () – trả về đường dẫn tuyệt đối (đầy đủ)
Sử dụng các hàm tương đương an toàn để kiểm tra độ dài bộ đệm bất cứ khi nào có thể. Cụ thể:
- gets () – \> fgets ()
- strcpy () – \> strncpy ()
- strcat () – \> strncat ()
- sprintf () – \> snprintf ()
Những chức năng không có giá trị tương đương an toàn nên được viết lại với các bước kiểm tra an toàn được triển khai. Thời gian dành cho việc đó sẽ có lợi trong tương lai. Hãy nhớ rằng bạn chỉ phải làm điều đó một lần.
Sử dụng các trình biên dịch có khả năng xác định các chức năng không an toàn, lỗi logic và kiểm tra xem bộ nhớ có bị ghi đè khi nào và ở đâu không.