Tấn công tràn bộ đệm (Buffer Overflow)

Một phần của tài liệu Nghiên cứu cơ chế thâm nhập hệ thống máy tính thông qua lỗ hổng bảo mật và ứng dụng trong công tác bảo đảm an ninh mạng (Trang 38 - 45)

Năm 1995, Dr.Mudge đã phát hiện ra lỗ hổng bảo mật của Unix là Buffer Overflow. Ông đã công bố: “Bằng cách nào viết một chƣơng trình khai thác lỗ hổng Buffer Overflow”.

Kỹ thuật Buffer overflow, cho phép một số lƣợng lớn dữ liệu đƣợc cung cấp bởi ngƣời dùng mà vƣợt quá lƣợng bộ nhớ cấp phát ban đầu bởi ứng dụng do đó gây cho hệ thống lâm vào tình trạng tràn bộ nhớ.Ví dụ :

Hình 2-10. Đoạn code chứa lỗi tràn bộ đệm

Kỹ thuật khai thác tràn bộ đệm (buffer overflow exploit) đƣợc xem là một trong những kĩ thuật hacking điển hình nhất.

2.3.1. Lỗi tràn bộ đệm, tổ chức bộ nhớ, Stack, gọi hàm, shellcode

- Lỗi tràn bộ đệm: .

+ Khái niệm về lỗi tràn bộ nhớ đệm:

Lỗi tràn bộ nhớ đệm hay gọi tắt là lỗi tràn bộ đệm là một lỗi lập trình mà có thể gây ra ngoại lệ khi truy nhập bộ nhớ máy tính. Khi một chƣơng trình bị lỗi tràn bộ đệm đƣợc khởi chạy, nó sẽ làm cho bộ nhớ máy tính quá tải, khi đó máy tính sẽ từ chối mọi truy cập hợp lệ.

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. Dữ liệu bị ghi đè có thể bao gồm các bộ nhớ đệm, các biến và dữ liệu điều khiển luồng chạy của chƣơng trình khác.

Các lỗi tràn bộ đệm có thể phá vỡ một tiến trình 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 nhằm làm cho chƣơng trình hoạt động không nhƣ mong đợi.

+ Định nghĩa lỗi tràn bộ đệm:

Một lỗi tràn bộ đệ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 nhớ đệm đó.

+ Phân tích lỗi tràn bộ đệm:

Giả sử một chƣơng trình định nghĩa hai phần tử dữ liệu kề nhau trong bộ nhớ nhƣ sau: X là bộ nhớ chứa xâu ký tự có độ dài 8 byte và giá trị khởi tạo là 0.

Y là bộ nhớ chứa một số nguyên kích thƣớc 2 byte và giá trị khởi tạo là 2, kích thƣớc 1 byte.

X X X X X X X X Y Y

Hình 2-11. Giá trị khởi tạo của dữ liệu trên bộ nhớ đệm.

Bây giờ, chƣơng trình ghi một xâu ký tự “haannhung” vào bộ đệm X, theo sau là một byte 0 đánh dấu kết thúc xâu. Vì không kiểm tra độ dài nên xâu ký tự mới đã ghi đè lên giá trị của Y.

X X X X X X X X Y Y

„h‟ „a‟ „a‟ „n‟ „n‟ „h‟ „u‟ „n‟ „g‟ 0

Hình 2-12. Ghi xâu ký tự mới vào bộ đệm.

Tuy lập trình viên không có ý định sửa đổi Y, nhƣng giá trị của Y đã bị thay thế bởi một số đƣợc tạo nên bởi phần tử cuối xâu ký tự.

Tổ chức bộ nhớ của một tiến trình:

Hình 2-13. Tổ chức bộ nhớ của tiến trình

Mỗi tiến trình thực thi đều đƣợc hệ điều hành cấp cho một không gian bộ nhớ ảo (logic) giống nhau. Không gian nhớ này gồm 3 vùng text, data và stack.

Text: Là vùng cố định, chứa các mã lệnh thực thi (instruction) và dữ liệu chỉ đọc (read-only). Dữ liệu ở vùng này là chỉ đọc, mọi thao tác nhằm ghi lên vùng nhớ này đều gây ra lỗi segmentation và violation.

Data: Chứa các dữ liệu đã đƣợc khởi tạo hoặc chƣa khởi tạo giá trị. Các biến toàn cục và biến tĩnh đƣợc chứa trong vùng này. Vùng data tƣơng ứng với phân đoạn data – bss của tập tin thực thi.

Stack: Là vùng nhớ đƣợc dành riêng khi thực thi chƣơng trình, dùng để chứa giá trị các biến cục bộ của hàm, tham số gọi hàm cũng nhƣ giá trị trả về. Thao tác trên bộ nhớ stack đƣợc thực hiện theo cơ chế LIFO (Last In, First Out) với hai lệnh quan trọng nhất là Push và Pop.

- Stack (ngăn xếp):

Trong khoa học máy tính, một ngăn xếp (stack) là một cấu trúc dữ liệu trừu tƣợng hoạt động theo nguyên lý “vào sau ra trƣớc” (Last In First Out (LIFO)).

Hình 2.14: Hoạt động của dữ liệu trên Stack. + Kiểu dữ liệu trên Stack:

Một ngăn xếp là một cấu trúc dữ liệu dạng thùng chứa (container) của các phần tử (thƣờng đƣợc gọi là các nút (node)) và có hai phép toán cơ bản đó là push và pop.

