BruceFan's Blog

Stay hungry, stay foolish

0%

使用Rust开发安卓eBPF程序

一、Rust编译工具简介

在Rust中如果刚开始学习语法,写一些简单的单一文件,那么可以使用rustc来进行编译

1
$ rustc main.rs

rustc与gcc和clang类似,编译成功后,rustc会输出一个二进制的可执行文件。不过随着项目的增长,我们就需要功能更强大的编译工具。
Cargo是Rust的构建系统和包管理器,大多数Rustacean们使用Cargo来管理Rust项目,它可以处理很多任务,如构建代码、下载依赖库并编译这些库。cargo的基本用法非常简单:

1
2
$ cargo new hello_world  // 新建名为hello_world的项目
$ cd hello_world

进入hello_world项目,cargo生成了一个Cargo.toml文件和一个src目录,src目录中是项目的主文件main.rs。
代码清单:Cargo.toml

1
2
3
4
5
6
7
8
[package]
name = "hello_world"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

这个文件使用TOML(Tom’s Obvious, Minimal Language)格式,这是Cargo配置文件的格式。
[package]是一个section标题,表明下面的语句用来配置一个包。接着三行设置了cargo编译程序所需的配置:项目的名称、项目的版本以及要使用的Rust版本。
[dependencies]这个section列举了项目依赖的代码包,在Rust中代码包被称为crates。

1
2
3
4
5
6
7
$ cargo build  // 编译Rust项目
Compiling hello_world v0.1.0 (/Users/fanrong/Computer/Rust/hello_world)
Finished dev [unoptimized + debuginfo] target(s) in 1.84s
$ cargo run // 运行Rust项目
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/hello_world`
Hello, world!

Cargo的基本用法就是这么简单,更多Cargo相关信息请查阅Cargo官方文档

二、环境搭建

环境:
Ubuntu 22.04
Pixel 6 root
要用Rust开发安卓eBPF程序,这里的方法是借助Aya框架,Aya框架是一套纯Rust开发的eBPF框架,只需要libc crate来执行系统调用,支持BTF,而且当使用musl(静态链接)链接时,它提供了真正的CO-RE(compile once,run everywhere)。
首先搭建Aya开发环境:
1.安装Rust stable和nightly的toolchain

1
2
3
4
5
6
$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
$ rustup toolchain install nightly --component rust-src
// 切换到nightly版本
$ rustup default nightly
// 添加ARM64架构支持
$ rustup target add aarch64-unknown-linux-musl

2.安装bpf-linker和cargo-generate,linker依赖LLVM:

1
2
3
4
$ sudo apt install lld
$ cargo install bpf-linker
$ sudo apt install libssl-dev pkg-config
$ cargo install cargo-generate

三、开始一个新项目

1.创建项目:

1
2
3
4
5
6
7
8
9
10
11
$ cargo generate https://github.com/aya-rs/aya-template
⚠️ Favorite `https://github.com/aya-rs/aya-template` not found in config, using it as a git repository: https://github.com/aya-rs/aya-template
🤷 Project Name: kprobe-rs
🔧 Destination: /home/fanrong/Computer/BPF/practice/kprobe-rs ...
🔧 project-name: kprobe-rs ...
🔧 Generating template ...
✔ 🤷 Which type of eBPF program? · kprobe
🤷 Where to attach the (k|kret)probe? (e.g try_to_wake_up): do_sys_openat2
🔧 Moving generated files into: `/home/fanrong/Computer/BPF/practice/kprobe-rs`...
Initializing a fresh Git repository
✨ Done! New project created /home/fanrong/Computer/BPF/practice/kprobe-rs

