osquery源码解读之分析socket_events

说明

前面分析了process_open_socketshell_history,两者都是通过读取Linux系统中的文件提取信息,本篇文章将要分析的socket_events是借助于linux中的auditd工具来实现的。本篇文章照例借着对socket_events的分析,来了解Linux中的auditd工具的相关用法

auditd介绍

Linux auditd工具可以将审计记录写入日志文件,包括记录系统调用和文件访问。下图显示的就是Linux audit架构示意图。

说明:上图的实线代表数据流,虚线代表组件之间的控制关系。上图包括有两大部分:中间的是 Linux 内核中的几种系统调用(user, task, exit, exclude),右侧是一系列应用程序(auditd、audispd、auditctl、autrace、ausearch 和 aureport 等)。Linux 内核中的几种系统调用是:

  • User :记录用户空间中产生的事件;它的作用是过滤消息的,内核传递给审计后台进程之前先查询它。
  • Task:跟踪应用程序的子进程(fork);当一个任务被创建时,也就是父进程通过 fork 和克隆创建子进程时记录该事件;
  • Exit:当一个系统调用结束时判断是否记录该调用;
  • Exclude:删除不合格事件;Exclude 是用来过滤消息的,也就是不想看到的消息可以在这里写规则进行过滤。

从图中可以看到 audit 是内核中的一个模块,内核的运行情况都会在 audit 中记录,当然这个记录的规则是由root来设置的。内核的 audit 模块是由应用层的一个应用程序 auditd 来控制的。audit 产生的数据都会传送到 auditd 中,然后再由 auditd 进行其它操作。auditd.conf 是 auditd 的配置文件,确定 auditd 是如何启动的,日志文件放在哪里等等。audit.rules 是 audit 的规则文件,确定 audit 的日志中记录哪些操作。它通过一个对 audit 进行控制的应用程序 auditctl 进行操作。root 用户也可以直接调用 auditctl 进行操作。通过audit.rulesauditctl设置规则的区别是在于audit.rules设置的规则是一直有效的,只要系统系统auditd服务就会生效。而auditctl设置的规则是位于内存当中,只在当前环境有效,如果重启电脑之后或者是重启auditd服务之后,那么通过auditctl设置的规则就会失效。默认的是将日志保存在 audit.log 文件中,默认路径/var/log/audit/audit.log

更多详细地介绍可以去看Linux 用户空间审计工具 audit

关于auditd的规则的设定方法,可以参考A Brief Introduction to auditdLinux 用户空间审计工具 audit。大致来说存在两种方式的监控,分别是监控系统调用行为以及文件访问相关的行为。其规则大致设定大致如下。

1
2
-a exit,always -S <syscall>
-w <filename>

具体如auditctl -a exit,always -F arch=b64 -S connect其中-S connect表示监控connect系统调用表;auditctl -w /etc/passwd -p wa表示监控/etc/passwd的文件修改行为。

由于使用auditd能够监控系统的内核调用,那么用户态所有的操作行为全部可以被记录。因为也很多人利用auditd进行安全监测,如Web中间件EXECVE审计反弹shell监控

auditd监控实战

我们通过一个实际的例子来对audit.log的日志结构以及unix中的connect内核调用进行一个简单的说明。设置规则

1
2
3
4
5
# auditctl -l
No rules
# auditctl -a always,exit -F arch=b64 -S connect
# auditctl -l
-a always,exit -F arch=b64 -S connect

执行curl www.baidu.com,查看/var/log/audit/audit.log日志,找到其中与curl www.baidu.com相关的日志记录:

1
2
3
type=SYSCALL msg=audit(1544195763.393:260010): arch=c000003e syscall=42 success=no exit=-115 a0=3 a1=7ffccb794910 a2=10 a3=7ffccb7941e0 items=0 ppid=53240 pid=17096 auid=1000 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=pts5 ses=1 comm="curl" exe="/usr/bin/curl" subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 key=(null)
type=SOCKADDR msg=audit(1544195763.393:260010): saddr=0200005073EFD21B0000000000000000
type=PROCTITLE msg=audit(1544195763.393:260010): proctitle=6375726C007777772E62616964752E636F6D

