浅析bpftrace

admin 2022年10月8日14:23:12评论199 views字数 16775阅读55分55秒阅读模式

一、eBPF简单介绍

eBPF顾名思义来源于BPF(如下图),实际上BPF最初的目的是用于高效网络报文过滤,经过重新设计,eBPF 就不再局限于网络协议栈,它后期已经成为内核顶级的子系统,演进为一个通用执行引擎。换句话说 其实就是从提高底层网络抓包性能 变成了在底层执行更多通用代码的东西 。

浅析bpftrace

因此 eBPF 就适用于诸如性能分析、软件定义网络、网络安全等诸多场景。

按我个人的理解,我觉得eBPF本质上它就是内核中的一个虚拟机,以一种安全的方式在各种各样的内核hook点执行字节码,我们可以在三环编写和程序,用llvm作为后端把前端代码(比如go bpftrace python等)变成字节码,然后字节码在虚拟机里变成对应体系结构的硬编码来执行。

浅析bpftrace

一般 eBPF 的工作逻辑是:

1 .BPF Program 通过 LLVM/Clang 编译成 eBPF 定义的字节码 prog.bpf。

2. 通过系统调用 bpf() 将 bpf 字节码指令传入内核中。

3. 经过 verifier 检验字节码的安全性、合规性。

4. 在确认字节码安全后将其加载对应的内核模块执行。

LLVM 是伊利诺伊大学的一个开源项目,LLVM 提供了完整的 C/C++工具链 ,Clang 属于其中的一个子项目,是 LLVM 原生的 ” C/C++/Objective-C” 编译器前端,Clang 负责完成 词法分析 和 语法分析 ,并将分析结果转换为 Abstract Syntax Tree (抽象语法树) ,最后使用 LLVM 作为后端代码的生成器

go bpftrace c python写的ebpf代码通过llvm/clang -》 字节码-》字节码进零环ebpf的虚拟机再转换为硬编码执行。


二、bpftrace简单介绍

下面以bpftrace为例 来初步认识eBPF。bpftrace 是一种基于eBPF的高级跟踪语言,可用于Linux 内核 (4. x)。它由 Alastair Robertson 创建,参考了DTrace 和 SystemTap 等前身跟踪器。

bpftrace 既然是基于eBPF的 那么它也是使用 LLVM 作为后端将脚本编译为 BPF 的字节码,并且利用 BCC与 Linux 的BPF系统进行交互。bpftrace 语言写起来就像是awk 和 C这两种语言的混合体。

浅析bpftrace


三、从hello world开始

各种语言入门的经典案例无疑是hello world

那么让我们从hello world开始

root@zy-virtual-machine:~/桌面# bpftrace -e 'BEGIN {printf("Hello, World!n")}'

Attaching 1 probe. . .

Hello, World!

^C

如图平平无奇输出了个hello world,只有按下 Ctrl-C 或调用 exit() 函数程序才能继续跑。当程序退出时,将打印所有填充的map,关于这句话是什么意思以后会再解释。

-e 选项允许指定你要用bpftrace跑的程序,这是一种构造单行代码的方法。


四、浅尝第一个示例

那么让我们准备第一个示例,下面的程序功能是在进程调用 nanosleep 系统调用时打印一行xx is sleeping并回车。

关于程序的语法细节将在后面进行部分解释。

root@zy-virtual-machine:~/桌面# bpftrace -e 'tracepoint:syscalls:sys_enter_nanosleep { printf("%s is sleeping. n", comm); }'

Attaching 1 probe. . .

snapd is sleeping.

snapd is sleeping.

containerd is sleeping.

containerd is sleeping.

dockerd is sleeping.

dockerd is sleeping.

dockerd is sleeping.

containerd is sleeping. 

这里可以看出docker相关的进程一直在调用,nanosleep 系统调用。


五、编写独立运行的bpftrace脚本

我们可以把bpftrace程序保存为文件脚本,然后通过指定文件名来执行。通常使用 . bt 文件扩展名,但扩展名可以被忽略。

root@zy-virtual-machine:~/桌面# cat zy_nanosleep_test. bt

tracepoint:syscalls:sys_enter_nanosleep

{

printf("%s is sleeping. n", comm);

}

root@zy-virtual-machine:~/桌面# bpftrace zy_nanosleep_test. bt

Attaching 1 probe. . .

