Cường hóa Kernel: Cách tôi sử dụng Seccomp và Linux Capabilities để chặn đứng leo thang đặc quyền

Security tutorial - IT technology blog
Security tutorial - IT technology blog

2:14 sáng: Cuộc gọi không ai mong muốn

Điện thoại tôi rung bần bật trên kệ đầu giường. 2:14 sáng. Dashboard giám sát cho edge gateway đang báo đỏ rực. Ai đó đã chọc thủng một dịch vụ web cũ trong một cụm production của chúng tôi. Đến lúc tôi mở được terminal, kẻ tấn công đã bắt đầu di chuyển ngang (lateral movement), quét mạng nội bộ 10.0.0.0/8 để tìm các mục tiêu giá trị cao hơn — một khởi đầu đầy căng thẳng cho quá trình ứng phó sự cố Linux.

Các bản log kể lại một câu chuyện quen thuộc. Điểm xâm nhập là một lỗ hổng đã biết trong thiết lập PHP-FPM 7.4. Kẻ tấn công đã chiếm được shell với quyền www-data. Thông thường, đó là một tài khoản bị hạn chế, đặc quyền thấp. Tuy nhiên, ngay cả người dùng thấp nhất cũng có thể giao tiếp với Linux kernel thông qua hàng trăm hệ thống gọi (syscalls). Sử dụng unshare and ptrace, kẻ tấn công đã dò tìm các điểm yếu của namespace và cuối cùng tìm thấy đường thoát khỏi container để chiếm quyền kiểm soát máy chủ host.

Đây không chỉ là lỗi của mã nguồn PHP. Đó là sự thất bại của các ranh giới ở cấp độ hệ điều hành (OS). Chúng tôi đã cho phép một ứng dụng chạm vào những phần của kernel mà lẽ ra nó không có lý do gì để đụng tới.

Vấn đề cốt lõi: Cái bẫy “Được ăn cả, ngã về không”

Bảo mật Linux trước đây vốn mang tính nhị phân. Hoặc bạn là root (UID 0) và có thể làm mọi thứ, hoặc bạn là một user bình thường và hầu như không thể làm gì, điều nhắc nhở chúng ta cần thắt chặt bảo mật Linux Sudoers để kiểm soát đặc quyền tốt hơn. Để một web server có thể bind vào cổng 80, chúng ta thường chạy tiến trình đó với quyền root. Đây là một rủi ro cực lớn. Nếu tiến trình đó bị chiếm quyền, kẻ tấn công sẽ thừa hưởng toàn bộ sức mạnh của quản trị viên.

Hầu hết các sự cố leo thang đặc quyền xảy ra vì chúng ta trao cho các tiến trình nhiều quyền lực hơn mức chúng cần. Một web server cần đọc các file tĩnh và lắng nghe trên network socket. Nó không cần tải các kernel module, thay đổi đồng hồ hệ thống hay khởi động lại máy. Thế nhưng, theo mặc định, many hệ thống cho phép bất kỳ tiến trình nào truy cập tất cả các syscall, nhiều trong số đó có thể bị lợi dụng để dò tìm lỗi kernel.

Trong khi cường hóa các node xác thực phụ sau sự cố này, tôi đã tạo các server secret bằng công cụ tại toolcraft.app/vi/tools/security/password-generator. Nó chạy hoàn toàn trên trình duyệt, nghĩa là không có dữ liệu nào rời khỏi máy cục bộ. Tôi cũng muốn mức độ cô lập nghiêm ngặt, cục bộ tương tự cho mối quan hệ giữa ứng dụng của mình và kernel.

Chiêu lược 1: Phân quyền chi tiết qua Linux Capabilities

Linux Capabilities (được giới thiệu từ kernel 2.2) chia nhỏ sức mạnh tuyệt đối của root thành các đặc quyền riêng biệt. Hiện có khoảng 40 capability khác nhau. Ví dụ:

  • CAP_NET_BIND_SERVICE: Cho phép một tiến trình bind vào các cổng dưới 1024.
  • CAP_CHOWN: Cho phép một tiến trình thay đổi quyền sở hữu file.
  • CAP_SYS_TIME: Cho phép một tiến trình thiết lập đồng hồ hệ thống.

Bằng cách sử dụng các capability này, chúng ta có thể cấp cho tiến trình đúng quyền lực mà nó yêu cầu. Nếu tôi có một công cụ chẩn đoán cần bắt gói tin mạng, tôi không cấp quyền root cho nó. Tôi cấp cho nó CAP_NET_RAW. Điều này hạn chế phạm vi ảnh hưởng (blast radius) nếu công cụ đó bị kiểm soát.

Triển khai thực tế

Sử dụng getcap để kiểm tra các capability hiện có của file và setcap để áp dụng chúng. Đây là cách tôi tước bỏ quyền lực của một file thực thi xuống mức tối thiểu trong quá trình dọn dẹp sau sự cố:

# Loại bỏ tất cả quyền root và chỉ cho phép bind vào các cổng thấp
sudo setcap 'cap_net_bind_service=+ep' /usr/bin/my-web-app

# Xác nhận lại các quyền
getcap /usr/bin/my-web-app

Đối với môi trường container, hãy bắt đầu từ vị thế không tin tưởng (zero trust). Loại bỏ mọi đặc quyền và chỉ thêm lại những gì thực sự cần thiết:

# Cách bảo mật nhất để khởi chạy một container
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE my-secure-app

Chiến lược 2: Lọc giao diện với Seccomp

