osquery动态调试和重打包

说明

前面的几篇文章都是静态分析osquery的源代码,网上鲜有网站讲如何调试osquery的源代码。为了更加方便地理解osquery的整个运行机制,本篇文章对osquery的动态调试的环境搭建以及osquery的基本架构进行一个简要的说明。

调试环境搭建

本文是利用ubuntu18配合clion进行调试的。除此之外,还需要安装gitcmakeruby,python2以及pip(latest)

下载源代码

git clone https://github.com/facebook/osquery.git,注意此时的分支是experimental分支(此分支不适用于cmake的方式构建和调试),我们首先要切换至master分支。git checkout master

安装依赖

make sysprep运行这个将会从aws上面下载编译/运行所需要的所有的依赖。由于依赖较多,所以这将是一个漫长的过程。make sysprep将会做

  1. 更新系统
  2. 安装需要的依赖
  3. 初始化CMake项目引用的第三方git子模块
  4. 安装osquery的toolchain
  5. 使用Linuxbrew安装需要在/usr/local/osquery中需要存在的lib

设置编译选项

首先需要在CMakeLists.txt的最前面加上set(CMAKE_BUILD_TYPE DEBUG),表示开启调试。

然后由于我们使用clion打开osquery的源代码。由于osquery采用的是clang调试。所以在File/Settings/Build,Execution,Deployment/CMake中的Environment中进行如下的设置:

  • LDFLAGS,-L/usr/local/osquery/legacy/lib -L/usr/local/osquery/lib -B/usr/local/osquery/legacy/lib -rtlib=compiler-rt -fuse-ld=lld
  • CXX,/usr/local/osquery/bin/clang++
  • CC,/usr/local/osquery/bin/clang
  • PATH,/usr/local/osquery/bin:<YOUR_ORIGINAL_PATH>

分析CMakeLists.txt中的250行左右的设置,我们发现

1
2
3
4
5
# make debug (environment variable from Makefile)
if(DEFINED ENV{DEBUG})
set(CMAKE_BUILD_TYPE "DEBUG")
WARNING_LOG("Setting DEBUG build")
....

其中DEFINED ENV{DEBUG}表示需要通过环境变量设置DEBUG才能够使之生效,所以我们还要在Environment中加上DEBUG:whatever。最终就变为了:

创建文件夹

由于cmake在编译时,需要向cmake-build-debug/generated中写入内容。但是cmake并不会自动地创建generated这个文件夹,所以我们需要在cmake-build-debug文件夹中创建generated文件夹。

设置调试模式

之前的文章中也说到,osquery存在osqueryiosqueryd两种模式。为了方便分析调试osquery的功能,我们采用osqueryi的模式进行调试。我们编辑Run/Debug Configuration,选择daemon,在Program arguments中添加-S的参数。如下所示:

运行测试

Debug 'daemon',如果在Messages中出现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[100%] Building CXX object osquery/CMakeFiles/libosquery.dir/numeric_monitoring/numeric_monitoring.cpp.o
[100%] Building CXX object osquery/CMakeFiles/libosquery.dir/numeric_monitoring/plugin_interface.cpp.o
[100%] Building CXX object osquery/CMakeFiles/libosquery.dir/numeric_monitoring/pre_aggregation_cache.cpp.o
[100%] Building CXX object osquery/CMakeFiles/libosquery.dir/profiler/posix/profiler.cpp.o
[100%] Linking CXX static library libosquery.a
[100%] Built target libosquery
[100%] Built target libosquery_additional
[100%] Building CXX object osquery/CMakeFiles/daemon.dir/devtools/shell.cpp.o
[100%] Building CXX object osquery/CMakeFiles/daemon.dir/main/main.cpp.o
[100%] Building CXX object osquery/CMakeFiles/daemon.dir/main/posix/main.cpp.o
[100%] Linking CXX executable osqueryd
-- Building osqueryd: /home/develope/osquery/cmake-build-debug/osquery/osqueryd
[100%] Built target daemon

Build finished

Debug中出现

1
2
3
/path/to/cmake-build-debug/osquery/osqueryd -S
Using a virtual database. Need help, type '.help'
osquery>

至此,环境就搭建成功了。

启动分析

首先程序会进入osquery/main/posix/main.cpp,

1
2
3
4
5
int main(int argc, char* argv[]) {
// On POSIX systems we can jump immediately into startOsquery.
// A short abstraction exists to allow execute-as-service checks in Windows.
return osquery::startOsquery(argc, argv);
}