此事件由三个记录组成(每个以type=作为开始),共享相同的时间戳和编号(其中1544192911.452是时间戳,28850是事件编号)。每个记录包含好几对 name=value ,由空格或者逗号分开。理解审核日志文件audit.log中的日志文件进行了详细地解释。审核系统引用对日志中的每一个name=value都进行了详细地说明。在本文仅仅只对其中几个关键的字段进行说明。

  • type=SYSCALL,其中SYSCALL就表示连接到 Kernel 的系统调用触发了这个记录,在审核记录类型中记录了所有的类型。
  • syscall=42,表示当前的系统调用值是42,由于我们记录的connect内核调用,说明42就表示connect内核调用。Linux系统调用列表记录了所有的内核调用
  • success=no,表示这个系统调用是成功还是失败。在本例中是没有成功。
  • exit=-115,表示的是系统调用的返回值。
  • exe="/usr/bin/curl",记录了进程的可执行路径。
  • saddr=0200005073EFD21B0000000000000000,表示的是系统调用的远程地址。(因为是connect内核调用,那么必然会与远程服务器通信)
  • proctitle=6375726C007777772E62616964752E636F6D,记录的是具体的执行的命令。
  • a0-a3,记录了内核调用的前四个参数。

由于其中的很多信息都进行了编码不便于我们理解,可以使用ausearch解析上面的audit的日志,由于已经知道了event id28850,直接使用ausearch --interpret -a 260010解析。

1
2
3
type=PROCTITLE msg=audit(12/07/2018 10:16:03.393:260010) : proctitle=curl www.baidu.com 
type=SOCKADDR msg=audit(12/07/2018 10:16:03.393:260010) : saddr={ fam=inet laddr=115.239.210.27 lport=80 }
type=SYSCALL msg=audit(12/07/2018 10:16:03.393:260010) : arch=x86_64 syscall=connect success=no exit=EINPROGRESS(Operation now in progress) a0=0x3 a1=0x7ffccb794910 a2=0x10 a3=0x7ffccb7941e0 items=0 ppid=53240 pid=17096 auid=username uid=root gid=root euid=root suid=root fsuid=root egid=root sgid=root fsgid=root tty=pts5 ses=1 comm=curl exe=/usr/bin/curl subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 key=(null)

其中proctitle=curl www.baidu.com就解析出了我们的命令;saddr={ fam=inet laddr=115.239.210.27 lport=80 }解析除了wwww.baidu.com的IP地址(当然是DNS的地址),auid=username显示的是哪个用户执行的命令。

比较有意思的是a0=0x3 a1=0x7ffccb794910 a2=0x10 a3=0x7ffccb7941e0记录内核调用的4个参数。一般情况下audit.log都会记录内核调用函数的前4个参数,但是看connect()的实现,CONNECT(2)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
SYNOPSIS         top
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
DESCRIPTION top
The connect() system call connects the socket referred to by the file
descriptor sockfd to the address specified by addr. The addrlen
argument specifies the size of addr. The format of the address in
addr is determined by the address space of the socket sockfd; see
socket(2) for further details.

If the socket sockfd is of type SOCK_DGRAM, then addr is the address
to which datagrams are sent by default, and the only address from
which datagrams are received. If the socket is of type SOCK_STREAM
or SOCK_SEQPACKET, this call attempts to make a connection to the
socket that is bound to the address specified by addr.

Generally, connection-based protocol sockets may successfully
connect() only once; connectionless protocol sockets may use
connect() multiple times to change their association. Connectionless
sockets may dissolve the association by connecting to an address with
the sa_family member of sockaddr set to AF_UNSPEC (supported on Linux
since kernel 2.2).

仅仅只有三个参数。我们通过trace curl www.baidu.com的方式查看:

