SecOps Portal
Trang chủ › Nghiên Cứu Bảo Mật

Phân tích kỹ thuật lỗ hổng tràn bộ đệm Buffer Overflow

Ban biên tập SecOps Portal · · Cập nhật

Tràn bộ đệm (buffer overflow) xảy ra khi chương trình ghi dữ liệu vượt quá kích thước vùng nhớ đã cấp cho một bộ đệm, làm ghi đè lên dữ liệu liền kề trong bộ nhớ. Lỗi này chủ yếu xuất hiện ở ngôn ngữ cho phép thao tác bộ nhớ trực tiếp như C và C++, và có thể dẫn tới sập chương trình, rò rỉ dữ liệu hoặc nghiêm trọng nhất là thực thi mã tùy ý. Cách phòng chống gồm ba tầng: viết mã kiểm tra giới hạn, dùng ngôn ngữ hoặc thư viện an toàn bộ nhớ, và bật các cơ chế giảm thiểu của trình biên dịch và hệ điều hành.

Bài viết này giải thích nguyên lý ở mức khái niệm để lập trình viên và người làm bảo mật hiểu rủi ro, không đi vào chi tiết khai thác.

Bộ đệm và bộ nhớ chương trình

Bộ đệm là một vùng nhớ liên tục có kích thước cố định, ví dụ một mảng 64 byte để chứa tên người dùng. Khi chương trình chạy, bộ nhớ được chia thành nhiều vùng, trong đó hai vùng quan trọng nhất với lỗi tràn bộ đệm là:

  • Stack (ngăn xếp): chứa biến cục bộ của hàm, tham số, và thông tin để quay lại hàm gọi, như địa chỉ trả về.
  • Heap: chứa vùng nhớ cấp phát động trong lúc chạy, cùng siêu dữ liệu quản lý cấp phát.

C và C++ không tự kiểm tra việc ghi có vượt giới hạn mảng hay không. Trách nhiệm hoàn toàn thuộc về lập trình viên.

Vì sao tràn bộ đệm xảy ra

Nguyên nhân thường gặp là sao chép dữ liệu có độ dài do bên ngoài quyết định vào bộ đệm cố định mà không kiểm tra:

  • Dùng các hàm không kiểm tra độ dài như gets, strcpy, strcat, sprintf.
  • Tính sai kích thước, quên chỗ cho ký tự kết thúc chuỗi, lỗi lệch một (off-by-one).
  • Tràn số nguyên khi tính kích thước cấp phát, dẫn tới cấp vùng nhớ nhỏ hơn dự kiến.
  • Tin vào trường độ dài trong dữ liệu đầu vào, ví dụ tệp hoặc gói tin khai báo độ dài sai.

Tràn stack và tràn heap

Đặc điểmTràn stackTràn heap
Vùng bị ghi đèBiến cục bộ khác, địa chỉ trả về của hàmĐối tượng lân cận, siêu dữ liệu của bộ cấp phát
Hậu quả kinh điểnThay đổi luồng thực thi khi hàm trả vềLàm hỏng cấu trúc dữ liệu, ghi đè con trỏ hàm
Cơ chế giảm thiểu chínhStack canary, ASLR, DEPBộ cấp phát tăng cường, ASLR, DEP

Về mặt khái niệm, nguy hiểm lớn nhất của tràn stack là kẻ tấn công có thể ghi đè địa chỉ trả về, khiến chương trình "quay về" một nơi do họ chọn. Tràn heap phức tạp hơn nhưng cũng có thể dẫn tới kiểm soát luồng thực thi thông qua con trỏ bị ghi đè.

Các lỗi bộ nhớ "họ hàng"

  • Đọc vượt giới hạn (out-of-bounds read): không ghi đè nhưng làm lộ dữ liệu trong bộ nhớ, như khóa bí mật. Một số lỗ hổng nổi tiếng trong thư viện mã hóa từng thuộc loại này.
  • Use-after-free: dùng vùng nhớ sau khi đã giải phóng.
  • Double free: giải phóng một vùng nhớ hai lần.
  • Chuỗi định dạng (format string): truyền dữ liệu người dùng làm chuỗi định dạng cho printf.

Cùng với tràn bộ đệm, nhóm lỗi an toàn bộ nhớ này chiếm tỷ lệ đáng kể trong các lỗ hổng nghiêm trọng được công bố ở phần mềm viết bằng C/C++, theo nhiều báo cáo công khai của các hãng phần mềm lớn.

Cơ chế giảm thiểu của hệ thống

