Tràn bộ đệm ngăn xếp

Khai thác lỗi tràn bộ đệm trên stack

Các phương pháp kinh điển cho việc khai thác một lỗi tràn bộ đệm stack cơ bản là ghi đè lên hàm trả về vị trí của con trỏ để cho kẻ tấn công kiểm soát được dữ liệu (thường là trên stack của chính nó). Điều này được minh họa trong ví dụ dưới đây:

Một ví dụ với strcpy

#include <string.h>void foo (char *bar){
 char c[12];

 strcpy(c, bar); // no bounds checking}int main (int argc, char **argv){
 foo(argv[1]);}
Những mã này lấy một tham số từ dòng lệnh và sao chép nó vào một ô nhớ được đặt là c trong stack. Điều này chỉ thực hiện tốt khi đối với tham số nhỏ hơn 12 ký tự (Hình B). Bất kỳ tham số nào dài hơn 11 ký tự sẽ gây tràn bộ đệm (Số lượng ký tự nhiều nhất để đảm bảo an toàn là nhỏ hơn 1 so với kich thước bộ đệm vì trong lập trình C chuỗi ký tự được kết thúc bởi một ký tự “zero byte”. Vì vậy khi nhập 12 ký tự, ta cần 13 bytes để lưu trữ, tiếp theo là các “zero byte”, sau đó sẽ kết thúc việc ghi đè lên bộ nhớ, “zero byte” sẽ là byte ngoài cuối của bộ đệm.).

Sự khác biệt giữa các bậc thềm liên quan với nhau

Số lượng các bậc thềm có sự khác biệt tinh vi trong cách khai báo ngăn chứa mà có thể làm ảnh hưởng đến cách khai thác hoạt động của bộ chống tràn ngăn xếp. Một vài kiến trúc máy tính lưu trữ địa chỉ trả về cấp cao của ngăn xếp trong register.Điều này có nghĩa là bất cứ địa chỉ trả về được ghi đè nào cũng sẽ không được sử dụng cho đến khi ngăn xếp sau được bung ra. Một ví dụ khác về máy chi tiết cụ thể có thể ảnh hưởng tới sự lựa chọn của khai thác kĩ thuật là một thực tế rằng hầu hết kiến trúc máy phong cách RISC(Reduced Instruction Set Computing) (Tối giản hướng dẫn cài đặt tính toán) sẽ không cho phép việc truy cập không sắp xếp tới bộ nhớ. Kết hợp với chiều dài nhất định dành cho các máy opcode, giới hạn của loại máy này có thể thực hiện 1 bước nhảy đến kĩ thuật ESP hầu như không thể khai báo (có 1 ngoại lệ xảy ra khi chương trình thực sự có chứa mã không chắc chắn để nhảy đến register của ngăn xếp một cách rõ ràng).

Ngăn xếp tăng trưởng

Cùng trong chủ đề về chống tràn bộ đệm ngăn xếp, có 1 kiến trúc thường được đem ra thảo luận nhưng rất hiếm khi thấy đó là khi ngăn xếp tăng trưởng theo chiều ngược lại. Sự thay đổi này trong kiến trúc thường xuyên được đề nghị như là 1 giải pháp cho vấn đề chống tràn bộ đệm bởi vì bất cứ sự tràn nào của bộ đệm ngăn xếp khi xảy ra trong cùng 1 khung ngăn xếp không thể ghi đè lại con trỏ trả về. Nghiên cứu sâu hơn nữa về yêu cầu bảo vệ này, ta nhận thấy nó là giải pháp chất phác tốt nhất. Bất cứ sự tràn nào mà xảy ra bên trong bộ đệm từ những khung ngăn xếp trước vẫn sẽ ghi đè lên con trỏ trả về và cho phép khai thác độc hại của lỗi. Ví dụ, trong ví dụ bên trên, con trỏ trả về cho foo sẽ không được ghi đè lại vì thực chất sự tràn xảy ra bên trong khung ngăn xếp cho strcpy. Tuy nhiên, bởi vì bộ đệm bị tràn trong suốt lúc gọi strcpy còn sót lại trong khung ngăn xếp trước đó, con trỏ trả về cho strcpy sẽ có địa chỉ bộ nhớ số lượng cao hơn bộ đệm. Điều này có nghĩa thay vì con trỏ trả về cho foo bị ghi đè, thì con trỏ trả về cho strcpy sẽ bị ghi đè. Tóm lại điều này có nghĩa là việc tăng trưởng ngăn xếp theo chiều ngược lại sẽ làm thay đổi một vài chi tiết về như thế nào để có thể khai thác việc chống tràn bộ đệm ngăn xếp, nhưng nó sẽ không làm giảm đáng kể số lượng các lỗi có thể khai thác.

Phương pháp bảo vệ

Trong những năm qua, một số lượng những phương pháp đã được phát triển để ngăn chặn lỗi tràn bộ nhớ đệm. Những phương pháp này thường được phân thành ba loại:
+ Phát hiện lỗi tràn bộ nhớ đệm và ngăn chặn chương trình chuyển hướng đến vùng mã độc.
+ Ngăn chặn mã độc từ bộ nhớ đệm mà không cần phát hiện ra lỗi tràn bộ nhớ đệm.
+ Ngẫu nhiên hóa vùng dữ liệu mà việc tìm các mã có thể thực thi trở nên không đáng tin.

