首页 > 其他分享 >ebpf在Android安全上的应用:ebpf的一些基础知识(上篇)

ebpf在Android安全上的应用:ebpf的一些基础知识(上篇)

时间:2024-04-24 18:44:22浏览次数:24  
标签:err bpf ebpf 基础知识 sys 内核 Android cilium

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类型

标签:err,bpf,ebpf,基础知识,sys,内核,Android,cilium
From: https://www.cnblogs.com/aWxvdmVseXc0/p/18156088

相关文章

  • Android逆向与安全——360 dex加固与脱壳
    参考:https://zhuanlan.zhihu.com/p/566412180前言现在市面上对APP的安全合规管控越来越严格了,也就要求了APP在上架之前一定要做合规检测和加固处理。对APP就是加固的好处,可以提高APP的安全性,提高APP被逆向分析破解的门槛,同时通过加固保护可以提高过安全合规的检测。由于APP加......
  • 关于使用UE5打包Android的测试
    UE5打包Android,不同于UE4,在官方文档中需要Androidstudio4.0或者3.5,还有AndroidSDK,NDK等设置SetupAndroid, 在UE5Editor配置如下:  其中 gamedatainside.apk需要打钩,否则会出现报错:TherewasanerrorinstallingthegameortheobbfileProjectSDKOverri......
  • Android开发笔记[18]-使用本地模块
    摘要将摄像头预览页面封装到Android模块中并在app中使用这个本地模块.关键信息AndroidStudio:Iguana|2023.2.1Gradle:distributionUrl=https://services.gradle.org/distributions/gradle-8.4-bin.zipjvmTarget='1.8'minSdk26targetSdk34compileSdk34开发语言:K......
  • 【android】获取手机安装的所有程序
     1.获取包管理器对象PackageManagerpm=context.getPackageManager();2.得到所有安装的程序包名List<PackageInfo>infos=pm.getInstallPackages(PackageManager.GET_UNINSTALLED_PACKAGES);3.然后遍历这个集合for(PackageInfopackInfo:infos){Drawabl......
  • 使用 MediaCodec 在 Android 上进行硬解码
    要使用MediaCodec在Android上进行硬解码,并获取RGBA数据,你可以按照以下步骤进行操作:创建MediaExtractor对象并设置要解码的MP4文件路径:MediaExtractorextractor=newMediaExtractor();extractor.setDataSource(filePath);根据需要选择音频或视频轨道:inttrackCo......
  • Android Studio 蓝牙 示例代码(转)
    原文:https://blog.csdn.net/qq_40511184/article/details/122698077因为androidstudio升级,下面代码中的startactivityresult函数有变化,不能使用,需要更换为publicActivityResultLauncher<Intent>register;ActivityResultLauncher<Intent>startBlueTooth=registerForActi......
  • 关于Android Studio双击exe没有反应解决办法
    首先说原因,我的因为之前卸载了,然后重新下载后一直打不开,查看了很多博客找不到解决办法,最终在AndroidStudio双击exe没有反应解决办法_androidstudio双击没反应-CSDN博客这个博客中得到了启发 先说解决办法第一步,到AndroidStudio的bin目录下,查看studio.bat文件查看的话......
  • Android进程创建流程-2
    一、java线程创建流程1.部分调用逻辑Runtime::AttachCurrentThread(constchar*thread_name,boolas_daemon,jobjectthread_group,boolcreate_peer)Runtime::Init(RuntimeArgumentMap&&runtime_options_in)//runtime.cc备注说"ClassLinkerneedsan......
  • 系统架构基础知识入门指南-下
    接上篇文章,这篇文章聊聊技术同学如何由点及面的了解并掌握系统架构知识。 大家可以先回想一下,我们入职一家新公司做技术工作,一般都是如何开展工作的。首先,我们需要了解团队和项目的技术规范和迭代发布上线流程。其次,还要了解自己所在岗位负责哪些业务,对应的沟通合作对象是谁......
  • android studio Edit Custom VM Options后无法启动
    异常描述:想要修改虚拟器的内存,就百度了方法,设置了Help——EditCustomVMOptions,然后AndroidStudio就无法启动了,直接弹这个弹窗:所以,建议大家写文,还是要有头有尾,该上图上图,不能啪啪几个字让人猜啊,容易误导人的啊啊啊!!!解决问题:按这个路径查找到更改的文件,C:\Users\XXXX\AppDat......