一文全面瞭解 LSM BPF (含實戰,強烈建議收藏)

  1. 安全背景知識

國際上對計算機安全概括了三個特性:私密性(Confidentiality)、 完整性(Integrity)、可用性(Availability),簡稱 CIA。

計算機系統應對安全挑戰的辦法大致有四種:隔離、控制、審計、混淆。

訪問控制就是對訪問進行控制,其用於表示主體可以針對客體進行的操作。訪問控制主要工作包括定義主體和客體,定義操作和設定訪問策略。一般來說主體主要是系統中運行的進程,客體爲內核中的資源對象包括文件、目錄、管道、設備、套接字、共享內存、消息隊列等系統資源。

針對訪問控制就用戶控制維度來看,又可分爲:

內核安全知識可觀看視頻 內核安全入門視頻 [1],視頻的製作者爲《Linux 內核安全模塊深入剖析》書籍的作者李志。

Linux 內核安全的開發從上世紀 90 年代中後期開始,經過 20 多年的開發, Linux 內核中安全相關模塊已經比較全面的,有用於強制訪問控制的 LSM 、有用於完整性保護的 IMA 和 EVM 、有用於加密的密鑰管理模塊和加密算法庫、還有日誌和審計模塊、以及一些零碎的安全增強特性。本文的主角 LSM(Linux Security Module)則是與強制訪問控制 MAC 相關的內核安全通用框架。

  1. 內核安全策略模塊通用框架 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

關於 LSM 的完整介紹可進一步參考 LSM 簡介 [5] 和 LSM,Linux 內核的安全防護盾

2.2 LSM 架構

LSM 框架提供了一個模塊化的架構,該架構提供了內置於內核中的 "鉤子"(hook),並允許安裝安全模塊,從而加強了訪問控制。

LSM 框架主要由五大部分組成:

具體實現上, LSM 框架通過提供一系列的 Hook 即鉤子函數來控制對內核對象的操作,其本質是插樁法。以文件 open 函數訪問過程爲例,Hook 函數的訪問示意圖如下:

關於 LSM 的實現細節建議進一步閱讀 深入理解 LSM pdf[6]。

2.3 LSM 中的鉤子函數

LSM 背後的核心概念是 LSM 鉤子。LSM 鉤子暴露在內核的關鍵位置,可通過掛鉤進行管制的操作示例包括:

系統中 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,表示忽略運行結果),返回值中的值一般定義如下:

  1. 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 程序之前,請確保:

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] 倉庫,這個就不在展示。

  1. 總結

本文簡單介紹了 LSM 框架的基礎知識,並基於 LSM BPF 給出了 BCC 和 libbpf 庫的實現樣例,希望能夠讓你快速入門 LSM BPF 的編程。如果你對 LSM BPF 的應用場景希望有更多的瞭解,推薦你進一步閱讀 使用 eBPF LSM 熱修復 Linux 內核漏洞 [17],在該文中作者基於容器環境中 USER 命名空間提權的場景給出了基於 LSM BPF 規避風險的完整實踐,這包括原理、選擇 hook 函數和 BPF 代碼實現。

  1. 附錄: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