containerd is sleeping.

containerd is sleeping.

containerd is sleeping.

containerd is sleeping.

^C

通过文件执行bpftrace程序如上所示,直接加文件名即可。

当然,也有办法可以使其独立运行。

只需要在顶部添加一个解释器行(#!),

其中包含安装的 bpftrace 的路径(/usr/local/bin 是默认值)或 env 的路径(通常只是 /usr/bin/env)

如果你和我一样找不到可以find一下,我在docker里编译好了后,就复制到桌面上来了,自己手工复制到了bin目录里,所以是如下路径。

root@zy-virtual-machine:~/桌面# find / -name bpftrace

/usr/bin/bpftrace

/root/桌面/bpftrace

/var/lib/docker/overlay2/783ae9b2363fcc9f6935e577798926914e3e9d9c61ba059f46661ba1d37c42c0/diff/usr/bin/bpftrace


root@zy-virtual-machine:~/桌面# cat zy_nanosleep_test. bt

#!/usr/bin/bpftrace

tracepoint:syscalls:sys_enter_nanosleep

{

printf("%s is sleeping. n", comm);

}


root@zy-virtual-machine:~/桌面# chmod +x . /zy_nanosleep_test. bt

root@zy-virtual-machine:~/桌面# . /zy_nanosleep_test. bt

Attaching 1 probe. . .

containerd is sleeping.

^C


六、尝试挂钩自己的自定义程序

挂钩自己的自定义的程序

sudo bpftrace -l "uprobe:. /a. out"

可以知道都能挂钩一个程序的哪些函数 当然 被剥离的符号没法挂钩 bpftrace这样似乎就找不到地址了

那么 我随便写一个c程序

cat zytest. c

#include


int testzy(int a,int b){

return a+b;

}


int main(){

printf("%dn",testzy(1,2));

printf("%dn",testzy(3,4));

printf("%dn",testzy(5,6));

}

并探测该程序可以挂钩的点

root@zy-virtual-machine:~/桌面# sudo bpftrace -l "uprobe:./a. out"

uprobe:. /a. out:__do_global_dtors_aux

uprobe:. /a. out:__libc_csu_fini

uprobe:. /a. out:__libc_csu_init

uprobe:. /a. out:_fini

uprobe:. /a. out:_init

uprobe:. /a. out:_start

uprobe:. /a. out:deregister_tm_clones

uprobe:. /a. out:frame_dummy

uprobe:. /a. out:main

uprobe:. /a. out:register_tm_clones

uprobe:. /a. out:testzy

readelf --syms a.out


可以先查看对应的符号表

strip --strip-all a.out  

这样剥离后就不能再挂钩了

readelf --syms a.out


可以查看对应的符号表 一般剥离后就没有了

root@zy-virtual-machine:~/桌面# bpftrace -e 'uprobe:./test3:testzy { printf("return %d+%dn",arg0,arg1); }'

No probes to attach

可以看到我们的testzy函数就是一个可以挂钩的地方

那么我们假装这是一个恶意函数,尝试通过挂钩来还原出传进去的参数。

这边监听后 随着我们执行我们的代码

root@zy-virtual-machine:~/桌面# . /a. out

3

7

11

可以看到这边监控程序也成功捕获到目标 并且正确地返回了传入的参数

root@zy-virtual-machine:~/桌面# bpftrace -e 'uprobe:. /a. out:testzy { printf("return %d+%dn",arg0,arg1); }'

Attaching 1 probe. . .

return 1+2

return 3+4

return 5+6


直接上函数地址也一样 所以说这个符号表就是找函数地址的地方

bpftrace -e 'uprobe:./a.out:0x1149{ printf("return %d+%dn",arg0,arg1); }'


另一个类似的例子

bpftrace -e 'uprobe:./a:test{ printf("return %d+%d+%dn",arg0,arg1,arg2); }'


七、关于动作块

下面 来系统地解释一下 我上面那堆程序到底是什么东西

就比如这句程序

bpftrace -e 'tracepoint:syscalls:sys_enter_nanosleep { printf("%s is sleeping. n", comm); }'

其中“{. . . . }”大括号及其里面的东西 叫做动作块

一个 bpftrace 程序可以有多个动作块。

root@zy-virtual-machine:~/桌面# bpftrace -e 'BEGIN{printf("123n");} uprobe:. /a. out:testzy { printf("return %d+%dn",arg0,arg1); }'

Attaching 2 probes. . .

123

return 1+2

return 3+4

return 5+6

就比如这样 有两个动作块 BEGIN里面的动作块先执行 然后再监控a. out的testzy函数 如果testzy函数执行了 就把传入的参数打印出来

当然 此时你可能仍然不理解uprobe这个位置是什么 并且都能用什么东西

这里我先粗浅地简单介绍一下

这个东西叫做探针 是用来探测不同时机发生什么事并执行相应代码的

能用的探针如下 右边是相应的解释

kprobe - 内核函数启动

kretprobe - 内核函数返回

uprobe - 用户级函数启动

uretprobe - 用户级函数返回

tracepoint - 内核静态跟踪点

usdt - 用户级静态跟踪点

profile - 定时采样

interval - 定时输出

software - 内核软件事件

hardware - 处理器级事件

比如uprobe 就是指用户三环的函数启动时 就开始执行动作块里的代码

root@zy-virtual-machine:~/桌面# bpftrace -e 'uprobe:. /a. out:testzy { printf("return %d+%dn",arg0,arg1); }'

所以我这行代码的意思是

当用户层三环函数testzy启动时 开始执行动作块里的代码 也就是获取函数启动时的传入参数

同理可以这样(为了简洁起见 这里没有把. /a. out的执行贴出来)

bpftrace -e 'uretprobe:. /a. out:testzy { printf("return %dn",retval); }'

Attaching 1 probe. . .

return 3

return 7

return 11

因此我这行代码的意思现在就变成了当用户层三环函数testzy返回时 开始执行动作块里的代码 也就是获取返回时的返回值


八、关于过滤器


root@zy-virtual-machine:~/桌面# bpftrace -e 'uretprobe:. /a. out:testzy / retval > 3 / { printf("return %dn",retval); }'

Attaching 1 probe. . .

return 7

return 11

如图所示,其实就是在//里面加个条件 用来给动作方块的执行限定条件


九、关于注释

穿插着说两个更加简单易懂的

我们的bt脚本文件里写注释的方法如下

单行注释的写法

// single-line comment

多行注释的写法

/*

 * multi-line comment

 */


十、整数、字符和字符串

bpftrace支持整数、字符和字符串。如下

root@zy-virtual-machine:~/桌面# bpftrace -e 'BEGIN { printf("%lu %lu %lu", 1000000000, 1e9, 1_000_000_000)}'

Attaching 1 probe. . .

^C1000000000 1000000000 1000000000

字符括在单引号中,例如'a’

字符串括在双引号中,例如"string"

root@zy-virtual-machine:~/桌面# bpftrace -e 'BEGIN { printf("%lu %lu %lu", 1000000, 1e6, 1_000_000)}'

Attaching 1 probe. . .

^C1000000 1000000 1000000

当然,科学计数法和1下划线接数字的写法不一定可以在所有版本使用 ,我的两台虚拟机装了两个不同版本的bpftrace 其中一台就不识别科学计数法和下划线的这种写法。


六、->: C的结构体选定结构体成员符号


root@zy-virtual-machine:~/桌面# bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s %sn", comm, str(args->filename)); }'

Attaching 1 probe. . .

xdg-desktop-por /usr/share/applications/org. gnome. Terminal. desktop

gnome-terminal- /usr/share/icons/Yaru/scalable/actions/tab-new-symbolic. svg

gnome-terminal- /usr/share/icons/Yaru/scalable/actions/open-menu-symbolic. svg

gnome-terminal- /usr/share/icons/Yaru/scalable/actions/edit-find-symbolic. svg

gnome-terminal- /usr/share/icons/Yaru/scalable/actions/window-minimize-symbolic

这样可以从结构体返回其中的成员


十二、struct

可以在需要时定义自己的结构。因为在某些情况下,内核结构不在内核头文件包中声明,而是在 bpftrace tools中手动声明。

比如dcsnoop. bt这个脚本就可以跟踪目录条目高速缓存 (dcache) 查找

#!/usr/bin/env bpftrace

#ifndef BPFTRACE_HAVE_BTF

#include

#include


// from fs/namei. c:

struct nameidata {

        struct path     path;

        struct qstr     last;

        // [. . . ]

};

#endif


BEGIN

{

printf("Tracing dcache lookups. . .  Hit Ctrl-C to end. n");

printf("%-8s %-6s %-16s %1s %sn", "TIME", "PID", "COMM", "T", "FILE");

}


// comment out this block to avoid showing hits:

kprobe:lookup_fast,

kprobe:lookup_fast. constprop. *

{

$nd = (struct nameidata *)arg0;

printf("%-8d %-6d %-16s R %sn", elapsed / 1000000, pid, comm,

    str($nd->last. name));

}


kprobe:d_lookup

{

$name = (struct qstr *)arg1;

@fname[tid] = $name->name;

}


kretprobe:d_lookup

/@fname[tid]/

{

printf("%-8d %-6d %-16s M %sn", elapsed / 1000000, pid, comm,

    str(@fname[tid]));

delete(@fname[tid]);

}


它就定义了自己的结构体,运行起来的效果如下

3799     358    systemd-journal  R syslog

3799     358    systemd-journal  R run/systemd/journal/syslog

3799     358    systemd-journal  R systemd/journal/syslog

3799     358    systemd-journal  R journal/syslog

3799     358    systemd-journal  R syslog

3799     358    systemd-journal  R run/systemd/journal/syslog

3799     358    systemd-journal  R systemd/journal/syslog

3799     358    systemd-journal  R journal/syslog

3799     358    systemd-journal  R syslog

3799     358    systemd-journal  R run/systemd/journal/syslog

3799     358    systemd-journal  R systemd/journal/syslog

3799     358    systemd-journal  R journal/syslog

3799     358    systemd-journal  R syslog

3799     358    systemd-journal  R run/systemd/journal/syslog

3799     358    systemd-journal  R systemd/journal/syslog

3799     358    systemd-journal  R journal/syslog

3799     358    systemd-journal  R syslog

3799     358    systemd-journal  R run/systemd/journal/syslog

3799     358    systemd-journal  R systemd/journal/syslog

3799     358    systemd-journal  R journal/syslog



十三、三元运算符? :


root@zy-virtual-machine:~/桌面# bpftrace -e 'uprobe:. /a. out:testzy {printf("%dn",arg0+arg1>3?1:0); }'

Attaching 1 probe. . .

0

1

1

我们都知道刚刚我们的c程序传入的参数分别是

1 2

3 4

5 6

那么其中总和分别为 3 7 11

不难理解会出现 0 1 1的结果


十四、if-else 语句if () {. . . } else {. . . }


root@zy-virtual-machine:~/桌面# bpftrace -e 'uprobe:. /a. out:testzy {if(arg0!=3){printf("%dn",arg0+arg1>3?1:0); }}'

Attaching 1 probe. . .

0

1

在三元运算符的外层我又套了个if判断,把3 4这组参数筛掉(故意筛除参数一为3的条件组)在这种情况下 输出自然就少了一个


十五、unroll () {. . . }


root@zy-virtual-machine:~/桌面# bpftrace -e 'BEGIN { $i = 1; unroll(5) { printf("i: %dn", $i); $i = $i + 2; } }'

Attaching 1 probe. . .

i: 1

i: 3

i: 5

i: 7

i: 9

显而易见,这就是个循环语句。

当然,循环的话其实也可以用while等这里不做演示。


十六、自增运算符++--


root@zy-virtual-machine:~/桌面# bpftrace -e 'BEGIN { $zy = 0; $zy++; $zy++;  $zy++; $zy++; printf("zy: %dn", $zy); }'

Attaching 1 probe. . .

zy: 4


十七、尝试挂钩自己的自定义程序

一般来说,整数在内部表示为 64 位有符号。

如果有需要也可以强制转换为以下内置类型:

类型

解释

uint8

无符号 8 位整数

int8

有符号 8 位整数

uint16

无符号 16 位整数

int16

有符号 16 位整数

uint32

无符号 32 位整数

int32

有符号 32 位整数

uint64

无符号 64 位整数

int64

有符号 64 位整数


root@zy-virtual-machine:~/桌面#  bpftrace -e 'BEGIN { $zy = 1<<16; printf("%d %dn", (uint16)$zy, $zy); }'

Attaching 1 probe. . .

0 65536

^C


十八、while循环

内核要求: 5. 3

bpftrace 支持 C 样式循环:

root@zy-virtual-machine:~/桌面# bpftrace -e 'i:ms:100 { $i = 0; while ($i <= 100) { printf("%d | ", $i); $i=$i+2} exit();}'

Attaching 1 probe. . .

0 | 2 | 4 | 6 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 76 | 78 | 80 | 82 | 84 | 86 | 88 | 90 | 92 | 94 | 96 | 98 | 100 |

顺便一提,也可以使用 continue break,效果可参见c语言的。


十九、提前终止

exit()退出当前的探针


二十、元组( , )

支持 x 元组,其中 x 是大于 1 的任何整数。

支持使用运算符进行索引。

当然元组在创建后是不可变的。

root@zy-virtual-machine:~/桌面# bpftrace -e 'BEGIN { $www = (123, 456, "qwq"); printf("%d %sn", $www. 1, $www. 2); }'

Attaching 1 probe. . .

456 qwq


二十一、探针

· kprobe- 内核函数启动

· kretprobe- 内核函数返回

· uprobe- 用户级功能启动

· uretprobe- 用户级函数返回

· tracepoint- 内核静态跟踪点

· usdt- 用户级静态跟踪点

· profile- 定时采样

· interval- 定时输出

· software- 内核软件事件

· hardware- 处理器级事件

· 某些探针类型允许通配符匹配多个探针,例如, 还可以使用逗号分隔的列表为动作快指定多个附加点。kprobe:vfs_*



二十二、动态跟踪,内核级kprobe/kretprobe

语法:

kprobe:function_name[+offset]

kretprobe:function_name

使用kprobes(Linux内核功能)检测函数执行的开始或者结束(也就是返回)。

例子如下

root@zy-virtual-machine:~/桌面# bpftrace -e 'kprobe:do_nanosleep { printf("sleep by tid %dn", tid); }'

Attaching 1 probe. . .

sleep by tid 966

sleep by tid 966

sleep by tid 966

sleep by tid 2806

sleep by tid 2806

还可以在探测的函数中指定偏移量:

root@zy-virtual-machine:~/桌面# gdb -q /usr/lib/debug/boot/vmlinux-5.13.0-52-generic --ex 'disassemble do_sys_open'

Reading symbols from /usr/lib/debug/boot/vmlinux-5. 13. 0-52-generic. . .

Dump of assembler code for function do_sys_open:

   0xffffffff81324430 <+0>: callq  0xffffffff81077870 <__fentry__>

   0xffffffff81324435 <+5>: push   %rbp

   0xffffffff81324436 <+6>: mov    %rsp,%rbp

   0xffffffff81324439 <+9>: sub    $0x20,%rsp

   0xffffffff8132443d <+13>: mov    %gs:0x28,%rax

   0xffffffff81324446 <+22>: mov    %rax,-0x8(%rbp)

   0xffffffff8132444a <+26>: mov    %edx,%eax

   0xffffffff8132444c <+28>: test   $0x200000,%edx

   0xffffffff81324452 <+34>: je     0xffffffff81324485

   0xffffffff81324454 <+36>: and    $0x2b0000,%eax

   0xffffffff81324459 <+41>: xor    %ecx,%ecx

   0xffffffff8132445b <+43>: lea    -0x20(%rbp),%rdx

   0xffffffff8132445f <+47>: mov    %rax,-0x20(%rbp)

   0xffffffff81324463 <+51>: mov    %rcx,-0x18(%rbp)

   0xffffffff81324467 <+55>: movq   $0x0,-0x10(%rbp)

   0xffffffff8132446f <+63>: callq  0xffffffff813239f0

   0xffffffff81324474 <+68>: mov    -0x8(%rbp),%rdx

   0xffffffff81324478 <+72>: sub    %gs:0x28,%rdx

   0xffffffff81324481 <+81>: jne    0xffffffff8132449a

   0xffffffff81324483 <+83>: leaveq

   0xffffffff81324484 <+84>: retq   


当然 如果提示你找不到debug目录下文件,说明你可能没安装调试符号。

要解决这个问题虽然不难,不过如果不注意很容易和我一样纠结一下午。

可以从 Ubuntu 官方网址 中找到调试符号安装包

搜索的时候可以使用 "linux-image-unsigned-“uname”-dbgsym" 作为关键词,uname就是你本地安装的内核版本

可以uname -r来搜索

顺便一提,有时候你前面不指定amd三个字母,就容易搜出arm的。这里要根据你自己虚拟机的体系结构来下。

下的时候挑那种800mb,900mb的来下载。不要下那种小的,小的显然就不对的。

比如我下载的就是https://launchpad. net/~canonical-kernel-team/+archive/ubuntu/ppa/+build/23856258/+files/linux-image-unsigned-5. 13. 0-52-generic-dbgsym_5. 13. 0-52. 59_amd64. ddeb

当你下载好后,dpkg -i 你要安装的调试符号包. ddeb即可

root@zy-virtual-machine:~/桌面# bpftrace -e 'kprobe:do_sys_open+6 { printf("here is +6 offsetn"); }'

Attaching 1 probe. . .

here is +6 offset

here is +6 offset

here is +6 offset

here is +6 offset

here is +6 offset

here is +6 offset

如果地址与指令边界一致且在函数内,可使用 vmlinux(调试符号)对其进行检查。

不然就没法添加 比如

root@zy-virtual-machine:~/桌面# bpftrace -e 'kprobe:do_sys_open+4 { printf("in heren"); }'

Attaching 1 probe. . .

ERROR: Could not add kprobe into middle of instruction: /usr/lib/debug/boot/vmlinux-5. 13. 0-52-generic:do_sys_open+4

root@zy-virtual-machine:~/桌面# bpftrace -e 'kprobe:do_sys_open+5 { printf("in heren"); }'

Attaching 1 probe. . .

^C

当然啦,如果bpftrace是使用

ALLOW_UNSAFE_PROBE的option 编译的,则可以使用 --unsafe 选项跳过检查不过就算在这种情况下,linux内核仍然检查指令对齐。所以没法挂钩子的地方就是没法挂钩子的。(废话)


2.动态跟踪,内核级参数kprobe/kretprobe

语法:

kprobe: arg0, arg1, . . . , argN

kretprobe: retval

可以通过这些变量名称访问参数或返回值

例子:

root@zy-virtual-machine:~/桌面# bpftrace -e 'kprobe:do_sys_open { printf("opening: %sn", str(arg1)); }'

Attaching 1 probe...

opening: /etc/passwd

opening: /etc/group

opening: /var/lib/apt/lists/partial/mirrors.tuna.tsinghua.edu.cn_ubuntu_

opening: /var/lib/apt/lists/partial/mirrors.tuna.tsinghua.edu.cn_ubuntu_

opening: /var/lib/apt/lists/partial/mirrors.tuna.tsinghua.edu.cn_ubuntu_

opening: /proc/33259/cgroup

opening: /proc/1/cgroup

opening: /etc/passwd

opening: /etc/group

root@zy-virtual-machine:~/桌面# bpftrace -e 'kretprobe:do_sys_open { printf("returned: %dn", retval); }'

Attaching 1 probe...

returned: 9

returned: 9

returned: 9

returned: 9

returned: 9

returned: 9

returned: 13

returned: 13

returned: 13

returned: 13

^C


root@zy-virtual-machine:~/桌面# bpftrace -e 'kprobe:vfs_open { printf("open path: %sn",str(((struct path *)arg0)->dentry->d_name. name)); }'

Attaching 1 probe...

open path: org.gnome.Terminal.desktop

open path: tab-new-symbolic.svg

open path: open-menu-symbolic.svg

open path: edit-find-symbolic.svg

open path: window-minimize-symbolic.svg

open path: window-maximize-symbolic.svg

open path: tab-new-symbolic.svg

open path: open-menu-symbolic.svg



二十三、动态跟踪,用户级uprobe/uretprobe

语法:

uprobe:library_name:function_name[+offset]

uprobe:library_name:address

uretprobe:library_name:function_name

这些使用uprobes(Linux内核功能)来检测三环函数执行的开始或者结束(就是返回)。

顺便一提,可能有些读者并不特别清楚零环三环到底是什么,而不清楚其本质是什么,因此这里顺带简单提一句。可以理解为cpu有0,1,2,3环之分。但操作系统只用了0和3环。每个程序在运行时,cs和ss寄存器的段选择子(就是里面表面存的16位数值 实际上cs和ss存的不止这些 详情可参考保护模式) 这16位数值的某两位可能是00 可能是11

二进制翻译成十进制就分别是0和3,也就是代表了当前程序的cpu执行权限级别。也就是当前程序是0环还是3环。

一般驱动程序这类的是0环的,咱们自己写的程序大多是3环的。把程序丢进调试器里就能知道当前程序的当前环数(因为可能程序会从3环中途运行时触发中断门什么的,从而导致三环进零环)

我们可以使用objdump来列出/bin/bash的符号

root@zy-virtual-machine:~/桌面# objdump -tT /bin/bash | grep readline

0000000000124e60 g    DO .bss 0000000000000008  Base        rl_readline_state

00000000000b7c90 g    DF .text 0000000000000252  Base        readline_internal_char

00000000000b7160 g    DF .text 000000000000015f  Base        readline_internal_setup

00000000000870e0 g    DF .text 000000000000004c  Base        posix_readline_initialize

00000000000b84f0 g    DF .text 000000000000009a  Base        readline

0000000000124530 g    DO .bss 0000000000000004  Base        bash_readline_initialized

0000000000121310 g    DO .data 0000000000000008  Base        rl_readline_name

000000000011c1fc g    DO .data 0000000000000004  Base        rl_readline_version

0000000000087370 g    DF .text 00000000000007ab  Base        initialize_readline

0000000000121798 g    DO .bss 0000000000000004  Base        current_readline_line_index

00000000001217a8 g    DO .bss 0000000000000008  Base        current_readline_prompt

000000000008faa0 g    DF .text 0000000000000045  Base        pcomp_set_readline_variables

00000000001217a0 g    DO .bss 0000000000000008  Base        current_readline_line

00000000000b72c0 g    DF .text 0000000000000120  Base        readline_internal_teardown

000000000011c1f8 g    DO .data 0000000000000004  Base        rl_gnu_readline_p

root@zy-virtual-machine:~/桌面# 


这样可以列出包含来自 /bin/bash 的“readline”的各种函数。

当然 也可以直接这样来确认可以挂钩bash程序的什么函数

root@zy-virtual-machine:~/桌面# sudo bpftrace -l "uprobe:/bin/bash"

uprobe:/bin/bash:__libc_csu_fini

uprobe:/bin/bash:__libc_csu_init

uprobe:/bin/bash:_getenv

uprobe:/bin/bash:_hs_append_history_line

uprobe:/bin/bash:_hs_history_patsearch

uprobe:/bin/bash:_hs_replace_history_data

uprobe:/bin/bash:_rl_abort_internal

uprobe:/bin/bash:_rl_add_macro_char

uprobe:/bin/bash:_rl_adjust_point

uprobe:/bin/bash:_rl_any_typein

uprobe:/bin/bash:_rl_arg


综上我们可以监控bash命令 最简单的实现如下

bpftrace -e 'uretprobe:/bin/bash:readline{printf("%sn",str(retval));}'

这样每输入一条指令我们的监控程序都会获取并打印出来,显然这在安全领域有很大的用场

由于详细解释bpftrace的语法细节是很冗长的,所以暂时介绍到这里。详情可参考后面的主要参考文章。

以下是我觉得很实用的一些bpftrace指令

监控进程退出

bpftrace -e 'kprobe:do_exit{printf("进程退出:%s quit. n", comm)}' 

监控文件打开和相应的进程

bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("进程打开 进程名是:%s 文件名是:%sn", comm, str(args->filename)); }' 

通过 exec() 系统调用跟踪新进程 

bpftrace -e 'BEGIN{printf("%-10s %-5s %sn", "TIME(ms)", "PID", "ARGS");} tracepoint:syscalls:sys_enter_exec*{printf("%-10u %-5d ", elapsed/1000000, pid);join(args->argv);}' 

打印系统范围内输入的 bash 命令 

bpftrace -e 'BEGIN{printf("Tracing bash commands. . .  Hit Ctrl-C to end. n");printf("%-9s %-6s %sn", "TIME", "PID", "COMMAND");} uretprobe:/bin/bash:readline{time("%H:%M:%S ");printf("%-6d %sn", pid, str(retval));}'


主要参考文章

超好用的 Linux 性能工具:bpftrace

https://github.com/iovisor/bpftrace/blob/master/docs/reference_guide. md


浅析bpftrace
END
浅析bpftrace



浅析bpftrace





线


原文始发于微信公众号(火线云安全研究团队):浅析bpftrace

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年10月8日14:23:12
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   浅析bpftracehttps://cn-sec.com/archives/1300278.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息