跟踪startOsquery,进入到osquery/main/main.cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int startOsquery(int argc, char* argv[], std::function<void()> shutdown) {
// Parse/apply flags, start registry, load logger/config plugins.
osquery::Initializer runner(argc, argv, osquery::ToolType::SHELL_DAEMON);
......
runner.installShutdown(shutdown);
runner.initDaemon();

// When a watchdog is used, the current daemon will fork/exec into a worker.
// In either case the watcher may start optionally loaded extensions.
runner.initWorkerWatcher(kWatcherWorkerName);

if (runner.isDaemon()) {
return startDaemon(runner);
}
return startShell(runner, argc, argv);
}

startOsquery()中,发现是根据runner.isDaemon()的结果来判断是运行osqueryd还是osqueryi。发现是利用osquery::Initializer runner(argc, argv, osquery::ToolType::SHELL_DAEMON);初始化了runner

跟踪runner(),进入到osquery/core/init.cpp:

发现会根据osquery的启动路径判断采用什么模式。如果文件路径中存在osqueryd,则采用osqueryd模式,设置kToolType = ToolType::DAEMON;binary_ = "osqueryd";否则设置kToolType = ToolType::SHELL;binary_ = "osqueryi";

在后面还存在对参数的检测,如下:

其中*argv_保存的就是参数值。通过auto help = std::string((*argv_)[i]);得到参数类型。如果发现是-S,设置kToolType = ToolType::SHELL;binary_ = "osqueryi"否则设置kToolType = ToolType::DAEMON;binary_ = "osqueryd"

回到osquery/main/main.cpp中:

1
2
3
4
5
6
7
8
9
10
11
12
13
static bool isDaemon() {
return kToolType == ToolType::DAEMON;
}

/// Check the program is the osquery shell.
static bool isShell() {
return kToolType == ToolType::SHELL;
}

if (runner.isDaemon()) {
return startDaemon(runner);
}
return startShell(runner, argc, argv);

这也就是为什么我们在前面设置调试模式中需要通过-S来开启osqueryi的调试方法的原因了。

最后就会在Debug中出现如下的界面:

1
2
3
4
5
6
7
8
9
10
11
/path/to/cmake-build-debug/osquery/osqueryd -S
Thrift: Thu Jan 3 23:33:51 2019 TSocket::open() connect() <Host: Port: 0>Connection refused
I0104 13:07:53.836697 111926 database.cpp:563] Checking database version for migration
I0104 13:07:53.885915 111926 database.cpp:587] Performing migration: 0 -> 1
I0104 13:07:53.925979 111926 database.cpp:619] Migration 0 -> 1 successfully completed!
I0104 13:07:53.926038 111926 database.cpp:587] Performing migration: 1 -> 2
I0104 13:07:53.926084 111926 database.cpp:619] Migration 1 -> 2 successfully completed!
I0104 13:07:53.999048 111926 dispatcher.cpp:81] Adding new service: ExtensionWatcher (0x3288a18) to thread: 140001576392448 (0x3270540) in process 111926
I0104 13:07:54.022429 111926 dispatcher.cpp:81] Adding new service: ExtensionRunnerCore (0x3241f38) to thread: 140001567999744 (0x32711a0) in process 111926
Using a virtual database. Need help, type '.help'
osquery>

动态调试

以一个简单的SQL语句select * from hosts;来说明如何调试。
当我们在osquery>中输入select * from hosts;按下回车之后,上面的启动流程又会重新走一遍。整个调用执行过程如下:

当执行到osquery/devtools/shell.cpp中的process_input(struct callback_data* p, FILE* in)函数中的rc = shell_exec(zSql, shell_callback, p, &zErrMsg);其中zSql就是我们在shell中输入的SQL语句。如下

之后会进入到shell.cpp::shell_exec()中的rc = sqlite3_step(pStmt);来调用。之后就会进入到sqlite3.c中使用sqlite来执行所输入的查询语句。

但是在前面中已经说到了,osquery的shell查询语法采用的是sqlite,但是底层的数据存储采用的是rocksdb。所以这个中间就会存在一个转换的过程。所以当third-party/sqlite3/sqlite3.c在中执行rc = pModule->xFilter(pVCur, iQuery, pOp->p4.z, nArg, apArg);,就会调用osquery/sql/virtual_table.cpp::xFilter(...)函数。

xFilter(..)函数中的关键代码是pCur->data = table->generate(context);通过context去寻找到对应的表,同时生成对应的数据。

通过上图可以发现,在context中保存了我们查询所有的信息,包括表名、列名等信息(具体信息,需要进一步地深入研究,但是本人的水平较菜,信息较多,不知如何查看)。

之后程序根据context的信息会进入到cmake-build-debug/generated/additional_amalgamation.cpp。其中存在etcHostsTablePlugin类,调用其中的generate(QueryContext& context)方法。如下:

