Lỗi tràn bộ nhớ đệm

Trong các lĩnh vực an ninh máy tính và lập trình, một lỗi tràn bộ nhớ đệm hay gọi tắt là lỗi tràn bộ đệm (tiếng Anh: buffer overflow) là một lỗi lập trình có thể gây ra một ngoại lệ truy nhập bộ nhớ máy tính và chương trình bị kết thúc, hoặc khi người dùng có ý phá hoại, họ có thể lợi dụng lỗi này để phá vỡ an ninh hệ thống.
Lỗi tràn bộ đệm là một điều kiện bất thường khi một tiến trình lưu dữ liệu vượt ra ngoài biên của một bộ nhớ đệm có chiều dài cố định. Kết quả là dữ liệu đó sẽ đè lên các vị trí bộ nhớ liền kề. Dữ liệu bị ghi đè có thể bao gồm các bộ nhớ đệm khác, các biến và dữ liệu điều khiển luồng chạy của chương trình (program flow control).
Các lỗi tràn bộ đệm có thể làm cho một tiến trình đổ vỡ hoặc cho ra các kết quả sai. Các lỗi này có thể được kích hoạt bởi các dữ liệu vào được thiết kế đặc biệt để thực thi các đoạn mã phá hoại hoặc để làm cho chương trình hoạt động một cách không như mong đợi. Bằng cách đó, các lỗi tràn bộ đệm gây ra nhiều lỗ hổng bảo mật (vulnerability) đối với phần mềm và tạo cơ sở cho nhiều thủ thuật khai thác (exploit). Việc kiểm tra biên (bounds checking) đầy đủ bởi lập trình viên hoặc trình biên dịch có thể ngăn chặn các lỗi tràn bộ đệm.

Mô tả kỹ thuật

Một lỗi tràn bộ nhớ đệm xảy ra khi dữ liệu được viết vào một bộ nhớ đệm, mà do không kiểm tra biên đầy đủ nên đã ghi đè lên vùng bộ nhớ liền kề và làm hỏng các giá trị dữ liệu tại các địa chỉ bộ nhớ kề với vùng bộ nhớ đệm đó. Hiện tượng này hay xảy ra nhất khi sao chép một xâu ký tự từ một bộ nhớ đệm này sang một vùng bộ nhớ đệm khác.

Mã nguồn ví dụ

Mã nguồn C dưới đây thể hiện một lỗi lập trình thường gặp. Sau khi được biên dịch, chương trình sẽ tạo ra một lỗi tràn bộ đệm nếu nó được gọi với một tham số dòng lệnh là một xâu ký tự quá dài, vì tham số này được dùng để ghi vào một bộ nhớ đệm mà không kiểm tra độ dài của nó.
/* overflow.c - demonstrates a buffer overflow */#include <stdio.h>#include <string.h>int main(int argc, char *argv[]){
 char buffer[10];
 if (argc < 2)
 {
 fprintf(stderr, "USAGE: %s string\n", argv[0]);
 return 1;
 }
 strcpy(buffer, argv[1]);
 return 0;}
Các xâu ký tự độ dài không quá 9 sẽ không gây tràn bộ đệm. Các xâu ký tự gồm từ 10 ký tự trở lên sẽ gây tràn bộ đệm: hiện tượng này luôn luôn là một lỗi sai nhưng không phải lúc nào cũng gây ra việc chương trình chạy sai hay gây lỗi segmentation fault.
Chương trình trên có thể được viết lại cho an toàn bằng cách sử dụng hàm strncpy như sau:
#include <string.h>int main(int argc, char *argv[]){
 char buffer[10];
 if (argc < 2)
 {
 fprintf(stderr, "USAGE: %s string\n", argv[0]);
 return 1;
 }
 strncpy(buffer, argv[1], sizeof(buffer));
 buffer[sizeof(buffer) - 1] = '\0';
 return 0;}

Khai thác

Có các kỹ thuật khác nhau cho việc khai thác lỗi tràn bộ nhớ đệm, tùy theo kiến trúc máy tính, hệ điều hành và vùng bộ nhớ. Ví dụ, khai thác tại heap (dùng cho các biến cấp phát động) rất khác với việc khai thác các biến tại stack.

