Làm chủ Linux Seccomp: Lọc System Call để cô lập và gia cố tiến trình

Linux tutorial - IT technology blog
Linux tutorial - IT technology blog

Bối cảnh & Lý do sử dụng

Các kernel Linux 6.x hiện đại cung cấp hơn 450 system call. Một microservice viết bằng Go thông thường phục vụ JSON qua HTTP chỉ cần khoảng 40 lệnh trong số đó. Nó không bao giờ cần gọi ptrace, reboot, mount, hay nạp kernel module qua init_module. Dù vậy, theo mặc định, các tiến trình unprivileged thông thường vẫn có thể gửi gần như bất kỳ syscall nào.

Lỗi kernel vẫn luôn là một trong những con đường tin cậy nhất để nâng từ lỗ hổng ứng dụng lên quyền root. Khi kẻ tấn công đạt được quyền thực thi mã từ xa (remote code execution) trong một web worker, chúng sẽ lập tức dò quét giao diện kernel để leo thang đặc quyền. Nếu tiến trình bị xâm nhập đó không thể mở raw socket, tạo subshell hoặc trace các tiến trình khác, kẻ tấn công sẽ rơi vào ngõ cụt.

Secure Computing Mode (Seccomp) giải quyết bài toán này ngay tại ranh giới kernel. Được tích hợp lần đầu trong Linux 2.6.12 và nâng cấp với bộ lọc BPF ở phiên bản 3.5, Seccomp kiểm tra số hiệu và đối số của syscall trước khi thực thi. Nếu một thread gọi một lệnh bị cấm, kernel sẽ lập tức can thiệp. Kernel có thể chấm dứt tiến trình, trả về mã lỗi EPERM, hoặc ghi bản ghi audit vào syslog.

Seccomp khác biệt căn bản so với các hệ thống Mandatory Access Control như SELinux hay AppArmor. Các công cụ đó đánh giá đường dẫn, network endpoint và nhãn bảo mật (security label). Seccomp hoạt động sâu hơn một tầng: nó tước bỏ vốn từ vựng mà một file thực thi dùng để giao tiếp với kernel.

Cài đặt

Các bản phân phối doanh nghiệp hiện đại đều được kích hoạt sẵn Seccomp trong kernel. Bạn không cần module bên thứ ba hay driver tùy chỉnh. Tuy nhiên, bạn sẽ cần các development header trong user-space, runtime library và các công cụ tracing để xây dựng cũng như kiểm thử profile.

Trên Debian 12 và Ubuntu 22.04 hoặc 24.04:

sudo apt-get update
sudo apt-get install -y libseccomp-dev seccomp strace auditd

Trên RHEL 9, Rocky Linux, hoặc Fedora:

sudo dnf install -y libseccomp-devel strace audit

Kiểm tra xem kernel đang hoạt động đã được biên dịch hỗ trợ Seccomp hay chưa:

grep -i seccomp /boot/config-$(uname -r)

Đảm bảo cả CONFIG_SECCOMP=y và CONFIG_SECCOMP_FILTER=y đều xuất hiện trong kết quả đầu ra. Các cloud image tiêu chuẩn trên AWS, GCP và Azure đều đã bật sẵn cả hai tùy chọn này.

Cấu hình

Bạn có thể triển khai Seccomp ở ba tầng khác nhau: biên dịch trực tiếp vào mã nguồn C hoặc Go, áp dụng qua systemd unit file, hoặc cấu hình dưới dạng container profile trong Docker và containerd. Hãy chọn tầng phù hợp với mô hình vận hành của bạn.

1. Lọc trực tiếp trong ứng dụng native bằng libseccomp

Nhúng trực tiếp bộ lọc vào các service C hoặc C++ mang lại tính bảo mật linh hoạt (portable security). Các ràng buộc sẽ đi theo binary tới bất kỳ nơi nào nó chạy, ngay cả khi không có container runtime hay lớp bọc systemd. Chương trình dưới đây áp dụng một allowlist nghiêm ngặt ngay sau khi khởi động, chỉ cho phép I/O tiêu chuẩn và thoát tiến trình:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <seccomp.h>
#include <sys/prctl.h>

