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.
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.