使用 eBPF LSM 熱修復 Linux 內核漏洞
譯者注
原文鏈接:Live-patching security vulnerabilities inside the Linux kernel with eBPF Linux Security Module[1]
前段時間,我們討論了 Tetragon 產品實時阻斷能力的實現原理,那你知道它爲什麼沒選擇 eBPF LSM 嗎?系統內核版本要求是最大限制,eBPF LSM 需要 5.7 以後版本。但對於安全產品,阻斷一個函數的調用,遠比殺死一個進程影響要小。bpf_send_signal
顆粒度是進程,eBPF LSM 的顆粒度是函數,更精確。除此之外,控制範圍也不一樣,可以對函數調用堆棧做調整,達到替換執行的目標函數。業務場景就是對於漏洞的熱更新了。
而本文就是一個簡單的 eBPF LSM 實現思路,核心內容是確定精準 HOOK 點的思路。怎麼找 HOOK 點?HOOK 點掛載後,對性能影響是什麼?如何做權衡?接下來,我們一起了解一下。
前言
Linux Security Modules[2](LSM)是一個鉤子的基於框架,用於在 Linux 內核中實現安全策略和強制訪問控制。直到現在,能夠實現實施安全策略目標的方式只有兩種選擇,配置現有的 LSM 模塊(如 AppArmor、SELinux),或編寫自定義內核模塊。
Linux Kernel 5.7[3] 引入了第三種方式:LSM 擴展伯克利包過濾器 [4](eBPF)(簡稱 BPF LSM)。LSM BPF 允許開發人員編寫自定義策略,而無需配置或加載內核模塊。LSM BPF 程序在加載時被驗證,然後在調用路徑中,到達 LSM 鉤子時被執行。
實踐出真知
Namespaces 命名空間
現代操作系統提供了允許對內核資源進行partitioning
的工具。例如 FreeBSD 有jails
,Solaris 有zones
。Linux 不一樣,提供了一組看似獨立的工具,每個進程都允許隔離特定的資源。他就是Namespaces
,經過多年來不停迭代,孕育了Docker
、lxc
、firejail
應用。大部分Namespaces
是沒有爭議的,如 UTS 命名空間,它允許主機系統隱藏主機名和時間。其他的則比較複雜但簡單明瞭————衆所周知,NET 和 NS(mount)命名空間很難讓人理解。最後,還有一個非常特殊、非常有趣的USER Namespaces
。
USER Namespaces
很特別,因爲它允許所有者作爲root
操作。其工作原理超出了本文的範圍,但是,可以說它是讓Docker
等工具不作爲真正的 root 操作,或者說是rootless
容器。
由於其特性,允許未授權用戶訪問USER Namespaces
總是會帶來很大的安全風險。其中最大的風險是提權
。
提權原理
提權
是操作系統的常見攻擊面。user 獲得權限的一種方法是通過 unshare syscall[5] 將其命名空間映射到root
空間,並指定CLONE_NEWUSER
標誌。這會告訴unshare
創建一個具有完全權限的新用戶命名空間,並將新用戶和 Group ID 映射到以前的命名空間。即使用 unshare(1)[6] 程序將 root 映射到原始命名空間:
$ id
uid=1000(fred) gid=1000(fred) groups=1000(fred) …
$ unshare -rU
# id
uid=0(root) gid=0(root) groups=0(root),65534(nogroup)
# cat /proc/self/uid_map
0 1000 1
多數情況下,使用unshare
是沒有風險的,都是以較低的權限運行。但是,已經被用於提權了,比如 CVE-2022-0492[7],那麼本文就重點以這個場景爲例。
Syscalls clone
和clone3
也很值得研究,都有CLONE_NEWUSER
的功能。但在這篇文章中,我們將重點關注unshare
。
Debian 用 add sysctl to disallow unprivileged CLONE_NEWUSER by default[8] 補丁解決了這個問題,但它沒有被合併到源碼 mainline 主線中。另一個類似的補丁 sysctl: allow CLONE_NEWUSER to be disabled 嘗試合併到 mainline,但被拒絕了。理由是在某些特定應用中無法切換到該特性 [9]。在 Controlling access to user namespaces[10] 一文中,作者寫道:
... 目前的補丁似乎沒有一條通往 mainline 主線的捷徑。
如你所示,補丁最終沒有包含到 vanilla 內核 [11] 中。
我們的解決方案 LSM BPF
基於上面一些經驗,可以看到限制USER Namespaces
的代碼似乎行不通,我們決定使用LSM BPF
來規避這些問題。並且不需要修改內核,還可以自定義檢測防禦的規則。
尋找合適的候選鉤子
首先,讓我們跟蹤我們的目標系統調用。我們可以在 include/linux/syscalls.h[12] 文件中找到原型。
/* kernel/fork.c */
很清晰得看到,在 kernel/fork.c[13] 文件中,註釋部分中留下了下一個位置的線索。在 ksys_unshare()[14] 那裏調用。深入研究該函數,發現了一個對 unshare_userns()[15] 的調用。這讓我看到了希望。
現在,我們已經確定了 syscall 實現,但是接下來的問題是用哪些鉤子?怎麼選擇合適的鉤子?
從 man-pages[16] 中瞭解到 unshare 用於改變task
,那麼,我們重點關注 include/linux/lsm_hooks.h[17] 中的關於task
的鉤子。在函數 unshare_userns()[18] 中,可以找到對 prepare_creds()[19] 的調用。對於 cred_prepare[20] 的 HOOK 來說看上去不錯。爲了驗證對 prepare_creds()[21] 的理解是否正確,接下來繼續分析 security_prepare_creds()[22] 的調用,可以確認,其最終會調用這個 HOOK:
…
rc = call_int_hook(cred_prepare, 0, new, old, gfp);
…
暫不過多討論這個問題,現在能確認的是這個 HOOK 比較合適,因爲prepare_creds()
正好在unshare_userns()
中的create_user_ns()
之前被調用,而 unshare_userns()[23] 是我們試圖阻止的操作。
LSM BPF 解決方案
我們將使用 eBPF 編譯一次到處運行 (CO-RE)[24] 的方法對代碼進行編譯。在不同版本內核的 IDC 中,會特別適用。(不過,國內外大部分五至十年的互聯網公司,都有着大量低於 5.0 的內核版本)。本文的演示,將只對 x86_64 CPU 架構系統驗證。ARM64 的 LSM BPF 仍在開發中。你可以訂閱 BPF 郵件列表 [25] 來了解最新進展。
此解決方案在Kernel Version >=5.15
上進行了測試,配置如下:
BPF_EVENTS
BPF_JIT
BPF_JIT_ALWAYS_ON
BPF_LSM
BPF_SYSCALL
BPF_UNPRIV_DEFAULT_OFF
DEBUG_INFO_BTF
DEBUG_INFO_DWARF_TOOLCHAIN_DEFAULT
DYNAMIC_FTRACE
FUNCTION_TRACER
HAVE_DYNAMIC_FTRACE
如果 CONFIG_LSM 列表中不包含bpf
,則需要你自己重新編譯,並開啓lsm=bpf
選項.
內核空間代碼
開始看內核空間代碼:deny_unshare.bpf.c
:
#include <linux/bpf.h>
#include <linux/capability.h>
#include <linux/errno.h>
#include <linux/sched.h>
#include <linux/types.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_core_read.h>
#define X86_64_UNSHARE_SYSCALL 272
#define UNSHARE_SYSCALL X86_64_UNSHARE_SYSCALL
CO-RE
接下來,我們以下列方式爲 CO-RE 重新定位建立必要的結構:deny_unshare.bpf.c
:
…
typedef unsigned int gfp_t;
struct pt_regs {
long unsigned int di;
long unsigned int orig_ax;
} __attribute__((preserve_access_index));
typedef struct kernel_cap_struct {
__u32 cap[_LINUX_CAPABILITY_U32S_3];
} __attribute__((preserve_access_index)) kernel_cap_t;
struct cred {
kernel_cap_t cap_effective;
} __attribute__((preserve_access_index));
struct task_struct {
unsigned int flags;
const struct cred *cred;
} __attribute__((preserve_access_index));
char LICENSE[] SEC("license") = "GPL";
…
用戶空間
加載程序並將其附加到目標的鉤子上是用戶空間的功能。有幾種方法可以做到這一點:
-
Cilium ebpf[26] 項目
-
Rust bindings[27]
-
ebpf.io[28] 項目
landscape
展示的其他類庫
這裏,我們將使用原生 libbpf[29]。
#include <bpf/libbpf.h>
#include <unistd.h>
#include "deny_unshare.skel.h"
static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
{
return vfprintf(stderr, format, args);
}
int main(int argc, char *argv[])
{
struct deny_unshare_bpf *skel;
int err;
libbpf_set_strict_mode(LIBBPF_STRICT_ALL);
libbpf_set_print(libbpf_print_fn);
// Loads and verifies the BPF program
skel = deny_unshare_bpf__open_and_load();
if (!skel) {
fprintf(stderr, "failed to load and verify BPF skeleton\n");
goto cleanup;
}
// Attaches the loaded BPF program to the LSM hook
err = deny_unshare_bpf__attach(skel);
if (err) {
fprintf(stderr, "failed to attach BPF skeleton\n");
goto cleanup;
}
printf("LSM loaded! ctrl+c to exit.\n");
// The BPF link is not pinned, therefore exiting will remove program
for (;;) {
fprintf(stderr, ".");
sleep(1);
}
cleanup:
deny_unshare_bpf__destroy(skel);
return err;
}
Makefile
最後,進行編譯,這裏使用 Makefile
CLANG ?= clang-13
LLVM_STRIP ?= llvm-strip-13
ARCH := x86
INCLUDES := -I/usr/include -I/usr/include/x86_64-linux-gnu
LIBS_DIR := -L/usr/lib/lib64 -L/usr/lib/x86_64-linux-gnu
LIBS := -lbpf -lelf
.PHONY: all clean run
all: deny_unshare.skel.h deny_unshare.bpf.o deny_unshare
run: all
sudo ./deny_unshare
clean:
rm -f *.o
rm -f deny_unshare.skel.h
#
# BPF is kernel code. We need to pass -D__KERNEL__ to refer to fields present
# in the kernel version of pt_regs struct. uAPI version of pt_regs (from ptrace)
# has different field naming.
# See: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=fd56e0058412fb542db0e9556f425747cf3f8366
#
deny_unshare.bpf.o: deny_unshare.bpf.c
$(CLANG) -g -O2 -Wall -target bpf -D__KERNEL__ -D__TARGET_ARCH_$(ARCH) $(INCLUDES) -c $< -o $@
$(LLVM_STRIP) -g $@ # Removes debug information
deny_unshare.skel.h: deny_unshare.bpf.o
sudo bpftool gen skeleton $< > $@
deny_unshare: deny_unshare.c deny_unshare.skel.h
$(CC) -g -Wall -c $< -o $@.o
$(CC) -g -o $@ $(LIBS_DIR) $@.o $(LIBS)
.DELETE_ON_ERROR:
結果
打開一個新終端,運行命令
$ make run
…
LSM loaded! ctrl+c to exit.
在另一個終端裏,可以看到成功的被阻止了。
$ unshare -rU
unshare: unshare failed: Cannot allocate memory
$ id
uid=1000(fred) gid=1000(fred) groups=1000(fred) …
這個策略有個附加的特性,可以允許傳遞授權。
$ sudo unshare -rU
# id
uid=0(root) gid=0(root) groups=0(root)
在無特權場景中,系統調用會提前中止。有特權情況下的性能影響是什麼?
性能對比
我們將使用一行 unshare 命令來映射用戶命名空間,並在中執行一個命令來進行測量:
$ unshare -frU --kill-child -- bash -c "exit 0"
使用系統調用 unshare enter/exit 的 CPU 週期分辨率,我們將以 root 用戶身份測量以下內容:
-
命令在沒有策略的情況下運行
-
與策略一起運行的命令
我們將使用 ftrace[30] 記錄測量結果:
$ sudo su
# cd /sys/kernel/debug/tracing
# echo 1 > events/syscalls/sys_enter_unshare/enable ; echo 1 > events/syscalls/sys_exit_unshare/enable
此時,我們將專門爲 unshare 啓用對系統調用enter/exit
的跟蹤。現在,我們設置enter/exit
調用的time-resolution
來計算 CPU 週期:
# echo 'x86-tsc' > trace_clock
接下來,我們開始評測
# unshare -frU --kill-child -- bash -c "exit 0" &
[1] 92014
在新終端裏運行策略,執行下一個 syscall
# unshare -frU --kill-child -- bash -c "exit 0" &
[2] 92019
現在,我們收集到兩種 CALLS 結果進行對比
# cat trace
# tracer: nop
#
# entries-in-buffer/entries-written: 4/4 #P:8
#
# _-----=> irqs-off
# / _----=> need-resched
# | / _---=> hardirq/softirq
# || / _--=> preempt-depth
# ||| / _-=> migrate-disable
# |||| / delay
# TASK-PID CPU# ||||| TIMESTAMP FUNCTION
# | | | ||||| | |
unshare-92014 [002] ..... 762950852559027: sys_unshare(unshare_flags: 10000000)
unshare-92014 [002] ..... 762950852622321: sys_unshare -> 0x0
unshare-92019 [007] ..... 762975980681895: sys_unshare(unshare_flags: 10000000)
unshare-92019 [007] ..... 762975980752033: sys_unshare -> 0x0
分別是:
-
unshare-92014 used 63294 cycles.
-
unshare-92019 used 70138 cycles.
可以看到二者之間有 6,844(~10%) 個週期的差異,還行。
兩次測量之間有 6,844(~10%) 個週期損失。不錯嘛!
這些數字是針對單個系統調用的,代碼調用的頻率越高,這些數字就越多。Unshare 通常在任務創建時調用,在程序的正常執行期間不會重複調用。對於你的場景,需要仔細考慮評估。
結尾
我們瞭解了LSM BPF
是什麼,如何使用 unshare 將user
映射到root
,以及如何通過在 eBPF 中實現程序來解決真實場景的問題。跟蹤準確的鉤子不是一件容易的事,需要有豐富的經驗,以及豐富的內核代碼經驗。這些一個策略代碼是用 C 語言編寫的,所以我們可以根據因地制宜,不同的問題做不同的策略,代碼經過輕微調整,就可以快速擴展,增加其他鉤子點等。最後,我們對比了這個 LSM 程序的性能影響,性能與安全的權衡,是你需要考慮的問題。
Cannot allocate memory
(無法分配內存)不是拒絕權限的最準確的描述。我們提出了一個補丁 [31],用於將錯誤代碼從cred_prepare
掛鉤傳到調用堆棧。
最後,我們的結論就是eBPF LSM
鉤子非常適合實時修復 Linux 內核漏洞,你要來試試嗎?
參考資料
[1]
Live-patching security vulnerabilities inside the Linux kernel with eBPF Linux Security Module: https://blog.cloudflare.com/live-patch-security-vulnerabilities-with-ebpf-lsm/
[2]
Linux Security Modules: https://www.kernel.org/doc/html/latest/admin-guide/LSM/index.html
[3]
Linux Kernel 5.7: https://cdn.kernel.org/pub/linux/kernel/v5.x/ChangeLog-5.7
[4]
LSM 擴展伯克利包過濾器: https://docs.kernel.org/bpf/prog_lsm.html
[5]
syscall: https://en.wikipedia.org/wiki/System_call
[6]
unshare(1): https://man7.org/linux/man-pages/man1/unshare.1.html
[7]
CVE-2022-0492: https://nvd.nist.gov/vuln/detail/CVE-2022-0492
[8]
add sysctl to disallow unprivileged CLONE_NEWUSER by default: https://sources.debian.org/patches/linux/3.16.56-1+deb8u1/debian/add-sysctl-to-disallow-unprivileged-CLONE_NEWUSER-by-default.patch/
[9]
無法切換到該特性: https://lore.kernel.org/all/87poq5y0jw.fsf@x220.int.ebiederm.org/
[10]
Controlling access to user namespaces: https://lwn.net/Articles/673597/
[11]
vanilla 內核: https://wiki.debian.org/vanilla
[12]
include/linux/syscalls.h: https://elixir.bootlin.com/linux/v5.18/source/include/linux/syscalls.h#L608
[13]
kernel/fork.c: https://elixir.bootlin.com/linux/v5.18/source/kernel/fork.c#L3201
[14]
ksys_unshare(): https://elixir.bootlin.com/linux/v5.18/source/kernel/fork.c#L3082
[15]
unshare_userns(): https://elixir.bootlin.com/linux/v5.18/source/kernel/fork.c#L3129
[16]
man-pages: https://man7.org/linux/man-pages/man2/unshare.2.html
[17]
include/linux/lsm_hooks.h: https://elixir.bootlin.com/linux/v5.18/source/include/linux/lsm_hooks.h#L605
[18]
unshare_userns(): https://elixir.bootlin.com/linux/v5.18/source/kernel/user_namespace.c#L171
[19]
prepare_creds(): https://elixir.bootlin.com/linux/v5.18/source/kernel/cred.c#L252
[20]
cred_prepare: https://elixir.bootlin.com/linux/v5.18/source/include/linux/lsm_hooks.h#L624
[21]
prepare_creds(): https://elixir.bootlin.com/linux/v5.18/source/kernel/cred.c#L291
[22]
security_prepare_creds(): https://elixir.bootlin.com/linux/v5.18/source/security/security.c#L1706
[23]
unshare_userns(): https://elixir.bootlin.com/linux/v5.18/source/kernel/user_namespace.c#L181
[24]
一次到處運行 (CO-RE): https://nakryiko.com/posts/bpf-core-reference-guide/#defining-own-co-re-relocatable-type-definitions
[25]
BPF 郵件列表: https://lore.kernel.org/bpf/
[26]
Cilium ebpf: https://github.com/cilium/ebpf
[27]
Rust bindings: https://github.com/libbpf/libbpf-rs
[28]
ebpf.io: https://ebpf.io/projects/
[29]
libbpf: https://github.com/libbpf/libbpf
[30]
ftrace: https://docs.kernel.org/trace/ftrace.html
[31]
補丁: https://lore.kernel.org/all/20220608150942.776446-1-fred@cloudflare.com
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/UJEC8nmfQbdsWdJMfju0ig