ebpf采集mysql请求信息及ebpf对应用安全的思考

本文笔者继续介绍ebpf 的应用:使用bpftrace采集mysql连接信息,包括数据库地址、db_nameuser_name。在展示采集操作的同时,附上对ebpf对云时代应用安全的一些思考。

# 目标

使用bpftrace对一个运行中进程的mysql请求进行采集,目标采集内容包括数据库地址、db_nameuser_name

目标进程代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// blog/main.go
package main

import (
	"fmt"
	"log"
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
	_ "github.com/go-sql-driver/mysql"
	"xorm.io/xorm"
)

var sqlE *xorm.Engine

func init() {
	fmt.Println("init from main")
	var err error
	sqlE, err = xorm.NewEngine("mysql",
		"test:mysqltest@tcp(localhost:3306)/test_db?charset=utf8&parseTime=true") // 随便写个数据库信息,假装是正确的
	if err != nil {
		log.Printf("init mysql failed: %+v
", err)
	}
}

type Resp struct {
	Code int    `json:"code"`
	Msg  string `json:"msg"`
}

type SqlInfo struct {
	Id      int64     `json:"id" xorm:"pk bigint(20)"`
	Created time.Time `json:"created" xorm:"created"`
	Info    string    `json:"info"`
}

func (sql *SqlInfo) TableName() string {
	return "sql_info"
}

func Mysql(c *gin.Context) {
	info := c.Query("info")
	if info == "" {
		now := time.Now().Format("2008-01-02 15:04:05")
		info = now
	}
	sqlInfo := SqlInfo{
		Info: info,
	}
	affected, err := sqlE.Insert(&sqlInfo)
	if err != nil {
		log.Printf("insert db with error: %+v
", err)
	} else {
		log.Printf("affect column nums: %d
", affected)
	}

	c.JSON(http.StatusOK, &Resp{Code: 0, Msg: "mysql req over"})
	return
}

func main() {
	r := gin.Default()
	srv := &http.Server{
		Addr: "0.0.0.0:9981",
	}
	log.Println("server start at: 0.0.0.0:9981")
	r.GET("/sql", Mysql)
	srv.Handler = r
	err := srv.ListenAndServe()
	if err != nil {
		log.Fatal("error with start listener")
	}
}

# bpftrace 代码

直接上代码了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#! /bin/bpftrace

/* 保存在 blog/blog.bt 
 这里使用的 uprobe 函数为 go-sql-driver 里的内容。源代码在:
 https://github.com/go-sql-driver/mysql/blob/master/connector.go#L23
 定义为:
 func (c *connector) Connect(ctx context.Context) (driver.Conn, error) {...}
*/
uprobe:./blog:"github.com/go-sql-driver/mysql.(*connector).Connect"
{
    printf("Connect
");
    $cfg_addr = *(uint64*)sarg0; // 获取 c.cfg 的地址
    $user_addr = *(uint64*)($cfg_addr); // 获取 c.cfg.User
    $user_len = *(uint64*)($cfg_addr+8); // 获取 len(c.cfg.User)
    //$pwd_addr = *(uint64*)($cfg_addr+16); // 请注意这里注释掉的内容
    //$pwd_len = *(uint64*)($cfg_addr+24);
    $addr_addr = *(uint64*)($cfg_addr+48);
    $addr_len = *(uint64*)($cfg_addr+56);
    $db_addr = *(uint64*)($cfg_addr+64);
    $db_len = *(uint64*)($cfg_addr+72);
    printf("user: %s
", str($user_addr, $user_len));
    //printf("pwd: %s
", str($pwd_addr, $pwd_len));
    printf("addr: %s
", str($addr_addr, $addr_len));
    printf("db: %s
", str($db_addr, $db_len));
}

而后启动应用程序blog,启动监听程序sudo bpftrace ./blog.bt,请求blog/sql接口以触发blogsql的请求。整个过程对blog程序来说没有任何的不平凡之处,但是我们已经获取了采集结果。 附上执行结果如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ ./blog
init from main
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:	export GIN_MODE=release
 - using code:	gin.SetMode(gin.ReleaseMode)

2022/10/21 20:15:38 server start at: 0.0.0.0:9981
[GIN-debug] GET    /sql                      --> main.Mysql (3 handlers)
2022/10/21 20:18:52 insert db with error: dial tcp 127.0.0.1:3306: connect: connection refused
[GIN] 2022/10/21 - 20:18:52 | 200 |    3.679499ms |       127.0.0.1 | GET      "/sql"

此时,在采集侧:

1
2
3
4
5
6
$ sudo ./blog.bt
Attaching 1 probe...
Connect
user: test
addr: localhost:3306
db: test_db

我们已经获取了需要的信息。

# 对采集代码的一些说明

bpftrace语法部分,参见github-bpftrace-reference_guid。里面有ebpf的一些简单介绍以及bpftrace的使用说明。代码里的主要逻辑,则需要参见golang的语法来理解。部分简述如下:

  • 类里的方法,实际调用的时候,第一个参数为对象的地址;
  • go-1.16 及之前的版本,参数存储在栈上;

剩下的内容就比较好理解了:ebpf提供的核心功能包括按需读取用户空间内的数据。结合golang-常见类型字节数可以比较快的推导出我们需要的信息在地址内的偏移量。同时,在bpftrace无侵入遍历golang链表曾经提到过,如果目标对象比较大,无法在ebpf代码里完整定义该对象(内核限制单个ebpfhook点程序的栈空间大小在512B),我们访问对象里的成员时,使用的方法就是偏移量访问。

# ebpf 与应用安全的一些思考

最后提一点自己的思考。
请回到bpftrace代码里,里面的pasword信息获取的操作被注释掉了。其实我们去掉注释,仍然能够按照预期获取结果。这就意味着,如果我们拥有机器上的权限,并且机器满足我们的采集需求,应用里的核心信息(这里是数据库的密码)将被简单的获取。无论数据库密码如何存储:配置文件、源代码、通过网络配置下发等。只要有涉及数据库访问的用户态函数,有涉及数据库密码传递的内容,这些信息存在被获取的风险,只要采集人拥有root权限。
这里引出另外一个问题:如果一个用户拥有机器上的管理员权限,TA是否应该拥有机器上所有进程信息的准入权。这里的进程信息,显然是包括机器上容器内的进程信息的,无论是公有云或者私有云。

Hello, World!
使用 Hugo 构建
主题 StackJimmy 设计