publisher/subscriber架构

osquery使用了auditd来获取系统调用。关于系统调用的方法优缺点后面会讲到,现在学习介绍下osquery的p/s框架,优秀的框架对我们很有借鉴意义。

auditd监控实战

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

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

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

1
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)
2
type=SOCKADDR msg=audit(1544195763.393:260010): saddr=0200005073EFD21B0000000000000000
3
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
type=PROCTITLE msg=audit(12/07/2018 10:16:03.393:260010) : proctitle=curl www.baidu.com 
2
type=SOCKADDR msg=audit(12/07/2018 10:16:03.393:260010) : saddr={ fam=inet laddr=115.239.210.27 lport=80 } 
3
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显示的是哪个用户执行的命令。

osquery解析audit

使用文档参考:https://s0osquery0readthedocs0io.icopy.site/en/latest/deployment/process-auditing/

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

1
-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语句进行相应。

进程监控

https://he1m4n6a.github.io/2020/02/22/反入侵策略总结-内核监控方式/ 上篇总结了监控系统调用的几种方法,进程监控主要监控两方面:

  1. 新建进程
  2. 进程执行命令

新建进程用的是forkvforkclone这三个函数,我们在内核中监控三个对应的系统调用即可。

  • fork fork创造的子进程是父进程的完整副本,复制了父亲进程的资源,包括内存的内容task_struct内容

  • vfork vfork创建的子进程与父进程共享数据段,而且由vfork()创建的子进程将先于父进程运行

  • clone Linux上创建线程一般使用的是pthread库 实际上linux也给我们提供了创建线程的系统调用,就是clone

注意:当监控clone调用时候,需要剔除CLONE_THREAD

执行命命监控的是execveexecveat这两个系统调用。

  • execve 执行程序
  • execveat 执行一个关于目录描述符的程序

网络监控

和进程监控类似,只不过监控的是内核调用connecbind

1
const auto &event_data = boost::get<SyscallAuditEventData>(event.data);
2
// 判断类别
3
if (event_data.syscall_number == __NR_connect) {
4
	row["action"] = "connect";
5
} else if (event_data.syscall_number == __NR_bind) {
6
	row["action"] = "bind";
7
} else {
8
	continue;
9
}

文件监控

什么是inotify

inotify是Linux中用于监控文件系统变化的一个框架,不同于前一个框架dnotify, inotify可以实现基于inode的文件监控。也就是说监控对象不再局限于目录,也包含了文件。不仅如此,在事件的通知方面,inotify摈弃了dnotify的信号方式,采用在文件系统的处理函数中放置hook函数的方式实现。

在inotify中,对于一个文件或目录的监控被称为一个watch。 给某一个文件或目录添加一个watch就表示要对该文件添加某一类型的监控。监控的类型由一个掩码Mask表示,mask有:

1
IN_ACCESS : 文件的读操作
2
3
IN_ATTRIB : 文件属性变化
4
5
IN_CLOSE_WRITE : 文件被关闭之前被写
6
7
IN_CLOSE_NOWRITE : 文件被关闭
8
9
IN_CREATE : 新建文件
10
11
IN_DELETE : 删除文件
12
13
IN_MODIFY : 修改文件
14
15
IN_MOVE_SELF : 被监控的文件或者目录被移动
16
17
IN_MOVED_FROM : 文件从被监控的目录中移出
18
19
IN_MOVED_TO : 文件从被监控的目录中移入
20
21
IN_OPEN : 文件被打开

原理

inotify通过在文件系统的操作函数(vfs_open, vfs_unlink等)中插入hook函数改变代码的执行路径,从而产生相应的事件。以下是一个hook函数的列表: img

使用

