Basic Linux Exploit – Buffer Overflow – Phần 1 – Giới thiệu về stack và lỗi buffer overflow

Tram Ho

Chào mọi người, đây là bài viết đầu tiên của mình mở đầu series binary exploit trên linux. Bài viết đầu tiên của mình sẽ tập trung giới thiệu về cấu trúc của stack, lỗi buffer overflow và cách khai thác cơ bản. Từ các bài viết sau sẽ dần nâng cao độ khó, bypass các phương pháp chống khai thác trên linux.

1. Stack và các bước khi một lệnh gọi hàm được thực hiện

Series này mình sẽ không đi sâu vào stack, hay khái niệm và các chức năng của các thanh ghi, nhưng kiến thức này hiện tại trên google đã có rất nhiều bài viết, mọi người có thể tìm hiểu đọc thêm trước khi đọc bài viết của mình. Trong bài này, mình thực hiện giới thiệu và khai thác trên hệ thông 32 bits.

Đầu tiên, khi một lệnh gọi hàm được thực hiện, có 3 bước sẽ được thực hiện:

  • Đặt các tham số của hàm vào stack theo thứ tự ngược lại. Ví dụ void func(int a, int b)
    được đặt vào stack theo thứ tự b rồi đến a.
  • Đặt con trỏ EIP vào stack, đây được coi là return address để khi hàm thực hiện xong, chương trình sẽ biết được lệnh tiếp theo để thực hiện
  • Lệnh gọi hàm được gọi (call function)

Sau khi hàm được gọi, nó có trách nhiệm thực hiện lần lượt các nhiệm vụ sau:

  • Lưu thanh ghi EBP hiện tại vào stack
  • Lưu ESP vào EBP
  • Giảm EBP để tạo khoảng trống lưu trữ biến cục bộ của hàm vào stack

=> Quá trình này được gọi là function prolog

Ví dụ về function prolog: (assembly theo cấu trúc intel)

Sau khi hàm thực hiện xong, esp tăng đến ebp để xóa stack (xóa vùng bộ nhớ ban đầu đã tạo), eip đã lưu được lấy ra để thực hiện lệnh tiếp theo

=> quá trình này được gọi là function epilog
Ví dụ:

Trong đó, lệnh leave có dạng như sau:

Lệnh ret chính là lấy giá trị eip trong stack ra.
Dưới đây là ví dụ 1 stack fram khi thực hiện 1 lệnh gọi hàm func(int value1, int value2)

Với các bước được thực hiện khi lệnh gọi hàm thực hiện, và kết thúc, con trỏ esp và ebp luôn được đưa về đúng giá trị ban đầu và chương trình luôn biết lệnh tiếp theo phải thực hiện sau khi kết thúc function.

2. Debug với gdb (gef) và cách khai thác lỗi buffer overflow đơn giản

Mình có 1 file victim.c đơn giản như sau:

Sau khi đọc qua chương trình, mình nhận ra chương trình sử dụng hàm strcpy (copy chuỗi), một lỗ hổng của hàm này là không quy định số kí tự copy, vì vậy, nếu mảng array chứa tối đa 64 kí tự, mà argv[1] lớn hơn 64 kí tự thì hàm vẫn thực hiện, từ đó gây ra lỗi trong stack.

Sau khi lệnh gọi hàm main được gọi, stack sẽ có dạng như sau:

Mình tiến hành compile chương trình bằng gcc với các option như sau:

gcc -m32 -z execstack -mpreferred-stack-boundary=2 -fno-stack-protector victim.c -o victim

Trong đó:

  • -m32: biên dịch chương trình 32 bits
  • -z execstack: cho phép thực thi trong ngăn xếp
  • -mpreferred-stack-boundary=2 : Sử dụng DWORD size stack
  • fno-stack-protector: tắt stack canary

Ở đây mình đã tắt hết các cơ chế bảo vệ stack, phần sau của series thực hiện bypass các cơ chế này mình sẽ nói chi tiết hơn.

Đồng thời, các bạn cần tắt chế độ ALSR (Address Space Layout Randomization) trên linux. Ở phần nâng cao mình sẽ nói về cách bypass ALSR.

echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

Tiến hành gỡ lỗi bằng gdb (gef) (link github gef: https://github.com/hugsy/gef)

Tạo breakpoint tại main

Chạy chương trình với đối số: “test”:

Đây là các thông tin trước khi chương trình chạy vào hàm main, chú ý vào phần assembly, ta lấy function prolog, như sau:

Đây là lý do vì sao trong phần minh họa ở trên, mình có thêm 4 bytes của thanh ebx. Như vậy theo tính toán, để ghi đè thanh ghi eip, nhằm chuyển hướng đến shell code sau khi function main thực hiện xong, ta cần số bytes là:

Tiến hành chạy chương trình với input 76 bytes:

Kết quả nhận được như sau:

Trong đó 0x42 tương ứng với B trong ascii. Vậy tính toán của chúng ta đã chính xác. Nhờ vào việc kiểm soát con trỏ eip, giờ đây có thể chuyển hướng chương trình tùy thích. Mình sẽ thực hiện chuyển hướng đến 1 shellcode trong bài viết tiếp theo.

Chia sẻ bài viết ngay

Nguồn bài viết : Viblo