一文全面瞭解 LSM BPF (含實戰,強烈建議收藏)
- 安全背景知識
國際上對計算機安全概括了三個特性:私密性(Confidentiality)、 完整性(Integrity)、可用性(Availability),簡稱 CIA。
-
私密性就是數據不被未授權的人看到;
-
完整性指存儲或傳輸的信息不被篡改;
-
可用性是指自己的設備在需要使用的時候能夠使用。
計算機系統應對安全挑戰的辦法大致有四種:隔離、控制、審計、混淆。
訪問控制就是對訪問進行控制,其用於表示主體可以針對客體進行的操作。訪問控制主要工作包括定義主體和客體,定義操作和設定訪問策略。一般來說主體主要是系統中運行的進程,客體爲內核中的資源對象包括文件、目錄、管道、設備、套接字、共享內存、消息隊列等系統資源。
針對訪問控制就用戶控制維度來看,又可分爲:
-
自主訪問控制 DAC(Discretionary Access Control):資源所有者決定誰可以訪問以及他們可以執行哪些操作。
-
強制訪問控制 MAC(Mandatory Access Control):是一種更嚴格的訪問控制方式,訪問權限由管理員或專家預先定義的策略來管理控制,系統環境中普通用戶無法決定的訪問控制。
內核安全知識可觀看視頻 內核安全入門視頻 [1],視頻的製作者爲《Linux 內核安全模塊深入剖析》書籍的作者李志。
Linux 內核安全的開發從上世紀 90 年代中後期開始,經過 20 多年的開發, Linux 內核中安全相關模塊已經比較全面的,有用於強制訪問控制的 LSM 、有用於完整性保護的 IMA 和 EVM 、有用於加密的密鑰管理模塊和加密算法庫、還有日誌和審計模塊、以及一些零碎的安全增強特性。本文的主角 LSM(Linux Security Module)則是與強制訪問控制 MAC 相關的內核安全通用框架。
- 內核安全策略模塊通用框架 LSM
2.1 LSM 框架介紹
LSM(Linux Security Module)[2] 中文翻譯爲內核安全模塊,儘管從名字上是安全模塊,但 LSM 其實是一個在內核各個安全模塊的基礎上提出(抽象出)的輕量級安全訪問控制框架。該框架只是提供一個支持安全模塊的接口,本身不能增強系統安全性,具體的工作交給各安全模塊來做。通俗一點講 LSM 只是搭建了一個舞臺,唱戲的主角還得有具體實現的模塊來捧場。
LSM 在內核中體現爲一組安全相關的函數,是提供實施強制訪問控制(MAC)模塊的必要組件,可實現策略與內核源代碼解耦。安全函數在系統調用的執行路徑中會被調用,對用戶態進程訪問內核資源對象進行強制訪問控制,受保護的對象類型包括文件、目錄、任務對象、憑據等。從版本 5.4 開始,該框架目前包括整個內核的 224 個掛鉤點、一個用於註冊要在這些掛鉤點調用的函數的 API,以及一個用於保留與受保護內核對象關聯的內存以供 LSM 使用的 API。如果要想要進一步瞭解可參考內核文檔 LSM 部分 [3] 內容。
截止到內核 5.7 版本,共有 9 個 LSM 安全模塊實現:SELinux、SMACK、AppArmor、TOMOYO、Yama、LoadPin、SafeSetID 和 Lockdown 和 BPF(5.7 版本支持) 。安全模塊主要實現如下圖所示:
Seccomp 和 LSM 都可實現內核級別限制進程與系統的交互方式,但安全計算模式(seccomp)是關於限制進程可以進行的系統調用,相比之下,LSM 是關於控制對內核中對象的訪問。LSM 與安全框架 seccomp 的區別和聯繫可進一步閱讀 LSM vs seccomp[4]。
關於 Major 和 Minor LSM
-
Major LSM:通過用戶空間加載配置策略來實現 MAC,一次只能使用單個 LSM,因爲它們都假定它們對嵌入在受保護內核對象中的安全上下文指針和安全標識符具有獨佔訪問權限,例如 SELinux、SMACK、AppArmor 和 TOMOYO。
-
Minor LSM – 次要 LSM 實現了特定的安全功能,並堆疊在主要 LSM 之上,並且大多數需要較少的安全性,較少的上下文。次要 LSM 通常只包含用於啓用 / 禁用選項的標誌,而不是將從用戶空間加載的策略文件作爲系統的一部分。如 Yama、loadPin、SetSafeID 和 Lockdown。
關於 LSM 的完整介紹可進一步參考 LSM 簡介 [5] 和 LSM,Linux 內核的安全防護盾 。
2.2 LSM 架構
LSM 框架提供了一個模塊化的架構,該架構提供了內置於內核中的 "鉤子"(hook),並允許安裝安全模塊,從而加強了訪問控制。
LSM 框架主要由五大部分組成:
-
在關鍵的特定內核數據結構中加入了安全域,例如
struce file
結構中的void *f_security;
; -
在內核源碼中不同的關鍵點處插入對安全鉤子函數的調用;
-
提供了一個通用的安全系統調用,允許安全模塊爲安全相關的應用編寫新的系統調用,其風格類似於原有的 Linux 系統調用 socketcall(),是一個多路的系統調用;
-
提供了註冊和註銷函數,使得訪問控制策略可以以內核模塊方式實現,主要是通過 security_add_hooks 和 security_delete_hooks;
-
將 capabilities 邏輯的大部分功能移植爲一個可選的安全模塊。
具體實現上, LSM 框架通過提供一系列的 Hook 即鉤子函數來控制對內核對象的操作,其本質是插樁法。以文件 open 函數訪問過程爲例,Hook 函數的訪問示意圖如下:
-
通過系統調用進入內核之後,系統首先進行錯誤檢查;
-
錯誤檢查通過之後,進行傳統的權限檢查即自主訪問控制(Discretionary Access Control,DAC)檢查(傳統權限檢查主要是基於用戶的,用戶通過驗證之後就可以訪問資源);
-
通過之後纔會進行強制訪問控制 MAC。強制訪問控制是不允許主體干涉的一種訪問控制,其採用安全標識、信息分級等信息敏感性進行訪問控制,並通過比較主體的級別和資源的敏感性來確定是否允許訪問。
關於 LSM 的實現細節建議進一步閱讀 深入理解 LSM pdf[6]。
2.3 LSM 中的鉤子函數
LSM 背後的核心概念是 LSM 鉤子。LSM 鉤子暴露在內核的關鍵位置,可通過掛鉤進行管制的操作示例包括:
-
文件系統操作
-
打開、創建、移動和刪除文件
-
掛載和卸載文件系統
-
task/process operations 任務 / 進程操作
-
分配和釋放任務,更改任務的用戶和組標識
-
套接字操作
-
創建和綁定套接字
-
接收和發送消息
系統中 LSM 鉤子都列在 Linux 內核源碼的頭文件 lsm_hooks.h[7] [kernel 6.2.0] 中查看到。不同的操作對應場景可通過頭部註釋獲,在內核 6.2.0 中以下類別:
$ grep "Security hooks for" include/linux/lsm_hooks.h
* Security hooks for program execution operations.
* Security hooks for mount using fs_context.
* Security hooks for filesystem operations.
* Security hooks for inode operations.
* Security hooks for kernfs node operations
* Security hooks for file operations
* Security hooks for task operations.
* Security hooks for Netlink messaging.
* Security hooks for Unix domain networking.
* Security hooks for socket operations.
* Security hooks for SCTP
* Security hooks for Infiniband
* Security hooks for XFRM operations.
* Security hooks for individual messages held in System V IPC message queues
* Security hooks for System V IPC Message Queues
* Security hooks for System V Shared Memory Segments
* Security hooks for System V Semaphores
* Security hooks for Audit
* Security hooks for the general notification queue:
* Security hooks for using the eBPF maps and programs functionalities through
* Security hooks for perf events
* Security hooks for io_uring
每個定義的鉤子函數都包含對應的參數,這些參數根據哪些程序可以實施策略決策提供上下文,並列在 lsm_hook_defs.h[8], 內核 6.2.0 版本定義數量爲 247 個,這裏我們給出部分定義實現的樣例:
其中 LSM_HOOK 定義格式如下:
LSM_HOOK(<return_type>, <default_value>, <hook_name>, args...)
LSM 提供的大多數鉤子都需要返回一個整數值(也有部分返回 void,表示忽略運行結果),返回值中的值一般定義如下:
-
0 等同於授權;
-
ENOMEM 無可用內存;
-
EACCESS,安全策略拒絕訪問;
-
EPERM,執行此操作需要權限。
- LSM BPF
在 LSM BPF 出現之前,能夠實現實施安全策略目標的方式有兩種選擇:配置現有的 LSM 模塊(如 AppArmor、SELinux),或編寫自定義內核模塊。LSM BPF 則提供了第三種實現的方案 [9],靈活且安全,具有可編程性。
Linux 5.7[10] 引入在 LSM 中提供了對於 BPF 的支持 [11](簡稱 LSM BPF)。使用 LSM BPF,開發人員能夠在無需配置或加載內核模塊的情況下編寫精細策略。LSM BPF 程序會在加載時進行驗證,然後在調用路徑中到達 LSM hook 時執行。這些 BPF 程序允許特權用戶對 LSM 鉤子進行運行時檢測,以使用 eBPF 實現系統範圍的 MAC(強制訪問控制)和審計策略。
截止到內核 6.2.0 版本, BPF 自身安全相關的 LSM hook 函數有 7 個 [12],通過編譯條件宏 CONFIG_BPF_SYSCALL 控制,主要設涉及 bpf 系統調用、BPF 程序和 BPF map 相關操作:
#ifdef CONFIG_BPF_SYSCALL
LSM_HOOK(int, 0, bpf, int cmd, union bpf_attr *attr, unsigned int size)
LSM_HOOK(int, 0, bpf_map, struct bpf_map *map, fmode_t fmode)
LSM_HOOK(int, 0, bpf_prog, struct bpf_prog *prog)
LSM_HOOK(int, 0, bpf_map_alloc_security, struct bpf_map *map)
LSM_HOOK(void, LSM_RET_VOID, bpf_map_free_security, struct bpf_map *map)
LSM_HOOK(int, 0, bpf_prog_alloc_security, struct bpf_prog_aux *aux)
LSM_HOOK(void, LSM_RET_VOID, bpf_prog_free_security, struct bpf_prog_aux *aux)
#endif /* CONFIG_BPF_SYSCALL */
在繼續並嘗試編寫 LSM BPF 程序之前,請確保:
-
內核版本至少爲 5.7;
-
LSM BPF 已啓用。
LSM BPF 的啓用可以通過以下方式進行驗證,正確的輸出應包含 bpf
:
$ grep CONFIG_BPF_LSM /boot/config-6.2.0-34-generic
CONFIG_BPF_LSM=y
$ cat /sys/kernel/security/lsm
capability,lockdown,landlock,yama,apparmor,bpf
如果沒有,則必須通過將 LSM BPF 添加到內核配置參數中來手動啓用它。在我本地 Ubuntu 22.04 系統中的輸出中內容中,並沒有包括 bpf 選項,因此我們需要手動啓用。
lockdown,capability,landlock,yama,apparmor
通過調整 GRUB 配置 /etc/default/grub
並在內核參數中添加以下內容來實現:
GRUB_CMDLINE_LINUX="lsm=lockdown,capability,landlock,yama,apparmor,bpf"
然後通過執行 update-grub
命令 重新構建 GRUB 配置(每個命令可能在不同的 Linux 發行版中可用或不可用):
$ update-grub
# 通過 grep 啓動命令行確認
$ grep lsm /boot/grub/grub.cfg
#update-grub2
#grub2-mkconfig -o /boot/grub2/grub.cfg
#grub-mkconfig -o /boot/grub/grub.cfg
確定添加到啓動參數後,重新啓動系統生效。
3.1 BCC 實踐
BCC 項目已經提供了 LSM 功能支持,我們可以使用宏 LSM_PROBE
進行函數定義,這裏我們實現一個禁止調用 bpf()
系統調用的功能,該程序正常執行後,我們任何涉及到 bpf() 系統調用都將顯示權限不足,用魔法來打敗魔法:
import os
import sys
import time
from bcc import BPF, libbcc
# support_lsm() 函數檢查系統是否支持 BTF 和具有 bpf_lsm_bpf 函數變量,並不能反應 LSM BPF 是否可以工作
if not BPF.support_lsm():
print("LSM not supported")
prog = """
#include <uapi/asm-generic/errno-base.h>
LSM_PROBE(bpf, int cmd, union bpf_attr *attr, unsigned int size)
{
bpf_trace_printk("LSM BPF hook Worked");
return -EPERM;
}
"""
b = BPF(text=prog)
while True:
b.trace_print()
正常的運行效果圖如下:
需要注意的是,如果修改了 GRUB 參數添加了 bpf,但是並沒有重啓系統 ,運行上述程序並會報錯,但是並不能達到實際運行效果。在程序運行後,我們使用 bpftool 參數仍然可以運行:
$ bpftool prog list
922: lsm name bpf tag 30c9cae9d49a8659 gpl
loaded_at 2024-01-04T14:58:00+0800 uid 0
xlated 120B jited 79B memlock 4096B
btf_id 398
pids python3(927881)
這是因爲上述代碼中的 BPF.support_lsm()
只是通過靜態檢查系統是否開啓了 BTF 和聲明瞭 bpf_lsm_bpf 函數符號,並不能正確反饋系統是否 lsm 中是否啓用了 BPF 支持。
# src/python/bcc/__init__.py
def support_lsm():
if not lib.bpf_has_kernel_btf():
return False
# kernel symbol "bpf_lsm_bpf" indicates BPF LSM support
if BPF.ksymname(b"bpf_lsm_bpf") != -1:
return True
return False
bpftrace 目前還不支持 LSM BPF,相關工作已經在討論開發中,相關工作可參見 Support attaching to bpf LSM hooks [13]。
3.2 libbpf-bootstrap 框架實踐
環境準備參考官方倉庫 編譯章節 [14],關於 libbpf-bootstrap 介紹文檔可查考 Building BPF applications with libbpf-bootstrap[15]。
在 /examples/c 目錄中添加 lsm.bpf.c:
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <errno.h>
char LICENSE[] SEC("license") = "GPL";
SEC("lsm/bpf")
int BPF_PROG(lsm_bpf, int cmd, union bpf_attr *attr, unsigned int size, int ret)
{
/* ret is the return value from the previous BPF program
* or 0 if it's the first hook.
*/
if (ret != 0)
return ret;
bpf_printk("LSM: block bpf() worked");
return -EPERM;
}
這裏需要注意的一點是,函數最後多了一個 int ret
,這是由於 LSM hook 是以鏈表的方式管理,ret 用於反饋上一個 BPF 程序處理的結果,如果當前運行的 BPF 爲首個運行,這 ret 的值爲 0。
修改 Makefile 文件:
-APPS = minimal minimal_legacy bootstrap uprobe kprobe fentry usdt sockfilter tc ksyscall task_iter
+APPS = minimal minimal_legacy bootstrap uprobe kprobe fentry usdt sockfilter tc ksyscall task_iter lsm
運行編譯命令,會自動生成 lsm.skel.h 文件,編譯成功後會生成 lsm 二進制文件:
# make
BPF .output/lsm.bpf.o
GEN-SKEL .output/lsm.skel.h
CC .output/lsm.o
BINARY lsm
運行效果與 BCC 編寫的類似:
另外基於 cilium/ebpf Go 庫的代碼可以參考 lsm-ebpf-experimenting[16] 倉庫,這個就不在展示。
- 總結
本文簡單介紹了 LSM 框架的基礎知識,並基於 LSM BPF 給出了 BCC 和 libbpf 庫的實現樣例,希望能夠讓你快速入門 LSM BPF 的編程。如果你對 LSM BPF 的應用場景希望有更多的瞭解,推薦你進一步閱讀 使用 eBPF LSM 熱修復 Linux 內核漏洞 [17],在該文中作者基於容器環境中 USER 命名空間提權的場景給出了基於 LSM BPF 規避風險的完整實踐,這包括原理、選擇 hook 函數和 BPF 代碼實現。
- 附錄:LSM 熱修內核漏洞查找 hook 點過程
解決一個問題最難的是找到阻斷問題的關鍵路徑點,爲了方便大家理解,這裏將使用 eBPF LSM 熱修復 Linux 內核漏洞文章中的確定 LSM hook 點的過程給出,供後續參考,完整的 bpf 代碼可在 lsm_bpf_monitoring[18] 找到。
允許無特權的用戶訪問 USER 命名空間始終會帶來極大的安全風險。其中一種風險就是特權提升。特權提升是操作系統的常見攻擊面。用戶可以獲取特權的一種方式是通過 unshare syscall[19] 將其命名空間映射到根命名空間,並指定 CLONE_NEWUSER 標誌。這會指示 unshare 創建有完整權限的新用戶命名空間,並將新用戶和組 ID 映射到之前的命名空間。你可以使用 unshare(1)[20] 程序將根映射到我們的原始命名空間:
通過 手冊頁 [21] 可以知道,unshare 用於改變任務,所以我們來看一下 include/linux/lsm_hooks.h[22] 中基於任務的 hook。早在函數 unshare_userns()[23] 中,我們就看到對 prepare_creds()[24] 的調用。這非常類似於 cred_prepare[25] hook。爲了驗證我們是否通過 prepare_creds()[26] 獲得匹配,我們觀察對安全性 hook security_prepare_creds()[27] 的調用,後者最終會調用該 hook:
unshare (系統調用 )
-> ksys_unshare
-> unshare_userns
-> prepare_creds
-> security_prepare_creds (LSM hook 函數 )
unshare 系統調用定義 [28]
SYSCALL_DEFINE1(unshare, unsigned long, unshare_flags)
{
return ksys_unshare(unshare_flags);
}
ksys_unshare()[29] 中調用函數與 user 相關的調用函數是 unshare_userns()[30]
int unshare_userns(unsigned long unshare_flags, struct cred **new_cred)
{
// ...
cred = prepare_creds();
// ...
}
unshare_userns()[31] 源碼如下:
int unshare_userns(unsigned long unshare_flags, struct cred **new_cred)
{
// ...
cred = prepare_creds();
// ...
return err;
}
prepare_creds()[32] 函數有關於 LSM hook 的調用:
struct cred *prepare_creds(void)
{
// ...
if (security_prepare_creds(new, old, GFP_KERNEL_ACCOUNT) < 0)
goto error;
}
BPF 與安全相關參見:https://lwn.net/Kernel/Index/#BPF-Security
作者水平有限,如有錯誤懇請批評指正。
參考資料
[1]
內核安全入門視頻 : https://www.bilibili.com/video/BV1nS4y137mh/
[2]
LSM(Linux Security Module): https://www.kernel.org/doc/html/latest/admin-guide/LSM/index.html
[3]
內核文檔 LSM 部分: https://www.kernel.org/doc/html/latest/admin-guide/LSM/index.html
[4]
LSM vs seccomp: https://www.starlab.io/blog/linux-security-modules-lsms-vs-secure-computing-mode-seccomp
[5]
LSM 簡介 : https://www.starlab.io/blog/a-brief-tour-of-linux-security-modules/
[6]
深入理解 LSM pdf: https://elinux.org/images/0/0a/ELC_Inside_LSM.pdf
[7]
lsm_hooks.h: https://elixir.bootlin.com/linux/v6.2/source/include/linux/lsm_hooks.h
[8]
lsm_hook_defs.h: https://elixir.bootlin.com/linux/v6.2/source/include/linux/lsm_hook_defs.h
[9]
LSM BPF 則提供了第三種實現的方案 : https://blog.cloudflare.com/zh-cn/live-patch-security-vulnerabilities-with-ebpf-lsm-zh-cn/
[10]
Linux 5.7: https://cdn.kernel.org/pub/linux/kernel/v5.x/ChangeLog-5.7
[11]
BPF 的支持 : https://docs.kernel.org/bpf/prog_lsm.html
[12]
LSM hook 函數有 7 個 : https://elixir.bootlin.com/linux/v6.2/source/include/linux/lsm_hook_defs.h#L395
[13]
Support attaching to bpf LSM hooks : https://github.com/iovisor/bpftrace/issues/2783
[14]
編譯章節 : https://github.com/libbpf/libbpf-bootstrap?tab=readme-ov-file#building
[15]
Building BPF applications with libbpf-bootstrap: https://nakryiko.com/posts/libbpf-bootstrap/
[16]
lsm-ebpf-experimenting: https://github.com/fabstu/lsm-ebpf-experimenting
[17]
使用 eBPF LSM 熱修復 Linux 內核漏洞 : https://blog.cloudflare.com/zh-cn/live-patch-security-vulnerabilities-with-ebpf-lsm-zh-cn/
[18]
lsm_bpf_monitoring: https://github.com/linuxkerneltravel/lmp/tree/develop/eBPF_Supermarket/LSM_BPF/lsm_bpf_monitoring
[19]
syscall: https://en.wikipedia.org/wiki/System_call
[20]
unshare(1): https://man7.org/linux/man-pages/man1/unshare.1.html
[21]
手冊頁 : https://man7.org/linux/man-pages/man2/unshare.2.html
[22]
include/linux/lsm_hooks.h: https://elixir.bootlin.com/linux/v5.18/source/include/linux/lsm_hooks.h#L605
[23]
unshare_userns(): https://elixir.bootlin.com/linux/v5.18/source/kernel/user_namespace.c#L171
[24]
prepare_creds(): https://elixir.bootlin.com/linux/v5.18/source/kernel/cred.c#L252
[25]
cred_prepare: https://elixir.bootlin.com/linux/v5.18/source/include/linux/lsm_hooks.h#L624
[26]
prepare_creds(): https://elixir.bootlin.com/linux/v5.18/source/kernel/cred.c#L291
[27]
security_prepare_creds(): https://elixir.bootlin.com/linux/v5.18/source/security/security.c#L1706
[28]
unshare 系統調用定義 : https://elixir.bootlin.com/linux/v5.18/source/kernel/fork.c#L3201
[29]
ksys_unshare(): https://elixir.bootlin.com/linux/v5.18/source/kernel/fork.c#L3082
[30]
unshare_userns(): https://elixir.bootlin.com/linux/v5.18/source/kernel/user_namespace.c#L171
[31]
unshare_userns(): https://elixir.bootlin.com/linux/v5.18/source/kernel/user_namespace.c#L171
[32]
prepare_creds(): https://elixir.bootlin.com/linux/v5.18/source/kernel/cred.c#L252
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/w5f-24Z6IqP7GTVxqJoLaA