searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

eBPF kprobe调用路径分析

2024-09-10 09:23:16
7
0

传统BPF 主要用于网络包处理,扩展BPF简称eBPF能够实现更多功能,体现在跟多的hook点,更丰富的helper函数。

根本上讲,不论是BPF还是eBPF都是创建一个虚拟的执行环境,默认使用的环境为原生执行环境,符合当前处理器体系架构,执行效率高,原生的指令配套原生的CPU,效率杠杠的。在这种环境下,安全防护手段、攻击手段方方面面,演进过程中,出现了一种防护手段。这种手段随虚拟化产生,老早的IBM服务器本身就是共享型的大型服务器,这时候能做到的虚拟就是终端虚拟,虚拟的目的其实就是共享资源,起初是没有硬件虚拟化加持,技术演进过程中因为资源利用率不足,衍生出硬件虚拟化,目的也是提高资源利用率,让一台机器共享给多人使用。然后虚拟化又分为完全虚拟化、半虚拟化,在软件层面,也会用类似名称去描述,比如,虚拟机加密

虚拟机加密的工作原理大致可以分为以下几个步骤:

1.代码转换:首先,将高层次语言(如C++、C#等)编译成x86汇编代码或中间表示(IR)。然后,将这些汇编代码转换为虚拟机特定的字节码指令。这个转换过程会混淆和加密指令,生成难以理解的字节码。
2.虚拟机执行:在程序运行时,一个专门的虚拟机解释器负责解释和执行这些字节码指令。解释器逐条读取字节码指令,并在底层的x86指令系统上执行相应的操作。
3.动态执行:虚拟机执行时,解释器逐条读取字节码指令并在底层的x86指令系统上执行相应的操作。这种方式允许更高的灵活性和安全性,因为代码在运行时才会被解释执行。

不管是硬件虚拟化,还是软件虚拟化,还是虚拟机加密,说到底,就是两个字“隔离“。隔离执行环境,保证虚拟机内部执行环境的安全性,这里的安全也只是有限安全,可以只考虑机密性,数据被破坏了也没关系,大不了直接撂摊子宕机,也要保证核心机密不泄露,类似以biba、blp一样,允许破坏数据也要保证机密性。当然这也是一般情况,实际不同“虚拟化”机制实现各有千秋,各有特色,总有一个侧重点。

回过头来,eBPF的“虚拟化“方式,它是构造了一个精简指令集执行环境,说白了就是苟在一个 “加减乘除“ 基本操作的环境,任何一种编程语言都可以这么玩。“根据给定的输入,执行返回结果“ 这里给定输入,分为精简指令、复杂指令,指令长度不同而已,执行,就分为用特有的硬件去执行,还是用软件模拟执行。 eBPF是支持通过硬件执行的,目前所知是智能网卡,能够执行特定的eBPF程序,只要是网络方面。但大部分eBPF,是通过软件模拟实现的。这跟Qemu、KVM有异曲同工之处。

进一步,虚拟机执行,就要区分,创建虚拟机、进入虚拟机、虚拟机内部执行、退出虚拟机一系列操作,区分不同的运行时上下文。eBPF类似,加载eBPF程序,attach到指定hook点,触发eBPF执行,结束退出。hook点从哪里来?eBPF直接用了Linux内核中已有的hook机制,典型的kprobe、tracepoint,它们都是在内核中,特定函数特定位置,增加钩子。常见钩子就是jump, 钩子是跟特定执行流关联,当内核执行流走到钩子位置,跳转到预埋的函数,执行完成后再回到原始位置(下一个指令)继续执行。kprobe有init3、jump机制,效率不同机制不同而已,本质还是为了执行扩展逻辑,这些扩展逻辑就可以花里胡哨了,什么内核热修复、热更新常用到这个。因此kprobe类型的ebpf程序,就是借用了成熟的kprobe 、kretprobe机制增加钩子,跳转到“虚拟机“执行ebpf逻辑

ebpf虚拟机,它是个每条指令定长的虚拟机,简单说,输入就是定长的字符串。下面用c写一个最简单的"虚拟机"

#include  
#include  
 
// 定义指令集 
typedef enum { 
    OP_ADD,    // 加法 
    OP_SUB,    // 减法 
    OP_PRINT,  // 打印 
    OP_HALT    // 停止 
} OpCode; 
 
// 定义指令结构 
typedef struct { 
    OpCode code; 
    int operand; 
} Instruction; 
 
// 虚拟机状态 
typedef struct { 
    int *memory;       // 内存 
    int *registers;    // 寄存器 
    int pc;            // 程序计数器 
} VMState; 
 
// 初始化虚拟机 
void initVM(VMState *vm, int memSize) { 
    vm->memory = (int *)malloc(sizeof(int) * memSize); 
    vm->registers = (int *)malloc(sizeof(int) * 10); // 假设有10个寄存器 
    vm->pc = 0; 

 
// 执行指令 
void executeInstruction(VMState *vm, Instruction *instr) { 
    switch (instr->code) { 
        case OP_ADD: 
            vm->registers[0] += instr->operand; 
            break; 
        case OP_SUB: 
            vm->registers[0] -= instr->operand; 
            break; 
        case OP_PRINT: 
            printf("Register 0: %d\n", vm->registers[0]); 
            break; 
        case OP_HALT: 
            printf("VM Halted.\n"); 
            exit(0); 
        default: 
            printf("Unknown instruction.\n"); 
            exit(1); 
    } 

 
// 运行虚拟机 
void runVM(VMState *vm, Instruction *program, int programSize) { 
    while (vm->pc pc]); 
        vm->pc++; 
    } 

 
int main() { 
    // 定义一个简单的程序 
    Instruction program[] = { 
        {OP_ADD, 5}, 
        {OP_PRINT, 0}, 
        {OP_SUB, 3}, 
        {OP_PRINT, 0}, 
        {OP_HALT, 0} 
    }; 
 
    int programSize = sizeof(program) / sizeof(program[0]); 
 
    // 初始化虚拟机 
    VMState vm; 
    initVM(&vm, 1024); 
 
    // 运行虚拟机 
    runVM(&vm, program, programSize); 
 
    // 释放内存 
    free(vm.memory); 
    free(vm.registers); 
 
    return 0; 

基本的虚拟机,少不了这几个组成部分:

  1. 指令集定义:使用枚举类型定义了虚拟机的指令集,包括加法、减法、打印和停止指令。
    2.  指令结构:每个指令由操作码和操作数组成。
    3. 虚拟机状态:虚拟机状态包括内存、寄存器和程序计数器。
    4. 初始化虚拟机:分配内存和寄存器,并初始化程序计数器。
    5. 执行指令:根据指令的操作码执行相应的操作。
    5. 运行虚拟机:循环执行程序中的指令,直到遇到停止指令。

这个简单的虚拟机实现展示了如何使用C语言来模拟一个基本的计算机系统。通过扩展指令集和增加更多的功能,可以构建一个更复杂的虚拟机。

 

回过头来,eBPF在加载的时候就做了很多校验,包括对要放在虚拟机内部执行的代码做各种权限检查、函数替换等。为什么要替换函数?因为ebpf程序对虚拟机内部能执行的函数做了约束,所有函数都必须在可靠范围,才能保证整体可控,因此可靠性是体现在各个函数上的。如下面所示,从代码上看,kprobe ebpf就是定义了一个类型,在加载eBPF程序时候,明确用哪个hook,调用对应的verifier_ops检查helper函数。helper函数其实就是在虚拟机中,所能使用的,非常有限的一个函数集合。如果实在原生运行环境上,就跟ko模块一样,ko模块中的函数可以调用内核中所有导出的函数,而eBPF程序不行,它受到非常大的约束,以至于,虚拟机中能实现的功能,相对于ko模块,非常有限。除非,将对应功能函数也写成ebpf helper函数,这样才能在eBPF中使用

 

 

另外,对于eBPF程序中,helper函数是否有修改的功能,这里修改,是可能影响内核执行流的修改。比如修改kprobe返回值。biba、blp就可以通过这个函数,在lsm中新增模块实现。社区对这个很敏感,严格限制这类改动,也做了限制,并非所有函数都能修改,除非显示声明可修改返回值的函数。这又是一个约束,看出来了么?所谓eBPF可靠性,就是一层一层的约束,你也可以猜到,约束越多,你能做的事情也会受限。因此,使用eBPF程序对内核影响较内核模块方式要小,可靠性、安全性高,但这些纯粹相对而言,第一个很低端ebpf程序,不如一个精打细算的内核模块高效,这也不一定。对于生产环境而已,我始终认为,能经得起推敲,能满足需求,能稳定运行的就行,并不一定非得哪个哪个。不像有些公司,把python写的改成go,对外吹嘘对牛多牛,效率多高多高,线上运行一塌糊涂,何必呢,换个材料造轮子而已。

 

最后唠唠eBPF map,有程序总得有数据吧,eBPF的程序就是helper加常规的加减乘除,数据就是map,结构化的键值对,在eBPF程序中,所能持久化存储的就是map,通过key - value形式访问。那么其他内存地址空间,可否读写?可以,当然可以,用helper去读,不能直接读。用helper去写,不能直接写。你能想到的操作,必定有一层约束。大部分ebpf程序类型都会集成map的这些helper操作,说到底也是内核函数,ko模块中也可以调用,只是。。ebpf只能调用这些函数,其他函数调用不了而已。

 

综上所述,本文简单介绍了ebpf基本原理,kprobe hook方式,map的基本原理。实践出真知,建议读者看看内核代码,不复杂,很简单。

 

 

0条评论
0 / 1000
y****n
9文章数
0粉丝数
y****n
9 文章 | 0 粉丝
原创

eBPF kprobe调用路径分析

2024-09-10 09:23:16
7
0

传统BPF 主要用于网络包处理,扩展BPF简称eBPF能够实现更多功能,体现在跟多的hook点,更丰富的helper函数。

根本上讲,不论是BPF还是eBPF都是创建一个虚拟的执行环境,默认使用的环境为原生执行环境,符合当前处理器体系架构,执行效率高,原生的指令配套原生的CPU,效率杠杠的。在这种环境下,安全防护手段、攻击手段方方面面,演进过程中,出现了一种防护手段。这种手段随虚拟化产生,老早的IBM服务器本身就是共享型的大型服务器,这时候能做到的虚拟就是终端虚拟,虚拟的目的其实就是共享资源,起初是没有硬件虚拟化加持,技术演进过程中因为资源利用率不足,衍生出硬件虚拟化,目的也是提高资源利用率,让一台机器共享给多人使用。然后虚拟化又分为完全虚拟化、半虚拟化,在软件层面,也会用类似名称去描述,比如,虚拟机加密

虚拟机加密的工作原理大致可以分为以下几个步骤:

1.代码转换:首先,将高层次语言(如C++、C#等)编译成x86汇编代码或中间表示(IR)。然后,将这些汇编代码转换为虚拟机特定的字节码指令。这个转换过程会混淆和加密指令,生成难以理解的字节码。
2.虚拟机执行:在程序运行时,一个专门的虚拟机解释器负责解释和执行这些字节码指令。解释器逐条读取字节码指令,并在底层的x86指令系统上执行相应的操作。
3.动态执行:虚拟机执行时,解释器逐条读取字节码指令并在底层的x86指令系统上执行相应的操作。这种方式允许更高的灵活性和安全性,因为代码在运行时才会被解释执行。

不管是硬件虚拟化,还是软件虚拟化,还是虚拟机加密,说到底,就是两个字“隔离“。隔离执行环境,保证虚拟机内部执行环境的安全性,这里的安全也只是有限安全,可以只考虑机密性,数据被破坏了也没关系,大不了直接撂摊子宕机,也要保证核心机密不泄露,类似以biba、blp一样,允许破坏数据也要保证机密性。当然这也是一般情况,实际不同“虚拟化”机制实现各有千秋,各有特色,总有一个侧重点。

回过头来,eBPF的“虚拟化“方式,它是构造了一个精简指令集执行环境,说白了就是苟在一个 “加减乘除“ 基本操作的环境,任何一种编程语言都可以这么玩。“根据给定的输入,执行返回结果“ 这里给定输入,分为精简指令、复杂指令,指令长度不同而已,执行,就分为用特有的硬件去执行,还是用软件模拟执行。 eBPF是支持通过硬件执行的,目前所知是智能网卡,能够执行特定的eBPF程序,只要是网络方面。但大部分eBPF,是通过软件模拟实现的。这跟Qemu、KVM有异曲同工之处。

进一步,虚拟机执行,就要区分,创建虚拟机、进入虚拟机、虚拟机内部执行、退出虚拟机一系列操作,区分不同的运行时上下文。eBPF类似,加载eBPF程序,attach到指定hook点,触发eBPF执行,结束退出。hook点从哪里来?eBPF直接用了Linux内核中已有的hook机制,典型的kprobe、tracepoint,它们都是在内核中,特定函数特定位置,增加钩子。常见钩子就是jump, 钩子是跟特定执行流关联,当内核执行流走到钩子位置,跳转到预埋的函数,执行完成后再回到原始位置(下一个指令)继续执行。kprobe有init3、jump机制,效率不同机制不同而已,本质还是为了执行扩展逻辑,这些扩展逻辑就可以花里胡哨了,什么内核热修复、热更新常用到这个。因此kprobe类型的ebpf程序,就是借用了成熟的kprobe 、kretprobe机制增加钩子,跳转到“虚拟机“执行ebpf逻辑

ebpf虚拟机,它是个每条指令定长的虚拟机,简单说,输入就是定长的字符串。下面用c写一个最简单的"虚拟机"

#include  
#include  
 
// 定义指令集 
typedef enum { 
    OP_ADD,    // 加法 
    OP_SUB,    // 减法 
    OP_PRINT,  // 打印 
    OP_HALT    // 停止 
} OpCode; 
 
// 定义指令结构 
typedef struct { 
    OpCode code; 
    int operand; 
} Instruction; 
 
// 虚拟机状态 
typedef struct { 
    int *memory;       // 内存 
    int *registers;    // 寄存器 
    int pc;            // 程序计数器 
} VMState; 
 
// 初始化虚拟机 
void initVM(VMState *vm, int memSize) { 
    vm->memory = (int *)malloc(sizeof(int) * memSize); 
    vm->registers = (int *)malloc(sizeof(int) * 10); // 假设有10个寄存器 
    vm->pc = 0; 

 
// 执行指令 
void executeInstruction(VMState *vm, Instruction *instr) { 
    switch (instr->code) { 
        case OP_ADD: 
            vm->registers[0] += instr->operand; 
            break; 
        case OP_SUB: 
            vm->registers[0] -= instr->operand; 
            break; 
        case OP_PRINT: 
            printf("Register 0: %d\n", vm->registers[0]); 
            break; 
        case OP_HALT: 
            printf("VM Halted.\n"); 
            exit(0); 
        default: 
            printf("Unknown instruction.\n"); 
            exit(1); 
    } 

 
// 运行虚拟机 
void runVM(VMState *vm, Instruction *program, int programSize) { 
    while (vm->pc pc]); 
        vm->pc++; 
    } 

 
int main() { 
    // 定义一个简单的程序 
    Instruction program[] = { 
        {OP_ADD, 5}, 
        {OP_PRINT, 0}, 
        {OP_SUB, 3}, 
        {OP_PRINT, 0}, 
        {OP_HALT, 0} 
    }; 
 
    int programSize = sizeof(program) / sizeof(program[0]); 
 
    // 初始化虚拟机 
    VMState vm; 
    initVM(&vm, 1024); 
 
    // 运行虚拟机 
    runVM(&vm, program, programSize); 
 
    // 释放内存 
    free(vm.memory); 
    free(vm.registers); 
 
    return 0; 

基本的虚拟机,少不了这几个组成部分:

  1. 指令集定义:使用枚举类型定义了虚拟机的指令集,包括加法、减法、打印和停止指令。
    2.  指令结构:每个指令由操作码和操作数组成。
    3. 虚拟机状态:虚拟机状态包括内存、寄存器和程序计数器。
    4. 初始化虚拟机:分配内存和寄存器,并初始化程序计数器。
    5. 执行指令:根据指令的操作码执行相应的操作。
    5. 运行虚拟机:循环执行程序中的指令,直到遇到停止指令。

这个简单的虚拟机实现展示了如何使用C语言来模拟一个基本的计算机系统。通过扩展指令集和增加更多的功能,可以构建一个更复杂的虚拟机。

 

回过头来,eBPF在加载的时候就做了很多校验,包括对要放在虚拟机内部执行的代码做各种权限检查、函数替换等。为什么要替换函数?因为ebpf程序对虚拟机内部能执行的函数做了约束,所有函数都必须在可靠范围,才能保证整体可控,因此可靠性是体现在各个函数上的。如下面所示,从代码上看,kprobe ebpf就是定义了一个类型,在加载eBPF程序时候,明确用哪个hook,调用对应的verifier_ops检查helper函数。helper函数其实就是在虚拟机中,所能使用的,非常有限的一个函数集合。如果实在原生运行环境上,就跟ko模块一样,ko模块中的函数可以调用内核中所有导出的函数,而eBPF程序不行,它受到非常大的约束,以至于,虚拟机中能实现的功能,相对于ko模块,非常有限。除非,将对应功能函数也写成ebpf helper函数,这样才能在eBPF中使用

 

 

另外,对于eBPF程序中,helper函数是否有修改的功能,这里修改,是可能影响内核执行流的修改。比如修改kprobe返回值。biba、blp就可以通过这个函数,在lsm中新增模块实现。社区对这个很敏感,严格限制这类改动,也做了限制,并非所有函数都能修改,除非显示声明可修改返回值的函数。这又是一个约束,看出来了么?所谓eBPF可靠性,就是一层一层的约束,你也可以猜到,约束越多,你能做的事情也会受限。因此,使用eBPF程序对内核影响较内核模块方式要小,可靠性、安全性高,但这些纯粹相对而言,第一个很低端ebpf程序,不如一个精打细算的内核模块高效,这也不一定。对于生产环境而已,我始终认为,能经得起推敲,能满足需求,能稳定运行的就行,并不一定非得哪个哪个。不像有些公司,把python写的改成go,对外吹嘘对牛多牛,效率多高多高,线上运行一塌糊涂,何必呢,换个材料造轮子而已。

 

最后唠唠eBPF map,有程序总得有数据吧,eBPF的程序就是helper加常规的加减乘除,数据就是map,结构化的键值对,在eBPF程序中,所能持久化存储的就是map,通过key - value形式访问。那么其他内存地址空间,可否读写?可以,当然可以,用helper去读,不能直接读。用helper去写,不能直接写。你能想到的操作,必定有一层约束。大部分ebpf程序类型都会集成map的这些helper操作,说到底也是内核函数,ko模块中也可以调用,只是。。ebpf只能调用这些函数,其他函数调用不了而已。

 

综上所述,本文简单介绍了ebpf基本原理,kprobe hook方式,map的基本原理。实践出真知,建议读者看看内核代码,不复杂,很简单。

 

 

文章来自个人专栏
文章 | 订阅
0条评论
0 / 1000
请输入你的评论
0
0