✦
一、eBPF简单介绍
✦
eBPF顾名思义来源于BPF(如下图),实际上BPF最初的目的是用于高效网络报文过滤,经过重新设计,eBPF 就不再局限于网络协议栈,它后期已经成为内核顶级的子系统,演进为一个通用执行引擎。换句话说 其实就是从提高底层网络抓包性能 变成了在底层执行更多通用代码的东西 。
因此 eBPF 就适用于诸如性能分析、软件定义网络、网络安全等诸多场景。
按我个人的理解,我觉得eBPF本质上它就是内核中的一个虚拟机,以一种安全的方式在各种各样的内核hook点执行字节码,我们可以在三环编写和程序,用llvm作为后端把前端代码(比如go bpftrace python等)变成字节码,然后字节码在虚拟机里变成对应体系结构的硬编码来执行。
一般 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这两种语言的混合体。
✦
三、从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));}'
✦
主要参考文章
✦
https://github.com/iovisor/bpftrace/blob/master/docs/reference_guide. md
索隐实验室是长沙火线云网络科技有限公司的安全研究团队之一,索隐寓意发现隐秘的攻击。索隐实验室聚焦威胁检测与分析技术,包括入侵检测、溯源分析、自动化应急响应等方向。
原文始发于微信公众号(火线云安全研究团队):浅析bpftrace
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论