intTypePromotion=1
zunia.vn Tuyển sinh 2024 dành cho Gen-Z zunia.vn zunia.vn
ADSENSE

KỸ THUẬT KHAI THÁC LỖI TRÀN TRONG BỘ ĐỆM

Chia sẻ: Ai Dieu | Ngày: | Loại File: DOC | Số trang:66

284
lượt xem
77
download
 
  Download Vui lòng tải xuống để xem tài liệu đầy đủ

Loạt bài viết này trình bày về tràn bộ đệm (buffer overflow) xảy ra trên stack và kỹ thuật khai thác lỗi bảo mật phổ biến nhất này. Kỹ thuật khai thác lỗi tràn bộ đệm (buffer overflow exploit) được xem là một trong những kỹ thuật hacking kinh điển nhất.

Chủ đề:
Lưu

Nội dung Text: KỸ THUẬT KHAI THÁC LỖI TRÀN TRONG BỘ ĐỆM

  1. ---------- BÀI TIỂU LUẬN KỸ THUẬT KHAI THÁC LỖI TRÀN BỘ ĐỆM
  2. KỸ THUẬT KHAI THÁC LỖI TRÀN BỘ ĐỆM trang này đã được đọc lần Tóm tắt : Loạt bài viết này trình bày về tràn bộ đệm (buffer overflow) xảy ra trên stack và kỹ thuật khai thác lỗi bảo mật phổ biến nhất này. Kỹ thuật khai thác lỗi tràn bộ đệm (buffer overflow exploit) được xem là một trong những kỹ thuật hacking kinh điển nhất. Bài viết được chia làm 2 phần: Phần 1: Tổ chức bộ nhớ, stack, gọi hàm, shellcode. Giới thiệu tổ chức bộ nhớ của một tiến trình (process), các thao tác trên bộ nhớ stack khi gọi hàm và kỹ thuật cơ bản để tạo shellcode - đoạn mã thực thi một giao tiếp dòng lệnh (shell). Phần 2: Kỹ thuật khai thác lỗi tràn bộ đệm. Giới thiệu kỹ thuật tràn bộ đệm cơ bản, tổ chức shellcode, xác định địa chỉ trả về, địa chỉ shellcode, cách truyền shellcode cho chương trình bị lỗi. Các chi tiết kỹ thuật minh hoạ ở đây được thực hiện trên môi trường Linux x86 (kernel 2.2.20, glibc-2.1.3), tuy nhiên về mặt lý thuyết có thể áp dụng cho bất kỳ môi trường nào khác. Người đọc cần có kiến thức cơ bản về lập trình C, hợp ngữ (assembly), trình biên dịch gcc và công cụ gỡ rối gdb (GNU Debugger). Nếu bạn đã biết kỹ thuật khai thác lỗi tràn bộ đệm qua các tài liệu khác, bài viết này cũng có thể giúp bạn củng cố lại kiến thức một cách chắc chắn hơn. Phần 1: Tổ chức bộ nhớ, stack, gọi hàm, shellcode Mục lục :  Giới thiệu  1. Tổ chức bộ nhớ o 1.1 Tổ chức bộ nhớ của một tiến trình (process) o 1.2 Stack  2. Gọi hàm o 2.1 Giới thiệu o 2.2 Khởi đầu o 2.3 Gọi hàm o 2.3 Kết thúc  3. Shellcode
  3. 3.1 Viết shellcode trong ngôn ngữ C o 3.2 Giải mã hợp ngữ các hàm o 3.3 Định vị shellcode trên bộ nhớ o 3.4 Vấn đề byte giá trị null o 3.5 Tạo shellcode o Giới thiệu Để tìm hiểu chi tiết về lỗi tràn bộ đệm, cơ chế hoạt động và cách khai thác lỗi ta hãy bắt đầu bằng một ví dụ về chương trình bị tràn bộ đệm. /* vuln.c */ int main(int argc, char **argv) { char buf[16]; if (argc>1) { strcpy(buf, argv[1]); printf("%s\n", buf); } } [SkZ0@gamma bof]$ gcc -o vuln -g vuln.c [SkZ0@gamma bof]$ ./vuln AAAAAAAA // 8 ký tự A (1) AAAAAAAA [SkZ0@gamma bof]$ ./vuln AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA A // 24 ký tự A (2) AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA A Segmentation fault (core dumped) Chạy chương trình vuln với tham số là chuỗi dài 8 ký tự A (1), chương trình hoạt động bình thường. Với tham số là chuỗi dài 24 ký tự A (2), chương trình bị lỗi Segmentation fault. Dễ thấy bộ
  4. đệm buf trong chương trình chỉ chứa được tối đa 16 ký tự đã bị làm tràn bởi 24 ký tự A. [SkZ0@gamma bof]$ gdb vuln -c core -q Core was generated by `./vuln AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA A'. Program terminated with signal 11, Segmentation fault. Reading symbols from /lib/libc.so.6...done. Reading symbols from /lib/ld-linux.so.2...done. #0 0x41414141 in ?? () (gdb) info register eip eip 0x41414141 1094795585 (gdb) Thanh ghi eip - con trỏ lệnh hiện hành - có giá trị 0x41414141, tương đương 'AAAA' (ký tự A có giá trị 0x41 hexa). Ta thấy, có thể thay đổi giá trị của thanh ghi con trỏ lệnh eip bằng cách làm tràn bộ đệm buf. Khi lỗi tràn bộ đệm đã xảy ra, ta có thể khiến chương trình thực thi mã lệnh tuỳ ý bằng cách thay đổi con trỏ lệnh eip đến địa chỉ bắt đầu của đoạn mã lệnh đó. Để hiểu rõ quá trình tràn bộ đệm xảy ra như thế nào, chúng ta sẽ xem xét chi tiết tổ chức bộ nhớ, stack và cơ chế gọi hàm của một chương trình. 1. Tổ chức bộ nhớ 1.1 Tổ chức bộ nhớ của một tiến trình (process)
  5. to chuc bo nho 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. Ý nghĩa của 3 vùng này như sau: Vùng 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). Vùng này được chia sẻ giữa các tiến trình thực thi cùng một file chương trình và tương ứng với phân đoạn text của file thực thi. 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 lỗi segmentation violation. Vùng 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 file thực thi. Vùng 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 thao tác theo cơ chế "vào sau ra trước" - LIFO (Last In, First Out) với hai lệnh quan trọng nhất là PUSH và POP. Trong phạm vi bài viết này, chúng ta chỉ tập trung tìm hiểu về vùng stack. 1.2 Stack Stack là một kiểu cấu trúc dữ liệu trừu tượng cấp cao được dùng cho các thao tác đặc biệt dạng LIFO. Tổ chức của vùng stack gồm các stack frame được push vào khi gọi một hàm và pop ra khỏi stack khi trở về. Một stack frame chứa các thông số cần thiết cho một hàm: biến cục bộ, tham số hàm, giá
  6. trị trả về; và các dữ liệu cần thiết để khôi phục stack frame trước đó, kể cả giá trị của con trỏ lệnh (instruction pointer) vào thời điểm gọi hàm. Địa chỉ đáy của stack được gán một giá trị cố định. Địa chỉ đỉnh của stack được lưu bởi thanh ghi "con trỏ stack" (ESP – extended stack pointer). Tuỳ thuộc vào hiện thực, stack có thể phát triển theo hướng địa chỉ nhớ từ cao xuống thấp hoặc từ thấp lên cao. Trong các ví dụ về sau, chúng ta sử dụng stack có địa chỉ nhớ phát triển từ cao xuống thấp, đây là hiện thực của kiến trúc Intel. Con trỏ stack (SP) cũng phụ thuộc vào kiến trúc hiện thực. Nó có thể trỏ đến địa chỉ cuối cùng trên đỉnh stack hoặc địa chỉ vùng nhớ trống kế tiếp trên stack. Trong các minh hoạ về sau (với kiến trúc Intel x86), SP trỏ đến địa chỉ cuối cùng trên đỉnh stack. Về lý thuyết, các biến cục bộ trong một stack frame có thể được truy xuất dựa vào độ dời (offset) so với SP. Tuy nhiên khi có các thao tác thêm vào hay lấy ra trên stack, các độ dời này cần phải được tính toán lại, làm giảm hiệu quả. Để tăng hiệu quả, các trình biên dịch sử dụng một thanh ghi thứ hai gọi là "con trỏ nền" (EBP – extended base pointer) hay còn gọi là "con trỏ frame" (FP – frame pointer). FP trỏ đến một giá trị cố định trong một stack frame, thường là giá trị đầu tiên của stack frame, các biến cục bộ và tham số được truy xuất qua độ dời so với FP và do đó không bị thay đổi bởi các thao tác thêm/bớt tiếp theo trên stack. Đơn vị lưu trữ cơ bản trên stack là word, có giá trị bằng 32 bit (4 byte) trên các CPU Intel x86. (Trên các CPU Alpha hay Sparc giá trị này là 64 bit). Mọi giá trị biến được cấp phát trên stack đều có kích thước theo bội số của word. Thao tác trên stack được thực hiện bởi 2 lệnh máy:  push value: đưa giá trị ‘value’ vào đỉnh của stack. Giảm giá trị của %esp đi 1 word và đặt giá trị ‘value’ vào word đó.  pop dest: lấy giá trị từ đỉnh stack đưa vào ‘dest’. Đặt giá trị trỏ bởi %esp vào ‘dest’ và tăng giá trị của %esp lên 1 word. 2. Hàm và gọi hàm 2.1 Giới thiệu
  7. Để giải thích hoạt động của chương trình khi gọi hàm, chúng ta sẽ sử dụng đoạn chương trình ví dụ 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 làm 3 bước: 1. Khởi đầu (prolog): trước khi chuyển thực thi cho một hàm cần chuẩn bị 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. 2. Gọi hàm (call): khi hàm được gọi, các tham số được đặt vào stack và con trỏ lệnh (IP – instruction pointer) được lưu lại để cho phép chuyển quá trình thực thi đến đúng điểm sau gọi hàm. 3. Kết thúc (epilog): khôi phục lại trạng thái như trước khi gọi hàm. 2.2 Khởi đầu 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ể)
  8. 3 lệnh máy này được gọi là bước khởi đầu (prolog) của hàm. Hình 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 1: Bước khởi đầu của hàm Giả sử k hoi dau ban đầu %ebp trỏ đến địa chỉ X bất kỳ trên bộ nhớ, %esp trỏ đến một địa chỉ Y thấp hơn bên dưới. Trước khi chuyển vào một hàm, cần phải lưu lại môi trường của stack frame hiện tại, do mọi giá trị trong
  9. một stack frame đều có thể được tham khảo qua %ebp, ta chỉ cần lưu %ebp là đủ. Vì %ebp được push vào stack, nên %esp sẽ giảm đi 1 word. Giá trị %ebp được push vào stack này được gọi là "con trỏ nền bảo lưu"
  10. (SFP - saved frame pointer). Lệnh thiet lap moi truong máy 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 một stack frame), lúc này %ebp và %esp sẽ trỏ cùng đến một vị trí có địa chỉ là (Y- 1word).
  11. Lệnh c ap phat v ung nho cho bien cuc bo máy thứ ba cấp phát vùng nhớ dà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 sao cho lớn hơn hoặc bằng kích
  12. thước của mảng. 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
  13. hệ cơ số 16). Một điều cần lưu ý ở đây là biến cục bộ luôn có độ dời âm so với con trỏ nền %ebp. Lệnh máy thực hiện phép gán i=0 trong hàm main() có thể minh hoạ điều này. Mã hợp ngữ dùng định vị gián tiếp để xác định vị trí của i: movl $0x0,0xfffffffc(%ebp) 0xfffffffc tương đương giá trị số nguyên bằng –4. Lệnh trên có nghĩa: đặt giá trị 0 vào biến ở địa chỉ có độ dời “-4” byte so với thanh ghi %ebp. i là biến đầu tiên trong hàm main() và có địa chỉ cách 4 byte ngay dưới %ebp. 2.3 Gọi hàm Cũng giống như bước khởi đầu, bước này cũng chuẩn bị môi trường cho phép nơi gọi hàm truyền các tham số cho hàm được gọi và trở về lại nơi gọi hàm khi kết thúc. Hình 2 : Gọi hàm Trước khi d at cac tham so len stack gọi hàm các tham số sẽ được đặt vào stack, theo thứ tự ngược lại, tham số cuối cùng sẽ được đặt vào trước. Trong ví dụ trên, trước tiên các giá trị
  14. 1 và 2 sẽ được đặt vào stack. Thanh ghi %eip giữ giá trị địa chỉ của lệnh kế tiếp, trong trường hợp này là chỉ thị gọi hàm. Khi thực g oi ham hiện lệnh call, %eip sẽ lấy giá trị địa chỉ của kế tiếp ngay sau gọi hàm (trên hình vẽ, giá trị này là Z+5 do lệnh gọi hàm chiếm 5 byte theo hiện thực của CPU Intel x86). Lệnh call sau đó sẽ
  15. lưu lại giá trị của %eip để có thể tiếp tục thực thi sau khi trở về. Quá trình này được thực hiện bằng một lệnh ngầm (không tường minh) đặt %eip lên stack: push %eip Giá trị lưu trên stack này được gọi là "con trỏ lệnh bảo lưu" (SIP – save instruction pointer), hay "địa chỉ trả về" (RET – return address). Giá trị được
  16. truyền như một tham số cho lệnh call chính là địa chỉ của lệnh khởi đầu (prolog) đầu tiên của hàm toto(). Giá trị này sẽ được chép vào %eip và trở thành lệnh được thực thi tiếp theo. Lưu ý rằng khi ở bên trong một hàm, các tham số và địa chỉ trả về có độ dời dương (+) so với con trỏ nền %ebp. Lệnh máy thực hiện phép gán j=0 minh hoạ điều này. Mã hợp ngữ sử dụng định vị gián tiếp để truy xuất biến j: movl $0x0,0xc(%ebp) 0xc có giá trị số nguyên bằng 12. Lệnh trên có nghĩa: đặt giá trị 0 vào biến ở địa chỉ có độ dời “+12” byte so với %ebp. j là tham số thứ 2 của hàm toto() và có địa chỉ cách 12 byte ngay trên %ebp (4 cho RET, 4 cho tham số đầu tiên và 4 cho tham số thứ 2). 2.4 Kết thúc Thoát khỏi một hàm được thực hiện trong 2 bước. Trước tiên, môi trường tạo ra cho hàm thực thi cần được "dọn dẹp" (nghĩa là khôi phục giá trị cho %ebp và %eip). Sau đó, chúng ta phải kiểm tra stack để lấy các thông tin liên quan đến hàm vừa thoát ra.
  17. Bước thứ nhất được thực hiện trong bên trong hàm với 2 lệnh: leave ret Bước kế tiếp được thực hiện nơi gọi hàm sẽ "dọn dẹp" vùng stack dùng chứa các tham số của hàm được gọi. Chúng ta sẽ tiếp tục ví dụ trên với hàm toto(). Hình 3 : Trở về Ở đây truoc k hi goi ham chúng ta mô tả lại đầy đủ hơn tình huống ban đầu, trước lệnh call và bước khởi đầu (prolog). Trước khi lệnh call xảy ra, %ebp ở địa chỉ X và %esp ở địa chỉ Y trên stack. Bắt đầu từ Y, chúng ta sẽ cấp
  18. phát các vùng nhớ dành cho tham số, giá trị bảo lưu của %eip và %ebp, và vùng nhớ dành cho các biến cục bộ của hàm. Lệnh sẽ được thực thi kế tiếp là leave, lệnh này tương đương với 2 lệnh sau: mov %ebp, %esp pop %ebp
  19. Lệnh leav e đầu tiên sẽ đưa %esp và %ebp trỏ đến cùng vị trí hiện tại của %ebp. Lệnh thứ hai lấy ra giá trị trên đỉnh stack đặt vào thanh ghi %ebp. Ta thấy, sau lệnh leave, stack trở lại trạng thái như trước khi xảy ra bước khởi đầu (prolog).
  20. Lệnh ret r et sẽ khôi phục giá trị %eip để nơi gọi hàm trở lại tiếp tục thực thi lệnh kế, là lệnh ngay sau hàm vừa thoát ra. Để làm điều này, giá trị ngay trên đỉnh stack sẽ được lấy ra đặt vào thanh ghi %eip. Chúng ta vẫn chưa trở lại được tình trạng ban đầu do các
ADSENSE

CÓ THỂ BẠN MUỐN DOWNLOAD

 

Đồng bộ tài khoản
2=>2