Inotify 提供 3 个系统调用,它们可以构建各种各样的文件系统监控器:

  • inotify_init() 在内核中创建 inotify 子系统的一个实例,成功的话将返回一个文件描述符,失败则返回 -1。就像其他系统调用一样,如果 inotify_init() 失败,请检查 errno 以获得诊断信息。
  • 顾名思义,inotify_add_watch() 用于添加监视器。每个监视器必须提供一个路径名和相关事件的列表(每个事件由一个常量指定,比如 IN_MODIFY)。要监控多个事件,只需在事件之间使用逻辑操作符 — C 语言中的管道线(|)操作符。如果 inotify_add_watch() 成功,该调用会为已注册的监视器返回一个惟一的标识符;否则,返回 -1。使用这个标识符更改或删除相关的监视器。
  • inotify_rm_watch() 删除一个监视器。

监控创建、删除和修改事件的目录代码:

1
#include <stdio.h>
2
#include <stdlib.h>
3
#include <errno.h>
4
#include <sys/types.h>
5
#include <sys/inotify.h>
6
 
7
#define EVENT_SIZE  ( sizeof (struct inotify_event) )
8
#define BUF_LEN     ( 1024 * ( EVENT_SIZE + 16 ) )
9
 
10
int main( int argc, char **argv ) 
11
{
12
  int length, i = 0;
13
  int fd;
14
  int wd;
15
  char buffer[BUF_LEN];
16
 
17
  fd = inotify_init();
18
 
19
  if ( fd < 0 ) {
20
    perror( "inotify_init" );
21
  }
22
 
23
  wd = inotify_add_watch( fd, "/home/strike", 
24
                         IN_MODIFY | IN_CREATE | IN_DELETE );
25
  length = read( fd, buffer, BUF_LEN );  
26
 
27
  if ( length < 0 ) {
28
    perror( "read" );
29
  }  
30
 
31
  while ( i < length ) {
32
    struct inotify_event *event = ( struct inotify_event * ) &buffer[ i ];
33
    if ( event->len ) {
34
      if ( event->mask & IN_CREATE ) {
35
        if ( event->mask & IN_ISDIR ) {
36
          printf( "The directory %s was created.\n", event->name );       
37
        }
38
        else {
39
          printf( "The file %s was created.\n", event->name );
40
        }
41
      }
42
      else if ( event->mask & IN_DELETE ) {
43
        if ( event->mask & IN_ISDIR ) {
44
          printf( "The directory %s was deleted.\n", event->name );       
45
        }
46
        else {
47
          printf( "The file %s was deleted.\n", event->name );
48
        }
49
      }
50
      else if ( event->mask & IN_MODIFY ) {
51
        if ( event->mask & IN_ISDIR ) {
52
          printf( "The directory %s was modified.\n", event->name );
53
        }
54
        else {
55
          printf( "The file %s was modified.\n", event->name );
56
        }
57
      }
58
    }
59
    i += EVENT_SIZE + event->len;
60
  }
61
 
62
  ( void ) inotify_rm_watch( fd, wd );
63
  ( void ) close( fd );
64
 
65
  exit( 0 );
66
}
  1. 这个应用程序通过 fd = inotify_init();
  2. 创建一个 inotify 实例,并添加一个监视器来监控修改、新建、删除的文件(由 wd = inotify_add_watch(...) 指定)。
  3. read() 方法在一个或多个警告到达之前是被阻塞的。警告的详细内容 — 每个文件、每个事件 — 是以字节流的形式发送的;因此,应用程序中的循环将字节流转换成一系列事件结构。

在文件 /usr/include/sys/inotify.h. 中,您可以找到事件结构的定义,它是一种 C 结构:

1
struct inotify_event 
2
{
3
  int wd;       /* The watch descriptor */
4
  uint32_t mask;    /* Watch mask */
5
  uint32_t cookie;  /* A cookie to tie two events together */
6
  uint32_t len;     /* The length of the filename found in the name field */
7
  char name __flexarr;  /* The name of the file, padding to the end with NULs */    
8
}

总结

进程、网络、文件的监控,都可以用上一章节hook系统调用的方式进行监控,但对于文件监控来说,因为linux自带了inotify的监控审计框架,我们可以直接通过用户态编程使用inotify的框架。对于进程和网络监控主要是对关键函数的hook,以及hook框架的选型。

参考:

https://www.sohu.com/a/244164762_467784

https://www.ibm.com/developerworks/cn/linux/l-ubuntu-inotify/