bpftool -S 签名功能实现解析.md
https://gitee.com/kiraskyler/Articles/blob/master/eBPF/bpftool -S 签名功能实现原理.md
文章目录
- 前言
-
- [use_loader eBPF加载器](#use_loader eBPF加载器)
- 使用流程
- [bpftool 流程](#bpftool 流程)
-
- eBPF加载器签名
- 加载器.data计算hash
- 内核层
-
- [map_create 保留attr.hash到map.hash中](#map_create 保留attr.hash到map.hash中)
- [bpf_obj_get_info_by_fd 获取map信息时候内核为map[0]即.data计算sha](#bpf_obj_get_info_by_fd 获取map信息时候内核为map[0]即.data计算sha)
- 校验加载器签名
- [加载器校验.data hash](#加载器校验.data hash)
- 结束
- 参考
前言
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_dw是compute_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_hash,attr->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函数
参考
文章
代码阅读与注释: