bpftool -S 签名功能实现解析

bpftool -S 签名功能实现解析.md

https://gitee.com/kiraskyler/Articles/blob/master/eBPF/bpftool -S 签名功能实现原理.md

文章目录

前言

use_loader eBPF加载器

bpftool -S功能会使用use_loader模式启动,参考文章:eBPF use_loader 加载器实现解析.md

复制代码
/root/qemu/linux-6.6.58/bpftool-comment/src/main.c: 530

		case 'L':
			use_loader = true; // L: use-loader
			break;
		case 'S':
			sign_progs = true;
			use_loader = true; // <- 使用eBPF加载器

使用流程

准备私钥、证书

生成私钥

复制代码
# openssl ecparam -name prime256v1 -genkey -out private.key

生成证书

复制代码
# openssl req -new -x509 \
  -key private.key \
  -out cert.pem \
  -days 3650 \
  -subj "/CN=My BPF Signer/O=MyOrg" \
  -addext "keyUsage = digitalSignature" \
  -addext "extendedKeyUsage = codeSigning"

编译内核时添加证书

复制代码
# cp cert.pem certs/signing_key_hornet.pem

.config中

# 否则bpftool添加证书时候报错
# add_key("asymmetric", "cert.pem", "0\202\1\3070\202\1m\240\3\2\1\2\2\0247\315,\33\25\20C!\310\2326p\266\205\326\276\307"..., 459, KEY_SPEC_SESSION_KEYRING) = -1
CONFIG_CRYPTO_ECDSA=y
CONFIG_CRYPTO_ECB=y
CONFIG_CRYPTO_CCM=y (待定)
CONFIG_CRYPTO_CMAC=y (待定)

CONFIG_SYSTEM_TRUSTED_KEYS="certs/signing_key_hornet.pem"
or
CONFIG_BPF_JIT_SIGNING_KEY="certs/signing_key_hornet.pem"(未测试)

内核其他编译选项

使用证书阶段需要添加的编译选项

复制代码
CONFIG_BPF_JIT=y

# 否则选择跳板执行时无可用跳板
/root/qemu/linux-stable/kernel/bpf/verifier.c: 24603

	key = bpf_trampoline_compute_key(tgt_prog, prog->aux->attach_btf, btf_id);
	tr = bpf_trampoline_get(key, &tgt_info);
	if (!tr)
		return -ENOMEM;

bpftool 流程

使用签名生成.skel.h

复制代码
bpftool gen skeleton <bpf.o> -k private.key -i cert.pem -S

eBPF加载器签名

参考eBPF use_loader 加载器实现解析.md文章,此刻opts.data放置的是用户的eBPF程序,opts.insns是一个bpftool生成的加载器,bpftool会加载这个加载器,通过加载器在内核态加载用户的eBPF程序。

trace_bpf__load(struct trace_bpf *skel)中的签名信息,放到opts会在bpf_load_and_run(&opts)时候传入内核

复制代码
/root/qemu/linux-stable/mine/simple_bpf_hornet/build/src/trace.bpf.skel.h:1425

	opts.signature = (void *)opts_sig;
	opts.signature_sz = sizeof(opts_sig) - 1;
	opts.excl_prog_hash = (void *)opts_excl_hash;
	opts.excl_prog_hash_sz = sizeof(opts_excl_hash) - 1;
	opts.keyring_id = skel->keyring_id;
	opts.ctx = (struct bpf_loader_ctx *)skel;
	opts.data_sz = sizeof(opts_data) - 1;
	opts.data = (void *)opts_data;
	opts.insns_sz = sizeof(opts_insn) - 1;
	opts.insns = (void *)opts_insn;

	err = bpf_load_and_run(&opts);

bpftool_prog_sign函数中,signature是对opts.insns加载器指令部分的签名,excl_prog_hash是同样是加载器指令部分的hash(sha256)值

复制代码
/root/qemu/linux-stable/mine/bpftool-comment/src/sign.c: 131

int bpftool_prog_sign(struct bpf_load_and_run_opts *opts)
{
    bd_in = BIO_new_mem_buf(opts->insns, opts->insns_sz); // 签名是针对opts->insns
    
	EVP_Digest(opts->insns, opts->insns_sz, opts->excl_prog_hash,
	   &opts->excl_prog_hash_sz, EVP_sha256(), NULL); // sha256生成hash也是针对opts->insns

keyring_id = KEY_SPEC_SESSION_KEYRING固定

复制代码
/root/qemu/linux-stable/mine/bpftool-comment/src/prog.c: 1930

static int try_loader(struct gen_loader_opts *gen)
{
    opts.keyring_id = KEY_SPEC_SESSION_KEYRING;

即签名是对ebpf指令构造的加载器程序进行的签名

加载器.data计算hash

emit_signature_match函数为加载器添加校验hash功能

复制代码
/root/qemu/linux-stable/mine/bpftool-comment/libbpf/src/gen_loader.c: 583

static void emit_signature_match(struct bpf_gen *gen) // efi校验 // for (i = 0; i < SHA256_DWORD_SIZE; i++) {if (!=) goto cleanup_label;}
{
	__s64 off;
	int i;

	for (i = 0; i < SHA256_DWORD_SIZE; i++) { 
		emit2(gen, BPF_LD_IMM64_RAW_FULL(BPF_REG_1, BPF_PSEUDO_MAP_IDX,
						 0, 0, 0, 0)); // r1 = .data,[movabs rdi,.data] 
		emit(gen, BPF_LDX_MEM(BPF_DW, BPF_REG_2, BPF_REG_1, i * sizeof(__u64))); // r2 = (u64*)(r1 + i*4),[mov rsi,QWORD PTR [rdi+i*4]]
		gen->hash_insn_offset[i] = gen->insn_cur - gen->insn_start; // 记录下一条指令地址,imm立即数将会在compute_sha_update_offsets函数中填充
		emit2(gen, BPF_LD_IMM64_RAW_FULL(BPF_REG_3, 0, 0, 0, 0, 0)); // 加载一个立即数,这个立即数是compute_sha_update_offse计算的.data hash,[movabs rdx,0x3cc3e1d69b2f97ec]

		off =  -(gen->insn_cur - gen->insn_start - gen->cleanup_label) / 8 - 1;
		if (is_simm16(off)) { // off == (__s64)(__s16)off,代码段小
			emit(gen, BPF_MOV64_IMM(BPF_REG_7, -EINVAL)); // [mov r13,-22]
			emit(gen, BPF_JMP_REG(BPF_JNE, BPF_REG_2, BPF_REG_3, off)); // [cmp rsi,rdx][jne cleanup_label],跳转到cleanup_label,即不相等则返回

#0  emit_signature_match (gen=<optimized out>) at gen_loader.c:591
#1  bpf_gen__init (gen=0x585b40, log_level=log_level@entry=0, nr_progs=<optimized out>, nr_maps=<optimized out>) at gen_loader.c:156
#2  0x00000000004493b8 in bpf_object_load (obj=obj@entry=0x5852a0, target_btf_path=0x0, extra_log_level=0) at libbpf.c:8937
#3  0x000000000044a435 in bpf_object__load (obj=obj@entry=0x5852a0) at libbpf.c:8980
#4  0x0000000000411bb4 in gen_trace (header_guard=0x7fffffffcb20 "__TRACE_BPF_SKEL_H__", obj_name=0x7fffffffcae0 "trace_bpf", obj=0x5852a0) at gen.c:706
#5  do_skeleton (argc=<optimized out>, argv=<optimized out>) at gen.c:1423
#6  0x0000000000404fcd in main (argc=<optimized out>, argv=<optimized out>) at main.c:564

上面添加的eBPF指令等价为:

复制代码
for (int i = 0; i <= SHA256_DWORD_SIZE; i+= 4)
    if (*(u64*)&map.sha[i] != sha_dw[i]) {
        err = -EINVAL;
        goto cleanup_label;
    }

其中,map指的是在启动时的第一个map添加

sha_dwcompute_sha_update_offsets函数计算的.data即用户程序的hash,此函数在生成eBPF加载器完毕时执行

复制代码
/root/qemu/linux-stable/mine/bpftool-comment/libbpf/src/gen_loader.c: 456

static void compute_sha_update_offsets(struct bpf_gen *gen)
{
	__u64 sha[SHA256_DWORD_SIZE];
	__u64 sha_dw;
	int i;

	libbpf_sha256(gen->data_start, gen->data_cur - gen->data_start, (__u8 *)sha); // 计算sha256
	for (i = 0; i < SHA256_DWORD_SIZE; i++) {
		struct bpf_insn *insn =
			(struct bpf_insn *)(gen->insn_start + gen->hash_insn_offset[i]);
		sha_dw = tgt_endian(sha[i]);
		insn[0].imm = (__u32)sha_dw; // 立即数填充到emit_signature_match函数中的BPF_LD_IMM64_RAW_FULL指令imm立即数中
		insn[1].imm = sha_dw >> 32;

#0  compute_sha_update_offsets (gen=0x585b40) at gen_loader.c:462
#1  bpf_gen__finish (gen=0x585b40, nr_progs=<optimized out>, nr_maps=<optimized out>) at gen_loader.c:402
#2  0x0000000000449867 in bpf_object_load (obj=obj@entry=0x5852a0, target_btf_path=0x0, extra_log_level=0) at libbpf.c:8957
#3  0x000000000044a435 in bpf_object__load (obj=obj@entry=0x5852a0) at libbpf.c:8980
#4  0x0000000000411bb4 in gen_trace (header_guard=0x7fffffffcb20 "__TRACE_BPF_SKEL_H__", obj_name=0x7fffffffcae0 "trace_bpf", obj=0x5852a0) at gen.c:706
#5  do_skeleton (argc=<optimized out>, argv=<optimized out>) at gen.c:1423
#6  0x0000000000404fcd in main (argc=<optimized out>, argv=<optimized out>) at main.c:564

内核层

map_create 保留attr.hash到map.hash中

map->excl_prog_sha = attr->excl_prog_hashattr->excl_prog_hash是加载器insn的hash值,在加载器加载时会校验该值,仅有加载器可使用该map

复制代码
/root/qemu/linux-stable/kernel/bpf/syscall.c: 1376

static int map_create(union bpf_attr *attr, bpfptr_t uattr)
{
	if (attr->excl_prog_hash) {
		bpfptr_t uprog_hash = make_bpfptr(attr->excl_prog_hash, uattr.is_kernel);

		map->excl_prog_sha = kzalloc(SHA256_DIGEST_SIZE, GFP_KERNEL);

		if (copy_from_bpfptr(map->excl_prog_sha, uprog_hash, SHA256_DIGEST_SIZE)) {

bpf_obj_get_info_by_fd 获取map信息时候内核为map[0]即.data计算sha

复制代码
static int array_map_get_hash(struct bpf_map *map, u32 hash_buf_size,
			       void *hash_buf)
{
	struct bpf_array *array = container_of(map, struct bpf_array, map);

	sha256(array->value, (u64)array->elem_size * array->map.max_entries,
	       hash_buf);
	memcpy(array->map.sha, hash_buf, sizeof(array->map.sha));

#0  array_map_get_hash (map=0xffff8880095f8000, hash_buf_size=<optimized out>, hash_buf=0xffff8880095f8000) at kernel/bpf/arraymap.c:185
#1  0xffffffff81416954 in bpf_map_get_info_by_fd (map=0xffff8880095f8000, uattr=uattr@entry=0x7ffeeb52c550, file=0xffff888009684e40, attr=0xffffc900006abe30) at kernel/bpf/syscall.c:5313
#2  0xffffffff8141d4f8 in bpf_obj_get_info_by_fd (uattr=0x7ffeeb52c550, attr=0xffffc900006abe30) at kernel/bpf/syscall.c:5415
#3  __sys_bpf (cmd=<optimized out>, uattr=..., size=16) at kernel/bpf/syscall.c:6203
#4  0xffffffff8141dbf9 in __do_sys_bpf (size=<optimized out>, uattr=<optimized out>, cmd=<optimized out>) at kernel/bpf/syscall.c:6274
#5  __se_sys_bpf (size=<optimized out>, uattr=<optimized out>, cmd=<optimized out>) at kernel/bpf/syscall.c:6272
#6  __x64_sys_bpf (regs=<optimized out>) at kernel/bpf/syscall.c:6272
#7  0xffffffff81f966c4 in do_syscall_x64 (nr=<optimized out>, regs=0xffffc900006abf58) at arch/x86/entry/syscall_64.c:63
#8  do_syscall_64 (regs=0xffffc900006abf58, nr=<optimized out>) at arch/x86/entry/syscall_64.c:94
#9  0xffffffff81000130 in entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:122

array->value是array类型map数据存储的地方,array->elem_size是array数组一个元素的大小,此时即.data的大小,array->map.max_entries是map中元素数量,此时是1,即一个.data,如果此时map变动(增删改)均会改变hash

复制代码
-exec x/500x array->value
......0......
0xffff8880095f8628:	0x0001eb9f

/root/qemu/linux-stable/mine/simple_bpf_hornet/build/src/trace.bpf.skel.h: 72

static inline int
trace_bpf__load(struct trace_bpf *skel)
{
	static const char opts_data[] __attribute__((__aligned__(8))) = "\
	......0.....
	\x9f\xeb\x01\0\

计算出来的hash与eBPF加载器中提前计算的.data的hash一致

复制代码
-exec x/20x hash_buf
0xffff8880095f8000:	0x9b2f97ec	0x3cc3e1d6	0x76d7b24e	0xe6376ffb
0xffff8880095f8010:	0x870c1d11	0xd7192ccd	0x49b68fda	0x24d225d2

0xffffffffa0209ffe:	movabs rdx,0x3cc3e1d69b2f97ec
0xffffffffa020a022:	movabs rdx,0xe6376ffb76d7b24e
0xffffffffa020a04a:	movabs rdx,0xd7192ccd870c1d11
0xffffffffa020a072:	movabs rdx,0x24d225d249b68fda

校验加载器签名

校验签名,此时的prog是加载器,当由加载器加载用户程序时刻attr->signature = 0不会校验签名

复制代码
/root/qemu/linux-stable/kernel/bpf/syscall.c:2870

static int bpf_prog_load(union bpf_attr *attr, bpfptr_t uattr, u32 uattr_size)
{
	if (attr->signature) {
		err = bpf_prog_verify_signature(prog, attr, uattr.is_kernel);

加载器加载时候传递的有fd_array,即保存.data的map fd,也会校验map的excl_prog_sha,该值是加载器insn部分的hash,由map创建时传递保存

复制代码
/usr/include/bpf/skel_internal.h: 351

static inline int bpf_load_and_run(struct bpf_load_and_run_opts *opts)
{
	err = map_fd = skel_map_create(BPF_MAP_TYPE_ARRAY, "__loader.map", 4, opts->data_sz, 1,
				       opts->excl_prog_hash, opts->excl_prog_hash_sz);
	
	attr.fd_array = (long) &map_fd; // .data所在map的fd
	
	err = skel_sys_bpf(BPF_PROG_RUN, &attr, test_run_attr_sz);

/root/CTKernel-LTS-6.6/kernel/bpf/verifier.c: 17674

static int check_map_prog_compatibility(struct bpf_verifier_env *env,
					struct bpf_map *map,
					struct bpf_prog *prog)

{
	enum bpf_prog_type prog_type = resolve_prog_type(prog);

	if (map->excl_prog_sha &&
	    memcmp(map->excl_prog_sha, prog->digest, SHA256_DIGEST_SIZE)) {
		verbose(env, "program's hash doesn't match map's excl_prog_hash\n");
		return -EACCES;
	}

#0  check_map_prog_compatibility (prog=0xffffc9000087d000, map=0xffff8880094d0000, env=0xffff888007460000) at kernel/bpf/verifier.c:20862
#1  __add_used_map (map=0xffff8880094d0000, env=0xffff888007460000) at kernel/bpf/verifier.c:20988
#2  __add_used_map (env=0xffff888007460000, map=0xffff8880094d0000) at kernel/bpf/verifier.c:20973
#3  0xffffffff81426b94 in add_used_map (fd=<optimized out>, env=<optimized out>) at kernel/bpf/verifier.c:21031
#4  resolve_pseudo_ldimm64 (env=env@entry=0xffff888007460000) at kernel/bpf/verifier.c:21125
#5  0xffffffff8144934b in bpf_check (prog=prog@entry=0xffffc90000883cc8, attr=attr@entry=0xffffc90000883e30, uattr=..., uattr_size=uattr_size@entry=168) at kernel/bpf/verifier.c:25217
#6  0xffffffff814195dd in bpf_prog_load (attr=attr@entry=0xffffc90000883e30, uattr=..., uattr_size=uattr_size@entry=168) at kernel/bpf/syscall.c:3088
#7  0xffffffff8141cff1 in __sys_bpf (cmd=BPF_PROG_LOAD, uattr=..., size=168) at kernel/bpf/syscall.c:6164
#8  0xffffffff8141dc19 in __do_sys_bpf (size=<optimized out>, uattr=<optimized out>, cmd=<optimized out>) at kernel/bpf/syscall.c:6274
#9  __se_sys_bpf (size=<optimized out>, uattr=<optimized out>, cmd=<optimized out>) at kernel/bpf/syscall.c:6272
#10 __x64_sys_bpf (regs=<optimized out>) at kernel/bpf/syscall.c:6272
#11 0xffffffff81f976c4 in do_syscall_x64 (nr=<optimized out>, regs=0xffffc90000883f58) at arch/x86/entry/syscall_64.c:63
#12 do_syscall_64 (regs=0xffffc90000883f58, nr=<optimized out>) at arch/x86/entry/syscall_64.c:94
#13 0xffffffff81000130 in entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:122

同理,这里第二次用户bpf程序执行时候也不会校验map

加载器校验.data hash

参考用户态生成的加载器compute_sha_update_offsets函数内容,校验.datahash

即,签名是给加载器签名,加载器校验用户程序的hash

此后,加载器再正常加载用户程序,无签名

结束

优点:

  • .data部分是原.o部分所有有效内容提取而来,所以校验时对原.bss/map/prog等均校验
  • 通用支持CO-RE

缺点:

  • use_loader模式仅对个别功能支持生成对应的load函数

参考

文章

代码阅读与注释:

相关推荐
小杰帅气18 小时前
进程优先级与切换调度
linux·运维·服务器
方便面不加香菜18 小时前
Linux基本指令(1)
linux
济61718 小时前
linux(第十四期)--Uboot移植(1)-- Ubuntu20.04
linux
奋斗的阿狸_198618 小时前
键盘组合键监听与 xterm 唤醒程序
linux·运维·服务器
小张成长计划..18 小时前
【linux】2:linux权限的概念
linux·运维·服务器
马踏岛国赏樱花18 小时前
Windows与Ubuntu双系统,挂载D/E盘到Ubuntu下时只能读的问题
linux·windows·ubuntu
ben9518chen18 小时前
Linux操作系统基本使用
linux·运维·服务器
一个平凡而乐于分享的小比特18 小时前
CPU上电启动到程序运行全流程详解
linux·uboot·根文件系统·cpu上电到启动
不像程序员的程序媛18 小时前
Linux开机自启动systemd配置
linux·运维·服务器