下篇:ebpf在Android安全上的应用:结合binder完成一个行为检测沙箱
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中内核和用户层之间的数据传输常用的框架有两种,分别是perf和ringbuffer,前者是从kernel module而来的,而后者是专门为ebpf定制的,体验性更好,所有一般都使用后者
在内核层,常规用法为首先使用bpf_ringbuf_reserve申请一个buffer,然后调用bpf_ringbuf_submit提交数据到缓冲区,更详细的可以参考文档https://www.kernel.org/doc/html/next/bpf/ringbuf.html
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是由工具生成而来的,包含了该机器内核所有的数据结构,有了这个头文件,就避免了我们去官网上查询相应的数据结构,还能避免不同版本之间带来的数据结构变动的问题
通常我们使用bpftool去生成,命令为bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
bpftool的github链接为https://github.com/libbpf/bpftool
kprobe可以简单理解为在内核插桩,目前有两种形式,分别是kprobe和kretprobe,前者是在函数开始处插桩,后者则是在函数返回之前插桩,使用举例如下
内核层:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | #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;
}
|
用户层:
[注意]看雪招聘,专注安全领域的专业人才平台!
最后于 2024-4-29 19:33
被windy_ll编辑
,原因: