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
配置環境過於麻煩,不方便快速移植到其他設備上;libbpf
和cilium
對比起來,在內核層代碼都是c
寫的,區別不大,但是在用戶層代碼上,go
還是比c
更方便編寫;至於使用android mk
的方式,其實最開始選用的是該方案,畢竟是Android
的原生支持,不論是在數據結構上面還是在函數上面支持度相比較前面幾個工具都是最優選擇,缺點就是佔用資源過大,性能不好的機器編譯時長不是一般的長
四、ebpf中的數據傳輸
ebpf中內核和用戶層之間的數據傳輸常用的框架有兩種,分別是perf
和ringbuffer
,前者是從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
的值置爲1bpf_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
bpftool
的github
鏈接爲https://github.com/libbpf/bpftool
七、ebpf常見的事件類型
7.1 kprobe
kprobe
可以簡單理解爲在內核插樁,目前有兩種形式,分別是kprobe
和kretprobe
,前者是在函數開始處插樁,後者則是在函數返回之前插樁,使用舉例如下:
內核層:
//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
還有其他事件類型,例如socket
、sockops
、tc
、xdp
等等,但這些更多與流量控制息息相關,跟我們在移動安全上的關聯性不是很大,這裏不在舉例說明,當然還有uprobe
事件類型,這個是用戶層插樁的,但用戶層插樁更推薦frida
這些框架,而且uprobe
在linux
使用體驗感還好,在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
類型