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

Trong phần mềm, **tràn bộ đệm ở ngăn xếp** (tiếng Anh: *stack buffer overflow* hay *stack buffer overrun*) xảy ra khi một chương trình viết địa chỉ bộ nhớ lên vùng gọi ngăn xếp của chương trình nhưng nằm ngoài vùng cấu trúc dữ liệu muốn nhắm tới mà thường là một bộ đệm với một độ dài thích hợp. Lỗi tràn bộ đệm của ngăn xếp bị gây ra khi một chương trình viết nhiều dữ liệu vào vùng đệm được định trên ngăn xếp hơn vùng mà bộ đệm đó được khởi tạo. Điều này sẽ làm hư hỏng dữ liệu liền kề trên ngăn xếp, trong vài trường hợp ở nơi bị tràn gây ra bởi lỗi và sẽ làm cho chương trình bị vỡ hay chạy sai. Tràn bộ đệm ngăn xếp là một dạng làm việc sai chức năng của chương trình được biết tới như tràn bộ đệm (buffer overflow). Làm đầy một bộ đệm trên ngăn xếp giống việc làm cho chương trình chạy sai hơn là làm đầy bộ nhớ trên đống (heap) vì ngăn xếp chưa nội dung của địa chỉ trả về cho tất cả các chức năng.
Tràn bộ đệm ngăn xếp có thể được gây ra cố ý như là một phần của cuộc tấn công đập ngăn xếp. Nếu chương trình đang bị ảnh hưởng chạy với các đặc quyền hoặc lấy dữ liệu từ mạng máy chủ ko tin cậy (như là máy chủ web) thì lỗi là 1 lỗ hổng bảo mật tiềm ẩn. Nếu bộ đễm ngăn xếp được làm đầy bởi dữ liệu được cung cấp bởi người dùng ko tin cậy thì khi đó người đó có thể làm hỏng ngăn xếp bằng cách như là đưa mã thực thi vào chương trình đang chạy và kiểm soát quá trình. đây là cách cổ điển và lâu đời nhất và tin cậy nhất với tin tặc để truy cậy trái phép vào máy tính.

## 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

```c
#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.).

### Hàm foo() với những giá trị nhập vào khác nhau

Lưu ý, trong hình C ở trên khi tham số được nhập vào từ hàm foo() lớn hơn 11 byte thì sẽ ghi đè lên “Saved Frame pointer”, ”Return Address”. Khi hàm foo() trả về, chương trình sẽ đem các địa chỉ trả về ra khỏi stack và thực hiện những lệnh từ địa chỉ đó. Do đó, những kẻ tấn công đã thay thế địa chỉ trả về bằng một con trỏ tới bộ đệm stack char c[12] trong đó chứa các dữ liệu mà kẻ tấn công cung cấp. Trong thực tế, chuỗi “A” sẽ được thay thế bởi những dòng lệnh phù hợp với chương trình để thực hiện những chức năng mong muốn. Nếu chương trình này có đặc quyền đặc biệt (ví dụ như quyền quản trị hệ thống), sau đó kẻ tấn công sử dụng những lỗ hổng này để đạt được đặc quyền quản trị hệ thống trên máy bị ảnh hưởng.

Những kẻ tấn công cũng có thể sửa đổi giá trị biến nội bộ để khai thác một số lỗi. Với ví dụ này: 
```c
#include <string.h>
#include <stdio.h>

void foo (char *bar)
{
 float My_Float = 10.5; // Addr = 0x0023FF4C
 char c[28]; // Addr = 0x0023FF30

 // Will print 10.500000
 printf("My Float value = %f\n", My_Float);

 /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 Memory map:
 @: c allocated memory
 #: My_Float allocated memory

 *c *My_Float
 0x0023FF30 0x0023FF4C
 |  |
 @@@@@@@@@@@@@@@@@@@@@@@@@@@@#####
 foo("my string is too long !!!!! XXXXX");

 memcpy will put 0x1010C042 (little endian) in My_Float value.
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/

 memcpy(c, bar, strlen(bar)); // no bounds checking...

 // Will print 96.031372
 printf("My Float value = %f\n", My_Float);
}

int main (int argc, char **argv)
{
 foo("my string is too long !!!!! \x10\x10\xc0\x42");
 return 0;
}
```


## 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.
## Tham khảo