1
2
3
socket(AF_INET6, SOCK_DGRAM, IPPROTO_IP) = 3
...
connect(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("115.239.211.112")}, 16) = -1 EINPROGRESS (Operation now in progress)

connect()的参数进行说明:

  • 3,就是表示前面定义的socket(AF_INET6, SOCK_DGRAM, IPPROTO_IP) = 3,得到的sockfd,这个也和a0=3是相吻合的。
  • {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("115.239.211.112")},表示的是const struct sockaddr *addr,得到就是sockaddr的结构体,包含了协议簇,端口和IP,这个和ausearch解析出来的结果一致。而a1=0x7ffccb794910,则表示的是这个结构体的地址。
  • 16,表示的是套接字地址结构的大小,默认IPv4的地址的长度都是16。a2=0x10转换为十进制也是16。根据这篇文章的说法,16表示IP和IPX,24表示IPv6,80表示AX
  • 至于a3=0x7ffccb7941e0这个值,因为不是connect()的参数,同时connect()的返回值应该是数值类型,所以a3=0x7ffccb7941e0也明显不是connect()的返回值,查阅了相关资料,也不明白这个值的含义,如果有师傅知道,也希望能够交流一下。

osquery解析audit

所以osquery如果想要借助于auditd来获取信息,就需要关闭系统的auditd服务,由osquery来接管。osquery接管了之后就会默认地加入三条auditd的规则。

1
2
3
-a always,exit -S connect
-a always,exit -S bind
-a always,exit -S execve

由于利用auditd获取数据的方式与之前说明的shell_history/process_open_socket方式完全不同,osquery采用了event publisher/subscriber的架构来处理。

An osquery event publisher is a combination of a threaded run loop and event storage abstraction. The publisher loops on some selected resource or uses operating system APIs to register callbacks. The loop or callback introspects on the event and sends it to every appropriate subscriber. An osquery event subscriber will send subscriptions to a publisher, save published data, and react to a query by returning appropriate data.

大致的中文意思是:osquery的事件发布结合了进程循环和事件存储。发布者(publisher )会对某些特定的资源循环或者是对操作系统的API回调。这些循环和回调得到信息之后就会发送至订阅者(subscriber)。这些订阅者就会保存数据,对SQL语句进行相应。

socket_events定义

specs/linux/socket_events.table中存在socket_events的定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
table_name("socket_events")
description("Track network socket opens and closes.")
schema([
Column("action", TEXT, "The socket action (bind, listen, close)"),
Column("pid", BIGINT, "Process (or thread) ID"),
Column("path", TEXT, "Path of executed file"),
Column("fd", TEXT, "The file description for the process socket"),
Column("auid", BIGINT, "Audit User ID"),
Column("success", INTEGER, "The socket open attempt status"),
Column("family", INTEGER, "The Internet protocol family ID"),
Column("protocol", INTEGER, "The network protocol ID",
hidden=True),
Column("local_address", TEXT, "Local address associated with socket"),
Column("remote_address", TEXT, "Remote address associated with socket"),
Column("local_port", INTEGER, "Local network protocol port number"),
Column("remote_port", INTEGER, "Remote network protocol port number"),
Column("socket", TEXT, "The local path (UNIX domain socket only)",
hidden=True),
Column("time", BIGINT, "Time of execution in UNIX time"),
Column("uptime", BIGINT, "Time of execution in system uptime"),
Column("eid", TEXT, "Event ID", hidden=True),
])
attributes(event_subscriber=True)
implementation("socket_events@socket_events::genTable")

相较于普通表,socket_events多了attributes(event_subscriber=True),这个就是表示是event publisher/subscriber的模式。

socket_events实现

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
REGISTER(SocketEventSubscriber, "event_subscriber", "socket_events");

Status SocketEventSubscriber::init() {
if (!FLAGS_audit_allow_sockets) {
return Status(1, "Subscriber disabled via configuration");
}

auto sc = createSubscriptionContext();
subscribe(&SocketEventSubscriber::Callback, sc);

return Status(0, "OK");
}
// 注册回调函数
Status SocketEventSubscriber::Callback(const ECRef& ec, const SCRef& sc) {
std::vector<Row> emitted_row_list;
// 使用ProcessEvents处理ec->audit_events得到的所有的请求
// 处理完毕之后结果写入到emitted_row_list
auto status = ProcessEvents(emitted_row_list, ec->audit_events);
if (!status.ok()) {
return status;
}

addBatch(emitted_row_list);
return Status(0, "Ok");
}
  1. REGISTER(SocketEventSubscriber, "event_subscriber", "socket_events");socket_events注册了SocketEventSubscriber这样的一个消息订阅机制;
  2. subscribe(&SocketEventSubscriber::Callback, sc);在初始化方法中订阅SocketEventSubscriber消息,并注册了回调函数;
  3. Callback(const ECRef& ec, const SCRef& sc)通过ec->audit_events得到所有的订阅消息,利用ProcessEvents(emitted_row_list, ec->audit_events);处理;
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
Status SocketEventSubscriber::ProcessEvents(std::vector <Row> &emitted_row_list,const std::vector <AuditEvent> &event_list) noexcept {
emitted_row_list.clear();
emitted_row_list.reserve(event_list.size());
// 判断是否是Syscall
for (const auto &event : event_list) {
if (event.type != AuditEvent::Type::Syscall) {
continue;
}
Row row = {};
const auto &event_data = boost::get<SyscallAuditEventData>(event.data);
// 判断类别
if (event_data.syscall_number == __NR_connect) {
row["action"] = "connect";
} else if (event_data.syscall_number == __NR_bind) {
row["action"] = "bind";
} else {
continue;
}
const AuditEventRecord *syscall_event_record =
GetEventRecord(event, AUDIT_SYSCALL);
if (syscall_event_record == nullptr) {
VLOG(1) << "Malformed syscall event. The AUDIT_SYSCALL record is missing";
continue;
}
const AuditEventRecord *sockaddr_event_record =GetEventRecord(event, AUDIT_SOCKADDR);
if (sockaddr_event_record == nullptr) {
VLOG(1) << "Malformed syscall event. The AUDIT_SOCKADDR record is missing";
continue;
}

std::string saddr;
GetStringFieldFromMap(saddr, sockaddr_event_record->fields, "saddr");
if (saddr.size() < 4) {
VLOG(1) << "Invalid saddr field in AUDIT_SOCKADDR: \"" << saddr << "\"";
continue;
}

// skip operations on NETLINK_ROUTE sockets
// 跳过NETLINK_ROUTE套接字上的操作
if (saddr[0] == '1' && saddr[1] == '0') {
continue;
}

CopyFieldFromMap(row, syscall_event_record->fields, "auid");
CopyFieldFromMap(row, syscall_event_record->fields, "pid");
GetStringFieldFromMap(row["fd"], syscall_event_record->fields, "a0");

row["path"] = DecodeAuditPathValues(syscall_event_record->fields.at("exe"));
row["fd"] = syscall_event_record->fields.at("a0");
row["success"] =
(syscall_event_record->fields.at("success") == "yes") ? "1" : "0";
row["uptime"] = std::to_string(tables::getUptime());

// Set some sane defaults and then attempt to parse the sockaddr value
row["protocol"] = '0';
row["local_port"] = '0';
row["remote_port"] = '0';

bool unix_socket;
if (!parseSockAddr(saddr, row, unix_socket)) {
VLOG(1) << "Malformed syscall event. The saddr field in the "
"AUDIT_SOCKADDR record could not be parsed: \""
<< saddr << "\"";
continue;
}

//Unix domain socket,https://www.jianshu.com/p/64c27c54e552
if (unix_socket && !FLAGS_audit_allow_unix) {
continue;
}

// 写入结果
emitted_row_list.push_back(row);
}

return Status(0, "Ok");
}

