Phân tích Pattern và Chuẩn bị Blacklist cho SQL Injection
Chúng ta cần xác định các chuỗi ký tự độc hại điển hình thường xuất hiện trong các cuộc tấn công SQL Injection trước khi xây dựng logic lọc.
Mục tiêu là trích xuất các pattern như 'UNION SELECT', 'DROP TABLE', 'OR 1=1', 'EXEC', hoặc các ký tự đặc biệt nguy hiểm như dấu nháy đơn ' và dấu chấm phẩy ;.
Để eBPF có thể quét hiệu quả, chúng ta sẽ lưu trữ các pattern này vào một BPF Map dạng Hash (BPF_MAP_TYPE_HASH) với khóa là số nguyên (ID) và giá trị là chuỗi pattern.
Việc này giúp giảm overhead so với việc quét toàn bộ text dài trong kernel space, chỉ cần so khớp từng từ khóa đã biết.
1. Xây dựng danh sách từ khóa đen (Blacklist)
Tạo file cấu hình chứa danh sách các chuỗi độc hại cần chặn. File này sẽ được đọc bởi program C của chúng ta khi khởi tạo map.
Đường dẫn: /etc/ebpf-sql-blacklist.conf
Định dạng: Mỗi dòng là một pattern, phân cách bằng dấu tab.
cat > /etc/ebpf-sql-blacklist.conf
Kết quả mong đợi: File được tạo thành công, chứa 17 dòng pattern cần lọc.
Verify bằng lệnh:
wc -l /etc/ebpf-sql-blacklist.conf
Triển khai eBPF Program để Scan Buffer
Chúng ta sẽ viết một chương trình C sử dụng BCC (BPF Compiler Collection) để hook vào điểm gọi hệ thống (syscall) liên quan đến việc gửi dữ liệu vào socket hoặc mở file database.
Chương trình sẽ quét buffer của syscall `sendto` hoặc `write` trước khi dữ liệu được chuyển đi.
Logic: Nếu buffer chứa bất kỳ pattern nào trong blacklist -> Kích hoạt action chặn.
2. Code nguồn eBPF (C)
File này sẽ chứa logic kernel-space để đọc memory và so khớp với map.
Đường dẫn: /root/ebpf-sql-protect/sqli_filter.c
Chú ý: Sử dụng `bpf_probe_read_user` để an toàn khi đọc memory của user-space. Dùng `bpf_strncmp` để so sánh.
cat > /root/ebpf-sql-protect/sqli_filter.c orig_ax;
char pattern[MAX_PATTERN_LEN];
int ret;
int id;
char *val;
// Duyệt qua tất cả các pattern trong blacklist_map
for (id = 1; id < MAX_PATTERNS; id++) {
val = bpf_map_lookup_elem(&blacklist_map, &id);
if (!val) continue;
// Đọc pattern từ map
ret = bpf_probe_read_kernel(&pattern, sizeof(pattern), val);
if (ret) continue;
// So sánh pattern với buffer
// Lưu ý: bpf_strncmp so sánh tối đa MAX_PATTERN_LEN byte
if (bpf_strncmp(buf, MAX_PATTERN_LEN, pattern) == 0) {
// Phát hiện pattern độc hại
(*stats)++;
// Chặn bằng cách trả về lỗi (thường là -1 hoặc 0 tùy context)
// Ở đây ta chỉ đếm, action kill/drop sẽ xử lý ở phần sau
return 0;
}
}
return 0;
}
char LICENSE[] SEC("license") = "GPL";
EOF
Kết quả mong đợi: File C được tạo, sẵn sàng để compile bằng clang.
Verify bằng lệnh kiểm tra cú pháp (cần cài clang):
clang -march=x86_64 -nostdinc -emit-llvm -c -O2 -g -D__BPF__ -I/usr/include/bpf /root/ebpf-sql-protect/sqli_filter.c -o /root/ebpf-sql-protect/sqli_filter.bc
Cấu hình Action và Tối ưu hóa Map
Sau khi phát hiện, chúng ta cần một cơ chế để thực thi hành động (Action). Có hai lựa chọn: Drop kết nối ngay lập tức hoặc Kill process đang thực thi.
Trong môi trường production, việc Kill process có thể gây downtime, nên ưu tiên Drop connection hoặc Log và Alert. Tuy nhiên, để bài này mạnh mẽ, chúng ta sẽ dùng BPF tracepoint để gửi tín hiệu SIGKILL.
Đồng thời, tối ưu bộ nhớ Map để tránh OOM (Out Of Memory) bằng cách giới hạn số lượng entries và kích thước string.
3. Script Python để Load và Quản lý Map
Chúng ta dùng Python với thư viện BCC để load .bc file, populate blacklist map, và thiết lập hook.
Đường dẫn: /root/ebpf-sql-protect/sqli_load.py
Script này sẽ đọc file config đã tạo ở phần 1 và load vào kernel map.
cat > /root/ebpf-sql-protect/sqli_load.py 31:
pattern_str = pattern_str[:31]
# Add vào map
blacklist_map[pattern_id] = pattern_str.encode('utf-8')
print(f"Loaded pattern ID {pattern_id}: {pattern_str}")
def main():
# Load BPF program
b = BPF(source_file=SOURCE_FILE)
# Load blacklist
load_blacklist(b)
# Initialize stats map
b["stats_map"][0] = 0
# Attach kprobe
b.attach_kprobe(event="sys_write", fn_name="bpf_sqli_check")
print("eBPF SQL Injection Filter is running...")
print("Attaching to sys_write syscall.")
print("Press Ctrl+C to stop.")
try:
while True:
# Trong production, ta sẽ không sleep vô hạn mà dùng log
# Ở đây để demo, ta in log nếu có hit
if b["stats_map"][0] > 0:
count = b["stats_map"][0]
print(f"WARNING: {count} potential SQL injection attempts detected!")
# Reset counter để tránh tràn số
b["stats_map"][0] = 0
# sleep nhẹ để không tốn CPU
import time
time.sleep(1)
except KeyboardInterrupt:
print("Detaching and cleaning up...")
if __name__ == "__main__":
main()
EOF
Kết quả mong đợi: Script được tạo, có thể chạy để load program vào kernel.
Verify quyền thực thi:
chmod +x /root/ebpf-sql-protect/sqli_load.py
4. Tối ưu hóa Map để tránh OOM
Vấn đề: Nếu map quá lớn hoặc string quá dài, kernel có thể từ chối load hoặc gây OOM.
Giải pháp: Đặt giới hạn cứng (hard limit) trong code C (`MAX_PATTERN_LEN = 32`, `MAX_PATTERNS = 64`).
Trong script Python, chúng ta cắt ngắn string nếu vượt quá giới hạn.
Điều này đảm bảo bộ nhớ map chỉ chiếm khoảng: 64 * 32 bytes = ~2KB, hoàn toàn an toàn cho production.
Verify Kết quả và Test Tấn công
Để xác nhận hệ thống hoạt động, chúng ta cần chạy program và thực hiện một cuộc tấn công giả lập.
Chạy script Python ở background để monitor syscall.
nohup /root/ebpf-sql-protect/sqli_load.py > /var/log/ebpf-sql.log 2>&1 &
Kết quả mong đợi: Process Python chạy, không có lỗi syntax, log file được tạo.
5. Test Case: Giả lập SQL Injection
Tạo một script shell đơn giản để gọi syscall `write` với dữ liệu độc hại.
Chúng ta sẽ cố gắng "gửi" chuỗi `UNION SELECT` vào một file hoặc pipe.
echo "SELECT * FROM users WHERE id=1 OR 1=1; DROP TABLE users; --" > /tmp/test_injection.txt
Sau đó, đọc file này để kích hoạt syscall `read` và `write` (trong môi trường thực tế, ứng dụng web sẽ gọi `sendto` hoặc `write` vào socket).
Để test trực tiếp hook `sys_write`, chúng ta dùng lệnh `dd` hoặc `cat` để write vào pipe.
echo "UNION SELECT password FROM admin" | cat
Quan sát log:
tail -f /var/log/ebpf-sql.log
Kết quả mong đợi: Khi lệnh echo chạy, log file sẽ xuất hiện dòng "WARNING: 1 potential SQL injection attempts detected!".
Điều này chứng tỏ eBPF đã intercept được buffer chứa từ khóa "UNION" hoặc "SELECT" trong blacklist.
Triển khai Action: Kill Process hoặc Drop Connection
Hiện tại code chỉ đếm. Để chặn thực sự, ta cần thêm logic vào `bpf_sqli_check`.
Tuy nhiên, việc kill process trực tiếp từ kprobe `sys_write` là nguy hiểm vì có thể kill nhầm các process hệ thống hợp lệ nếu pattern quá rộng.
Phương án an toàn hơn: Sử dụng `bpf_printk` để log chi tiết và kích hoạt một user-space program (thông qua BPF perf event) để quyết định kill.
Ở bước này, chúng ta sẽ cập nhật code C để return -1 (lỗi) thay vì 0, buộc syscall thất bại, từ đó gián tiếp chặn dữ liệu đi vào DB.
6. Cập nhật Code C để Chặn (Drop)
Chỉnh sửa file `/root/ebpf-sql-protect/sqli_filter.c`.
Thay đổi phần phát hiện pattern: trả về -1 để syscall `sys_write` trả về lỗi cho ứng dụng.
cat > /root/ebpf-sql-protect/sqli_filter.c orig_ax;
char pattern[MAX_PATTERN_LEN];
int ret;
int id;
char *val;
for (id = 1; id < MAX_PATTERNS; id++) {
val = bpf_map_lookup_elem(&blacklist_map, &id);
if (!val) continue;
ret = bpf_probe_read_kernel(&pattern, sizeof(pattern), val);
if (ret) continue;
if (bpf_strncmp(buf, MAX_PATTERN_LEN, pattern) == 0) {
(*stats)++;
// BLOCK ACTION: Return -1 to fail the write syscall
return -1;
}
}
return 0;
}
char LICENSE[] SEC("license") = "GPL";
EOF
Tiếp theo, cần recompile và reload lại program.
cd /root/ebpf-sql-protect && clang -march=x86_64 -nostdinc -emit-llvm -c -O2 -g -D__BPF__ -I/usr/include/bpf sqli_filter.c -o sqli_filter.bc
Restart script Python để load version mới:
pkill -f sqli_load.py && nohup /root/ebpf-sql-protect/sqli_load.py > /var/log/ebpf-sql.log 2>&1 &
7. Verify Action Chặn
Thực hiện lại test injection. Lần này, nếu chặn thành công, syscall sẽ trả về lỗi và dữ liệu không được ghi vào file hoặc gửi đi.
Test với lệnh write vào file (đây là syscall `write`):
echo "UNION SELECT" > /tmp/test_blocked.txt 2>&1 && echo "Write Success" || echo "Write Failed (Blocked)"
Kết quả mong đợi: Dòng "Write Failed (Blocked)" xuất hiện. File `/tmp/test_blocked.txt` sẽ trống hoặc không được tạo do syscall bị từ chối.
Log file `/var/log/ebpf-sql.log` sẽ tăng count lên.
Điều này chứng minh eBPF đã thành công trong việc chặn dữ liệu độc hại ngay tại kernel level trước khi nó đến ứng dụng database.
Điều hướng series:
Mục lục: Series: Triển khai Database chống tấn công với Linux eBPF và Kernel Audit
« Phần 4: Xây dựng eBPF program đầu tiên để chặn truy cập trái phép
Phần 6: Tích hợp eBPF và Audit vào hệ thống giám sát tập trung »