2.代码解析
生成的项目手脚架有两部分,一部分是ebpf代码,编译后生成字节码,另一部分是用户空间代码,用来把ebpf字节码注入到内核中。注入到内核中的字节码首先由ebpf-verifier验证安全性,程序中没有崩溃、死循环等,可以正常结束,字节码通过JIT编译器将字节码转换成机器指令以提高执行效率,然后将程序指令挂载到指定的钩子上以被触发执行。
kprobe-rs-ebpf中的代码是eBPF代码,这是一个在open系统调用加hook的kprobe程序

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
#![no_std] // 不能使用标准库
#![no_main] // 没有main函数

use aya_bpf::{macros::kprobe, programs::ProbeContext};
use aya_log_ebpf::info;

#[kprobe(name = "kprobe_rs")] // 表示这是一个kprobe程序
pub fn kprobe_rs(ctx: ProbeContext) -> u32 {
// 主入口点
match try_kprobe_rs(ctx) {
Ok(ret) => ret,
Err(ret) => ret,
}
}

fn try_kprobe_rs(ctx: ProbeContext) -> Result<u32, u32> {
// open系统调用被调用时打印
info!(&ctx, "function do_sys_openat2 called");
Ok(0)
}

#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
unsafe { core::hint::unreachable_unchecked() }
}

kprobe-rs中是用户空间部分的代码

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
use aya::programs::KProbe;
use aya::{include_bytes_aligned, Bpf};
use aya_log::BpfLogger;
use log::{info, warn, debug};
use tokio::signal;

#[tokio::main] // 主入口点
async fn main() -> Result<(), anyhow::Error> {
env_logger::init();

// Bump the memlock rlimit. This is needed for older kernels that don't use the
// new memcg based accounting, see https://lwn.net/Articles/837122/
let rlim = libc::rlimit {
rlim_cur: libc::RLIM_INFINITY,
rlim_max: libc::RLIM_INFINITY,
};
let ret = unsafe { libc::setrlimit(libc::RLIMIT_MEMLOCK, &rlim) };
if ret != 0 {
debug!("remove limit on locked memory failed, ret is: {}", ret);
}

// This will include your eBPF object file as raw bytes at compile-time and load it at
// runtime. This approach is recommended for most real-world use cases. If you would
// like to specify the eBPF program at runtime rather than at compile-time, you can
// reach for `Bpf::load_file` instead.

// include_bytes_aligned!()在编译时会拷贝BPF ELF目标文件的内容
// Bpf::load()读取前一个命令中BPF ELF目标文件的内容,创建maps,执行BTF重定向
#[cfg(debug_assertions)]
let mut bpf = Bpf::load(include_bytes_aligned!(
"../../target/bpfel-unknown-none/debug/kprobe-rs"
))?;
#[cfg(not(debug_assertions))]
let mut bpf = Bpf::load(include_bytes_aligned!(
"../../target/bpfel-unknown-none/release/kprobe-rs"
))?;
if let Err(e) = BpfLogger::init(&mut bpf) {
// This can happen if you remove all log statements from your eBPF program.
warn!("failed to initialize eBPF logger: {}", e);
}
// 提取kprobe程序
let program: &mut KProbe = bpf.program_mut("kprobe_rs").unwrap().try_into()?;
// 把它加载进内核
program.load()?;
program.attach("do_sys_openat2", 0)?;

info!("Waiting for Ctrl-C...");
signal::ctrl_c().await?;
info!("Exiting...");

Ok(())
}

3.编译运行

1
2
3
4
5
6
7
8
9
$ cargo xtask build-ebpf
$ export RUST_LOG=INFO // 设置LOG级别为INFO
$ cargo xtask run
...
[2023-07-08T19:30:28Z INFO kprobe_rs] Waiting for Ctrl-C...
[2023-07-08T19:30:30Z INFO kprobe_rs] function do_sys_openat2 called
[2023-07-08T19:30:30Z INFO kprobe_rs] function do_sys_openat2 called
[2023-07-08T19:30:30Z INFO kprobe_rs] function do_sys_openat2 called
...

现在生成的项目可以编译运行在Ubuntu上了,想要能在安卓上运行需要进行交叉编译。

