Phát hiện lỗ hổng kernel Linux eBPF và cách khắc phục
(bughunters.google.com)- Google đã phát hiện CVE-2023-2163 trong eBPF verifier bằng Buzzer và xác nhận lỗ hổng này có thể bị khai thác để leo thang đặc quyền cục bộ và thoát container
- eBPF cho phép mở rộng chức năng kernel trong thời gian chạy, nhưng vì thực thi bytecode tùy ý ở mức đặc quyền cao nên xác minh verifier trước khi nạp là trụ cột cốt lõi của bảo mật
- Vấn đề phát sinh khi path pruning của verifier đánh giá sai rằng các đường thực thi khác nhau thực chất tương đương với một đường đã an toàn
- Mã khai thác lợi dụng sự khác biệt giữa việc verifier coi một thanh ghi là 0 trong khi lúc chạy thực tế nó mang giá trị khác, từ đó làm nhiễm bẩn con trỏ stack eBPF và dẫn tới đọc/ghi tùy ý cùng vượt qua KASLR
- Bản vá đánh dấu cả các thanh ghi imprecise ảnh hưởng đến precise register là precise, và với cùng chiến lược fuzzing phép toán con trỏ thì sau đó không phát hiện thêm vấn đề nào
eBPF verifier mở rộng bề mặt tấn công của kernel
- eBPF là công nghệ cho phép mở rộng chức năng của Linux kernel trong thời gian chạy mà không cần các module kernel phức tạp
- Chương trình eBPF được viết bằng bytecode tùy chỉnh và phải trải qua bước kiểm tra an toàn trước khi được thực thi khi một sự kiện nhất định xảy ra
- Ví dụ tiêu biểu là chương trình eBPF chạy khi một syscall cụ thể được gọi
- Vì đây là cấu trúc có thể thực thi mã tùy ý ở mức đặc quyền cao nên bề mặt tấn công của kernel tăng lên đáng kể
- Trước khi được nạp, chương trình phải vượt qua verifier, và verifier sẽ kiểm tra xem các giả định bảo mật của eBPF có được đáp ứng hay không
- Khi lỗ hổng verifier bị khai thác, kết quả thường là leo thang đặc quyền cục bộ hoặc thoát container trong môi trường container
Buzzer và fuzzing phép toán con trỏ
- Google đã tạo Buzzer để kiểm toán tự động mã eBPF verifier
- Buzzer là một fuzzer tạo ra hàng loạt chương trình eBPF hợp lệ về mặt cú pháp và có thể thiết lập chiến lược để kích hoạt lỗi logic
- Việc phát hiện CVE-2023-2163 sử dụng chiến lược phép toán con trỏ
- Tạo phần đầu khởi tạo các thanh ghi bằng giá trị ngẫu nhiên
- Tạo chuỗi lệnh số học và nhảy ngẫu nhiên
- Chọn một thanh ghi ngẫu nhiên rồi thực hiện phép cộng với con trỏ phần tử eBPF map
- Ghi một giá trị ma thuật vào phần tử đó
- Nếu không quan sát được giá trị đã ghi từ không gian người dùng thì có thể đã xảy ra ghi vượt biên
- CVE-2023-2163 mà Buzzer tìm ra nằm trong logic path pruning của eBPF và dẫn tới mã khai thác có thể dùng cho cả thoát container lẫn leo thang đặc quyền cục bộ
Lỗi path pruning và precise tracking
- eBPF verifier mô phỏng các đường thực thi có thể có để xác nhận chương trình có thể chạy an toàn hay không
- Khi giá trị thanh ghi tại nhánh điều kiện không chắc chắn, verifier sẽ cố theo dõi việc thực thi của mọi trạng thái có thể
- Càng có nhiều nhảy điều kiện, số lượng đường thực thi càng tăng theo cấp số mũ, đồng thời làm giảm hiệu năng nạp chương trình vào kernel
- Để giảm vấn đề này, các nhà phát triển eBPF đã đưa vào path pruning
- Nếu verifier có thể đảm bảo rằng một trạng thái tương đương với trạng thái nào đó đã đi tới lệnh exit một cách an toàn, nó sẽ không tiếp tục khám phá đường đó nữa
- Để pruning hiệu quả hơn, khái niệm precise tracking được dùng cùng nhau
- Khi một thanh ghi tham gia phép toán con trỏ hoặc được truyền như hằng số vào helper function, nó sẽ được đánh dấu là precise
- Verifier phải khám phá mọi trạng thái liên quan đến thanh ghi đó
- Trong CVE-2023-2163, r9 ảnh hưởng đến độ chính xác của r6 nhưng không được đánh dấu đúng cách
- Verifier giả định rằng r9 không đóng góp vào độ precise của r6
- Sau khi kết luận ở một đường trước đó rằng có thể đi tới exit an toàn với r6, nó coi các trạng thái còn lại là tương đương và pruning chúng
- Khi chạy thực tế, đường 1:2:4:6 đã được thực thi, và tại bước 6, r6 mà verifier cho là 0 thực ra có thể mang giá trị khác và được dùng trong phép toán con trỏ
Đọc/ghi tùy ý và vượt qua KASLR
- Mã khai thác được công bố trong Google security research repository
- Việc phát triển mã khai thác tận dụng đáng kể công trình của @chompie và @_manfp
- Luồng tổng thể là trước tiên giành được đọc/ghi tùy ý, sau đó tìm credentials của tiến trình và vá uid cùng con trỏ
fs_structđể nâng đặc quyền - Ở bước đầu, giá trị thanh ghi bị nhiễm bẩn được biến thành 1
- Trong phân tích thủ công, giá trị r6 khi chạy là
0x400còn verifier coi nó là 0 - Lệnh
r6 >>= 10được dùng để tạo ra giá trị mong muốn là 1
- Trong phân tích thủ công, giá trị r6 khi chạy là
- Sau đó dùng helper function
bpf_skb_load_bytes_relativeđể làm nhiễm bẩn con trỏ trong stack eBPF- Verifier cho rằng
lenlà 8, nhưng khi chạy thực tế thì do giá trị r6 nênlentrở thành 9 - Kết quả là ghi 9 byte thay vì 8 byte, làm nhiễm bẩn byte đầu tiên của giá trị tại stack offset
-32
- Verifier cho rằng
- Khi thao tác con trỏ stack đã bị nhiễm bẩn, có thể làm lộ eBPF map pointer
- Từ không gian người dùng, đọc các giá trị R2 và R3 rồi kiểm tra xem R2 có trở thành
0xBACAhay không - Nếu điều kiện này đúng thì R3 sẽ là giá trị rò rỉ của map pointer
- Việc rò rỉ eBPF map pointer dẫn đến trạng thái vượt qua KASLR
- Từ không gian người dùng, đọc các giá trị R2 và R3 rồi kiểm tra xem R2 có trở thành
- Với cùng chiến lược, nếu ghi đè cả một vùng stack liên tiếp bằng con trỏ mong muốn thay vì chỉ một byte đơn lẻ, có thể đạt được đọc/ghi tùy ý
- Theo góc nhìn của verifier thì đây là thao tác trên BPF stack, nhưng trên thực tế có thể đọc và ghi bộ nhớ kernel
Từ rò rỉ map đến root shell
- Sau khi có rò rỉ map pointer, mã khai thác không khác nhiều so với mã của Chompie và có mượn một phần mã
- Quy trình khái quát như sau
- Lặp tìm chuỗi
init_pid_nstrong kstrtab - Tìm biểu tượng ksymtab tham chiếu chuỗi đó để lấy địa chỉ của cấu trúc init_pid_ns
- Duyệt radix tree để tìm entry có trường
commkhớp với tên file thực thi của exploit đang chạy- Nếu đang chạy trong container thì PID không phải heuristic đáng tin cậy, nên không chỉ dựa vào PID
- Vá uid thành 0 và vá con trỏ
fs_struct - Nếu vá
fs_structthành cùng giá trị với con trỏ mà PID 1 tham chiếu, thì khi chạy trong container có thể quan sát hệ thống tệp của host - Chạy
system("/bin/bash")để lấy root shell
- Lặp tìm chuỗi
- Mã công khai trên GitHub chỉ hoạt động như thoát container trên một số phiên bản Linux nhất định
- Trên Ubuntu và một số bản phân phối khác, do offset của cấu trúc dữ liệu bị ghi đè khác nhau nên nó chỉ hoạt động như leo thang đặc quyền cục bộ
- Để hoạt động trên mọi bản phân phối Linux sẽ cần điều chỉnh mã
Bản vá và kiểm chứng tiếp theo
- Có thể xem phân tích nguyên nhân và bản vá cho CVE-2023-2163 trên kernel mailing list
- Cách sửa là đánh dấu các imprecise register của phép toán đã ảnh hưởng đến precise register thành precise
- Chưa rõ việc sửa này có ảnh hưởng đến hiệu năng của eBPF verifier hay không
- Tiếp tục chạy cùng chiến lược fuzzing phép toán con trỏ nhưng không phát hiện thêm vấn đề nào
- Buzzer vẫn đang tiếp tục được phát triển và cộng đồng mã nguồn mở có thể đóng góp qua GitHub repo
1 bình luận
Ý kiến trên Hacker News
Trên các nền tảng eBPF được dùng phổ biến nhất, ngay từ đầu mã không có đặc quyền đã không thể tải chương trình eBPF, nên tác động của lỗi trong verifier thường không lớn
Những lỗi như vậy rốt cuộc là lỗ hổng root → ring0; không có nghĩa là chuyện nhỏ, nhưng với workload phía server thì nhìn chung là một sự đánh đổi có thể chấp nhận
Đặc biệt, lịch sử leo thang đặc quyền cục bộ trong kernel của eBPF khá tốt nếu so với toàn bộ kernel, và trong môi trường eBPF hiện nay, giá trị lớn nhất của verifier là giúp khó vô tình làm sập kernel bằng một chương trình eBPF sai
Với kernel module có thể nạp thông thường thì nhận định này gần như không đúng đến mức buồn cười
Trong trường hợp này, trên hầu hết nền tảng sẽ không cần đặc quyền
Và nếu có namespace người dùng không đặc quyền, người dùng có thể tự biến mình thành “root”, nên vấn đề root → ring0 cũng trở nên ít bị giới hạn hơn
Đây là xu hướng vẫn thấy trong các PoC lỗi eBPF kể từ sau khi các bản phân phối từng bật tính năng này rồi phần lớn lại tắt đi
Khi các công cụ như Cilium ngày càng lớn, đường tấn công đi vào môi trường container có cap_bpf ngày càng thực tế hơn
“Uno no es ninguno” theo nghĩa đen là “một không phải là không có”, tức gần với “One is not none”
Trong tiếng Tây Ban Nha, phủ định kép thường không thực sự là phủ định kép
Ví dụ “ở đây không có gì cả” được nói là “no hay nada aquí”, nhưng dịch từng từ thì trông giống như “không phải là không có gì ở đây”
Viện Hàn lâm Hoàng gia Tây Ban Nha cũng giải thích như vậy:
https://www.rae.es/espanol-al-dia/doble-negacion-no-vino-nad...
Cái gọi là “phủ định kép” xuất hiện vì sự hòa hợp phủ định bắt buộc phải có trong một số tình huống nhất định trong tiếng Tây Ban Nha và các ngôn ngữ Rôman khác; kết quả là trạng từ no và các yếu tố khác mang nghĩa phủ định cùng xuất hiện trong một câu
Việc hai “phủ định” này cùng tồn tại không làm triệt tiêu nghĩa phủ định của câu
Trước đây khi tôi thử dùng eBPF, nó không đủ sức biểu đạt cho việc tôi cần làm
Tôi tự hỏi việc tăng độ phức tạp trong không gian kernel để đổi lấy mức linh hoạt hạn chế như vậy có thật sự chính đáng không
Nếu dùng để lọc gói tin thì tôi hiểu, nhưng dùng cho các mục đích khác như sandboxing thì có vẻ kém thuyết phục
Lựa chọn của kernel không phải là eBPF hoặc không có gì, mà là eBPF hoặc một thứ khác tương tự
Có thể bạn không trực tiếp dùng nhiều, nhưng cũng có những người dùng nó suốt cả ngày
Tôi nhớ các kỹ sư FAANG từng nói rằng trên mọi server họ luôn chạy hàng chục, thậm chí có thể hàng trăm chương trình như vậy, chưa tính các lần dùng một lần
FAANG cũng thuê các nhà phát triển kernel chuyên trách, nên có thể nói họ cũng đang tài trợ cho độ phức tạp mà chính họ sử dụng
Tôi cũng từng giải quyết vấn đề bằng eBPF
Đó là những vấn đề mà nếu không có eBPF thì gần như không thể giải được với người không phải chuyên gia kernel; không thường xuyên cần đến, nhưng khi cần thì không có thứ thay thế
Trong một số trường hợp, ngay cả với chuyên gia kernel, lựa chọn cũng là dùng eBPF hoặc duy trì mãi mãi một bản vá kernel tùy chỉnh
Tôi biết là không an toàn, nhưng những người xử lý chúng hiểu rằng quyền năng lớn đi kèm trách nhiệm lớn
“Uno no es ninguno” có vẻ nên được dịch là “One is not none”
https://bughunters.google.com/blog/6303226026131456/a-deep-d...
Nhưng lại bị dịch thành “One is none”
Đây là kiểu phủ định kép khét tiếng khiến người nói ngoại ngữ, kể cả tôi, rất vất vả
https://spanish.stackexchange.com/questions/26777/how-does-d...
Dịch sát chữ thì là vậy, nhưng trong tiếng Tây Ban Nha, kỳ lạ là phủ định kép thường chỉ được dùng như một phủ định thông thường
Có lẽ dịch kiểu “one ain't nothin'” thì sát hơn
Ở nước chúng tôi có câu “con nhím trong quần”
Dù nó hữu dụng đến đâu, thứ này có vẻ không được viết một cách an toàn và thận trọng