osquery插件机制简介

说明

osquery本身将系统的很多信息都封装成为了表,目前osuqery所有的表在官方文档schema都进行了详细地说明,这些表的定义在源代码specs中也可以找到。osquery考虑到可能用户使用osquery对系统信息收集有特殊的需求,osquery提供了扩展的功能,供用户完成自定义的表的功能。

osquery对于插件的编写也写了一个文档,Extensions。需要注意的是插件和osquery是通过UNIX domain socket进行通信,这也是下面讲到使用C++编写的插件和使用Go编写插件的区别。

插件说明

osquery的插件采用Thrift API与osqueryi或者osqueryd通信,一般情况下插件都是采用C++编写,当然也可以采用PythonGo或者是任何其他支持Thrift的语言。因为C++是Osquery开发的语言,所以更易与Osquery想结合。

如果采用C++编写插件,需要引入<osquery/sdk.h>。在这个SDK中,已经实现了osquery需要的库,包括boost, thrift, glog, gflags, rocksdb.以下就是一个采用C++编写的简单的插件的例子。

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
// Note 1: Include the sdk.h helper.
#include <osquery/sdk.h>

using namespace osquery;

// Note 2: Define at least one plugin or table.
class ExampleTablePlugin : public TablePlugin {
private:
TableColumns columns() const override {
return {
std::make_tuple("example_text", TEXT_TYPE, ColumnOptions::DEFAULT),
std::make_tuple("example_integer", INTEGER_TYPE, ColumnOptions::DEFAULT),
};
}

QueryData generate(QueryContext& request) override {
QueryData results;
Row r;

r["example_text"] = "example";
r["example_integer"] = INTEGER(1);
results.push_back(r);
return results;
}
};

// Note 3: Use REGISTER_EXTERNAL to define your plugin or table.
REGISTER_EXTERNAL(ExampleTablePlugin, "table", "example");

int main(int argc, char* argv[]) {
// Note 4: Start logging, threads, etc.
osquery::Initializer runner(argc, argv, ToolType::EXTENSION);

// Note 5: Connect to osqueryi or osqueryd.
auto status = startExtension("example", "0.0.1");
if (!status.ok()) {
LOG(ERROR) << status.getMessage();
runner.requestShutdown(status.getCode());
}

// Finally, shutdown.
runner.waitForShutdown();
return 0;
}

按照代码中的注释,我们分析下这些代码的含义。

导入SDK

1
#include <osquery/sdk.h>

定义表

因为一个扩展可能就是一张表,那么在这个扩展中,我们至少需要定义一张表。此表需要继承TablePlugin这个表。分别是columns()generate(QueryContext& request)方法。

  1. columns()是DDL
  2. generate(QueryContext& request)是用于生成数据。

注册表

将第2步中的表注册到此扩展中。

1
REGISTER_EXTERNAL(ExampleTablePlugin, "table", "example");

扩展入口

扩展的入口是main()函数。

  1. 初始化扩展,osquery::Initializer runner(argc, argv, ToolType::EXTENSION);
  2. osqueryi或者是osqueryd进行通信,auto status = startExtension("example", "0.0.1");

其实可以发现扩展的表结构的定义与osquery自带的表结构的定义完全是一样的,参见表结构.

Go插件

本文的重点是在于如何使用Go语言编写一个插件。在osquery-go有一个简单的例子,代码如下:
my_table_plugin.go

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
package main

import (
"context"
"log"
"os"

"github.com/kolide/osquery-go"
"github.com/kolide/osquery-go/plugin/table"
)

func main() {
if len(os.Args) != 2 {
log.Fatalf(`Usage: %s SOCKET_PATH`, os.Args[0])
}

server, err := osquery.NewExtensionManagerServer("foobar", os.Args[1])
if err != nil {
log.Fatalf("Error creating extension: %s\n", err)
}

// Create and register a new table plugin with the server.
// table.NewPlugin requires the table plugin name,
// a slice of Columns and a Generate function.
server.RegisterPlugin(table.NewPlugin("foobar", FoobarColumns(), FoobarGenerate))
if err := server.Run(); err != nil {
log.Fatalln(err)
}
}

// FoobarColumns returns the columns that our table will return.
func FoobarColumns() []table.ColumnDefinition {
return []table.ColumnDefinition{
table.TextColumn("foo"),
table.TextColumn("baz"),
}
}

// FoobarGenerate will be called whenever the table is queried. It should return
// a full table scan.
func FoobarGenerate(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error) {
return []map[string]string{
{
"foo": "bar",
"baz": "baz",
},
{
"foo": "bar",
"baz": "baz",
},
}, nil
}