Capabilities kiểm soát đặc quyền, nhưng Seccomp (Secure Computing Mode) kiểm soát giao diện. Nó đóng vai trò như một bức tường lửa cho các syscall. Ngay cả một người dùng không phải root với không capability nào vẫn có thể gọi execve, fork hoặc open. Một Linux kernel hiện đại (v6.x) có hơn 450 syscall. Một ứng dụng thông thường chỉ sử dụng khoảng 40 đến 60. 400 syscall còn lại chính là bề mặt tấn công không cần thiết mà các kỹ thuật app sandboxing hiện đại nhắm tới để bảo vệ kernel.

Seccomp cho phép chúng ta định nghĩa một profile JSON để báo với kernel rằng: “Nếu tiến trình này cố gắng thực hiện một syscall không nằm trong danh sách cho phép này, hãy hủy nó ngay lập tức.”

Xây dựng một Seccomp Profile

Dưới đây là một đoạn mã của profile cường hóa mà tôi đã phát triển cho đội ngũ Nginx của chúng tôi. Nó sử dụng chiến lược “mặc định là từ chối” (default deny). Đây là cách duy nhất để đảm bảo an toàn.

{
    "defaultAction": "SCMP_ACT_ERRNO",
    "architectures": [ "SCMP_ARCH_X86_64" ],
    "syscalls": [
        {
            "names": [ "accept4", "epoll_wait", "pwrite64", "read", "write", "close" ],
            "action": "SCMP_ACT_ALLOW"
        }
    ]
}

Khi một ứng dụng chạy dưới profile này, kernel sẽ trả về lỗi EPERM nếu nó thử bất kỳ điều gì không có trong danh sách. Điều này ngăn chặn hiệu quả các reverse shell dựa trên lệnh gọi socket hoặc kẻ tấn công cố gắng sử dụng mount để xem các hệ thống file nhạy cảm trên host.

Capabilities và Seccomp: Chọn công cụ của bạn

Chúng không phải là đối thủ của nhau; chúng bảo vệ các lớp khác nhau. Đây là cách tôi giải thích sự khác biệt cho đội ngũ kỹ sư của mình:

Tính năng Linux Capabilities Seccomp
Tiêu điểm chính Quyền hạn (Tôi có thể làm gì?) Syscalls (Tôi giao tiếp với kernel như thế nào?)
Độ chi tiết Thô (~40 danh mục) Chi tiết (450+ lệnh gọi riêng lẻ)
Độ dễ sử dụng Ánh xạ trực tiếp Phức tạp; yêu cầu phân tích ứng dụng
Tốt nhất cho Thay thế các file thực thi SUID/Root Giảm thiểu các lỗ hổng kernel zero-day

Phòng thủ chiều sâu: Danh sách kiểm tra cường hóa

Sự cố lúc 2 giờ sáng đã chứng minh rằng chỉ dựa vào một lớp bảo mật duy nhất là một canh bạc. Để xây dựng các hệ thống kiên cường, bạn cần kết hợp các kỹ thuật này và thường xuyên thực hiện kiểm tra bảo mật máy chủ Linux để phát hiện sớm các rủi ro. Đây là danh sách kiểm tra tiêu chuẩn của tôi cho mọi dịch vụ mới:

1. Từ bỏ quyền Root

Không bao giờ chạy tiến trình với quyền UID 0. Luôn bao gồm chỉ thị USER trong Dockerfile của bạn. Đó là lớp phòng thủ cơ bản nhất.

2. Loại bỏ tất cả Capabilities

Bắt đầu từ con số không. Sử dụng --cap-drop=ALL trong Docker hoặc CapabilityBoundingSet= trong các unit Systemd. Chỉ thêm lại các đặc quyền cụ thể như CAP_NET_BIND_SERVICE nếu không còn cách nào khác.

3. Phân tích và áp dụng Seccomp

Sử dụng strace để quan sát chính xác những syscall nào ứng dụng của bạn sử dụng trong một chu kỳ khởi động và tải bình thường. Sau đó, tạo một profile chỉ cho phép những lệnh gọi cụ thể đó.

# Theo dõi một ứng dụng để xác định các syscall cần thiết
strace -c -f ./my-app

4. Tận dụng Sandboxing của Systemd

Đối với các dịch vụ không chạy trong container, Systemd hiện đại cung cấp các trình bao tuyệt vời cho các công nghệ này. Hãy thêm các dòng sau vào file .service của bạn:

[Service]
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM
NoNewPrivileges=yes
PrivateDevices=yes
ProtectSystem=strict

Việc kích hoạt NoNewPrivileges=yes là cực kỳ quan trọng. Nó đảm bảo rằng tiến trình — và bất kỳ tiến trình con nào nó tạo ra — không bao giờ có thể giành được các đặc quyền mới thông qua execve. Điều này vô hiệu hóa hiệu quả các file thực thi SUID như một con đường leo thang đặc quyền.

Sự cố lúc 2 giờ sáng đó đã kết thúc bằng việc chúng tôi khôi phục từ bản sao lưu và dành 48 giờ để viết lại các bản khai triển (deployment manifest). Đó là một bài học đắt giá. Tuy nhiên, hiện tại chúng tôi có một hệ thống không chỉ hy vọng vào mã nguồn không có lỗi. Chúng tôi giả định rằng mã nguồn của mình luôn có lỗ hổng và xây dựng một sandbox chặt chẽ, ví dụ như việc sử dụng AppArmor để sandbox ứng dụng, đến mức ngay cả một cuộc tấn công thành công cũng chỉ dẫn đến ngõ cụt.

Share: