神奇的BPF四 用bpftrace 开个后门

一 前言

前面的文章聊到bpftrace,这是个强大简洁的编写bpf程序的利器,内部的语法看起来比较容易,功能一点也不弱,比如

我们想查看现在系统中谁在执行什么程序:

[root@localhost ~]# bpftrace -e 'tracepoint:syscalls:sys_enter_execve { printf("%s-->%s\n",comm,str(args->filename));}'
Attaching 1 probe...
bash-->/usr/bin/cat
bash-->/usr/bin/more

系统中程序执行的系统调用次数统计:

[root@localhost ~]# bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[pid,comm]=count();}'
Attaching 1 probe...
^C
@[894, rpcbind]: 1
@[897, sedispatch]: 2
@[929, gmain]: 4
@[1004, sssd_be]: 5
@[931, HangDetector]: 8
@[730, systemd-journal]: 9
@[1106, systemd-logind]: 14
@[1, systemd]: 15
@[3936, sshd]: 17

二 bpftrace 基础语法

提供一个快速上手指南,并不讲究面面俱到,只说明重要内容。

2.1 执行形式

  1. 一句话命令 执行bpftrace -e '命令' 有些单行命令只有结束,按ctrl+c结束了才会输出内容。
  2. 文件形式,文件开头写上#!/usr/bin/bpftrace 如果用到system函数需要加上--unsafe
    命令形式: probe[,probe,...] /filter/ { action }
    即探测事件,过滤器和执行语句,多个语句的话用逗号分隔,最简单的:
 bpftrace -e 'BEGIN { printf("Hello eBPF!\n"); }' 

2.2 分析实际例子说明语法

 1 #!/usr/bin/bpftrace
  2 
  3 
  4 kprobe:vfs_read
  5 {
  6 @start[tid] = nsecs;
  7 }
  8 
  9 kretprobe:vfs_read
 10 /@start[tid]/
 11 {
 12 $duration_us = (nsecs - @start[tid]) / 1000;
 13 @us = hist($duration_us);
 14 delete(@start[tid]);
 15 }

这是一个统计read系统掉哦那个耗时的脚本, 第四行,kprobe说明插桩为内核动态函数插桩;
@start 说明定义一个名字为start的映射表变量,是用来保存bpftrace的临时数据,也会在程序结束的时候自动输出,@start的映射表为map类型,它的key为tid,这个tid为内部变量,具体说明见下面,nsecs是当前时间,纳秒为单位。这样我们在map中为每个线程在进入read函数的时候,保存各自的纳秒级别的时间戳了。
第九行,kretprobe为内核动态函数返回的插桩,第10行为匹配条件,即要存在这个线程的进入read函数的开始时间戳,不然这个值为0,计算的结果就有问题了。
第12行,$duration_us即为一个自定义变量,值为这个函数的执行时间,计算方法用当前函数返回时候的时间戳减去进入函数时候的时间戳,单位转成微秒;
第13行,定义一个名字为@us的映射表变量,保存的是耗时时间为值的2的幂次方的直方图(看输出结果就明白了),hist为生成直方图的内置函数;
第14行,delete函数删除@start映射表里面,key为tid的对象,为什么要删除那,是因为不删除的话,这个脚本结束的时候,凡是映射表的内容都会输出,影响结果查看。

执行下:(执行结果不符合预期,多打印了@start 映射,按道理是已经删除了)


输出结果

为了更实用点,我们可以打印出不同的线程调用read的返回值分布情况,只要改下第13行,改成:

@us[comm,tid] = hist($duration_us);

结果就如下图:


结果部分截图

说明下直方图,[ 表示大于等于,)表示小于,比如第一行sshd这个进程名,线程id为3936,它执行耗时在大于等于4微秒到小于等于8微秒中的情况有一条,耗时在大于等于8微秒,小于16微秒的数量有一条。

2.3 自己写的代码动态追踪

随便写个小程序,如下代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int fun1(int a) {
   return a+10;
}
int fun2(int a, int b) {
   return a*b;
}
int fun3(int a, int b){
   return a+b;
}
int main(int argc,char * argv[])
{
   int a = 2,b=4;
   printf("a*b=%d\n",fun2(a,b));
   getchar();
   printf("a+b=%d\n",fun3(a,b));
   getchar();
   printf("a+10=%d\n",fun1(a));
}

查看可以进行用户级动态函数:

[root@localhost ctest]# bpftrace -l 'uprobe:/root/ctest/a.out:*'
uprobe:/root/ctest/a.out:__do_global_dtors_aux
uprobe:/root/ctest/a.out:__libc_csu_fini
uprobe:/root/ctest/a.out:__libc_csu_init
uprobe:/root/ctest/a.out:_dl_relocate_static_pie
uprobe:/root/ctest/a.out:_fini
uprobe:/root/ctest/a.out:_init
uprobe:/root/ctest/a.out:_start
uprobe:/root/ctest/a.out:deregister_tm_clones
uprobe:/root/ctest/a.out:frame_dummy
uprobe:/root/ctest/a.out:fun1
uprobe:/root/ctest/a.out:fun2
uprobe:/root/ctest/a.out:fun3
uprobe:/root/ctest/a.out:main
uprobe:/root/ctest/a.out:register_tm_clones