int main(void) {
    /* Mặc định kill toàn bộ tiến trình khi gặp syscall không nằm trong danh sách */
    scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL_PROCESS);
    if (!ctx) {
        perror("seccomp_init");
        exit(EXIT_FAILURE);
    }

    /* Cho phép rõ ràng các syscall cần thiết */
    seccomp_rule_add(ctx, SCMP_ACT_PERMIT, SCMP_SYS(read), 0);
    seccomp_rule_add(ctx, SCMP_ACT_PERMIT, SCMP_SYS(write), 0);
    seccomp_rule_add(ctx, SCMP_ACT_PERMIT, SCMP_SYS(exit), 0);
    seccomp_rule_add(ctx, SCMP_ACT_PERMIT, SCMP_SYS(exit_group), 0);
    seccomp_rule_add(ctx, SCMP_ACT_PERMIT, SCMP_SYS(fstat), 0);

    /* Nạp chương trình BPF đã biên dịch vào kernel */
    if (seccomp_load(ctx) < 0) {
        perror("seccomp_load");
        seccomp_release(ctx);
        exit(EXIT_FAILURE);
    }
    seccomp_release(ctx);

    printf("Đã nạp bộ lọc. Thao tác ghi tiêu chuẩn hoạt động bình thường.\n");

    /* Thử thực hiện một syscall bị chặn: getpid() */
    printf("Đang gọi getpid()...\n");
    pid_t pid = getpid();
    printf("PID: %d (Không thể chạm tới)\n", pid);

    return 0;
}

Biên dịch và chạy binary thử nghiệm:

gcc -O2 -o seccomp_demo seccomp_demo.c -lseccomp
./seccomp_demo

Chương trình ghi dòng trạng thái ban đầu ra standard output. Ngay khi gọi getpid(), kernel sẽ lập tức chấm dứt thread với lỗi Bad system call (core dumped). Tiến trình không bao giờ chạy tới câu lệnh in cuối cùng.

2. Gia cố Daemon bằng systemd

Các daemon cũ và công cụ mã nguồn đóng hiếm khi cung cấp điểm hook nội bộ. Đối với các workload này, systemd cung cấp khả năng lọc mang tính khai báo (declarative filtering) thông qua chỉ thị SystemCallFilter với chi phí hiệu năng gần như bằng không (dưới 1% trong hầu hết các benchmark).

Lưu unit file này vào /etc/systemd/system/api-gateway.service:

[Unit]
Description=API Gateway nội bộ
After=network.target

[Service]
Type=simple
User=www-data
Group=www-data
ExecStart=/usr/local/bin/api-gateway --port 8080
Restart=always

# Bắt buộc kiến trúc native và allowlist syscall
SystemCallArchitectures=native
SystemCallFilter=@system-service @network-io
SystemCallFilter=~@privileged @resources @obsolete @mount @reboot
SystemCallErrorNumber=EPERM

[Install]
WantedBy=multi-user.target

Systemd gom nhóm các syscall phổ biến thành các tập hợp định sẵn như @system-service và @network-io. Tiền tố dấu ngã (~) dùng để loại bỏ các nhóm nguy hiểm khỏi mức cơ sở đó. Nếu ứng dụng cố thực hiện một hành động bị chặn, systemd sẽ trả về lỗi EPERM thay vì chấm dứt worker đột ngột.

Áp dụng các thay đổi cho unit:

sudo systemctl daemon-reload
sudo systemctl restart api-gateway.service

3. Giới hạn dịch vụ container bằng Docker profile

Theo mặc định, Docker chặn khoảng 44 syscall, bao gồm reboot, clock_settime và kexec_load. Cấu hình mặc định này cân bằng giữa khả năng tương thích rộng rãi và mức bảo mật cơ bản. Đối với các microservice quan trọng, hãy thay thế phạm vi bao quát này bằng một allowlist được tinh chỉnh riêng.

Lưu policy sau thành strict-profile.json:

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "architectures": [
    "SCMP_ARCH_X86_64"
  ],
  "syscalls": [
    {
      "names": [
        "read",
        "write",
        "open",
        "openat",
        "close",
        "fstat",
        "futex",
        "epoll_wait",
        "epoll_ctl",
        "exit_group"
      ],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}

Khởi chạy container kèm theo bộ lọc tùy chỉnh:

docker run --rm -it \
  --security-opt seccomp=strict-profile.json \
  alpine:latest sh

Các lệnh phụ thuộc vào những system call không có trong danh sách giờ đây sẽ thất bại với lỗi phân quyền ngay bên trong shell của container.

Kiểm tra & Giám sát

Đừng bao giờ đưa ngay allowlist nghiêm ngặt lên môi trường production. Nếu bạn đoán sai các syscall cần thiết, service của bạn sẽ crash ngay khi gặp phải một edge case hiếm thấy. Hãy sử dụng quy trình profiling và giám sát dưới đây để ngăn ngừa sự cố gián đoạn dịch vụ.

Trace các syscall cần thiết

Thu thập toàn bộ hoạt động runtime thực tế của binary trước khi khóa chặt bất kỳ thứ gì. Chạy service dưới strace trong một đợt chạy benchmark giả lập:

strace -c -f -S name /usr/local/bin/api-gateway --port 8080

Gửi ít nhất 5.000 đến 10.000 request HTTP thực tế tới tiến trình. Kích hoạt kết nối cơ sở dữ liệu, các health endpoint, metrics scraper và những quy trình xử lý lỗi. Khi bạn dừng strace bằng tổ hợp phím Ctrl+C, nó sẽ xuất ra bảng thực thi hiển thị tổng số lệnh gọi, số lỗi và thời gian đã trôi qua cho từng syscall. Hãy xây dựng allowlist trực tiếp từ báo cáo đó.

Kiểm tra trạng thái Seccomp của tiến trình

Xác nhận bộ lọc đang chủ động bảo vệ PID mục tiêu thông qua proc filesystem:

grep -i seccomp /proc/$(pgrep -f api-gateway)/status

Kernel báo cáo trạng thái của Seccomp: dưới dạng một trong ba số nguyên sau:

  • 0 (Disabled): Không có bộ lọc syscall nào đang hoạt động trên tiến trình này.
  • 1 (Strict): Chế độ legacy được kích hoạt qua prctl(PR_SET_SECCOMP, 1). Tiến trình chỉ có thể sử dụng read, write, _exit và sigreturn.
  • 2 (Filter): Các bộ lọc BPF đang tích cực đánh giá syscall qua seccomp(SECCOMP_SET_MODE_FILTER, ...).

Giám sát vi phạm bộ lọc trong audit log

Trong quá trình kiểm thử, hãy đặt action của rule thành SCMP_ACT_LOG. Kernel sẽ cho phép lệnh gọi tiếp tục thực thi trong khi vẫn ghi log lại mọi vi phạm, mang lại dữ liệu telemetry rõ ràng mà không gây rủi ro gián đoạn dịch vụ.

Tìm kiếm các bản ghi audit gần đây:

sudo ausearch -m SECCOMP -ts recent

Các bộ lọc bị kích hoạt sẽ sinh ra log trong /var/log/audit/audit.log:

type=SECCOMP msg=audit(1708701234.567:890): auid=1000 uid=33 gid=33 ses=1 \
subj=unconfined pid=1423 comm="api-gateway" exe="/usr/local/bin/api-gateway" \
syscall=101 compat=0 ip=0x7f9a1234 arch=c000003e code=0x0

Dịch chỉ số syscall dạng số thập phân thô bằng ausyscall:

ausyscall x86_64 101

Syscall 101 tương ứng với ptrace trên kiến trúc x86_64. Nếu một thư viện bên ngoài thực sự cần thao tác đó một cách hợp lệ, hãy thêm nó vào profile của bạn. Nếu một dependency chưa được kiểm chứng cố tình chèn mã vào tiến trình (process injection), bộ lọc Seccomp của bạn đã chặn đứng vụ tấn công thành công mà không tốn thêm bất kỳ chi phí tài nguyên nào lúc runtime.

Share: