说明
前面分析了process_open_socket
和shell_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.rules
和auditctl
设置规则的区别是在于audit.rules
设置的规则是一直有效的,只要系统系统auditd
服务就会生效。而auditctl
设置的规则是位于内存当中,只在当前环境有效,如果重启电脑之后或者是重启auditd
服务之后,那么通过auditctl
设置的规则就会失效。默认的是将日志保存在 audit.log 文件中,默认路径/var/log/audit/audit.log
。
更多详细地介绍可以去看Linux 用户空间审计工具 audit
关于auditd的规则的设定方法,可以参考A Brief Introduction to auditd和Linux 用户空间审计工具 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
3type=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 id
是28850
,直接使用ausearch --interpret -a 260010
解析。1
2
3type=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
24SYNOPSIS top
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
3socket(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
24table_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 | REGISTER(SocketEventSubscriber, "event_subscriber", "socket_events"); |
REGISTER(SocketEventSubscriber, "event_subscriber", "socket_events");
对socket_events
注册了SocketEventSubscriber
这样的一个消息订阅机制;subscribe(&SocketEventSubscriber::Callback, sc);
在初始化方法中订阅SocketEventSubscriber
消息,并注册了回调函数;Callback(const ECRef& ec, const SCRef& sc)
通过ec->audit_events
得到所有的订阅消息,利用ProcessEvents(emitted_row_list, ec->audit_events);
处理;
1 | Status SocketEventSubscriber::ProcessEvents(std::vector <Row> &emitted_row_list,const std::vector <AuditEvent> &event_list) noexcept { |
以上是整个的ProcessEvents()
实现,由于代码较长,我们分步讲解;
- 系统内核调用类别判断,
if (event.type != AuditEvent::Type::Syscall) {continue;}
,如果消息类型不是Syscall
则不处理; 内核调用类型判断;
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的数据,只提取得到connect
和bind
类型的数据;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)
这条数据const AuditEventRecord *sockaddr_event_record =GetEventRecord(event, AUDIT_SOCKADDR);
,判断是否存在AUDIT_SOCKADDR
类型的数据,即之前示例数据中的type=SOCKADDR msg=audit(1544195763.393:260010): saddr=0200005073EFD21B0000000000000000
的这条数据;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. */
因为
10
是hex数据,转换为十进制是16
,那么表示的就是PF_NETLINK
和PF_ROUTE
,所以官方说的skip operations on NETLINK_ROUTE sockets
指的就是过滤掉PF_NETLINK
和PF_ROUTE
这两种类型的通信。01
开头的saddr表示的是PF_LOCAL
即PF_UNIX
和PF_FILE
类型的通信;02
开头的saddr表示PF_INET
即IPv4的通信日志;如果是0A
则表示的是PF_INET6
即IPv6的通讯地址等等。parseSockAddr(saddr, row, unix_socket)
判断具体的IP类型。如果saddr
以02
开头则表示是IPv4,以0A
开头表示IPv6,以01
开头则表示unix_socket
.if (unix_socket && !FLAGS_audit_allow_unix) {continue;}
过滤掉unix_socket
类型的日志;- 解析saddr写入返回结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15CopyFieldFromMap(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网络编程的相关知识都有了一定的接触,学习了很多。
以上