写了个追踪脚本:

#!/usr/bin/bpftrace
  
uprobe:/root/ctest/a.out:fun2
{
@start[comm,tid] = nsecs;
}

uretprobe:/root/ctest/a.out:fun2
/@start[comm,tid]/
{
 $duration_us = (nsecs - @start[comm,tid]) / 1000;
 if ($duration_us >100) {
    printf("func2 cost too long:%d\n",$duration_us);
 }else {
    printf("func2 cost normal:%d ret:%d\n",$duration_us,retval);
 }
}

执行./user.bt 然后执行a.out结果如下:

[root@localhost bpfstest]# ./user.bt 
Attaching 2 probes...
func2 cost normal:9 ret:8
^C

@start[a.out, 12459]: 16150154585278

是不是觉得很棒,不用改任何代码,就知道函数执行的耗时,对于耗时情况我们可以灵活设置策略,比如告警啥的。

三 bpftrace建立个后门

在写前几篇文章的时候,飞哥联系我说,这玩意不完全,容易建后面,刚好看到一篇用bpftrace建立后面的文章(有不少变动),刚好测试下。
原理比较简单,就是对监听的端口进行跟踪,收到远程的连接信息,分析下,如果是发送的秘密字符串,那就执行一个命令,返回执行命令的结果。

3.1 环境建立

随便下载个web容器运行,如下:

yum install nginx
 nginx -c /etc/nginx/nginx.conf

确认启动如下:

[root@localhost ~]# curl http://127.0.0.1
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
    <head>
        <title>Test Page for the Nginx HTTP Server on Red Hat Enterprise Linux</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <style type="text/css">
            /*<![CDATA[*/
            body {
                background-color: #fff;
                color: #000;
                font-size: 0.9em;
...

确保程序是ok的。

3.2 编写跟踪程序

这个简单就不介绍了,直接上代码:

#!/usr/bin/bpftrace --unsafe 
#include <net/sock.h>
#include <linux/in.h>
#include <linux/in6.h>

BEGIN
{
  printf("Welcome to Offensive BPF... Use Ctrl-C to exit.\n");
}

tracepoint:syscalls:sys_enter_accept*
{ 
  @sk[tid] = args->upeer_sockaddr;
}

tracepoint:syscalls:sys_exit_accept*
/ @sk[tid] /
{
  @sys_accepted[tid] = @sk[tid]; 
  $sa = (struct sockaddr *) @sk[tid];
  if ($sa->sa_family == AF_INET) {
     $s = (struct sockaddr_in *) @sk[tid];
     $port = ($s->sin_port>>8) | (($s->sin_port<<8) &0xff00);
     printf("%-16s %-5d  \n",ntop(AF_INET,$s->sin_addr.s_addr),$port);
   }
}

tracepoint:syscalls:sys_enter_recvfrom
/ @sys_accepted[tid] /
{   
  @fds[tid]=args->fd;
  @sys_read[tid] = args->ubuf;
}

tracepoint:syscalls:sys_exit_recvfrom
{
   $len = args->ret;

  if ((@sys_read[tid] != 0) && ($len > 5))
  { 
    xxxxxxxxxxxxxxxxxxxxxx
  } 
}

tracepoint:syscalls:sys_enter_writev,tracepoint:syscalls:sys_enter_write
/ @exec[tid] == 1 /
{
  printf("write to client:");
 xxxxxxxxxxxxxxxxxxxxxxx
}

END
{
    clear(@cmds);
    clear(@fds);
    clear(@exec);
    clear(@sk);
    clear(@sys_read);
    clear(@sys_accepted);
    printf("Exiting. Bye.\n");
}

这段代码有些部分我做了模糊处理,主要是有点敏感,如果想获取完整版本的代码关注TSparks发送”代码“,来获取吧。
代码比较简单,在accept的地方可以获取连接的客户端的ip和端口信息,保存起来,我们再接收消息或发送消息的地方,可以判断下这个ip是不是我们控制ip,如果是我们控制ip,就分析下请求的消息,根据请求的消息执行命令,然后把结果发送给我们控制主机。

3.3 如何检查这种后门那

其实也比较简单,首先我们可以查看系统中是否有bpf程序在运行,比如通过bpftool查看:

[root@localhost ~]# bpftool prog
617: tracepoint  name sys_enter_accep  tag 26c093d1d907ce74  gpl
    loaded_at 2022-03-12T20:41:02-0500  uid 0
    xlated 128B  jited 80B  memlock 4096B  map_ids 406
618: tracepoint  name sys_enter_accep  tag 26c093d1d907ce74  gpl
    loaded_at 2022-03-12T20:41:02-0500  uid 0
    xlated 128B  jited 80B  memlock 4096B  map_ids 406
619: tracepoint  name sys_exit_accept  tag 0dbf07b51d6729c4  gpl
    loaded_at 2022-03-12T20:41:02-0500  uid 0
    xlated 1040B  jited 577B  memlock 4096B  map_ids 406,407,409
620: tracepoint  name sys_exit_accept  tag 0dbf07b51d6729c4  gpl

还可以通过sysctl -a|grep bpf 查看内核选项,关闭一些选项:

sysctl -w kernel.unprivileged_bpf_disabled=1

还有就比较简单了,如果是bpftrace写的脚本检查unsafe选项。

四 bpftrace 的一些说明

4.1 内置变量表

uid:用户id。
tid:线程id
pid:进程id。
cpu:cpu id。
cgroup:cgroup id.
probe:当前的trace点。
comm:进程名字。
nsecs:纳秒级别的时间戳。
kstack:内核栈描述
curtask:当前进程的task_struct地址。
args:获取该kprobe或者tracepoint的参数列表
arg0:获取该kprobe的第一个变量,tracepoint不可用
arg1:获取该kprobe的第二个变量,tracepoint不可用
arg2:获取该kprobe的第三个变量,tracepoint不可用
retval: kretprobe中获取函数返回值
args->ret: kretprobe中获取函数返回值
BEGIN :bpftrace开始
END: bpftrace 结束

4.2 探针类型表

Alias   Type    Description
t   tracepoint  Kernel static instrumentation points
U   usdt    User-level statically defined tracing
k   kprobe  Kernel dynamic function instrumentation (standard)
kr  kretprobe   Kernel dynamic function return instrumentation (standard)
f   kfunc   Kernel dynamic function instrumentation (BPF based)
fr  kretfunc    Kernel dynamic function return instrumentation (BPF based)
u   uprobe  User-level dynamic function instrumentation
ur  uretprobe   User-level dynamic function return instrumentation
s   software    Kernel software-based events
h   hardware    Hardware counter-based instrumentation
w   watchpoint  Memory watchpoint events
p   profile Timed sampling across all CPUs
i   interval    Timed reporting (from one CPU)
iter    Iterator tracing over kernel objects
BEGIN   Start of bpftrace
END End of bpftrace

4.3 内置函数

Function    Description
printf("...")   Print formatted string
time("...") Print formatted time
join(char *arr[])   Join array of strings with a space
str(char *s [, int length]) Return string from s pointer
buf(void *p [, int length]) Return a hexadecimal string from p pointer
strncmp(char *s1, char *s2, int length) Compares two strings up to length
sizeof(expression)  Returns the size of the expression
kstack([limit]) Kernel stack trace up to limit frames
ustack([limit]) User-level stack trace up to limit frames
ksym(void *p)   Resolve kernel address to symbol
usym(void *p)   Resolve user-space address to symbol
kaddr(char *name)   Resolve kernel symbol name to address
uaddr(char *name)   Resolve user-space symbol name to address
ntop([int af,]int|char[4:16] addr)  Convert IP address data to text
reg(char *name) Return register value
cgroupid(char *path)    Return cgroupid for /sys/fs/cgroup/... path
time("...") Print formatted time
system("...")   Run shell command
cat(char *filename) Print file content
signal(char[] sig | int sig)    Send a signal to the current task
override(u64 rc)    Override a kernel function return value
exit()  Exits bpftrace
@ = count() Count events
@ = sum(x)  Sum the value
@ = hist(x) Power-of-2 histogram for x
@ = lhist(x, min, max, step)    Linear histogram for x
@ = min(x)  Record the minimum value seen
@ = max(x)  Record the maximum value seen
@ = stats(x)    Return the count, average, and total for this value
delete(@x[key]) Delete the map element
clear(@x)   Delete all keys from the map

五 参考网址

bpftrace 语法简单介绍: [bpftrace Cheat Sheet (brendangregg.com)](https://www.brendangregg.com/BPF/bpftrace-cheat-sheet.html)

bpftrace 做的后门 [进攻性 BPF:使用 bpftrace 托管后门 ·拥抱红色 (embracethered.com)](https://embracethered.com/blog/posts/2021/offensive-bpf-bpftrace-message-based/)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,544评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,430评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,764评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,193评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,216评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,182评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,063评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,917评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,329评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,543评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,722评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,425评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,019评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,671评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,825评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,729评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,614评论 2 353

推荐阅读更多精彩内容

  • 一 前言 对于第一次BPF的介绍,只是简单介绍了BPF,写了一个没啥用的例子,如何利用BCC写一个有点用的程序以及...
    明翼阅读 2,476评论 2 1
  • 一 前言 作为一个coder,时不时会遇到性能问题,有时候明明看资源,cpu,io都占用不高,程序的性能就是上不去...
    明翼阅读 2,907评论 0 1
  • 一 前言 前面介绍了利用BCC 写eBPF的代码,虽然可以利用python加载,说实话,写起来并不容易,程序本身难...
    明翼阅读 763评论 0 1
  • 关于Mongodb的全面总结 MongoDB的内部构造《MongoDB The Definitive Guide》...
    中v中阅读 31,928评论 2 89
  • 1、冯诺伊曼结构:运算器、控制器、存储器、输入输出。 和高级编程语言里面一个函数方法类似:输入输出就相当于出入参,...
    许先森的许阅读 1,074评论 0 3