四、交叉编译

Rust可以支持的平台非常多,ARM、MIPS、PowerPC、RISCV、X86等:

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
$ rustup target list
aarch64-apple-darwin
aarch64-apple-ios
aarch64-apple-ios-sim
aarch64-linux-android
aarch64-pc-windows-msvc
aarch64-unknown-fuchsia
aarch64-unknown-linux-gnu
aarch64-unknown-linux-musl (installed)
aarch64-unknown-none
aarch64-unknown-none-softfloat
aarch64-unknown-uefi
arm-linux-androideabi
arm-unknown-linux-gnueabi
arm-unknown-linux-gnueabihf
arm-unknown-linux-musleabi
arm-unknown-linux-musleabihf
armebv7r-none-eabi
armebv7r-none-eabihf
armv5te-unknown-linux-gnueabi
armv5te-unknown-linux-musleabi
armv7-linux-androideabi
armv7-unknown-linux-gnueabi
armv7-unknown-linux-gnueabihf
...

这里用到的是aarch64-unknown-linux-musl,支持aarch64架构的静态编译工具。
还需要对项目中的文件进行一些修改,首先是项目整体的配置文件.cargo/config.toml,末尾加上这个编译工具:

1
2
[target.aarch64-unknown-linux-musl]
linker = "ld.lld"

eBPF代码的编译文件kprobe-rs-ebpf/Cargo.toml,加入aarch64编译条件:

1
2
[features]
aarch64 = []

用户空间部分代码的编译文件kprobe-rs/Cargo.toml,加入aarch64系统调用依赖

1
2
[target.'cfg(target_arch = "aarch64")'.dependencies]
syscalls = { version = "*", default-features = false, features = ["aarch64"] }

其他也主要是相关的编译文件,需要在其中加入arm64架构的配置。详细修改内容可以下载我在github上的完整代码,与cargo generate生成的原Aya项目进行对比。

五、编译运行

下载:

1
2
$ git clone https://github.com/fanrong1992/Rust_Android_eBPF.git
$ cd Rust_Android_eBPF/kprobe

编译:

1
$ cargo xtask build --arch aarch64-unknown-linux-musl --release

运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ adb push target/aarch64-unknown-linux-musl/release/kprobe-rs /data/local/tmp
$ adb shell
$ su
# cd /data/local/tmp
# RUST_LOG=INFO ./kprobe-rs
[2023-07-07T04:26:28Z INFO kprobe_rs] Waiting for Ctrl-C...
[2023-07-07T04:26:30Z INFO kprobe_rs] function do_sys_openat2 called
[2023-07-07T04:26:30Z INFO kprobe_rs] function do_sys_openat2 called
[2023-07-07T04:26:30Z INFO kprobe_rs] function do_sys_openat2 called
[2023-07-07T04:26:30Z INFO kprobe_rs] function do_sys_openat2 called
[2023-07-07T04:26:30Z INFO kprobe_rs] function do_sys_openat2 called
[2023-07-07T04:26:30Z INFO kprobe_rs] function do_sys_openat2 called
[2023-07-07T04:26:30Z INFO kprobe_rs] function do_sys_openat2 called
...

到这里Rust开发的eBPF程序就可以在安卓上正常运行了。

六、总结

总的来说,用Rust写eBPF程序在安卓上运行的优点是前后端都可以用Rust实现,整个流程也比较简单。但是目前例子和应用较少,不知道会不会存在什么坑,真正应用到项目中还需要更多人一起进行探索。

参考:
https://kaisery.github.io/trpl-zh-cn/ch01-03-hello-cargo.html
http://pwn4.fun/2022/08/08/Aya%E5%85%A5%E9%97%A8/
https://www.freesion.com/article/1938967007/
https://aya-rs.dev/book/start/development/
https://bbs.kanxue.com/thread-277628.htm
https://github.com/ri-char/eStrace