以上是整个的ProcessEvents()实现,由于代码较长,我们分步讲解;

  1. 系统内核调用类别判断,if (event.type != AuditEvent::Type::Syscall) {continue;} ,如果消息类型不是Syscall则不处理;
  2. 内核调用类型判断;

    1
    2
    3
    4
    5
    6
    7
    8
    // 判断类别
    if (event_data.syscall_number == __NR_connect) {
    row["action"] = "connect";
    } else if (event_data.syscall_number == __NR_bind) {
    row["action"] = "bind";
    } else {
    continue;
    }

    __NR_connect__NR_bind过滤syscall的数据,只提取得到connectbind类型的数据;

  3. const AuditEventRecord *syscall_event_record =GetEventRecord(event, AUDIT_SYSCALL);,判断是否存在AUDIT_SYSCALL类型的数据,即之前示例数据中的type=SYSCALL msg=audit(1544195763.393:260010): arch=c000003e syscall=42 success=no exit=-115 a0=3 a1=7ffccb794910 a2=10 a3=7ffccb7941e0 items=0 ppid=53240 pid=17096 auid=1000 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=pts5 ses=1 comm="curl" exe="/usr/bin/curl" subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 key=(null)这条数据
  4. const AuditEventRecord *sockaddr_event_record =GetEventRecord(event, AUDIT_SOCKADDR);,判断是否存在AUDIT_SOCKADDR类型的数据,即之前示例数据中的type=SOCKADDR msg=audit(1544195763.393:260010): saddr=0200005073EFD21B0000000000000000的这条数据;
  5. if (saddr[0] == '1' && saddr[1] == '0') {continue;} 如果发现saddr是以10则过滤掉。官方的注释是skip operations on NETLINK_ROUTE sockets,这个也是一个十六机制,代表了通信的家族(family)。具体的family的值和名称可以看sockets.h。在ubuntu18中的/usr/include/x86_64-linux-gnu/bits/socket.h中也可以查看。显示部分的内容如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    /* Protocol families.  */
    #define PF_UNSPEC 0 /* Unspecified. */
    #define PF_LOCAL 1 /* Local to host (pipes and file-domain). */
    #define PF_UNIX PF_LOCAL /* POSIX name for PF_LOCAL. */
    #define PF_FILE PF_LOCAL /* Another non-standard name for PF_LOCAL. */
    #define PF_INET 2 /* IP protocol family. */
    #define PF_AX25 3 /* Amateur Radio AX.25. */
    #define PF_IPX 4 /* Novell Internet Protocol. */
    #define PF_APPLETALK 5 /* Appletalk DDP. */
    #define PF_NETROM 6 /* Amateur radio NetROM. */
    #define PF_BRIDGE 7 /* Multiprotocol bridge. */
    #define PF_ATMPVC 8 /* ATM PVCs. */
    #define PF_X25 9 /* Reserved for X.25 project. */
    #define PF_INET6 10 /* IP version 6. */
    #define PF_ROSE 11 /* Amateur Radio X.25 PLP. */
    #define PF_DECnet 12 /* Reserved for DECnet project. */
    #define PF_NETBEUI 13 /* Reserved for 802.2LLC project. */
    #define PF_SECURITY 14 /* Security callback pseudo AF. */
    #define PF_KEY 15 /* PF_KEY key management API. */
    #define PF_NETLINK 16
    #define PF_ROUTE PF_NETLINK /* Alias to emulate 4.4BSD. */

    因为10是hex数据,转换为十进制是16,那么表示的就是PF_NETLINKPF_ROUTE,所以官方说的skip operations on NETLINK_ROUTE sockets指的就是过滤掉PF_NETLINKPF_ROUTE这两种类型的通信。01开头的saddr表示的是PF_LOCALPF_UNIXPF_FILE类型的通信;02开头的saddr表示PF_INET即IPv4的通信日志;如果是0A则表示的是PF_INET6即IPv6的通讯地址等等。

  6. parseSockAddr(saddr, row, unix_socket)判断具体的IP类型。如果saddr02开头则表示是IPv4,以0A开头表示IPv6,以01开头则表示unix_socket.
  7. if (unix_socket && !FLAGS_audit_allow_unix) {continue;} 过滤掉unix_socket类型的日志;
  8. 解析saddr写入返回结果:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    CopyFieldFromMap(row, syscall_event_record->fields, "auid");
    CopyFieldFromMap(row, syscall_event_record->fields, "pid");
    GetStringFieldFromMap(row["fd"], syscall_event_record->fields, "a0");

    row["path"] = DecodeAuditPathValues(syscall_event_record->fields.at("exe"));
    row["fd"] = syscall_event_record->fields.at("a0");
    row["success"] =(syscall_event_record->fields.at("success") == "yes") ? "1" : "0";
    row["uptime"] = std::to_string(tables::getUptime());

    // Set some sane defaults and then attempt to parse the sockaddr value
    row["protocol"] = '0';
    row["local_port"] = '0';
    row["remote_port"] = '0';

    emitted_row_list.push_back(row);