最终程序会进入到osquery/tables/networking/etc_hosts.cpp。在此文件中存在两个函数genEtcHosts()parseEtcHostsContent()。其中genEtcHosts()是入口函数,而parseEtcHostsContent()则是具体的功能实现函数,用于读取不同平台下的存储Linux下的文件。例如在Linux下读取的是/etc/hosts文件。在windows平台下是读取system32\\drivers\\etc\\hostssystem32\\drivers\\etc\\hosts.ics文件。

函数实现比较简单,就不作说明了。最终将查询的数据层层向上返回,这样sqlite就查询到了数据,输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
osquery> select * from hosts;
select * from hosts;select * from hosts;
+-----------+----------------------------+
| address | hostnames |
+-----------+----------------------------+
| 127.0.0.1 | localhost |
| 127.0.1.1 | ubuntu |
| ::1 | ip6-localhost ip6-loopback |
| fe00::0 | ip6-localnet |
| ff00::0 | ip6-mcastprefix |
| ff02::1 | ip6-allnodes |
| ff02::2 | ip6-allrouters |
+-----------+----------------------------+

一个简单的动态分析过程就结束了。整个过程中,比较复杂的地方是在于sqliteVirtualTable的交互。这一点由于个人能力有限,也无法抽丝剥茧,分析得很透彻。

打包

之前的文章都是分析源代码,我们可以修改源代码,然后配合本篇的文章重新编译运行得到一个新的可运行的osquery进程。但是很多时候我们会有这样的需求,我们通过修改osquery完成一些我们定制化的需求,然后安装在其他的主机上面。此时我们就需要将我们修改之后的osquery打包成为一个rpm/deb/tar.gz的包以供其他的主机安装。

package

osquery中的文档说明直接使用make package即可进行打包。但是可能会出现以下的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[develope@localhost osquery]$ make package
which: no ctags in (/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin:/home/develope/.local/bin:/home/develope/bin)
which: no cscope in (/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin:/home/develope/.local/bin:/home/develope/bin)
# Alias for packages (do not use CPack)
-- Welcome to osquery's build -- thank you for your patience! :)
-- For a brief tutorial see: http://osquery.readthedocs.io/en/stable/development/building/
-- If at first you dont succeed, perhaps: make distclean; make depsclean
-- Building for platform CentOS (centos, centos7)
-- Building osquery version 3.3.1-1-g5188ce5 sdk 3.3.1
-- Cannot find fpm executable in path
-- Configuring done
-- Generating done
-- Build files have been written to: /home/develope/osquery/build/centos7
Building linux packages (no custom config)

最为关键的错误信息是:Cannot find fpm executable in path表示缺少fpm包;根据https://www.digitalocean.com/community/tutorials/how-to-use-fpm-to-easily-create-packages-in-multiple-formats这篇文章的介绍,fpm是一个打包工具,可以将代码打包成为多个平台的安装包;

fpm

根据文档installing-things-fpm-needs进行安装

1
2
sudo yum install ruby-devel gcc make rpm-build rubygems
gem install --no-ri --no-rdoc fpm

安装成功之后,使用fpm –version查看版本;

package

再次使用make package打包。打包成功之后就会在home/develope/osquery/build/centos7生成以下文件

1
2
3
4
5
6
-rw-rw-r--.  1 develope develope  7450048 Nov 20 07:28 osquery-3.3.1-1-g5188ce5-1.arch-x86_64.pkg.tar.xz
-rw-rw-r--. 1 develope develope 10359040 Nov 20 07:27 osquery_3.3.1-1-g5188ce5_1.linux.amd64.deb
-rw-rw-r--. 1 develope develope 10296480 Nov 20 07:28 osquery-3.3.1_1_g5188ce5-1.linux.x86_64.rpm
-rw-rw-r--. 1 develope develope 10350920 Nov 20 07:28 osquery-3.3.1-1-g5188ce5_1.linux_x86_64.tar.gz
-rw-rw-r--. 1 develope develope 41435800 Nov 20 07:27 osquery-dbg_3.3.1-1-g5188ce5_1.linux.amd64.deb
-rw-rw-r--. 1 develope develope 43372360 Nov 20 07:28 osquery-debuginfo-3.3.1_1_g5188ce5-1.linux.x86_64.rpm

rpm包和deb包均有两个版本(debug版本和正式版本)

总结

前面的文章都是静态分析,本篇文章是从动态分析的角度来看osquery的运行过程,从而能够对osquery的整个运行机制,整个执行流程能够有更加清晰的认识。最后还要感觉组内大佬们无私的帮助以及alessandrogar的耐心解答。

拥有快速学习能⼒的⽩帽子,是不能有短板的。有的只是⼤量的标准板和⼏块长板。

以上

参考

  1. https://alessandrogar.io/post/osquery-development-with-qtcreator-and-clion/