Android12无root使用eBPF

eBPF介绍

eBPF(extended Berkeley Packet Filter)是一种内核技术,它允许开发人员在不修改内核代码的情况下运行特定的功能。在出厂版本高于Android12的内核上,谷歌都要求加入eBPF作为安卓内核原生的一部分,因此eBPF具有开销低、无感知等等优点。除了固定的tracepoint之外,还提供kprobe追踪内核指令、uprobe追踪用户态程序,追踪非常灵活全面。
想要在安卓上开发自定义的eBPF程序,除了自己写源码手动编译并整合进系统外,也可以选择BCC或者bpftrace这样的现成工具。但是这两个工具都需要系统具有root权限,同时也不利于我们对eBPF的实现有一个较为深入的认识,因此本文将采取手动编译的方式使用eBPF。

Kernel-side与User-side程序


根据安卓官方的介绍,在 Android 启动期间,系统会加载位于 /system/etc/bpf/ 的所有 eBPF 程序。这些程序是 Android 构建系统根据 C 程序构建而成的二进制对象,并附带了 Android 源代码树中的 Android.bp 文件。构建系统将生成的对象存储在 /system/etc/bpf 中,这些对象将成为系统映像的一部分。

因此,使用eBPF需要分为两步:

  1. 编写内核态的eBPF程序,编译后储存在/system/etc/bpf/目录
  2. 编写用户态的eBPF程序,访问bpfmap以读取内核态程序的输出

具体如何编写网上已经有充足的教程,本文不再赘述,这里推荐一篇我自己参考的文章:https://pshocker.github.io/2022/06/18/Android-eBPF%E7%9B%91%E6%8E%A7%E6%89%80%E6%9C%89%E7%B3%BB%E7%BB%9F%E8%B0%83%E7%94%A8/

非特权使用eBPF

内核态程序本身就具有root权限;而用户态程序想要读取bpfmap,就必须具有root权限。接下来是本文的主要内容:如何在一台不进行root的手机上使用eBPF

最初我进行的尝试是先使用magisk获取临时root,进行必要配置之后还原成没有root的镜像。主要思路是赋予用户态程序的可执行文件suid权限,在运行时将自己变成root,并且也确实能够将自己的euid甚至ruid改为root,但是读取bpfmap时仍提示权限不足。翻阅相关资料之后发现,除了权限之外,安卓系统还有一套capabilities的访问控制机制,其中就包括控制bpfmap读取权限的CAP_BPF。而运行程序时,会将调用者和被调用文件的capabilities做一个与操作,也就是只取两者较低的权限,因此这个方法无法达到我们想要的效果。

失败之后,决定另辟蹊径,思考如何在非root手机上获取真正的root。在这里我主要受到了magisk获取root权限的方式的启发:与传统linux的su不同,magisk 在开机后会启动一个高权限 root 用户的进程 magiskd,调用 su 会通过 socket 连接到 magiskd,然后在 magiskd 进程中执行命令。那么我们也可以对系统进行类似的修改,启动一个真正的root进程,并与其通信以完成对bpfmap的读取。

由于我们可以对系统进行修改,获取特权进程并非难事:init.rc中规定了系统启动时会触发的一系列服务,此时使用的权限就是root权限,我们可以将我们的程序注入system/core/rootdir/init.rc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#定义service
service myroot /system/bin/myroot.sh
class main
# class 包括core main late_start等,可以不设置。默认default
user root
# 用户属性,默认root,一般给root即可,权限最大
group root
# 所属组,默认root
disabled
# disabled 即通过class不能启动,只能start screencap 这种name的方式启动
oneshot
# 只运行一次

#启动条件
on property:sys.boot_completed=1
start myroot

这样,系统便会在启动后调用我们存放在/system/bin/myroot.sh的脚本,其内容如下:

1
2
3
4
5
6
#!/bin/sh
while true
do
/system/bin/bpf_cli|nc 0.0.0.0 23334
sleep 1
done

这样,该进程便会陷入死循环,等待本地23334端口的连接,执行用户态bpf程序/system/bin/bpf_cli并传出程序的输出。

配置完成后,我们便可以通过一个普通进程连接23334端口,从而以root权限执行bpf用户态程序,实现在非root的环境里使用eBPF。值得一提的是,要使该进程能够在启用SELinux的环境下执行相应功能,我们还需要对sepolicy进行修改,主要流程大致如下:
0. 在system/sepolicy/private/file_contexts里加入对相关文件的记录:

1
2
/system/bin/myroot.sh      u:object_r:myroot_exec:s0
/system/bin/bpf_cli u:object_r:myroot_exec:s0

创建system/sepolicy/private/myroot.te,写入如下内容:

1
2
3
4
type myroot, domain, coredomain;
type myroot_exec, exec_type, file_type, system_file_type;

init_daemon_domain(myroot)

(注意此处要同时修改system/sepolicy/prebuilts/api/32.0/private/file_contexts和system/sepolicy/prebuilts/api/32.0/private/myroot.te,否则无法通过编译)

  1. 将SELinux设置为permissive
  2. 执行程序,在dmesg里寻找所有相关的avc denied记录
  3. 将记录输入audit2allow,将输出增添至myroot.te
  4. 重复2-3直到不再有新的报错,将将SELinux设置为enforcing

参考文章

  1. 安卓官方eBPF介绍(https://source.android.com/docs/core/architecture/kernel/bpf?hl=zh-cn)
  2. 编写并编译eBPF程序(https://pshocker.github.io/2022/06/18/Android-eBPF%E7%9B%91%E6%8E%A7%E6%89%80%E6%9C%89%E7%B3%BB%E7%BB%9F%E8%B0%83%E7%94%A8/)
  3. 新增具有root权限的开机服务(http://www.shibaking.com/blog/2020-03-16-Android8.1%E4%B8%8B%E6%96%B0%E5%A2%9E%E5%BC%80%E6%9C%BA%E6%9C%8D%E5%8A%A1.html)
  4. 修改sepolicy(https://www.codeleading.com/article/90661709951/)