可以看到使用Go的用法大致和C++的用法类似,但是还是存在不同。在C++中,使用osquery::Initializer runner(argc, argv, ToolType::EXTENSION);初始化插件。在GO中使用server, err := osquery.NewExtensionManagerServer("foobar", os.Args[1])初始化插件,但是Go语言编写的需要额外传入一个参数。文章后面通过select value from osquery_flags where name = 'extensions_socket';这个语句得到UNIX domain socket。但是我们通过osqueryd来加载这个插件时,我们是不会在控制台输入的,那么这就意味着我们需要在Go代码固化UNIX domain socket这个值。如此,我们可以手动地指定UNIX domain socket,以此解决手需要手动输入的问题。我们将如下:

1
2
3
4
5
6
7
8
if len(os.Args) != 2 {
log.Fatalf(`Usage: %s SOCKET_PATH`, os.Args[0])
}

server, err := osquery.NewExtensionManagerServer("foobar", os.Args[1])
if err != nil {
log.Fatalf("Error creating extension: %s\n", err)
}

修改为:

1
2
3
4
server, err := osquery.NewExtensionManagerServer("foobar", "/var/osquery/osquery.em")
if err != nil {
log.Fatalf("Error creating extension: %s\n", err)
}

与C++编写的插件不同,在这里注册表时,需要自己手动传入表定义的相关方法,即FoobarColumns()FoobarGenerate.上述的代码的逻辑也非常的清晰,声明了一个名为foobar的表,列名分别是foobaz,并向表中插入了两条记录,都是"baz": "baz".

编译Go插件

1
go build -o my_table_plugin.ext my_table_plugin.go

编译得到test.ext

运行Go插件

以普通用户尝试通过如下的方式加载插件

1
osqueryi --extension /path/to/test.ext

会出现Error creating extension: waiting for unix socket to be available: /var/osquery/osquery.em: context deadline exceeded的问题。
出现这个的原因是在于osqueryi无法找到/var/osquery/osquery.em。出现这个的原因是在于在之前的插件初始化中使用了osquery.NewExtensionManagerServer("foobar", "/var/osquery/osquery.em")来初始化,这个错误的意思就是无法找到/var/osquery/osquery.em。此时我们需要手动指定osquery.em的值。运行如下

1
osqueryi --extensions_socket=/var/osquery/osquery.em  --extension /path/to/test.ext

结果又出现了init.cpp:654] Cannot start extension manager: Cannot create extension socket: /var/osquery/osquery.em的错误。出现这个问题的原因是权限的问题。我们以root权限运行即可。
当我们以root权限运行时,又出现了如下的错误:

1
Extension binary has unsafe permissions: /path/to/test.ext

这个错误的提示也比较地明确,osquery提示我们运行此插件是不安全的,我们加上--allow_unsafe=true表示允许运行安全的插件。那么最终命令的命令就变为了:

1
osqueryi --allow_unsafe=true --extensions_socket=/var/osquery/osquery.em  --extension /path/to/test.ext

运行之后,就出现了I0317 10:39:16.987525 15839 interface.cpp:105] Registering extension (foobar, 62760, version=, sdk=)。这个就表示插件加载成功,表名是foobar

1
2
3
4
5
6
7
osquery> select * from foobar;
+-----+-----+
| foo | baz |
+-----+-----+
| bar | baz |
| bar | baz |
+-----+-----+

成功地显示出我们在代码中插入的两条记录。

注意

看起来利用osquery的扩展能够帮助我们完成很多的功能,但是这其中还是有很多需要注意的地方。

osquery插件加载

限于osquery自身插件的设计原理,导致osquery在加载大量的插件时会出现严重的性能问题。比较好的做法是在一个插件中一次性尽可能地多加载一些插件。

osquery插件的权限问题

根据前面的分析可知,osquery作为一个HIDS的agent,在运行时需要以root权限运行,同时如果需要加载插件也必须以root权限运行,这样就会存在潜在的风险。如果插件一直一root权限运行同时还需要读取系统中的文件信息,如果攻击者篡改了此文件的内容,就有可能通过osquery的插件获取root权限,这样就会危及到主机的安全。所以编写插件非常重要的一点就是当插件初始化成功之后,里面要将其从root权限降级为nobody用户。

当然更多的插件的问题需要自己在实际编写过程中才能够进一步地发现,而本文也仅仅是对osquery的插件编写进行了一个简单的介绍。

有关更多osquery的插件,可以去看这个repo,Trail of Bits osquery Extensions。这个上面有针对不同平台不同功能的插件,如果有兴趣可以去看看

总结

osquery的插件机制能够很方便地为我们提供一个扩展osquery自身功能的方法来完成我们自定义并且osquery目前还没有的功能。一个良好的扩展机制也是作为一个HIDS的agent的必备的特性。当然osquery还有一些其他的特性等待我们进一步挖掘。