Stack Canaries

Stack canaries, được gọi tên tương tự như chim hoàng yến trong mỏ than, nó được sử dụng nhằm phát hiện lỗi tràn bộ nhớ đệm trước khi một mã độc được thực thi. Phương pháp này hoạt động bằng cách đặt một số nguyên nhỏ trong bộ nhớ trước con trỏ trở về, giá trị được chọn ngẫu nhiên khi bắt đầu chương trình. Hầu hết các lỗi tràn bộ nhớ đệm ghi đè lên bộ nhớ có giá trị từ thấp đến cao, vì vậy trước khi ghi đè lên con trỏ trở về, giá trị canary cũng sẽ bị ghi đè. Giá trị này được kiểm tra để chắc chắn rằng nó chưa bị thay trước khi một đoạn mã sử dụng con trỏ trở về. Kỹ thuật này có thể tăng đáng kể độ khó để sử dụng lỗi tràn bộ nhớ đệm vì nó buộc người tấn công phải đoạt quyền kiểm soát con trỏ kiểm soát bằng những phương pháp không cổ điển như phá hỏng những biến quan trọng khác trên bộ nhớ đệm.

Ngăn xếp bất khả thi

Một cách khác để ngăn chặn khai thác lỗi tràn bộ nhớ đệm là áp dụng 1 chính sách về bộ nhớ trên bộ nhớ đệm mà nó ngăn chặn việc thực hiện từ các ngăn xếp (W X,”WriteXORExecute”). Điều này có nghĩa rằng để thực hiện shellcode từ ngăn xếp kẻ tấn công phải tìm một cách để vô hiệu hóa bảo vệ thực thi từ bộ nhớ, hoặc tìm một cách để đưa shell code của họ vào vùng nhớ không được bảo vệ. Phương pháp này nay đang trở nên phổ biến hơn nên phần cứng hỗ trợ cho cờ ko được thực thi và có sẵn trong hầu hết các bộ vi xử lý máy tính để bàn. Trong khi phương pháp này chắc chắn sẽ khiến các phương pháp khai thác lỗi tràn bộ đệm kinh điển bị thất bại. nó không phải là không có vấn đề của chính nó. Đầu tiên, người ta thường tìm cách để lưu trữ shellcode trong vùng bộ nhớ không được bảo vệ như heap, và như vậy rất ít sự thay đổi cần thiết trong cách khai thác. Ngay cả nếu điều này là không phải như vậy, vẫn có những cách khác. Những thiệt hại nặng nhất được gọi là phương pháp “trở về libc” để tạo shellcode. Trong cuộc tấn công này payload độc hại sẽ tải ngăn xếp mà không phải với shellcode, nhưng với một cuộc gọi stack thích hợp thì có thể thực hiện được việc hướng tới một chuỗi các cuộc gọi thư viện tiêu chuẩn. thường là với các tác động của việc vô hiệu hóa bộ nhớ thực hiện biện pháp bảo vệ và việc cho phép shellcode chạy như bình thường. Điều này hiệu quả vì việc thực hiện không bao giờ thực sự hướng vào ngăn xếp riêng của mình. Một biến thể của “return-to-libc” là “return-oriented programming” trong đó thiết lập một loạt các địa chỉ trả về một giá trị. Mỗi địa chỉ trong số đó thực hiện một dãy nhỏ hướng dẫn của “máy hái đào” trong mã chương trình hiện có hoặc hệ thống thư viện, trình tự mà kết thúc bằng trả về một giá trị. Những cái mà người ta gọi là mỗi tiện ích thực hiện một số điểu khiển register đơn giản hoặc thực hiện tương tự như trước khi trả về một giá trị, và xâu chuỗi chúng lại với nhau đạt được mục đích của kẻ tấn công. Nó thậm chí có thể sử dụng "returnless"-“return-oriented programming” bằng cách khai thác hướng dẫn hoặc nhóm các hướng dẫn mà tính năng giống như một hướng dẫn trả về 1 giá trị.

Ngẫu nhiên hóa

Thay vì tách đoạn mã từ dữ liệu, một phương pháp nhẹ nhàng khác là ngẫu nhiên hóa vùng bộ nhớ của chương trình đang thực thi. Vì người tấn công cần phải xác định vị trí của đoạn mã có thể thực thi, ví dụ như 1 nội dung quan trọng(payload) được cung trước hay xây dựng bằng dựng lại mã ví dụ như trong ret2libc. Ngẫu nhiên hóa bố cục bộ nhớ sẽ ngăn kẻ tấn công biết được vị trí chính xác của các dòng mã, tuy nhiên việc này ko có nghĩa là sẽ ngẫu nhiên hóa tất cả. Thường thì các mã thực thi sẽ chạy tại 1 địa chỉ cố định kể cả ngẫu nhiên hóa sơ đồ không gian địa chỉ (ASLR - Address Space Layout Randomization) kết hợp với ngăn xếp bất khả thi, kẻ tấn cong vẫn có thể sử dụng vị trí cố định này. Vì thế tất cả chương trình nên được biên soạn = PIE (position-independent executables) để tất cả cái vị trí cố định này cũng dc ngẫu nhiên hóa.