Cản trở đối với các thủ thuật khai thác

Việc xử lý bộ đệm trước khi đọc hay thực thi nó có thể làm thất bại các cố gắng khai thác lỗi tràn bộ đệm. Các xử lý này có thể giảm bớt mối đe dọa của việc khai thác lỗi, nhưng có thể không ngăn chặn được một cách tuyệt đối. Việc xử lý có thể bao gồm: chuyển từ chữ hoa thành chữ thường, loại bỏ các ký tự đặc biệt (metacharacters) và lọc các xâu không chứa ký tự là chữ số hoặc chữ cái. Tuy nhiên, có các kỹ thuật để tránh việc lọc và xử lý này; alphanumeric code (mã gồm toàn chữ và số), polymorphic code (mã đa hình), Self-modifying code (mã tự sửa đổi) và tấn công kiểu return-to-libc. Cũng chính các phương pháp này có thể được dùng để tránh bị phát hiện bởi các hệ thống phát hiện thâm nhập (Intrusion detection system).

Chống tràn bộ đệm

Nhiều kỹ thuật đa dạng với nhiều ưu nhược điểm đã được sử dụng để phát hiện hoặc ngăn chặn hiện tượng tràn bộ đệm. Cách đáng tin cậy nhất để tránh hoặc ngăn chặn tràn bộ đệm là sử dụng bảo vệ tự động tại mức ngôn ngữ lập trình. Tuy nhiên, loại bảo vệ này không thể áp dụng cho mã thừa kế (legacy code), và nhiều khi các ràng buộc kỹ thuật, kinh doanh hay văn hóa lại đòi hỏi sử dụng một ngôn ngữ không an toàn. Các mục sau đây mô tả các lựa chọn và cài đặt hiện có.

Lựa chọn ngôn ngữ lập trình

Lựa chọn về ngôn ngữ lập trình có thể có một ảnh hưởng lớn đối với sự xuất hiện của lỗi tràn bộ đệm. Năm 2006, C và C++ nằm trong số các ngôn ngữ lập trình thông dụng nhất, với một lượng khổng lồ các phần mềm đã được viết bằng hai ngôn ngữ này. C và C++ không cung cấp sẵn các cơ chế chống lại việc truy nhập hoặc ghi đè dữ liệu lên bất cứ phần nào của bộ nhớ thông qua các con trỏ bất hợp lệ; cụ thể, hai ngôn ngữ này không kiểm tra xem dữ liệu được ghi vào một mảng (cài đặt của một bộ nhớ đệm) có nằm trong biên của mảng đó hay không. Tuy nhiên, cần lưu ý rằng các thư viện chuẩn của C++, thư viện khuôn mẫu chuẩn - STL, cung cấp nhiều cách an toàn để lưu trữ dữ liệu trong bộ đệm, và các lập trình viên C cũng có thể tạo và sử dụng các tiện ích tương tự. Cũng như đối với các tính năng bất kỳ khác của C hay C++, mỗi lập trình viên phải tự xác định lựa chọn xem họ có muốn chấp nhận các hạn chế về tốc độ chương trình để thu lại các lợi ích tiềm năng (độ an toàn của chương trình) hay không.
Một số biến thể của C, chẳng hạn Cyclone, giúp ngăn chặn hơn nữa các lỗi tràn bộ đệm bằng việc chẳng hạn như gắn thông tin về kích thước mảng với các mảng. Ngôn ngữ lập trình D sử dụng nhiều kỹ thuật đa dạng để tránh gần hết việc sử dụng con trỏ và kiểm tra biên do người dùng xác định.
Nhiều ngôn ngữ lập trình khác cung cấp việc kiểm tra tại thời gian chạy, việc kiểm tra này gửi một cảnh báo hoặc ngoại lệ khi C hoặc C++ ghi đè dữ liệu. Ví dụ về các ngôn ngữ này rất đa dạng, từ Python tới Ada, từ Lisp tới Modula-2, và từ Smalltalk tới OCaml. Các môi trường bytecode của Java và .NET cũng đòi hỏi kiểm tra biên đối với tất cả các mảng. Gần như tất cả các ngôn ngữ thông dịch sẽ bảo vệ chương trình trước các hiện tượng tràn bộ đệm bằng cách thông báo một trạng thái lỗi định rõ (well-defined error). Thông thường, khi một ngôn ngữ cung cấp đủ thông tin về kiểu để thực hiện kiểm tra biên, ngôn ngữ đó thường cho phép lựa chọn kích hoạt hay tắt chế độ đó. Việc phân tích tĩnh (static analysis) có thể loại được nhiều kiểm tra kiểu và biên động, nhưng các cài đặt tồi và các trường hợp rối rắm có thể giảm đáng kể hiệu năng. Các kỹ sư phần mềm phải cẩn thận cân nhắc giữa các phí tổn cho an toàn và hiệu năng khi quyết định sẽ sử dụng ngôn ngữ nào và cấu hình như thế nào cho trình biên dịch.

Sử dụng các thư viện an toàn

Vấn đề tràn bộ đệm thường gặp trong C và C++ vì các ngôn ngữ này để lộ các chi tiết biểu diễn mức thấp của các bộ nhớ đệm với vai trò các chỗ chứa cho các kiểu dữ liệu. Do đó, phải tránh tràn bộ đệm bằng cách gìn giữ tính đúng đắn cao cho các phần mã chương trình thực hiện việc quản lý bộ đệm. Việc sử dụng các thư viện được viết tốt và đã được kiểm thử, dành cho các kiểu dữ liệu trừu tượng mà các thư viện này thực hiện tự động việc quản lý bộ nhớ, trong đó có kiểm tra biên, có thể làm giảm sự xuất hiện và ảnh hưởng của các hiện tượng tràn bộ đệm. Trong các ngôn ngữ này, xâu ký tự và mảng là hai kiểu dữ liệu chính mà tại đó các hiện tượng tràn bộ đệm thường xảy ra; do đó, các thư viện ngăn chặn lỗi tràn bộ đệm tại các kiểu dữ liệu này có thể cung cấp phần chính của sự che chắn cần thiết. Dù vậy, việc sử dụng các thư viện an toàn một cách không đúng có thể dẫn đến tràn bộ đệm và một số lỗ hổng khác; và tất nhiên, một lỗi bất kỳ trong chính thư viện chính nó cũng là một lỗ hổng. Các cài đặt thư viện "an toàn" gồm The Better String Library, Arri Buffer API và Vstr. Thư viện C của hệ điều hành OpenBSD cung cấp các hàm hữu ích strlcpy strlcat, nhưng các hàm này nhiều hạn chế hơn nhiều so với các cài đặt thư viện an toàn đầy đủ.
Tháng 9 năm 2006, Báo cáo kỹ thuật số 24731 của hội đồng tiêu chuẩn C đã được công bố; báo cáo này mô tả một tập các hàm mới dựa trên các hàm vào ra dữ liệu và các hàm xử lý xâu ký tự của thư viện C chuẩn, các hàm mới này được bổ sung các tham số về kích thước bộ đệm.

Lịch sử khai thác

Khai thác lỗi tràn bộ đệm được biết đến đầu tiên là vào năm 1988. Đó là một trong các khai thác mà sâu Morris sử dụng để lan truyền chính mình trên Internet. Chương trình đã bị khai thác là một dịch vụ Unix có tên fingerd.
Sau đó, vào năm 1995, Thomas Lopatic đã tái phát hiện hiện tượng tràn bộ đệm một cách độc lập và công bố phát kiến của mình trên danh sách thư an ninh Bugtraq. Một năm sau, 1996, Elias Levy (còn gọi là Aleph One) công bố trên tạp chí Phrack bài báo "Smashing the Stack for Fun and Profit" (Phá bộ nhớ stack cho vui và để thu lợi),, đây là một hướng dẫn từng bước cho việc khai thác các lỗ hổng tràn bộ đệm trên stack.
Từ đó, ít nhất hai con sâu Internet đã khai thác lỗi tràn bộ đệm để xâm phạm hàng loạt hệ thống lớn. Năm 2001, Sâu Code Red khai thác lỗi tràn bộ đệm trong chương trình Internet Information Services (IIS) 5.0 của Microsoft, và vào năm 2003, sâu SQLSlammer đã phá hoại các máy chạy Microsoft SQL Server 2000.