ebpf在Android安全上的應用:ebpf的一些基礎知識(上篇)

ebpf在Android安全上的應用:ebpf的一些基礎知識(上篇)

一、ebpf介紹

eBPF 是一項革命性的技術,起源於 Linux 內核,它可以在特權上下文中(如操作系統內核)運行沙盒程序。它用於安全有效地擴展內核的功能,而無需通過更改內核源代碼或加載內核模塊的方式來實現。(PS:介紹來源於https://ebpf.io/zh-cn/what-is-ebpf/

對比kernel hook,ebpf最大的優點在於安全和可移植性,在ebpf載入之前,需要經過驗證器的驗證,能夠保證內核不會因爲ebpf程序而出現崩潰,可移植性體現在多版本支持,屏蔽掉了底層的細節,能最大程度保證開發者將重心放在程序的邏輯性上;同樣的,ebpf最大的缺點也體現在了爲了保證安全的驗證器上,例如循環次數有限制等,導致一些明明可以很簡潔的操作在ebpf中編程時必須要使用很蠢的方法間接實現(ps:對kernel hook感興趣的可以參考一下我之前的一篇文章https://www.52pojie.cn/thread-1672531-1-1.html


二、運行環境

OS:Android模擬器pixel 6 API level 33 x86_64

kernel:5.15.41


三、開發工具鏈

ebpf常見的開發工具有如下一些:

  • bcc:BCC 是一個框架,它允許用戶編寫 python 程序,並將 eBPF 程序嵌入其中。但是bcc想將bcc運行在android上時配置環境時相對麻煩,當然,環境配置好開發難度相比其他工具更低,同時,網上的資料相比其他工具也更多

  • libbpf:libbpf 是一個基於 C 的庫,包含一個 BPF 加載程序,該加載程序獲取已編譯的 BPF 目標文件並準備它們並將其加載到 Linux 內核中。 libbpf 承擔了加載、驗證 BPF 程序並將其附加到各種內核掛鉤的繁重工作,使 BPF 應用程序開發人員能夠只關注 BPF 程序的正確性和性能。官方鏈接:https://github.com/libbpf/libbpf

  • cilium:cilium是一個純 Go 庫,提供用於加載、編譯和調試 eBPF 程序的實用程序。官方鏈接:https://github.com/cilium/ebpf

  • Android mk:谷歌提供的android原生ebpf支撐,官方鏈接:https://source.android.google.cn/docs/core/architecture/kernel/bpf?hl=zh-cn

    本系列文章均選擇使用cilium,經過對比,bcc配置環境過於麻煩,不方便快速移植到其他設備上;libbpfcilium對比起來,在內核層代碼都是c寫的,區別不大,但是在用戶層代碼上,go還是比c更方便編寫;至於使用android mk的方式,其實最開始選用的是該方案,畢竟是Android的原生支持,不論是在數據結構上面還是在函數上面支持度相比較前面幾個工具都是最優選擇,缺點就是佔用資源過大,性能不好的機器編譯時長不是一般的長


四、ebpf中的數據傳輸

ebpf中內核和用戶層之間的數據傳輸常用的框架有兩種,分別是perfringbuffer,前者是從kernel module而來的,而後者是專門爲ebpf定製的,體驗性更好,所有一般都使用後者

在內核層,常規用法爲首先使用bpf_ringbuf_reserve申請一個buffer,然後調用bpf_ringbuf_submit提交數據到緩衝區,更詳細的可以參考文檔https://www.kernel.org/doc/html/next/bpf/ringbuf.html


五、ebpf中的常見函數

  • bpf_printk: ebpf內核層打印函數,用法和printf一致,該函數輸出到了/sys/kernel/tracing/trace_pipe文件中(PS:有些系統是/sys/kernel/debug/tracing/trace_pipe),值得注意的是,要開啓打印,需要將/sys/kernel/tracing/tracing_on的值置爲1
  • bpf_probe_read_user_str: 從用戶空間讀取字符串
  • bpf_probe_read: 從內核空間讀取內存, 以上函數用法都可以參考https://man7.org/linux/man-pages/man7/bpf-helpers.7.html

六、vmlinux.h

vmlinux.h是啥?vmlinux.h是由工具生成而來的,包含了該機器內核所有的數據結構,有了這個頭文件,就避免了我們去官網上查詢相應的數據結構,還能避免不同版本之間帶來的數據結構變動的問題

通常我們使用bpftool去生成,命令爲bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h

bpftoolgithub鏈接爲https://github.com/libbpf/bpftool


七、ebpf常見的事件類型

7.1 kprobe

kprobe可以簡單理解爲在內核插樁,目前有兩種形式,分別是kprobekretprobe,前者是在函數開始處插樁,後者則是在函數返回之前插樁,使用舉例如下:

內核層:

//go:build ignore

#include "vmlinux.h"

char __license[] SEC("license") = "GPL";

struct file_data {
    u32 uid;
    u8 filename[256];
};

struct event {
    struct file_data file;
};

struct {
    __uint(type,BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries,1 << 24);
} events SEC(".maps");

const struct event *unused __attribute__((unused));

SEC("kprobe/do_sys_openat2")
int kprobe_openat(struct pt_regs *ctx)
{
    u32 uid;
    struct event *openat2data;
    char *fp = (char *)(ctx->si);
    
    uid = bpf_get_current_uid_gid();
        
    openat2data = bpf_ringbuf_reserve(&events,sizeof(struct event),0);
    if(!openat2data)
    {
        return 0;
    }
    long res = bpf_probe_read_user_str(&openat2data->file.filename,256,fp);
    bpf_printk("uid: %d, filename: %s",uid,openat2data->file.filename);
    openat2data->file.uid = uid;
    bpf_ringbuf_submit(openat2data,0);
    
    return 0;
}

用戶層:

package main

import (
    "log"
    "os"
    "os/signal"
    "syscall"
    "errors"
    "bytes"
    "encoding/binary"
    "fmt"
    
    //"github.com/cilium/ebpf"
    "github.com/cilium/ebpf/link"
    "github.com/cilium/ebpf/rlimit"
    "github.com/cilium/ebpf/ringbuf"
    "golang.org/x/sys/unix"
)

//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -tags "linux" -type event --target=amd64 bpf blog.c -- -I./headers

func main() {
    stopper := make(chan os.Signal,1)
    signal.Notify(stopper,os.Interrupt,syscall.SIGTERM)

    if err := rlimit.RemoveMemlock(); err != nil {
        log.Fatal(err);
    }
    
    objs := bpfObjects{}
    if err := loadBpfObjects(&objs,nil); err != nil {
        log.Fatal(err);
    }
    defer objs.Close()
    
    se, err := link.Kprobe("do_sys_openat2",objs.KprobeOpenat,nil)
    if err != nil {
        log.Fatal(err)
    }
    defer se.Close()
    
    rd, err := ringbuf.NewReader(objs.Events)
    if err != nil {
        log.Fatal(err)
    }
    defer rd.Close()
    
    go func() {
        <-stopper
        
        if err := rd.Close(); err != nil {
            log.Fatal(err)
        }
    }()
    
    log.Println("Waiting for Data")
    
    var event bpfEvent
    
    for {
        record, err := rd.Read()
        if err != nil {
            if errors.Is(err,ringbuf.ErrClosed) {
                log.Println("Received signal, exiting...")
                return
            }
            log.Fatal(err)
            continue
        }
        if err := binary.Read(bytes.NewBuffer(record.RawSample),binary.LittleEndian,&event); err != nil {
            log.Fatal(err)
            continue
        }
        fmt.Printf("[%+v]: filename -> %s\n",event.File.Uid,unix.ByteSliceToString(event.File.Filename[:]))
    }
}

編譯:先go generate,然後go build即可

效果圖如下:

至於kretprobe,和kprobe區別不大,這裏不在舉例說明

7.2 tracepoint

tracepoint可以理解爲是在源碼中預埋的hook點位,相比較kprobe,穩定性被大大增強,當然缺點也很明顯,那就是數量有限,沒辦法自定義,查看所有tracepoint可在/sys/kernel/tracing/events/目錄下找到所有可追蹤的事件(PS: 有些機器可能是在/sys/kernel/debug/tracing/events/下),事件的格式信息在相應的事件目錄下的format文件中

內核層:

//go:build ignore

#include "vmlinux.h"

char __license[] SEC("license") = "GPL";

struct sys_enter_args {
   unsigned short common_type;
   unsigned char common_flags;
   unsigned char common_preempt_count;
   int common_pid;
   
   long id;
   unsigned long args[6];
};

SEC("tracepoint/raw_syscalls/sys_enter")
int trace_sys_enter(struct sys_enter_args *args)
{
    u32 syscall_nr;
    
    syscall_nr = args->id;
    
    bpf_printk("syscall_nr: %d",syscall_nr);
    
    return 0;
}

bpf_printk函數打印的結果在/sys/kernel/tracing/trace_pipe文件中(PS:有些機型在/sys/kernel/debug/tracing/trace_pipe文件中,下同,下面的不在重複解釋),觀看bpf_printk函數結果需要先將/sys/kernel/tracing/tracing_on文件中的值置爲1

用戶層:

package main

import (
	"log"
	"time"

	"github.com/cilium/ebpf/link"
	"github.com/cilium/ebpf/rlimit"
)

//go:generate go run github.com/cilium/ebpf/cmd/bpf2go --target=amd64 bpf blog.c -- -I./headers


func main() {

	if err := rlimit.RemoveMemlock(); err != nil {
		log.Fatal(err)
	}

	objs := bpfObjects{}
	if err := loadBpfObjects(&objs, nil); err != nil {
		log.Fatalf("loading objects: %v", err)
	}
	defer objs.Close()

	kp, err := link.Tracepoint("raw_syscalls","sys_enter",objs.TraceSysEnter,nil)
        if err != nil {
            log.Fatal(err)
        }
        defer kp.Close()

	ticker := time.NewTicker(1 * time.Minute)
	defer ticker.Stop()

	log.Println("Waiting for events..")

	for range ticker.C {
	    log.Printf("get rule\n")
        }
}

效果圖如下:

7.3 其他事件類型

ebpf還有其他事件類型,例如socketsockopstcxdp等等,但這些更多與流量控制息息相關,跟我們在移動安全上的關聯性不是很大,這裏不在舉例說明,當然還有uprobe事件類型,這個是用戶層插樁的,但用戶層插樁更推薦frida這些框架,而且uprobelinux使用體驗感還好,在Android端使用去插樁APP過於麻煩了。


八、一些使用技巧

8.1 將數據從用戶空間傳輸到內核空間

cilium中,ringbuffer並不支持將數據從用戶空間傳遞到內核空間,只支持將數據從內核空間發送到用戶空間,在新的數據傳輸框架BPF_MAP_TYPE_USER_RINGBUF支持將數據從用戶空間傳輸到內核空間,但是遺憾的是,cilium暫不支持該框架

在我們需要傳輸一些過濾條件或者動態的全局配置到內核層去過濾的時候需要怎麼做喃?可以考慮監控特定的文件名、特定的命令等來獲取數據,當然這種方式僅時候傳遞數據量不大的情況

8.2 獲取UID

UID是啥,UID是android中uid用於標識一個應用程序,uid在應用安裝時被分配,並且在應用存在於手機上期間,都不會改變,可以理解爲app的唯一身份標識,在ebpf中,可以用來過濾指定app的數據

ebpf可以使用bpf_get_current_uid_gid函數來獲取UID,該函數返回值爲u32類型

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