Exploit

Memory Layout and Stack Buffer Overflow

category
Exploit
date
Feb 18, 2022
slug
memory-layout-and-stack-buffer-overflow
author
status
Public
tags
binary analysis
exploit
summary
Giải ngố về Memory Layout và lỗi Stack Buffer Overflow
type
Post
thumbnail
updatedAt
Sep 9, 2023 06:12 PM
💡
Giải ngố về Memory Layout và lỗi Stack Buffer Overflow

1. Phân bổ bộ nhớ trong Hệ điều hành 32-bits

Hệ điều hành 32-bits chỉ hỗ trợ tối đa 4GB RAM. Phân bổ bộ nhớ như sau:
  • 1GB cho Kernel Space (Phần địa chỉ cao)
  • 3GB cho User Space (Phần địa chỉ thấp)
notion image

2. Tổ chức bộ nhớ của một chương trình

Một chương trình thông thường sẽ được cấp phát vùng nhớ ở User Space và vùng nhớ này có thể được chia nhỏ hơn thành các đoạn con: Text/Code, Data, BSS, Heap, Stack,..
notion image
Đoạn Text/Code: Thường nằm ở đầu tiên (thấp nhất) so với các đoạn còn lại mục đích để tránh bị tràn vào từ đoạn dữ liệu. Chứa code thực thi đã biên dịch. Quyền trên đoạn này là Read-only/Execute
Đoạn Data: Lưu dữ liệu (các biến, hằng) đã được khởi tạo giá trị. Đoạn này đôi khi còn chia ra thành Read-only Data và Read-Write Data. Lấy ví dụ:
char s[] = "hello world"; int debug = 1;
⇒ Những biến này sẽ được lưu vào Read-Write Data
const char* str = "hello world";
⇒ Biến này sẽ được lưu vào Read-only Data
Đoạn BSS: Lưu dữ liệu (các biến, hằng) chưa được khởi tạo giá trị. Phân đoạn này có quyền Read-Write.
Đoạn Heap
  • Sử dụng khi cấp phát bộ nhớ động, do người lập trình yêu cầu cho các biến không xác định kích thước tại thời điểm chạy.
  • Đoạn này thường nằm sau (trên) BSS và trước (dưới) Stack.
  • Heap sẽ lớn dần lên đến các địa chỉ bộ nhớ cao hơn.
  • Ví dụ cấp phát động:  malloc/new
Đoạn Stack
  • Thường nằm ở cuối cùng (cao nhất/trên cùng) so với các đoạn trước đó. Cụ thể là nằm sau (trên) Heap và trước (dưới) Kernel Space.
  • Cấp phát theo các Stack Frame - SF, các SF tuân theo cơ chế LIFO
  • Stack sẽ lớn dần lên đến các địa chỉ bộ nhớ thấp hơn ⇒ TỨC LÀ NGƯỢC VỚI HEAP
  • Stack Frame: Lưu dữ liệu cần thiết khi gọi một hàm (biến cục bộ, tham số truyền vào hàm, địa chỉ trở về của hàm,..)
  • Mỗi một hàm có thể coi như một SF, mỗi khi gọi hàm, thì một SF mới lại được cấp phát ở đầu ngăn xếp
  • Thanh ghi con trỏ ngăn xếp (Stack Pointer - SP) được điểu chỉnh liên tục cứ mỗi khi có SF mới đc cấp phát hay thu hồi và nó luôn trỏ vào đỉnh Stack, hay SF mới nhất đc cấp phát
  • Khi Stack Pointer gặp Heap Pointer hoặc nó đạt đến giới hạn RLIMIT_STACK ⇒ Hết bộ nhớ Stack
 
Cấp phát bộ nhớ cho các Stack Frame
Cấp phát bộ nhớ cho các Stack Frame
Bên trong một Stack Frame
Bên trong một Stack Frame

3. Tràn bộ đệm trên Stack

Lỗi này xảy ra khi dữ liệu đầu vào không được kiểm tra, xác minh trước khi đem đi xử lý dẫn đến việc ghi đè lên những vùng dữ liệu quan trọng khác.
Bên trong một Stack Frame, dữ liệu được mô tả như sau:
notion image
Theo minh họa trên thì ta có thứ tự các vùng nhớ con bên trong một stack frame với chiều giảm dần của địa chỉ (high → low) như sau:
notion image
Như vậy khi xảy ra tràn bộ đệm trên ngăn xếp, chúng ta hiểu rằng dữ liệu tại vùng buffer space đang có vấn đề, cụ thể là dữ liệu của một biến nào đó đã “lấn chiếm/ghi đè” sang vùng nhớ lân cận của biến khác gây sai lệch về dữ liệu so với ban đầu trước khi ghi đè.
Một đặc điểm quan trọng cần lưu ý đó là các vùng nhớ con bên trong một stack frame lần lượt được phân bổ theo chiều địa chỉ giảm dần (high -> low), điều này đồng nghĩa rằng bên trong một hàm: Biến nào được khai báo sau thì biến đó nằm ở vị trí có địa chỉ THẤP hơn biến khai báo trước.
Cụ thể hãy xem ví dụ sau:
#include <stdio.h> #include <string.h> void foo() { int a = 123; char b[16]; int c = 456; int d = 789; printf("address of a = %d\n", &a); printf("address of b = %d\n", &b); printf("address of c = %d\n", &c); printf("address of d = %d\n", &d); } int main(int argc, char const *argv[]) { foo(); return 0; }
Biên dịch sau đó chạy chương trình và quan sát kết quả:
$ gcc.exe -o bof.exe bof.c $ bof.exe address of a = 6356668 address of b = 6356652 address of c = 6356648 address of d = 6356644
💡
Phiên bản GCC sử dụng: gcc.exe (i686-posix-sjlj-rev0, Built by MinGW-W64 project) 8.1.0. Có sẵn tại: https://sourceforge.net/projects/mingw-w64/files/
Như vậy nếu xảy ra tràn bộ đệm ở biến b với kích thước dữ liệu vượt quá 16 bytes thì kể từ byte thứ 17 trở đi nó sẽ ghi đè lên vùng nhớ của biến a (biến được khai báo trước nó), trong khi biến cd (khai báo sau đó) không bị ảnh hưởng. Xem minh họa dưới đây: b ghi đè 2 bytes sang a
notion image
Nghiêm trọng hơn, nếu dữ liệu ghi đè vượt quá vùng buffer space sau đó tràn sang cả vùng return address thì lúc này hacker coi như đã kiểm soát được EIP và điều khiển được luồng thực thi của chương trình về đoạn shellcode mà hacker chuẩn bị sẵn.
Xem minh họa dưới đây để thấy hacker kiểm soát được EIP:
notion image

4. Tham khảo