Phân tích chuyên sâu về syscall mseal mới của Linux
(blog.trailofbits.com)- mseal trong Linux 6.10 là một syscall giảm thiểu khai thác, dùng để niêm phong các vùng bộ nhớ ảo khi đang chạy, khiến kẻ tấn công đã giành được quyền thực thi mã sau đó khó thay đổi quyền VMA hoặc bố trí của chúng hơn
mseal(start, len, flags)gắnVM_SEALEDvào một dải VMA đã căn chỉnh theo trang, và kernel sẽ từ chối thay đổi quamprotect,munmap,mmap(MAP_FIXED),mremapvà một số nhánhmadvisemang tính phá hủy- Nếu
memfd_createhaymemfd_secretgần với tệp ẩn danh dựa trên RAM và kiểm soát truy cập bộ nhớ bí mật, thì mseal tập trung vào việc ngăn chặn thay đổi quyền, unmap và remap sau khi kẻ tấn công từ xa đã thực thi được mã - Các mục tiêu phòng thủ tiêu biểu là tấn công dùng
mprotect(PROT_EXEC)để vượt qua NX và chạy shellcode, cùng các khai thác chỉ dữ liệu tạo lỗ hổng trong bộ nhớ bằngmunmap/remap rồi lấp đầy bằng dữ liệu do kẻ tấn công kiểm soát - Có khả năng được tích hợp từ glibc 2.41 trở đi, nhưng stack và heap không phải đối tượng được niêm phong tự động do có thể mở rộng hoặc thu hẹp trong lúc chạy, nên nhà phát triển cần cân nhắc kỹ thời điểm và phạm vi áp dụng
Những gì mseal bảo vệ
- Memory sealing là khả năng khiến một dải VMA đang chạy trở nên gần như bất biến trước các sửa đổi nguy hiểm về sau
- Dù kẻ tấn công có giành được primitive thực thi mã, họ vẫn khó thay đổi quyền hoặc thao túng bố trí bằng các thao tác bộ nhớ ảo trên VMA đã niêm phong
- Nhóm Chrome Security đã đưa syscall này vào để hỗ trợ chiến lược V8 CFI trên ChromeOS dựa trên Linux
- Sau nhiều vòng thảo luận và viết lại, nó đã được đưa vào Linux kernel, và có thể được dùng rộng hơn ngoài trình duyệt thông qua tích hợp glibc
Khác biệt với họ memfd
memfd_createvàmemfd_secretgần với họ file sealing hơn- Có thể tạo tệp ẩn danh dựa trên RAM
memfd_secretcho phép chỉ những tiến trình có file descriptor mới truy cập được vùng bộ nhớ đó- Có thể dùng cho ánh xạ không gian người dùng kiểu “secure enclave” để bảo vệ dữ liệu nhạy cảm trong bộ nhớ
mseallà syscall được thiết kế cho giảm thiểu khai thác chống lại kẻ tấn công từ xa muốn thực thi mã, hơn là kẻ tấn công cục bộ muốn lấy cắp thông tin nhạy cảm
Cách hoạt động bên trong kernel
- Chữ ký syscall là
int mseal(unsigned long start, size_t len, unsigned long flags)startvàlenbiểu thị điểm bắt đầu và độ dài của VMA hợp lệ cần niêm phonglenphải được căn chỉnh theo trang đúng cách- Hiện tại
flagschưa được dùng và phải là0
- Tính đến Linux 6.12, phần triển khai gọi
do_msealcurrent->mmtrỏ tớimm_struct, tức toàn bộ không gian địa chỉ bộ nhớ ảo của tiến trình đang gọimmaptrongmm_structchứa danh sáchvm_area_struct, tức các vùng nhớ liên tục được tạo bằngmmap- Các vùng như stack hay VDSO cũng có thể được biểu diễn như một VMA
do_msealtính địa chỉ cuối của dải rồi khóa vùng nhớ bằngmmap_write_lock_killablecheck_mm_sealduyệt từng VMA trong dải đích và trước tiên kiểm tra tính hợp lệ của biên- Nếu giữa dải có vùng nhớ chưa được cấp phát, nó trả về
-ENOMEM - Bước kiểm tra đầu tiên này nhằm tránh tình huống chỉ một phần VMA bị niêm phong khi xảy ra lỗi
- Nếu giữa dải có vùng nhớ chưa được cấp phát, nó trả về
apply_mm_sealduyệt lại cùng dải đó và thêm cờVM_SEALEDvào VMA đích thông quamseal_fixup
Các thao tác bộ nhớ bị cấm sau khi niêm phong
- Bộ patch của kernel thêm kiểm tra
VM_SEALEDvàomm/madvise.c,mm/mmap.c,mm/mprotect.c,mm/mremap.c,mm/mseal.c mprotectvàpkey_mprotectnội bộ gọican_modify_vmatrongmprotect_fixup, và nếu VMA đã niêm phong thì trả về-EPERM- Trên VMA đã niêm phong, các thao tác sau không được phép
- Thay đổi bit quyền bằng
mprotect,pkey_mprotect - Unmap bằng
munmap - Thay thế ánh xạ đã niêm phong bằng ánh xạ biến đổi, chưa niêm phong thông qua
mmap(MAP_FIXED) - Mở rộng hoặc thu hẹp kích thước bằng
mremap - Di chuyển sang đích mới bằng
mremap(MREMAP_MAYMOVE | MREMAP_FIXED) madvisevới một số cờ mang tính phá hủy
- Thay đổi bit quyền bằng
- Khi di chuyển bằng
mremap, việc kiểm tra niêm phong được áp dụng cho cả VMA nguồn lẫn đích - Nếu không có
MREMAP_DONTUNMAP, VMA nguồn sẽ bị unmap, và khi đó cũng phải vượt qua kiểm tra niêm phong củamunmap - Từ Linux 6.10 trở lên, có thể dùng
msealbằng cách gọi syscall trực tiếp, và ví dụ wrapper dùng số syscall462cùngflags=0
Chặn thực thi shellcode bằng cách siết chặt NX
- Kẻ tấn công có thể vẫn thích chạy shellcode dù đã có kỹ thuật tái sử dụng mã như ROP
- Rải shellcode vào stack hoặc heap không thực thi
- Kích hoạt chuỗi ROP ban đầu qua lỗ hổng
- Tắt bit NX của vùng chứa shellcode bằng
mprotect(PROT_EXEC) - Nhảy vào vùng đó để chạy shellcode
- Cuộc tấn công vào daemon SMB của MikroTik RouterOS sử dụng CVE-2018-7445 là ví dụ cho luồng này
- Rải shellcode dựa trên socket vào heap không thực thi
- Chuỗi ROP tạo từ tràn stack thay đổi quyền của bộ nhớ heap
- Sau đó thực thi shellcode
- Nếu niêm phong VMA tương ứng bằng
mseal, kiểm tracan_modify_vmasẽ chặn việc thay đổi quyền khi gọimprotect - Trong mã ví dụ, nếu không niêm phong trang shellcode trên stack thì vẫn có thể chạy nó sau
mprotect(PROT_READ|PROT_WRITE|PROT_EXEC) - Nếu niêm phong cùng trang đó bằng
mseal,mprotectsẽ không thể đổi quyền thực sự, và khi chạy shellcode sẽ phát sinh lỗi segmentation
Hạn chế khi áp dụng cho stack và heap
- Khi
msealđược đưa vào từ glibc 2.41 trở đi, dynamic loader dự kiến sẽ áp dụng niêm phong cho một tập VMA được xác định trước - Theo kế hoạch hiện tại, stack và heap sẽ không được niêm phong tự động
- Stack và heap có thể mở rộng trong lúc chạy, nên niêm phong tự động có thể làm hỏng hoạt động của ứng dụng
- Bộ cấp phát heap có thể gọi syscall
brkđể thu hồi không gian - Trên đường đi này, việc thu hẹp có thể diễn ra qua
arch_unmapvàdo_vmi_unmap - Trong trạng thái đã niêm phong, kiểu unmap này không được phép nên có thể làm hỏng cấp phát bộ nhớ động
- Bộ cấp phát heap có thể gọi syscall
- Nhà phát triển cần tự đánh giá theo ngữ cảnh ứng dụng xem nên áp dụng niêm phong cho vùng nào và vào thời điểm nào
- Macro ví dụ liên tục niêm phong các trang của stack frame được chọn, nơi có thể chứa dữ liệu không đáng tin cậy
- Nếu gọi lại
msealtrên VMA đã niêm phong, nó sẽ được xử lý như no-op và không phát sinh lỗi - Các tính năng như tự động mở rộng stack hay stack splitting cần được chú ý riêng
- Nếu kẻ tấn công vẫn quyết tâm dùng shellcode, họ có thể
mmapmột vùng thực thi mới rồi sao chép payload từ vùng chỉ đọc được sang đó, nhưng quy trình sẽ rườm rà hơn
Giảm thiểu khai thác chỉ dữ liệu dựa trên unmap
- Việc chặn
mprotectcũng ngăn vùng đã niêm phong chuyển sang trạng thái có thể ghi, giúp bảo vệ các biến dữ liệu mà nếu bị sửa đổi có thể tăng cường primitive khai thác - Các maintainer của Chrome xem xét kỹ thuật truyền con trỏ đã bị hỏng vào syscall unmap/remap để đục lỗ trong bộ nhớ rồi lấp lại bằng dữ liệu do kẻ tấn công kiểm soát
- Cách này không trực tiếp thay đổi các điểm chuyển điều khiển như địa chỉ trả về trên stack hay con trỏ hàm, nên có thể vượt qua các đảm bảo CFI ở cả forward-edge và backward-edge
- Trong các trình duyệt triển khai JIT compiler, kỹ thuật này có thể đặc biệt hữu ích
- Turbofan của V8 có thể tạo các vùng chuyển đổi giữa RW và RX
- Kẻ tấn công có thể khiến quá trình JIT tạo mã thực thi bằng JavaScript chạy thường xuyên trên hot path
- Sau khi lấp vùng đã unmap để ghi đè dữ liệu quan trọng, họ có thể tạo ra thay đổi dẫn đến thực thi mã
- Đây là một khai thác chỉ dữ liệu tác động đến luồng điều khiển bằng cách thao túng dữ liệu cụ thể trong bộ nhớ, mà không cần chiếm quyền điều khiển luồng trực tiếp hay làm rò rỉ con trỏ
msealcó thể chặn kịch bản đục lỗ như vậy bằng cách ngăn unmap và remap
Trường hợp House of Muney
- House of Muney dùng một kỹ thuật dựa trên unmap tương tự trong khai thác heap không gian người dùng
- Qualys đã dùng kỹ thuật này trong khai thác thực tế cho một lỗi Qmail cũ
- Kỹ thuật này dựa vào đặc tính
mallocvàfreesẽ lần lượt gọi trực tiếpmmapvàmunmapkhi chunk cấp phát lớn hơnM_MAP_THRESHOLD - Với các chunk lớn, không có freelist cache trung gian nên quy trình khai thác đơn giản hơn
- Nếu giả mạo metadata kích thước ở đầu chunk cấp phát thành kích thước trang khác rồi
free, có thể phát sinhmunmaptrên vùng nhớ liền kề chunk đó - Ví dụ của Dulin nhắm vào các vùng
.gnu.hashvà.dynsymbằngmunmaptùy ý- Sau đó lấp lại bằng một chunk
mmaplớn hơn - Từ đó có thể ghi đè một PLT entry chưa được resolve
- Khôi phục kiểu tấn công ghi đè GOT
- Sau đó lấp lại bằng một chunk
- PoC rút gọn cấp phát hai chunk lớn, thao túng trường kích thước của
top[-1], rồifree(top)để unmap cả vùng lân cận và lấp lại bằng dữ liệuXthông qua một cấp phát lớn hơn - Kỹ thuật này vẫn có thể hoạt động dù có CFI, và không cần rò rỉ ASLR từ trước
- Việc tích hợp
msealvào glibc với niêm phong tập VMA định sẵn được kỳ vọng sẽ tự động giảm thiểu kiểu tấn công này bằng cách bảo vệ mã nhị phân đã ánh xạ và thư viện động khỏi các mẹo unmap/remap - Nhà phát triển muốn siết chặt thêm có thể chọn niêm phong các vùng cấp phát
mmapsẽ không mở rộng hay bị unmap trong suốt vòng đời chương trình
Phạm vi sử dụng trong tương lai
mseallà một cơ chế giảm thiểu tương đối mới của Linux kernel và có thể còn thêm các trường hợp sử dụng chưa được đề cập- Khi tích hợp glibc hoàn tất và trưởng thành hơn, có thể sẽ có thêm các cải tiến để đáp ứng yêu cầu của syscall này
- Tham số
flagshiện chưa dùng cũng có thể được xác định công dụng cụ thể trong tương lai - Khi áp dụng vào phần mềm thực tế, cần đánh giá đồng thời vòng đời của vùng nhớ cần niêm phong và nhu cầu thay đổi của nó
1 bình luận
Ý kiến trên Hacker News
Sẽ rất hay nếu có người trong cuộc tóm tắt những phản đối và lo ngại đã xuất hiện trong cuộc tranh luận gay gắt trên mailing list của kernel
Bản thân tôi thường tránh mailing list vì đôi lúc nó quá cay nghiệt, mà tôi thì đã đủ đau đầu rồi
Cơ chế này tự nó có vẻ hợp lý, nhưng việc trước giờ kernel lại chưa có sẵn tính năng như thế này thì khá bất ngờ
Trong các flag được đề xuất có một cái cho phép bỏ qua niêm phong, và Linus phản ứng theo kiểu “ở chỗ này thì không cho
munmap, nhưng chỗ khác lại định bỏ qua niêm phong à”Sau đó ông còn nói mạnh hơn rằng “một khi đã niêm phong thì là đã niêm phong”, và không thể có kiểu “chỗ này thì tuân thủ niêm phong còn chỗ khác thì tùy tiện không tuân”
Về sau ông còn nói niêm phong là thứ không thể bị bỏ qua và cũng không phải thứ để thương lượng, thậm chí còn bảo rằng nếu còn tiếp tục đề xuất dùng flag để bỏ qua niêm phong thì sẽ đưa vào ignore list
Trong email Theo de Raadt gửi cho Jeff Xu, trước nhận định rằng cách tiếp cận của
mimmutable()vàmseal()khác nhau nhưng cả hai đều có mục tiêu làm cho ứng dụng Linux an toàn hơn bằng cách niêm phong bộ nhớ trước kẻ tấn công, ông đáp lại rằng “có vẻ như bạn đang làm mseal chỉ cho Chrome”Ông cho rằng nó sẽ không phù hợp với ứng dụng nói chung, với lý do là quá phức tạp và theo kinh nghiệm với
mimmutable()thì cuối cùng ứng dụng không tự xử lý mà việc đó sẽ doexecve(), khởi tạo libc vàld.sođảm nhiệmÔng đồng ý với Theo và Linus rằng ý tưởng cơ bản của
mimmutable()là tốt, nhưng cách nó được chia nhỏ và triển khai thì kinh khủngTôi tò mò không rõ system call này được dùng như thế nào
Chrome muốn nó, nhưng nếu kẻ tấn công có thể remap lại bằng flag khác thì các trang đã niêm phong sẽ không thể bị unmap
Vậy thì có phải trên thực tế nó không dùng được cho các trang được cấp phát lúc runtime, trừ khi bạn định giữ chúng suốt toàn bộ vòng đời tiến trình?
Nếu vậy thì chẳng phải sẽ khó áp dụng cho những mục tiêu cực kỳ hấp dẫn như bộ nhớ sandbox JS sao?
Tôi tự hỏi liệu cách giải quyết có phải là chạy loại công việc này trong một tiến trình riêng, niêm phong bộ nhớ rồi khi xong thì giết tiến trình đó không
Tôi không hiểu rõ cách Chrome quản lý bộ nhớ và tiến trình nên không chắc vì sao đây không phải vấn đề, và cũng tự hỏi có phải vì thế mà người ta thường giải thích tính năng này không hữu ích với phần lớn chương trình hay không
Theo tôi biết thì Chrome dùng cách này rất rộng rãi, và vì những lý do như cô lập bằng namespace nên đằng nào cũng cần tiến trình riêng, nên có thể hướng đi là như vậy
Thảo luận sau
mseal(), ngày 20 tháng 10 năm 2023: https://lwn.net/Articles/948129/mseal()đang đến gần, ngày 19 tháng 1 năm 2024: https://lwn.net/Articles/958438/Niêm phong bộ nhớ cho GNU C Library, ngày 12 tháng 6 năm 2024: https://lwn.net/Articles/978010/
Thật đáng tiếc khi các hệ điều hành vẫn phải triển khai những lời gọi như thế này dù kiến trúc x86_64 hiện đại đã có rất nhiều tính năng hỗ trợ lập trình và tính toán an toàn
Tôi cho rằng việc tiếp tục vá víu các hệ thống cũ kỹ được xây trên di sản lỗi thời và những tư duy, mô hình không còn phù hợp với thế giới và tri thức hiện tại đang làm chậm sự phát triển của điện toán và theo nghĩa đen đặt hàng tỷ người vào nguy hiểm
Tất nhiên tôi không định nói rằng những thay đổi như thế này không phải là một bước đi đúng hướng, nhưng nếu từ bỏ lý tưởng cũ rằng hệ điều hành phải hoạt động theo cách như hiện nay và thay vào đó cân nhắc các hệ thống hiện tại, tri thức hiện tại, cũng như những gì con người mong muốn ở hệ thống, thì ta có thể hình dung ra những hệ thống thoát khỏi gánh nặng và rủi ro đang đè lên nhà phát triển và người dùng ngày nay
Đúng là có các lỗi ở mức kiến trúc, nhưng phần mềm hiện tại còn chưa tận dụng đúng các tính năng đang có, nên viện dẫn lỗi kiến trúc để phản bác là đang lạc khỏi trọng tâm
Nếu được xây trên một nền móng lung lay thì sẽ luôn tồn tại những cách xâm nhập rẻ hơn
Tôi cũng muốn hiểu vì sao bạn cho rằng
mseallà không chính đáng, và đâu sẽ là lựa chọn tốt hơnCó thể ghi đè hoặc vô hiệu hóa syscall
msealbằng mẹoLD_PRELOADkhông?mseallà một syscall nhắm vào giảm thiểu tấn công thực thi mã từ xa hơn là chống kẻ tấn công cục bộ cố lấy cắp bí mật nhạy cảm trong bộ nhớ, khác với các cơ chế bảo vệ bộ nhớ hiện có của LinuxNếu kẻ tấn công từ xa có thể thay đổi môi trường cục bộ thì phải xem như hệ thống đã bị xâm nhập rồi
LD_PRELOADMuốn có tác dụng thì đó phải là hàm được import, còn syscall thô thì không thể chặn kiểu đó
Thảo luận về chặn Linux syscall: https://stackoverflow.com/questions/69859/how-could-i-interc...
Có thể cách dễ nhất để “vô hiệu hóa” tính năng là tự tạo một kernel đã vá, giả vờ
msealvẫn hoạt độngTuy nhiên, chương trình dùng
msealcó thể kiểm tra xem nó có thực sự hoạt động không, nên với kernel đã bị sửa hỏng thì còn cần cả cách âm thầm vô hiệu hóamsealđể ngăn ứng dụng phát hiện sau khi áp dụng kiểm traVí dụ, có thể theo dõi file thực thi bằng
ptrace, giám sát syscall rồi bỏ quamseal(2)Syscall này không dành cho mô hình đe dọa kiểu “kẻ tấn công đã có quyền truy cập trước cả khi tiến trình khởi tạo”
mseal, nhưng không thể ghi đè bản thân syscallTìm hiểu thì các cách ghi đè syscall bằng preload đều là thay wrapper, và nếu gọi syscall trực tiếp thì có vẻ sẽ không ghi đè được
Về mặt kỹ thuật, có lẽ vẫn có thể ghi đè chính hàm
syscallMeta: nguyên mẫu
mseal()trong bài cần được chỉnh sửaĐối số đầu tiên đang được ghi là
unsigned start addr, nhưng có lẽ đúng phải làunsigned long start_addrint mseal(unsigned long start, size_t len, unsigned long flags)Trong “Memory Sealing ‘Mseal’ System Call Merged for Linux 6.10” (2024) https://news.ycombinator.com/item?id=40474510#40474551, đã từng có thảo luận về việc CPython nên hỗ trợ syscall
mseal()như thế nàoOpenBSD đã có tính năng này từ trước https://man.openbsd.org/mimmutable.2
Không rõ vì sao một tính năng hiển nhiên như vậy đến giờ mới vào Linux
mimmutablecủa OpenBSD được giới thiệu trong OpenBSD 7.3, phát hành ngày 10 tháng 4 năm 2023, nên cũng không hẳn là “đã có từ lâu”Trong khi đó Linux và FreeBSD đã có
memfd_createtừ lâu, còn OpenBSD không có file ẩn danh nên phụ thuộc vàoshm_open