Hệ điều hành và trình biên dịch hiện đại có nhiều lớp làm cho việc khai thác khó hơn đáng kể:

  • Stack canary: trình biên dịch chèn một giá trị bí mật trước địa chỉ trả về; nếu giá trị bị thay đổi khi hàm kết thúc, chương trình dừng ngay.
  • DEP/NX: đánh dấu vùng dữ liệu là không thực thi được, nên dữ liệu chèn vào stack hay heap không chạy như mã.
  • ASLR: ngẫu nhiên hóa địa chỉ các vùng nhớ mỗi lần chạy, khiến kẻ tấn công khó biết nhảy tới đâu.
  • Control-flow integrity: kiểm tra luồng gọi hàm có hợp lệ không.
  • FORTIFY_SOURCE và cờ biên dịch tăng cường: thay một số hàm nguy hiểm bằng phiên bản có kiểm tra khi biết kích thước lúc biên dịch.

Các cơ chế này giảm rủi ro nhưng không loại bỏ lỗi. Kẻ tấn công có thể kết hợp nhiều lỗi, ví dụ một lỗi đọc vượt giới hạn để lộ địa chỉ, vượt qua ASLR, rồi dùng lỗi tràn để khai thác. Vì vậy sửa lỗi gốc vẫn là bắt buộc.

Viết mã phòng tránh tràn bộ đệm

  1. Không dùng gets; thay strcpy, strcat, sprintf bằng các hàm có giới hạn độ dài và kiểm tra giá trị trả về.
  2. Luôn kiểm tra độ dài dữ liệu đầu vào trước khi sao chép.
  3. Kiểm tra tràn số nguyên khi tính kích thước cấp phát.
  4. Trong C++, ưu tiên std::string, std::vector, std::array và truy cập có kiểm tra giới hạn ở chỗ quan trọng.
  5. Bật cảnh báo trình biên dịch ở mức cao và coi cảnh báo là lỗi.
  6. Dùng công cụ phát hiện lỗi bộ nhớ khi kiểm thử như AddressSanitizer, Valgrind.
  7. Áp dụng fuzzing cho mã xử lý dữ liệu từ bên ngoài như trình phân tích tệp, giao thức mạng.

Các nguyên tắc này là ứng dụng cụ thể của tư duy "không tin đầu vào" trong bài lập trình an toàn và nguyên tắc bảo mật.

Ngôn ngữ an toàn bộ nhớ

Các ngôn ngữ như Rust, Go, Java, C#, Python tự quản lý hoặc kiểm tra truy cập bộ nhớ, loại bỏ phần lớn lỗi tràn bộ đệm ngay từ thiết kế. Nhiều tổ chức và cơ quan an ninh mạng đã khuyến nghị chuyển dần các thành phần mới, nhất là thành phần xử lý dữ liệu không tin cậy, sang ngôn ngữ an toàn bộ nhớ. Với mã C/C++ hiện có, cách tiếp cận thực tế là ưu tiên viết lại những phần rủi ro cao và tăng cường kiểm thử cho phần còn lại.

Góc nhìn cho người vận hành hệ thống

Ngay cả khi không viết mã C, bạn vẫn chịu ảnh hưởng vì hệ điều hành, máy chủ web, thư viện mã hóa, firmware thiết bị mạng đều có thành phần viết bằng C/C++. Biện pháp của người vận hành là cập nhật kịp thời khi có bản vá, giảm số dịch vụ mở ra mạng và theo dõi thông báo lỗ hổng. Theo dõi phiên bản thư viện bạn dùng được trình bày trong bài quản lý và cập nhật thư viện mã nguồn mở. Kiểm tra xem máy chủ đang mở những dịch vụ nào có thể làm bằng công cụ trong bài rà quét cổng với Nmap.

Câu hỏi thường gặp

Ngôn ngữ như Python, Java có bị tràn bộ đệm không?

Mã viết bằng các ngôn ngữ này gần như không gặp tràn bộ đệm kiểu C vì truy cập mảng được kiểm tra. Tuy nhiên trình thông dịch, máy ảo hoặc thư viện native mà chúng dùng vẫn viết bằng C/C++ và có thể chứa lỗi.

ASLR và DEP đã bật thì có còn lo tràn bộ đệm?

Có. Các cơ chế này làm khai thác khó hơn nhưng không bất khả thi, đặc biệt khi kẻ tấn công kết hợp nhiều lỗi. Tràn bộ đệm cũng có thể gây sập dịch vụ ngay cả khi không khai thác được.

Fuzzing giúp gì trong phát hiện tràn bộ đệm?

Fuzzing tự động tạo rất nhiều đầu vào biến dạng để đưa vào chương trình. Khi kết hợp với công cụ như AddressSanitizer, nó phát hiện truy cập bộ nhớ sai ngay lúc xảy ra, kể cả những lỗi chưa gây sập.

Tràn bộ đệm có còn phổ biến không?

Vẫn còn, đặc biệt trong firmware thiết bị nhúng, router, phần mềm cũ. Lỗi an toàn bộ nhớ nói chung vẫn chiếm phần đáng kể trong các lỗ hổng nghiêm trọng của phần mềm viết bằng C/C++.

Bài liên quan