自此整个分析过程也全部结束了,其实可以发现socket_events也是对audit.log的解析,但是由于水平有限,所以其实还是有很多的字段或者是细节没有介绍得很详尽,望见谅;process_events的分析方法也是类似,在这里就不作过多地说明了;

扩展

其实在linux内核中就已经提供了审计的功能,是由kauditd对系统中的各种行为进行审计。内核线程kauditd通过netlink机制(关于netlink的文章可以看linux netlink通信机制。简而言之就是Netlink套接字是用以实现用户进程与内核进程通信的一种特殊的进程间通信IPC ,也是网络应用程序与内核通信的最常用的接口。)将审计消息定向发送给用户控件的审计后台auditd的主线程,auditd主线程再通过事件队列将审计消息传给审计后台的写log文件线程,写入log文件。系统的auditd的整体架构如下:

当用户开启了auditd服务之后,操作系统中的所有的操作信息、应用信息都会通过内核传递给内核态的kauditd,这也就意味着内核态的kauditd会接受所有的信息。用户的规则都是设置在audit.rules中,内核态的kauditd就会接收并处理用户态auditctl命令发送的消息——审计规则。此时kauditd就会根据这些审计对内核发送过来的消息进行过滤,过滤之后将消息通过netlink的方式传递至用户态的auditd服务,用户态的auditd服务就会将从netlink收到的消息存储至audit.log中。osquery能够使用audit接受数据的原因是在于我们需要手动地关闭用户态的auditd服务,然后osquery会从内核态的kauditd接受数据并处理数据。

后来与小伙伴还在讨论一个这样的问题,不开启auditd服务,开启auditd服务但是没有规则,开启auditd服务配置有规则,三者的性能损耗的比较以及原因。性能损耗的排列非常明显:开启服务配置规则>开启服务没有规则>没有开启服务。原因是在于:如果没有开启服务,那么内核态的kauditd线程不会启动,自然没有什么性能问题;如果开启服务但是没有规则,内核态的kauditd线程启动,内核还是会将消息发送至kauditd,虽然kauditd会过滤掉所有的日志,但是其中还是会有写入缓冲区,释放缓冲区的行为,所以还是会存在性能损耗;开启服务并配置有规则,这种情况下不仅会在内核态存在性能损耗同时因为用户态的auditd会大量地写入文件,因此会存在大量地性能损耗的问题。

更多更具体的有关audit的知识可以去看审计系统在内核的实现-基本框架

总结

在分析整个osquery的源码过程,对Linux的文件结构以及Unix网络编程的相关知识都有了一定的接触,学习了很多。

以上

参考

  1. https://www.ibm.com/developerworks/cn/linux/l-lo-use-space-audit-tool/index.html
  2. http://edsionte.com/techblog/archives/4562