文章目录
- 前言
- 1、概述
- 1.1、整体架构
- 1.2、trinity-main
- 1.3、childx
- 2、安装与使用
- 2.1、源码安装
- 2.1.1 部署系统依赖组件
- 2.1.2 使用源码安装系统
- 2.2、使用方法
- 3、测试用例
- 3.1、Splice系统调用压力测试
- 3.2、其它系统调用压力测试
- 3.3、自定义系统调用压力测试
- 4、总结
- 4.1、部署架构
- 4.2、漏洞检测对象
- 4.3、漏洞检测方法
- 4.4、种子生成/变异技术
- 5、参考文献
- 总结
前言
本博客的主要内容为Trinity的部署、使用与原理分析。本博文内容较长,因为涵盖了Trinity的几乎全部内容,从部署的详细过程到如何使用Trinity对操作系统的系统调用进行Fuzz测试,以及对Trinity进行漏洞检测的原理分析,相信认真读完本博文,各位读者一定会对Trinity有更深的了解。以下就是本篇博客的全部内容了。
1、概述
系统调用Fuzz测试并不是一个特别新的概念。早在1991年,人们就编写了一些应用程序,用垃圾数据轰炸系统调用输入,这些应用程序在使各种操作系统崩溃方面取得了各种程度的成功。
然而,在修复了明显的愚蠢错误之后,大多数情况下,这些调用将在其函数入口点附近的内核非常早期被拒绝,因为其函数入口点附近的内核进行了基本的参数验证。
Trinity是一个系统调用Fuzz测试工具,它采用一些技术向被调用的系统调用传递半智能参数。其智能特性包括:
- 如果系统调用期望某种数据类型作为参数(例如文件描述符,即fd),则会传递一个。这也是初始启动速度缓慢的原因,因为它生成可以从/sys、/proc和/dev读取文件的fd列表,然后补充这些文件的fd以供各种网络协议套接字使用。(在第一次运行时缓存了有关成功/失败的协议的信息,大大提高了后续运行的速度)。
- 如果系统调用只接受某些值作为参数(例如“flags”字段),则Trinity具有可以传递的所有有效标志的列表。为了使事情更有趣,偶尔它会对其中一个标志进行位翻转。
- 如果系统调用只接受一定范围的值,则传递的随机值通常会偏向于适应该范围。
Trinity将其输出记录到文件中(每个子进程一个文件),并在实际进行系统调用之前对文件进行fsync。这样,如果Trinity触发了导致内核恐慌的情况,我们应该能够通过检查日志准确了解到发生了什么情况。此外,Trinity工具基于C语言开发。
1.1、整体架构
关于Trinity的整体架构如下图所示,此图出自LCA: The Trinity fuzz tester,不过由于此篇文章年份较为久远,其中trinity-watchdog的功能已经被整合到trinity-main中了,不过Trinity的核心还是trinity-main与其各个child,所以trinity-watchdog就显得不是那么重要了,不过下面我们还是会对其进行详细分析。
对于以上架构,其中:
- trinity-main:执行各种初始化操作,然后创建执行系统调用Fuzz的子程序。trinity-main创建的共享内存区域用于记录各种全局信息(打开文件描述符号、执行的系统调用总数以及成功和失败的系统调用数等等)和每个子进程的各种信息(pid和执行的系统调用信息等等)。
- trinity-watchdog:确保系统正常工作。它会检查子进程是否正在运行(可能会在系统调用中被暂停),如果没有运行,则会将其杀死。当主进程检测到其中一个子进程已终止时(因为trinity-watchdog将其终止或出于其它原因)会启动一个新的子进程来替换它。trinity-watchdog还监视共享内存区域的完整性。
- childx:对系统调用进行Fuzz的各个子程序
1.2、trinity-main
trinity-main是Trinity工具运行的核心,而trinity-main运行是从trinity.c开始的,所以我们要着重分析trinity.c的主函数,看看它都做了什么事情。
int main(int argc, char* argv[])
{
int ret = EXIT_SUCCESS;
const char taskname[13]="trinity-main";
outputstd("Trinity " VERSION " Dave Jones <davej@codemonkey.org.uk>\n");
progname = argv[0];
mainpid = getpid();
getrlimit(RLIMIT_NOFILE, &max_files_rlimit);
page_size = getpagesize();
num_online_cpus = sysconf(_SC_NPROCESSORS_ONLN);
max_children = num_online_cpus * 4; /* possibly overridden in params. */
select_syscall_tables();
create_shm();
parse_args(argc, argv);
init_uids();
change_tmp_dir();
init_shm();
init_taint_checking();
if (munge_tables() == FALSE) {
ret = EXIT_FAILURE;
goto out;
}
if (show_syscall_list == TRUE) {
dump_syscall_tables();
goto out;
}
if (show_ioctl_list == TRUE) {
dump_ioctls();
goto out;
}
if (show_unannotated == TRUE) {
show_unannotated_args();
goto out;
}
init_syscalls();
do_uid0_check();
if (do_specific_domain == TRUE)
find_specific_domain(specific_domain_optarg);
pids_init();
init_logging();
init_object_lists(OBJ_GLOBAL);
setup_initial_mappings();
parse_devices();
/* FIXME: Some better object construction method needed. */
create_futexes();
create_sysv_shms();
setup_main_signals();
no_bind_to_cpu = RAND_BOOL();
prctl(PR_SET_NAME, (unsigned long) &taskname);
if (open_fds() == FALSE) {
if (shm->exit_reason != STILL_RUNNING)
panic(EXIT_FD_INIT_FAILURE); // FIXME: Later, push this down to multiple EXIT's.
_exit(EXIT_FAILURE);
}
setup_ftrace();
main_loop();
destroy_global_objects();
if (is_tainted() == TRUE)
stop_ftrace();
output(0, "Ran %ld syscalls. Successes: %ld Failures: %ld\n",
shm->stats.op_count, shm->stats.successes, shm->stats.failures);
if (show_stats == TRUE)
dump_stats();
shutdown_logging();
ret = set_exit_code(shm->exit_reason);
out:
exit(ret);
}
这段代码由C语言实现。该程序的主要功能是执行Trinity模糊测试工具,用于模拟系统调用以测试系统的稳定性和安全性。下面是主函数的主要步骤和功能:
- 输出Trinity的版本信息和作者信息。
- 初始化一些参数和数据结构,包括获取系统的文件描述符限制(RLIMIT_NOFILE)、获取页大小、获取在线CPU数量等。
- 选择要使用的系统调用表(select_syscall_tables)。
- 创建共享内存(create_shm)。
- 解析命令行参数(parse_args)。
- 初始化用户标识(init_uids)。
- 更改临时目录(change_tmp_dir)。
- 初始化共享内存(init_shm)。
- 初始化污点检查(init_taint_checking)。
- 如果需要,修改系统调用表(munge_tables)。
- 根据命令行参数,显示系统调用列表、IO控制列表、未注释参数列表等。
- 初始化系统调用(init_syscalls)。
- 检查是否以root用户身份运行程序(do_uid0_check)。
- 如果需要,查找特定的域(find_specific_domain)。
- 初始化进程标识符(pids_init)。
- 初始化日志记录(init_logging)。
- 初始化对象列表(init_object_lists)。
- 设置初始映射(setup_initial_mappings)。
- 解析设备(parse_devices)。
- 创建互斥锁(create_futexes)和System V共享内存(create_sysv_shms)。
- 设置主信号处理函数(setup_main_signals)。
- 设置线程名称(prctl)。
- 打开文件描述符(open_fds)。
- 设置Ftrace跟踪(setup_ftrace)。
- 进入主循环(main_loop)。
- 销毁全局对象(destroy_global_objects)。
- 如果系统被污染,停止Ftrace跟踪(stop_ftrace)。
- 输出模拟的系统调用统计信息(output)。
- 如果需要,显示更详细的统计信息(dump_stats)。
- 关闭日志记录(shutdown_logging)。
- 设置退出码并退出程序(exit)。
在这段代码中,我们主要关注上方我标红色的三部分,即:
- 选择要使用的系统调用表(select_syscall_tables)
select_syscall_tables
函数(此函数位于“/trinity/tables.c”源代码文件中)的具体实现如下图所示:
可以发现,此函数的主要目的是根据程序的运行的环境选择合适的系统调用表,并将选定的系统调用表复制到相应的数据结构中,以便后续程序可以根据需要使用这些系统调用。
不过有一个点需要注意,这里选择的系统调用表并不是系统自带的,而是经过Trinity修改后的系统调用结构体数组,以SYSCALLS64
(即syscalls_x86_64
)为例,其结构体数组(此结构体数组位于“/trinity/include/syscalls-x86_64.h”源代码文件中)具体实现如下图所示:
在这里定义了Trinity可以进行Fuzz的系统调用,这些系统调用分别在“/trinity/syscalls”目录中被实现。
后续就可以通过这些已经被Trinity实现的系统调用来生成对应的参数,从而对其进行Fuzz。我们可以随便打开一个Trinity自定义的系统调用,比如“alarm.c”。
可以发现,Trinity实现的自定义的系统调用都是由结构体表示的,而这些结构体中的成员才是Trinity生成测试数据的基础,所以我们就要关注这个syscallentry
结构体(此结构体位于“/trinity/include/syscall.h”源代码文件中)中究竟都定义了什么。
struct syscallentry {
void (*sanitise)(struct syscallrecord *rec);
void (*post)(struct syscallrecord *rec);
int (*init)(void);
char * (*decode)(struct syscallrecord *rec, unsigned int argnum);
unsigned int number;
unsigned int active_number;
const char name[80];
const unsigned int num_args;
unsigned int flags;
const enum argtype arg1type;
const enum argtype arg2type;
const enum argtype arg3type;
const enum argtype arg4type;
const enum argtype arg5type;
const enum argtype arg6type;
const char *arg1name;
const char *arg2name;
const char *arg3name;
const char *arg4name;
const char *arg5name;
const char *arg6name;
struct results results1;
struct results results2;
struct results results3;
struct results results4;
struct results results5;
struct results results6;
unsigned int successes, failures, attempted;
unsigned int errnos[NR_ERRNOS];
/* FIXME: At some point, if we grow more type specific parts here,
* it may be worth union-ising this
*/
/* ARG_RANGE */
const unsigned int low1range, hi1range;
const unsigned int low2range, hi2range;
const unsigned int low3range, hi3range;
const unsigned int low4range, hi4range;
const unsigned int low5range, hi5range;
const unsigned int low6range, hi6range;
/* ARG_OP / ARG_LIST */
const struct arglist arg1list;
const struct arglist arg2list;
const struct arglist arg3list;
const struct arglist arg4list;
const struct arglist arg5list;
const struct arglist arg6list;
const unsigned int group;
const int rettype;
};
这段代码定义了一个结构体syscallentry
,用于表示一个系统调用的条目。这个结构体包含了系统调用的各种信息,包括函数指针、系统调用号、名称、参数信息、标志等。下面是syscallentry
结构体中各个成员的含义:
sanitise
:指向一个函数,用于对系统调用进行清理或修正。post
:指向一个函数,用于在系统调用执行后进行处理。init
:指向一个函数,用于初始化系统调用相关的数据结构。decode
:指向一个函数,用于解码系统调用的参数。number
:系统调用号。active_number
:激活的系统调用号。name
:系统调用的名称。num_args
:系统调用的参数个数。flags
:系统调用的标志。arg1type
~arg6type
:系统调用参数的类型。arg1name
~arg6name
:系统调用参数的名称。results1
~results6
:系统调用的结果。successes
、failures
、attempted
:系统调用的成功、失败和尝试次数。errnos
:系统调用可能返回的错误码。low1range
~hi6range
:系统调用参数的范围。arg1list
~arg6list
:系统调用参数的列表。group
:系统调用的分组。rettype
:系统调用的返回类型。
可以发现,这个结构体用于描述生成系统调用测试用例的各种属性和信息,也就是说,Trinity的所有秘密都隐藏在这里,即Trinity就是根据此结构体生成的各个系统调用的测试数据。
- 初始化系统调用(init_syscalls)
init_syscalls
函数(此函数位于/trinity/tables.c)的具体实现如下图所示。
此函数中根据是否是双系统架构的系统调用表进行不同函数的调用,不过这两个函数都差不多,我们以init_syscalls_biarch
函数(此函数实现在“tables-biarch.c”源代码文件中)为例。
此函数主要进行的操作为:- 如果该条目存在且标记为激活状态(
ACTIVE
) - 并且该条目有初始化函数(
init
) - 则调用该初始化函数
- 如果该条目存在且标记为激活状态(
通过以上初始化过程,系统调用表中的每个系统调用条目将会被正确地初始化,以便后续在Fuzz测试或其它操作中使用。
- 进入主循环(main_loop)
main_loop
函数定义在“/trinity/main.c”源代码文件中,其具体内容如下所示。
void main_loop(void)
{
fork_children();
while (shm->exit_reason == STILL_RUNNING) {
handle_children();
taint_check();
if (shm_is_corrupt() == TRUE)
goto corrupt;
while (check_all_locks() == TRUE) {
reap_dead_kids();
if (shm->exit_reason == EXIT_REACHED_COUNT)
kill_all_kids();
}
if (syscalls_todo && (shm->stats.op_count >= syscalls_todo)) {
output(0, "Reached limit %lu. Telling children to exit.\n", syscalls_todo);
panic(EXIT_REACHED_COUNT);
}
check_children_progressing();
print_stats();
/* This should never happen, but just to catch corner cases, like if
* fork() failed when we tried to replace a child.
*/
if (shm->running_childs < max_children)
fork_children();
}
/* if the pid map is corrupt, we can't trust that we'll
* ever successfully finish pidmap_empty, so skip it */
if ((shm->exit_reason == EXIT_LOST_CHILD) ||
(shm->exit_reason == EXIT_SHM_CORRUPTION))
goto dont_wait;
handle_children();
/* Are there still children running ? */
while (pidmap_empty() == FALSE) {
static unsigned int last = 0;
if (last != shm->running_childs) {
last = shm->running_childs;
output(0, "exit_reason=%d, but %d children still running.\n",
shm->exit_reason, shm->running_childs);
}
/* Wait for all the children to exit. */
while (shm->running_childs > 0) {
taint_check();
handle_children();
kill_all_kids();
/* Give children a chance to exit before retrying. */
sleep(1);
}
reap_dead_kids();
}
corrupt:
kill_all_kids();
dont_wait:
output(0, "Bailing main loop because %s.\n", decode_exit(shm->exit_reason));
}
此函数的作用是执行主要的程序循环。以下是该函数的主要逻辑:
fork_children()
函数用于创建子进程。- 在共享内存(
shm
)的exit_reason
为STILL_RUNNING
时,执行循环体内的操作:handle_children()
处理子进程的相关操作。taint_check()
进行污点检查。- 如果共享内存被损坏(
shm_is_corrupt() == TRUE
),则跳转到corrupt
标签处处理。 - 在所有锁都被持有时,等待并回收已结束的子进程,如果
exit_reason
为EXIT_REACHED_COUNT
,则杀死所有子进程。 - 如果有待执行的系统调用并且已经达到执行系统调用的限制(
syscalls_todo
),则输出相关信息并panic
。 - 检查子进程是否正在进行。
- 打印统计信息。
- 如果当前运行的子进程数量小于最大子进程数量,则再次创建子进程。
- 如果
exit_reason
为EXIT_LOST_CHILD
或EXIT_SHM_CORRUPTION
,则跳转到dont_wait
标签处。 - 处理子进程的相关操作。
- 当还有子进程在运行时,进行如下操作:
- 等待所有子进程退出。
- 检查并处理污点。
- 处理子进程的相关操作。
- 杀死所有子进程。
- 休眠1秒。
- 回收已结束的子进程。
- 如果共享内存被损坏,直接杀死所有子进程。
- 输出程序退出信息。
以上程序代码看似很多,不过我们只需要关注标红色的部分,即“fork_children()
函数用于创建子进程。”,因为此部分内容才是真正创建子程序(即childx)来对各种系统调用进行Fuzz的核心部分,也就是说之前的内容都是铺垫和初始化,后面才到了Trinity工具的核心部分。关于子程序(即childx)这部分内容,将会在下一章节展开讲解。
1.3、childx
根据前面的分析,现在我们已经知道了子进程(即childx)是由fork_children
函数创建的。而在fork_children
函数(此函数实现在“/trinity/main.c”源代码文件)中,我们要关注spawn_child
函数,因为正是在此函数中,创建了真正的子进程。
在spawn_child
函数(此函数实现在“/trinity/main.c”源代码文件)中,我们要关注child_process
函数,因为当子进程创建完毕后,就需要child_process
函数来执行子进程的各种操作。
在child_process
函数(此函数实现在“/trinity/child.c”源代码文件)中,看起来有很多复杂的操作和函数,不过我们要抽丝剥茧,找到核心函数,即我们要关注random_syscall
函数,因为此函数才是在子进程中对系统调用进行Fuzz的核心函数。
random_syscall
函数(此函数实现在“/trinity/random-syscall.c”源代码文件)看起来就比较清晰了,要做的工作也一目了然。
此函数用于执行随机的系统调用,下面是此函数的主要执行步骤:
- 获取指向当前子进程的系统调用记录的指针
rec
。 - 调用
set_syscall_nr
函数设置系统调用号。如果设置失败,则返回失败。 - 使用
memset
函数将系统调用后缀缓冲区postbuffer
的内容清零。 - 调用
generate_syscall_args
函数生成系统调用的参数,并打印参数信息。 - 调用
output_syscall_prefix
函数输出系统调用前缀信息。 - 调用
do_syscall
函数执行系统调用,并记录系统调用的返回值和错误码。 - 调用
output_syscall_postfix
函数输出系统调用后缀信息。 - 调用
handle_syscall_ret
函数处理系统调用的返回值和错误码。 - 最后将函数的返回值设置为
TRUE
,表示成功执行了系统调用。
看起来调用了很多函数来处理这个过程,不过核心函数只有两个,即:
generate_syscall_args
此函数实现在“/trinity/generate-args.c”源代码文件中,其主要作用是生成用于系统调用Fuzz的各种参数。
这个函数其实也只是一个封装,因为其核心是调用的generic_sanitise
函数(此函数实现在“/trinity/generate-args.c”源代码文件中),这个函数是生成用于系统调用Fuzz的各种参数的函数封装。
可以发现,在此函数中,主要是通过调用fill_arg
函数(此函数实现在“/trinity/generate-args.c”源代码文件中)来对参数进行生成的。也就是说,Trinity工具最终就是通过fill_arg
函数来生成用于系统调用Fuzz的各种参数。
static unsigned long fill_arg(struct syscallrecord *rec, unsigned int argnum)
{
struct syscallentry *entry;
unsigned int call;
enum argtype argtype;
call = rec->nr;
entry = syscalls[call].entry;
if (argnum > entry->num_args)
return 0;
argtype = get_argtype(entry, argnum);
switch (argtype) {
case ARG_UNDEFINED:
if (RAND_BOOL())
return (unsigned long) rand64();
return (unsigned long) get_writable_address(page_size);
case ARG_FD:
if (RAND_BOOL()) {
unsigned int i;
/* If this is the 2nd or more ARG_FD, make it unique */
for (i = 0; i < argnum; i++) {
enum argtype arg;
arg = get_argtype(entry, i);
if (arg == ARG_FD)
return get_new_random_fd();
}
}
return get_random_fd();
case ARG_LEN:
return (unsigned long) get_len();
case ARG_ADDRESS:
return handle_arg_address(rec, argnum);
case ARG_NON_NULL_ADDRESS:
return (unsigned long) get_non_null_address();
case ARG_MMAP:
return (unsigned long) get_map();
case ARG_PID:
return (unsigned long) get_pid();
case ARG_RANGE:
return handle_arg_range(entry, argnum);
case ARG_OP: /* Like ARG_LIST, but just a single value. */
return handle_arg_op(entry, argnum);
case ARG_LIST:
return handle_arg_list(entry, argnum);
case ARG_CPU:
return (unsigned long) get_cpu();
case ARG_PATHNAME:
return (unsigned long) generate_pathname();
case ARG_IOVEC:
return handle_arg_iovec(entry, rec, argnum);
case ARG_IOVECLEN:
case ARG_SOCKADDRLEN:
/* We already set the len in the ARG_IOVEC/ARG_SOCKADDR case
* So here we just return what we had set there. */
return get_argval(rec, argnum);
case ARG_SOCKADDR:
return handle_arg_sockaddr(entry, rec, argnum);
case ARG_MODE_T:
return handle_arg_mode_t();
case ARG_SOCKETINFO:
return (unsigned long) get_rand_socketinfo();
}
BUG("unreachable!\n");
}
总之,该函数用于根据给定的系统调用记录和参数编号来填充参数值。下面是函数的主要步骤:
- 获取当前系统调用记录
rec
对应的系统调用入口entry
。 - 检查参数编号是否超出系统调用的参数数量,如果是,则直接返回零。
- 获取参数类型
argtype
,该类型是由get_argtype
函数根据系统调用入口和参数编号确定的。 - 根据参数类型执行相应的操作:
- 如果参数类型是
ARG_UNDEFINED
,则根据随机布尔值决定返回随机的64
位值或者可写地址的值。 - 如果参数类型是
ARG_FD
,则根据随机布尔值决定返回随机的文件描述符或者唯一的新文件描述符。 - 如果参数类型是
ARG_LEN
,则返回随机的长度值。 - 如果参数类型是
ARG_ADDRESS
,则调用handle_arg_address
函数处理地址参数。 - 如果参数类型是
ARG_NON_NULL_ADDRESS
,则返回随机的非空地址值。 - 如果参数类型是
ARG_MMAP
,则返回随机的内存映射地址。 - 如果参数类型是
ARG_PID
,则返回随机的进程ID。 - 如果参数类型是
ARG_RANGE
,则调用handle_arg_range
函数处理范围参数。 - 如果参数类型是
ARG_OP
,则调用handle_arg_op
函数处理操作参数。 - 如果参数类型是
ARG_LIST
,则调用handle_arg_list
函数处理列表参数。 - 如果参数类型是
ARG_CPU
,则返回随机的CPU编号。 - 如果参数类型是
ARG_PATHNAME
,则调用generate_pathname
函数生成路径名。 - 如果参数类型是
ARG_IOVEC
,则调用handle_arg_iovec
函数处理IO向量参数。 - 如果参数类型是
ARG_IOVECLEN
或ARG_SOCKADDRLEN
,则返回之前设置的参数长度。 - 如果参数类型是
ARG_SOCKADDR
,则调用handle_arg_sockaddr
函数处理套接字地址参数。 - 如果参数类型是
ARG_MODE_T
,则调用handle_arg_mode_t
函数处理权限参数。 - 如果参数类型是
ARG_SOCKETINFO
,则返回随机的套接字信息。
- 如果参数类型是
- 如果上述所有情况都不匹配,则产生一个BUG(错误)并输出错误消息。
对于此函数,我们就不继续向下分析了,因为我大致看了一眼,这里不管是否调用了其它函数,最终也都基本是通过随机数来生成的测试参数,并没有什么更有价值的东西,所以对于此函数分析到此即可。
do_syscall
此函数实现在“/trinity/syscall.c”源代码文件中,其主要目的是用于根据Trinity工具生成的系统调用参数来执行系统调用。
此函数的核心是调用__do_syscall
函数(此函数实现在“/trinity/syscall.c”源代码文件中)来完成真正的Fuzz测试。
static void __do_syscall(struct syscallrecord *rec, enum syscallstate state)
{
unsigned long ret = 0;
errno = 0;
shm->stats.op_count++;
if (dry_run == FALSE) {
int nr, call;
bool needalarm;
nr = rec->nr;
/* Some architectures (IA64/MIPS) start their Linux syscalls
* At non-zero, and have other ABIs below.
*/
call = nr + SYSCALL_OFFSET;
needalarm = syscalls[nr].entry->flags & NEED_ALARM;
if (needalarm)
(void)alarm(1);
lock(&rec->lock);
rec->state = state;
unlock(&rec->lock);
if (rec->do32bit == FALSE) {
ret = syscall(call, rec->a1, rec->a2, rec->a3, rec->a4, rec->a5, rec->a6);
} else {
ret = syscall32(call, rec->a1, rec->a2, rec->a3, rec->a4, rec->a5, rec->a6);
}
/* If we became tainted, get out as fast as we can. */
if (is_tainted() == TRUE) {
stop_ftrace();
panic(EXIT_KERNEL_TAINTED);
_exit(EXIT_FAILURE);
}
if (needalarm)
(void)alarm(0);
}
lock(&rec->lock);
rec->errno_post = errno;
rec->retval = ret;
rec->state = AFTER;
unlock(&rec->lock);
}
该函数负责实际执行系统调用,并将执行结果和可能的错误信息记录在系统调用记录中,下面是此函数的主要步骤:
- 将全局变量
errno
置为0
,以便记录系统调用执行过程中的错误信息。 - 增加共享内存中统计信息
stats
中的操作计数。 - 如果不是在干扰测试(
dry_run
)模式下执行系统调用,则执行以下步骤:- 获取系统调用号和调用方式(32位或64位)。
- 根据系统调用号调用相应的系统调用函数
syscall
或syscall32
,并传入相应的参数。 - 如果系统调用需要定时器警告(alarm),则设置定时器。
- 在执行系统调用之前,将系统调用记录的状态设置为
state
。 - 执行系统调用,并将返回值存储在
ret
中。 - 如果系统调用执行过程中产生了污染(tainted),则立即停止函数跟踪(ftrace)、触发内核污染退出(panic)并退出程序。
- 如果系统调用需要定时器警告,执行完系统调用后取消定时器。
- 将
errno
存储在系统调用记录中的errno_post
字段中。 - 将系统调用的返回值存储在系统调用记录中的
retval
字段中。 - 将系统调用记录的状态设置为
AFTER
。 - 解锁系统调用记录的锁。
每个子程序(即childx)都是按照上面分析的过程执行的,当我们想要对某个系统调用进行Fuzz时,就会创建对应的子程序,并通过generate_syscall_args
函数生成对应的测试数据,最终通过do_syscall
函数进行测试。
2、安装与使用
软件环境 | 硬件环境 | 约束条件 |
---|---|---|
Ubuntu-22.04.2-desktop-amd64(内核版本5.19.0-43-generic) | 内存16GB | 具体的约束条件可见“2.1、源码安装”章节所示的软件版本约束 |
其余的软件环境可见“2.1、源码安装”章节所示的软件环境 | 硬盘30GB | Trinity-v1.9 |
使用4个处理器,每个处理器4个内核,共分配16个内核 | ||
Trinity部署在VMware Pro 17上的Ubuntu22.04.2系统上(主机系统为Windows 11),硬件环境和软件环境也是对应的VMware Pro 17的硬件环境和软件环境 |
2.1、源码安装
2.1.1 部署系统依赖组件
- 首先使用如下命令更新软件源:
$ sudo apt-get update
- 首先使用如下命令下载并安装Trinity所需要的组件:
$ sudo apt-get install git
$ sudo apt-get install vim
$ sudo apt-get install build-essential libelf-dev libnuma-dev
2.1.2 使用源码安装系统
- 首先进入系统根目录,并执行如下命令下载Trinity源代码文件。注意:下载的Trinity源代码的版本为v1.9:
$ cd /
$ sudo git clone https://github.com/kernelslacker/trinity.git
- 然后进入Trinity源代码目录,进行配置文件的生成以及编译和安装:
$ cd trinity/
$ sudo ./configure
$ sudo make
$ sudo make install
- 完成以上操作后,Trinity就已经安装完毕,就可以继续后面的测试了
2.2、使用方法
Trinity的测试命令格式如下所示:
./trinity -C<num> -c<num> -l <path_to_log_file> -o <path_to_output_directory> --no-pause <path_to_executable>
对于以上各个参数的解释如下:
-C<num>
:指定测试的子进程数量为<num>
个-c<num>
:指定重复执行系统调用的次数为<num>
次,并为每个系统调用使用随机输入数据-l <path_to_log_file>
:指定日志输出到<path_to_log_file>
文件-o <path_to_output_directory>
:指定生成的测试用例输出到<path_to_output_directory>
目录--no-pause
:避免在测试过程中需要手动确认生成的测试用例<path_to_executable>
:指定要进行测试的可执行文件的路径
下面是一个测试命令示例:
./trinity -C10 -c1 -l log.txt -o testcases --no-pause ./example
此测试命令的含义为:对名为example的可执行文件启10个子进程测试,在运行时,Trinity将重复执行系统调用1次,并为每个系统调用使用随机输入数据,将日志输出到log.txt文件,将生成的测试用例输出到testcases目录。
其实Trinity还提供了将近二十种参数供读者使用,这些参数对后续的测试有相当大的作用,比如可以自定义随机种子、选择32/64位的系统调用、使用调试模式进行测试等。所以,为了方便后续测试使用,特将所有参数及其含义整理如下,在后续测试时,可时常查阅此表:
参数 | 含义 |
---|---|
--quiet/-q | 减少冗长输出。使用一次表示不输出寄存器的值,使用两次还会抑制系统调用计数。 |
--verbose | 增加输出信息的详细程度 |
-D | 调试模式。如果Trinity发生段错误,这个模式可以捕获核心转储,因为默认情况下子进程会忽略这些信号。 |
-sN | 使用N 作为随机种子(如果省略则使用当前时间作为种子)。注意,目前存在一些错误,因此使用相同的种子运行Trinity不一定能得到完全相同的结果。 |
--kernel_taint/-T | 控制应该考虑哪些内核污点标志。支持以下标志名称:PROPRIETARY_MODULE 、FORCED_MODULE 、UNSAFE_SMP 、FORCED_RMMOD 、MACHINE_CHECK 、BAD_PAGE 、USER 、DIE 、OVERRIDDEN_ACPI_TABLE 、WARN 、CRAP 、FIRMWARE_WORKAROUND和OOT_MODULE 。例如,要设置trinity仅监视BAD 、WARN 和MACHINE_CHECK 标志,应该指定“-T BAD,WARN,MACHINE_CHECK” 参数。 |
--list/-L | 列出已知系统调用及其偏移量。 |
--proto/-P | 对于网络套接字,仅使用特定的数据包族。 |
--victims/-V | 受害者文件/目录。默认情况下,Trinity会遍历“/dev”、“/sys”和“/proc”。使用此选项可以指定其他路径(目前仅限一个路径)。 |
-p | 在进行系统调用后暂停。 |
--children/-C | 子进程数。 |
-x | 排除一个系统调用不被调用。当你不断遇到已知的内核bug时,这个选项非常有用。 |
-cN | 使用随机输入执行系统调用N 。如果只是添加了一个系统调用,这个选项非常有用。 |
--group/-g | 用于指定启用一组系统调用。目前定义了“vm” 和“vfs” 两个组。 |
--logging/-l <arg> | ①:off :禁用写日志到文件。如果你有一个串行控制台,这非常有用,但你可能会丢失关于调用了哪个系统调用、设置了哪些映射等信息。但这会让事情变得更快,因为它不再在每个系统调用后执行fsync()。②:<hostname> :将数据包通过UDP发送到运行在另一台主机上的trinity服务器。注意:此功能仍在开发中。启用此功能将禁用日志写入文件。③:<dir> :指定trinity将转储其日志文件的目录。 |
--ioctls/-I | 将显示所有可用的ioctl。 |
--arch/-a | 显式选择32位或64位变体的系统调用。 |
3、测试用例
3.1、Splice系统调用压力测试
Splice系统调用压力测试指集中进行Splice系统调用的测试。splice()系统调用是用于在两个文件描述符之间移动数据的系统调用,可以用于高效地传输数据。通过指定-c splice
,Trinity工具就可以重复使用splice()
系统调用来进行测试和Fuzz。下面为进行Splice系统调用压力测试的详细步骤。
- 首先进入Trinity源代码目录,并执行如下命令进行Splice系统调用压力测试:
$ cd /
$ cd trinity/
$ ./trinity -c splice
- 命令成功执行后,Trinity就开始对Splice系统调用进行Fuzz,不过由于执行时间太长,只展示了Fuzz过程,并没有展示Fuzz结果:
注:实际执行中遇到的问题及解决方法
- 问题1:
-
在进行Splice系统调用压力测试的步骤1的第三步启动测试的时候,出现如下问题:
-
产生以上问题是因为无法打开日志文件,我们只需要执行
$ sudo ./trinity -c splice
命令,以root用户权限启动测试,虽然执行这条命令后会提示此命令执行失败,即提示我们不能用root用户权限启动,但是这时已经打开了日志文件,就可以继续使用非root用户权限进行后续的测试了 -
此时我们就解决了该问题,然后回到步骤1的第三步重新继续向下操作,其余的测试如果遇到这个问题也按照同样的方法解决
-
3.2、其它系统调用压力测试
- 首先进入Trinity源代码目录,并执行如下命令进行除Splice系统调用之外的每个系统调用的压力测试:
$ cd /
$ cd trinity/
$ ./trinity -x splice
- 命令成功执行后,Trinity就开始对除Splice系统调用之外的每个系统调用进行Fuzz,不过由于执行时间太长,只展示了Fuzz过程,并没有展示Fuzz结果:
3.3、自定义系统调用压力测试
- 首先进入Trinity源代码目录,并执行如下命令进行关闭日志,并抑制大部分输出以尽可能快地运行,且使用16个子进程的系统调用的压力测试:
$ cd /
$ cd trinity/
$ ./trinity -qq -l off -C16
- 命令成功执行后,Trinity就开始对系统调用进行自定义的Fuzz,不过由于执行时间太长,只展示了Fuzz过程,并没有展示Fuzz结果:
4、总结
4.1、部署架构
关于Trinity部署的架构图,如下所示。
对于以上架构图,我们具体来看Trinity是否对其中的组件进行了修改。详情可参见下方的表格。
是否有修改 | 具体修改内容 | |
---|---|---|
主机内核 | 无 | 无 |
主机操作系统 | 无 | 无 |
4.2、漏洞检测对象
- 检测的对象为主机内核
- 针对的内核版本为5.19.0-43-generic
- 针对的漏洞类型为崩溃性错误
4.3、漏洞检测方法
- 根据Trinity的系统调用结构体生成系统调用测试用例
- 使用
syscall()
函数(其函数原型为long syscall(long number, ...);
)执行系统调用,从而对内核进行Fuzz测试 - 将测试结果保存到主机中
- 目前可以进行测试的系统调用共325个
4.4、种子生成/变异技术
- 初始种子由Trinity生成
- 没有对种子进行变异
- 种子生成的策略基于随机,即随机生成特定类型的参数的具体值(比如bool类型、int类型和char类型等)
5、参考文献
[1] LCA: The Trinity fuzz tester
[2] [原创]内核漏洞挖掘技术系列(1)——trinity
[3] Linux system call fuzzer
总结
以上就是本篇博文的全部内容,可以发现,Trinity的部署与使用的过程并不复杂,并且Trinity的Fuzz测试过程的脉络也比较清楚,是一个典型的Fuzz测试的过程。总而言之,Trinity是一个不错的Fuzz测试的工具,值得大家学习。相信读完本篇博客,各位读者一定对Trinity有了更深的了解。