Phân tích luồng thực thi của các truy vấn SQL độc hại
Trước khi viết chính sách eBPF, ta cần xác định điểm hook chính xác trong kernel hoặc thư viện nơi lệnh SQL được xử lý. Đối với các ứng dụng Database phổ biến như MySQL hoặc PostgreSQL chạy trên Linux, truy vấn SQL thường đi qua hàm `exec` hoặc `send` của socket, hoặc trực tiếp vào hàm parse query của library (như libmysqlclient hoặc libpq).
Tuy nhiên, để chặn tấn công SQL Injection (SQLi) ở mức hệ thống (OS-level) mà không can thiệp vào code ứng dụng, ta sẽ hook vào điểm gọi hệ thống (system call) liên quan đến việc gửi dữ liệu qua mạng, cụ thể là `sys_write` hoặc `sys_sendto`. Tại đây, ta phân tích buffer dữ liệu để tìm các pattern đặc trưng của SQLi như `UNION SELECT`, `OR 1=1`, `DROP TABLE`, hoặc các dấu ngoặc đơn bất thường kèm theo dấu phẩy.
Luồng thực thi: Ứng dụng (Client) -> Socket Buffer -> System Call (write/send) -> Kernel Socket Layer -> eBPF Hook -> Phân tích Pattern -> Quyết định Chặn/Cho phép.
Kết quả mong đợi: Xác định được system call `__sys_write` là điểm hook tối ưu cho việc phân tích payload SQL trước khi nó rời khỏi user-space.
Viết script eBPF (BPF_C) để hook vào hàm xử lý truy vấn
Chúng ta sẽ tạo một file C chứa logic eBPF. Script này sẽ sử dụng BCC (BPF Compiler Collection) để biên dịch. Logic chính bao gồm: hook vào `sys_write`, đọc buffer từ user-space, tìm kiếm các chuỗi nguy hiểm (pattern matching) và ghi log hoặc trả về lỗi để chặn.
File nguồn sẽ được lưu tại: `/usr/local/src/ebpf-sqli-guard/sqli_guard.c`. Nội dung dưới đây sử dụng cấu trúc BCC để hook vào `write` và kiểm tra buffer.
#include "vmlinux.h"
#include
#include
// Định nghĩa cấu trúc để truyền dữ liệu từ eBPF vào user-space
struct event_data {
u32 pid;
u32 tid;
u64 timestamp;
u32 comm[16];
u32 port;
u8 payload[256];
u32 is_malicious;
};
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
__uint(max_entries, 1);
__type(key, u32);
__type(value, struct event_data);
} events SEC("maps");
// Hàm hook vào sys_write
SEC("tracepoint/syscalls/sys_enter_write")
int handle_write(struct trace_event_raw_sys_enter *ctx)
{
u64 pid_tgid = bpf_get_current_pid_tgid();
u32 pid = pid_tgid >> 32;
u32 tid = pid_tgid & 0xffffffff;
// Lấy tên process
char comm[16];
bpf_get_current_comm(&comm, sizeof(comm));
// Lấy pointer đến buffer từ context
void *buffer_ptr = (void *)ctx->args[1];
u64 count = ctx->args[2];
// Kiểm tra nếu count quá lớn hoặc quá nhỏ thì bỏ qua để tránh overhead
if (count > 256 || count == 0)
return 0;
// Khởi tạo event
u32 key = 0;
struct event_data *event = bpf_map_lookup_elem(&events, &key);
if (event == NULL)
return 0;
event->pid = pid;
event->tid = tid;
event->timestamp = bpf_ktime_get_ns();
bpf_probe_read_user(event->comm, sizeof(event->comm), comm);
event->is_malicious = 0;
// Đọc payload từ user-space vào event->payload
// Lưu ý: bpf_probe_read_user có thể fail nếu buffer không hợp lệ, cần xử lý
bpf_probe_read_user(event->payload, count, buffer_ptr);
// Logic phát hiện SQL Injection (Pattern Matching đơn giản)
// Kiểm tra các pattern: "UNION", "SELECT", "DROP", "OR 1=1", "'"
// Đây là logic giả lập, trong thực tế cần dùng automaton hoặc regex phức tạp hơn
// Ở đây ta dùng vòng lặp tìm kiếm byte
u8 *p = event->payload;
u32 len = count;
// Pattern 1: "UNION" (chữ hoa)
if (len >= 5 && p[0] == 'U' && p[1] == 'N' && p[2] == 'I' && p[3] == 'O' && p[4] == 'N') {
event->is_malicious = 1;
}
// Pattern 2: "DROP"
else if (len >= 4 && p[0] == 'D' && p[1] == 'R' && p[2] == 'O' && p[3] == 'P') {
event->is_malicious = 1;
}
// Pattern 3: Dấu ngoặc đơn kép " ' " thường dùng trong injection
else if (len >= 1 && p[0] == '\'') {
// Cần logic phức tạp hơn để tránh false positive, ở đây chỉ cảnh báo
event->is_malicious = 1;
}
// Gửi sự kiện đến user-space
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, event, sizeof(*event));
return 0;
}
// Hàm hook vào sys_exit_write để thực hiện chặn (Drop packet) nếu cần
// Lưu ý: Chặn trực tiếp ở sys_enter_write bằng cách return -1 là cách đơn giản nhất
// Tuy nhiên, để log trước khi chặn, ta dùng tracepoint exit và dùng map để lưu trạng thái
// Ở đây để đơn giản cho tutorial, ta chỉ log trong enter. Để chặn thực sự, ta cần dùng BPF_PROG_TYPE_CGROUP_SKB hoặc TC
// Nhưng với yêu cầu "chặn truy vấn", ta sẽ giả lập việc chặn bằng cách return -1 từ eBPF nếu phát hiện
// Lưu ý: Return -1 từ tracepoint sẽ không tự động chặn sys call, nó chỉ báo lỗi.
// Để chặn thực sự, ta cần dùng BPF_PROG_TYPE_KPROBE hoặc BPF_PROG_TYPE_CGROUP_SKB kết hợp với BPF_MAP_TYPE_CGROUP_SOCK
// Dưới đây là phiên bản chỉ Log. Để chặn, cần cấu hình thêm phần User-space Python script để kill connection hoặc reset socket.
char LICENSE[] SEC("license") = "GPL";
Code trên định nghĩa một BPF program hook vào `sys_enter_write`. Nó đọc buffer, tìm kiếm các pattern đơn giản của SQLi, đánh dấu `is_malicious = 1` và gửi dữ liệu ra user-space.
Kết quả mong đợi: File `.c` được tạo thành công, sẵn sàng để biên dịch bằng `clang` và `llvm-strip` (thành phần của BCC).
Triển khai chính sách eBPF để log và chặn các pattern tấn công cụ thể
Bây giờ ta viết script Python (User-space) để tải chương trình eBPF, xử lý sự kiện và thực hiện hành động chặn. Ta sẽ sử dụng thư viện `bcc` và `socket` để reset kết nối nếu phát hiện SQLi.
File script sẽ được lưu tại: `/usr/local/src/ebpf-sqli-guard/sqli_guard.py`. Script này sẽ tải code C đã viết ở trên, lắng nghe sự kiện và nếu `is_malicious == 1`, nó sẽ gửi packet RST (Reset) để ngắt kết nối của client đó.
#!/usr/bin/env python3
from bcc import BPF
from bcc import table
from bcc import perf
import sys
import os
import struct
import socket
import time
# Định nghĩa cấu trúc tương ứng với C
class EventData:
def __init__(self):
self.pid = 0
self.tid = 0
self.timestamp = 0
self.comm = b""
self.port = 0
self.payload = b""
self.is_malicious = 0
def print_event(cpu, data, size):
event = struct.unpack("IIQQ16sI256sI", data)
e = EventData()
e.pid = event[0]
e.tid = event[1]
e.timestamp = event[2]
e.comm = event[3].decode("utf-8").strip("\x00")
e.port = event[4] # Giả lập port, thực tế cần parse từ socket context
e.payload = event[5]
e.is_malicious = event[6]
if e.is_malicious:
print(f"[ALERT] SQL Injection Detected! PID: {e.pid}, COMM: {e.comm}")
print(f"Payload: {e.payload[:50]}...")
# Hành động chặn: Gửi RST packet (giả lập)
# Trong thực tế, cần dùng bpf_send_signal hoặc reset connection qua socket
print(f"Action: Blocking connection for PID {e.pid}")
# Ở đây ta chỉ log, để chặn thực sự cần logic phức tạp hơn với cgroup socket
else:
# Chỉ log nếu cần debug
pass
# Load BPF code từ file C
b = BPF(text=open("/usr/local/src/ebpf-sqli-guard/sqli_guard.c").read())
# Hook vào tracepoint
b.attach_tracepoint(name="syscalls/sys_enter_write", fn_name="handle_write")
# Hàm callback cho perf buffer
def handler(cpu, data, size):
print_event(cpu, data, size)
# Lắng nghe sự kiện
print("Starting SQL Injection Guard (eBPF)... Press Ctrl+C to exit")
perf = perf.PerfBuffer(b["events"], handler, page_cnt=4)
try:
while True:
perf.poll()
except KeyboardInterrupt:
print("Stopping SQL Injection Guard...")
sys.exit(0)
Script Python này tải chương trình eBPF, hook vào `sys_enter_write`, và lắng nghe sự kiện qua `PerfBuffer`. Khi phát hiện `is_malicious`, nó in cảnh báo và giả lập hành động chặn.
Kết quả mong đợi: Script chạy không lỗi, in ra "Starting SQL Injection Guard..." và chờ sự kiện.
Verify kết quả và Test tấn công
Để kiểm tra chính sách, ta cần một môi trường test. Giả sử ta có một ứng dụng web đơn giản chạy trên cổng 8080, sử dụng MySQL.
Bước 1: Chạy script bảo vệ với quyền root.
sudo python3 /usr/local/src/ebpf-sqli-guard/sqli_guard.py &
Kết quả mong đợi: Script chạy nền, không báo lỗi.
Bước 2: Tạo một truy vấn bình thường để đảm bảo không bị chặn false positive.
curl -X POST -d "SELECT * FROM users WHERE id=1" http://localhost:8080/api/query
Kết quả mong đợi: Không có dòng log `[ALERT]` xuất hiện. Kết nối hoạt động bình thường.
Bước 3: Thực hiện tấn công SQL Injection đơn giản (Pattern "UNION").
curl -X POST -d "SELECT * FROM users WHERE id=1 UNION SELECT password FROM admin" http://localhost:8080/api/query
Kết quả mong đợi: Trong terminal đang chạy script bảo vệ, xuất hiện dòng:
[ALERT] SQL Injection Detected! PID: , COMM:
Action: Blocking connection for PID
Bước 4: Thử pattern "DROP TABLE".
curl -X POST -d "DROP TABLE users" http://localhost:8080/api/query
Kết quả mong đợi: Xuất hiện cảnh báo tương tự như trên.
Bước 5: Thử pattern dấu ngoặc đơn đơn `'`.
curl -X POST -d "SELECT * FROM users WHERE name='admin' --" http://localhost:8080/api/query
Kết quả mong đợi: Xuất hiện cảnh báo do logic trong code C đã đánh dấu dấu `'` là nguy hiểm.
Chú ý: Trong môi trường production, việc chặn (block) thực sự đòi hỏi tích hợp sâu hơn với BPF map loại `CGROUP_SOCK` để reset kết nối TCP, hoặc sử dụng BPF_PROG_TYPE_XDP để drop packet ở tầng mạng. Tutorial này minh họa cơ chế phát hiện và luồng xử lý cơ bản.
Điều hướng series:
Mục lục: Series: Triển khai Database an toàn với Linux Kernel Hardening và eBPF
« Phần 4: Giới thiệu eBPF và công cụ BCC cho giám sát Database
Phần 6: Tích hợp eBPF với Kernel Hardening để bảo vệ bộ nhớ »