Push bổ sung một phần tử vào đỉnh (top) của Stack, nghĩa là sau các phần tử đã có trong ngăn xếp.

Pop sẽ lấy phần tử đang đứng ở đỉnh của Stack ra. Trong stack, các đối tƣợng có thể đƣợc thêm vào bất kỳ lúc nào nhƣng chỉ có đối tƣợng thêm vào sau cùng mới đƣợc phép lấy ra.

Ngoài ra, stack cũng hỗ trợ một số phép toán khác nhƣ:

isEmpty(): Kiểm tra xem stack có rỗng không.

Top(): Trả về giá trị của phần tử nằm ở đầu stack mà không hủy nó khỏi stack. Nếu stack rỗng thì trả về biến lỗi.

+ Các phép toán trên Stack:

Trong ngôn ngữ máy tính hiện nay, một ngăn xếp thƣờng đƣợc dùng với các phép toán “push” và “pop”. Độ dài (số phần tử) của ngăn xếp cũng là một tham số cần thiết của nó.

Sau đây là một đoạn mã giả mô tả một số phép toán trên Stack (ngăn xếp).

. Định nghĩa một nút (node) của Stack

record Node {

data; // dữ liệu đƣợc lƣu trữ trong một nút (node) next; // tham chiếu đến nút kế tiếp;

// nếu là nút cuối thì next = NULL }

. Định nghĩa một con trỏ Stack

record Stack {

Node stackPointer; // trỏ đến nút đỉnh của Stack; // trả về giá trị NULL nếu Stack rỗng.

}

. Định nghĩa hàm push để đưa dữ liệu vào Stack

function push (Stack stack, Node newNode) {

newNode.next := Stack.stackPointer; stack.stackPointer := newNode; }

. Định nghĩa hàm pop để lấy dữ liệu từ Stack ra

function pop (Stack stack) {

node := stack.stackPointer;

stack.stackPointer := stack.stackPointer.next; return node;

}

. Định nghĩa hàm peak để lấy giá trị từ đỉnh Stack

function peak (Stack stack) {

return stack.stackPointer; }

. Định nghĩa hàm length để tính độ dài của Stack

function length (Stack stack)

{ // trả về số lƣợng nút (node) của Stack length := 0;

node := stack.stackPointer; while node not null

{

length := length + 1; node := node.next; }

}

Hàm và gọi hàm:

Để tìm hiểu về quá trình gọi hàm của chƣơng trình, chúng ta hãy phân tích đoạn mã sau:

/*fct.c*/

void toto (int i, int j) { char str[5] = ―abcde‖; int k = 3; j = 0; return; }

int main (int argc, char **argv) { int i = 1; toto(1, 2); i = 0; printf(―i = %d\n‖, i); }

Quá trình gọi hàm có thể đƣợc chia thành 3 bƣớc nhƣ sau:

+ Khởi tạo:

Trƣớc khi chuyển thực thi cho một hàm, chƣơng trình cần thực hiện một số công việc nhƣ: lƣu lại trạng thái hiện tại của stack, cấp phát vùng nhớ cần thiết để thực thi.

+ Gọi hàm:

Khi hàm đƣợc gọi, các tham số đƣợc truyền vào stack và con trỏ lệnh (instruction pointer) đƣợc lƣu lại để cho phép chuyển quá trình thực thi đến đúng điểm gọi hàm.

+ Kết thúc:

Khôi phục lại trạng thái nhƣ trƣớc khi gọi hàm.

. Khởi tạo:

Một hàm luôn đƣợc khởi đầu với các lệnh máy sau: Push %ebp

Mov %esp,%ebp

Sub $0xNN,%esp // giá trị 0xNN phụ thuộc vào từng hàm cụ thể.

Ba lệnh trên đƣợc gọi là bƣớc khởi tạo của hàm. Hình 2.15 sau giải thích bƣớc khởi đầu của hàm toto() và giá trị của các thanh ghi %esp, %ebp .

Hình 2-15. Bước khởi tạo của hàm.

Quá trình khởi tạo của hàm sẽ thông qua 3 bƣớc sau:

. Thiết lập môi trường:

Lệnh thứ hai sẽ thiết lập một môi trƣờng mới bằng cách đặt %ebp trỏ đến đỉnh của stack (giá trị đầu tiên của 1 stack). Lúc này %ebp và %esp sẽ cùng trỏ đến một vị trí có địa chỉ là (Y-1word).

Hình 2-16. Hai thanh ghi cùng trỏ đến một địa chỉ. . Cấp phát vùng nhớ cho biến cục bộ:

Lệnh thứ ba cấp phát vùng nhớ cho biến cục bộ. Mảng ký tự có độ dài 5 byte. Tuy nhiên, stack sử dụng đơn vị lƣu trữ là word, do đó vùng nhớ đƣợc cấp cho mảng ký tự sẽ là một bội số của word. Dễ thấy giá trị đó là 8 byte (2 word). Biến k kiểu nguyên có kích thƣớc 4 byte, vì vậy kích thƣớc vùng nhớ dành cho biến cục bộ sẽ là 8 + 4 = 12 byte (3 word), đƣợc cấp phát bằng cách giảm %esp đi một giá trị 0xC (bằng 12 trong hệ cơ số 16).

Hình 2-17. Lệnh thứ ba được thực hiện.

Một phần của tài liệu Nghiên cứu cơ chế thâm nhập hệ thống máy tính thông qua lỗ hổng bảo mật và ứng dụng trong công tác bảo đảm an ninh mạng (Trang 38 - 45)