BruceFan's Blog

Stay hungry, stay foolish

0%

Aya入门

好长时间没有更新博客了,其实工作中也会写一些技术文档,但是工作内容相关的文档不能公开,自己也会学一些工作之外的技术,但是形成的文档感觉比较零散,也懒得发布到博客上。感觉还是要多写一些东西记录一下学习的知识和心得,要不总有一种知识不是自己的感觉,也不容易深入学习,形成体系。

工作和学习的几年方向转换比较多,没有在一个领域特别深入,工作基本就是这样,有什么需求就做什么,不过后面还是打算有针对性的学习一下云原生方面的安全,主要集中在用eBPF去做一些容器和K8S的安全,主要看一下Cilium和Aya这两个eBPF的库。有机会的话再学习一下intel的新机密计算技术TDX。
eBPF是一个内核子模块,可以在不改动和重新编译内核的情况下,在内核里进行一些操作。eBPF的开发一般是借助一些库来实现,如bcc、libbpf、cilium/ebpf和Aya等等。
选择学习Aya是因为这是用Rust写的,Rust语言在安全领域还是很有前景的,性能与C/C++接近,内存安全的一门系统编程语言,可以用来开发Linux内核,在机密计算、云原生领域也非常流行。
接下来几篇的计划是,这篇文章主要介绍一下Aya的环境和简单开发,后面会结合LSM例子深入学习,最后对github上的一个项目进行分析。

环境安装

我的系统: Ubuntu 20.04 64bit
首先安装rustup stable版本和nightly版本

1
2
$ rustup install stable
$ rustup toolchain install nightly --component rust-src

接着安装bpf-linker

1
$ cargo install bpf-linker

为了项目产生手脚架,还需要安装cargo-generate

1
$ cargo install cargo-generate

开始第一个项目

cargo generate可以生成一个新的项目

1
2
3
4
5
6
7
$ cargo generate https://github.com/aya-rs/aya-template
🤷 Project Name : kprobe-rs
🔧 Destination: /home/fanrong/Computer/BPF/bpf-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
...

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

1
$ cargo xtask build-ebpf

运行

1
2
3
4
5
6
7
$ cargo xtask run
...
[2022-08-09T15:30:28Z INFO kprobe_rs] Waiting for Ctrl-C...
[2022-08-09T15:30:30Z INFO kprobe_rs] function do_sys_openat2 called
[2022-08-09T15:30:30Z INFO kprobe_rs] function do_sys_openat2 called
[2022-08-09T15:30:30Z INFO kprobe_rs] function do_sys_openat2 called

技术更新真是快,上周用Aya的时候log还是用的simplelog,这周用发现打印不出来了,去Aya的git仓库查看commit发现Aya的log改用env_logger了,之前做项目用过pretty_env_logger,乍看起来差不太多,需要在终端export RUST_LOG=INFO才能打印出对应level的log。

代码分析

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
26
27
28
#![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 unsafe { try_kprobe_rs(ctx) } {
Ok(ret) => ret,
Err(ret) => ret,
}
}

unsafe 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
use aya::{include_bytes_aligned, Bpf};
use aya::programs::KProbe;
use aya_log::BpfLogger;
use clap::Parser;
use log::info;
use tokio::signal;

#[derive(Debug, Parser)]
struct Opt {
// 命令行参数,本程序中没有用到
}

#[tokio::main] // 主入口点
async fn main() -> Result<(), anyhow::Error> {
let opt = Opt::parse();

env_logger::init();

// 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"
))?;
BpfLogger::init(&mut bpf)?;
// 提取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(())
}

参考
https://aya-rs.dev/book/start/development/
https://aya-rs.dev/book/start/hello-xdp/