QEMU-QTest && Libfuzzer源码分析(上) 0x01 TL;DR QEMU
中的Libfuzzer
是Alexander Bulekov
在19年Google SummerCode
期间开发的一套QEMU
内置的Fuzz
工具。QEMU
最开始代码测试的时候开发了一套名为QTest
的测试工具,主要用它来编写测试用例。QEMU
中的设备很多,一个一个去写相应设备的测试用例是很耗时间的,因此就有了在QTest
结合Libfuzzer
的测试工具。不过当我整体审计完QTest
代码后,发现其实还是蛮有局限性的,待改进的空间也很大。
由于全文篇幅比较长,因此分为上下两篇叙述。
0x02 SourceCode Version QEMU
:5.2.0
0x03 Basic Principle 先简单梳理一下QTest
的设计原理。我尽量保持原汁原味,不改变原意。
QTest
大体将QEMU
内容划分为四类:Machine
、Driver
、Interface
、Test
,每一类为一个node
(节点)。节点之间的关系被称作edge
,edge
分为三类:consume
、produce
、contain
。
关于edge
官方解释是这样的(x
和y
为node
):
1 2 3 x consumes y : x可以使用y(和produces对应) x produces y : x给y提供接口 x contains y : y是x组件的一部分(x包含y)
QTest
基本框架步骤如下:
所有nodes
和edges
都创建在各自的文件下 –> machine/driver/test
。
启动QEMU
后查询一系列的可用devices
和machines
。
从可用的machines
开始遍历并执行深度优先遍历,查询与test
相应的情况。
一旦遍历到了test
,路径会重新走一遍并且所有drivers
会被相应分配,最终的interface
也会传给test
。
执行test
。
未被使用的对象会被清理以及路径发现(遍历)也会继续。
以下是一个编写新driver
以及interface
的例子:
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 53 54 55 56 57 58 59 60 61 #include "qgraph.h" struct My_driver { QOSGraphObject obj; Node_produced prod; Node_contained cont; } static void my_destructor (QOSGraphObject *obj) { g_free(obj); } static void my_get_driver (void *object, const char *interface) { My_driver *dev = object; if (!g_strcmp0(interface, "my_interface" )) { return &dev->prod; } abort (); } static void my_get_device (void *object, const char *device) { My_driver *dev = object; if (!g_strcmp0(device, "my_driver_contained" )) { return &dev->cont; } abort (); } static void *my_driver_constructor (void *node_consumed, QOSGraphObject *alloc) { My_driver dev = g_new(My_driver, 1 ); dev->obj.get_driver = my_get_driver; dev->obj.get_device = my_get_device; dev->obj.destructor = my_destructor; do_something_with_node_consumed(node_consumed); init_contained_device(&dev->cont); return &dev->obj; } static void register_my_driver (void ) { qos_node_create_driver("my_driver" , my_driver_constructor); qos_node_create_driver("my_driver_contained" , NULL ); qos_node_contains("my_driver" , "my_driver_contained" ); qos_node_produces("my_driver" , "my_interface" ); qos_node_consumes("my_driver" , "other_node" ); }
上面这个例子里,所有可能的关系类型都创建了,具体关系如下:
1 2 3 4 5 x86_64/pc --contains--> other_node --consumed_by--> my_driver | my_driver_contained <--contains--+ | my_interface <--produces--+
以下是编写一个新的test
的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include "qgraph.h" static void my_test_function (void *obj, void *data) { Node_produced *interface_to_test = obj; } static void register_my_test (void ) { qos_add_test("my_interface" , "my_test" , my_test_function); } libqos_init(register_my_test);
新的test
创建了,该test
是consume
了my_interface
这个node
,并且创建了一个有效的从machine
到一个test
的path
。最终的图表会像下面这样:
1 2 3 4 5 x86_64/pc -->contains--> other_node --consumed_by--> my_driver | my_driver_contained <--contains--+ | my_test <--consumed_by-- my_interface <--produces--+
假设有一个二进制文件QTEST_QEMU_BINARY=./qemu-system-x86_64
,那么一个有效的test path
就会像这样:/x86_64/pc/other_node/my_driver/my_interface/my_test
。
Command Line :
QEMU
启动需要有一些Option
参数,在QTest
框架中的体现就是Command Line
。Command Line
是使用node names
以及构建edges
时通过用户传递的可选参数构建的。Command Line
参数有三种类型:in node
、after node
、before node
。
1 2 3 4 5 in node: 根据node name创建,例如,machines会有“-M <machine>”传给command line,同时devices也会有“-device <device>”,该框架会自动完成创建。 after node: 以额外参数添加在node name中。当创建edges时该参数的添加是可选的,通过设置#QOSGraphEdgeOptions结构体中的@after_cmd_line和@extra_edge_opts属性即可。框架也会在@extra_edge_opts之前自动添加一个段落,因为这会在edge包含的options所指向的目标node之后添加属性,并且自动在@after_cmd_line之前添加一个空格,因为这是添加一个额外的device,并不是添加一个额外的属性。 before node: 以额外参数添加在node name中。当创建edges时也是可选的,通过设置#QOSGraphEdgeOptions结构体中的@before_cmd_line即可。这个属性会在edge包含的options所指向的目标node之前添加属性。这对于不是节点可视的命令来说很有用,例如“-fdsev”、“-netdev”。
尽管Command Line
在edges
中总会被使用,但不是所有的nodes names
在没一个路径遍历(path walk
)中会被用到。因为contained
或者produced
关系总会被QEMU
添加,因此只有consumes
会被用在建立Command Line
中。
使用例子如下:
1 2 3 4 5 6 7 8 9 QOSGraphEdgeOptions opts = { .arg = NULL , .size_arg = 0 , .after_cmd_line = "-device other" , .before_cmd_line = "-netdev something" , .extra_edge_opts = "addr=04.0" , }; QOSGraphNode * node = qos_node_create_driver("my_node" , constructor); qos_node_consumes_args("my_node" , "interface" , &opts);
最终构造出来的Command Line
如下:
1 -netdev something -device my_node,addr=04.0 -device other
QOSGraphEdgeOptions
结构体如下:
1 2 3 4 5 6 7 8 struct QOSGraphEdgeOptions { void *arg; uint32_t size_arg; const char *extra_device_opts; const char *before_cmd_line; const char *after_cmd_line; const char *edge_name; };
接下来就说几个比较重要的函数:
1 2 void qos_node_contains (const char *container, const char *contained, QOSGraphEdgeOptions *opts, ...) ;
用来创建一个或多个edges
,type
类型为QEDGE_CONTAINS
。如果@opts
为空,那么只会创建一个没有options
的单条edge
,如果不空,每个option
都会创建一条edge
。这个函数对于在同个machine node
下有着相同node names
的多个设备来说很有用。例如,arm/raspi2
包含了两个generic-sdhci
设备,正确的命令会是这样:
1 2 3 4 5 6 qos_node_create_machine("arm/raspi2" ); qos_node_create_driver("generic-sdhci" , constructor); QOSGraphEdgeOptions op1 = { .edge_name = "emmc" }; QOSGraphEdgeOptions op2 = { .edge_name = "sdcard" }; qos_node_contains("arm/raspi2" , "generic-sdhci" , &op1, &op2, NULL );
当然这也需要@container
(包含者)的get_device
函数对emmc
和sdcard
都有一个实现实例。
op1.arg
和op1.size_arg
代表传递给@contained
(被包含者)构造函数的参数用于正确的初始化。
1 2 3 void qos_add_test (const char *name, const char *interface, QOSTestFunc test_func, QOSGraphTestOptions *opts) ;
用于添加test node
,该test
会consume
一个interface node
,一旦图表的遍历算法找到了这个测试路径,@test_func
就会被执行。对于test node
来说,opts->edge.arg
和size_arg
代表传递给@test_func
的参数。
简单总结 :
QTest
框架主要围绕两类node
和edge
来展开,两类结构体以及两类的type
类型如下:
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 53 54 55 56 57 58 enum QOSEdgeType { QEDGE_CONTAINS, QEDGE_PRODUCES, QEDGE_CONSUMED_BY }; enum QOSNodeType { QNODE_MACHINE, QNODE_DRIVER, QNODE_INTERFACE, QNODE_TEST }; struct QOSGraphNode { QOSNodeType type; bool available; bool visited; char *name; char *command_line; union { struct { QOSCreateDriverFunc constructor; } driver; struct { QOSCreateMachineFunc constructor; } machine; struct { QOSTestFunc function; void *arg; QOSBeforeTest before; bool subprocess; } test; } u; QOSGraphEdge *path_edge; }; struct QOSGraphEdge { QOSEdgeType type; char *dest; void *arg; char *extra_device_opts; char *before_cmd_line; char *after_cmd_line; char *edge_name; QSLIST_ENTRY(QOSGraphEdge) edge_list; };
其他的例如QOSGraphEdgeOptions
、QOSGraphTestOptions
实际上是node
和edge
的一个拓展延伸(参数选项),最终还是要赋值到node
和edge
中去的。
值得一提的还有QOSGraphObject
,该结构体是用于test
、driver
、machine
的实例化作为他们的第一个字段(域)。定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 struct QOSGraphObject { QOSGetDriver get_driver; QOSGetDevice get_device; QOSStartFunct start_hw; QOSDestructorFunc destructor; GDestroyNotify free ; };
0x04 QTest Code Analyse 后续部分建议边调试(审计)边食用。先主要分析QTest
相关的代码,之后再来看libfuzzer
部分,QTest
部分主要逻辑在qos-test.c
文件中。main
函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int main (int argc, char **argv) { g_test_init(&argc, &argv, NULL ); qos_graph_init(); module_call_init(MODULE_INIT_QOM); module_call_init(MODULE_INIT_LIBQOS); qos_set_machines_devices_available(); qos_graph_foreach_test_path(walk_path); g_test_run(); qtest_end(); qos_graph_destroy(); g_free(old_path); return 0 ; }
module_call_init
值得一说,MODULE_INIT_QOM
是在type_init(function)
中指定的类型,具体为module_init(function, MODULE_INIT_QOM)
,看定义可以得知它是一个构造函数,在QEMU
运行之前就执行了,具体操作为:
1 2 3 4 5 6 7 8 9 10 11 12 13 void register_module_init(void (*fn)(void), module_init_type type) { ModuleEntry *e; ModuleTypeList *l; e = g_malloc0(sizeof (*e)); e->init = fn; e->type = type; l = find_type(type); QTAILQ_INSERT_TAIL(l, e, node); }
moudle_call_init
函数为:
1 2 3 4 5 6 7 8 void module_call_init (module_init_type type) { QTAILQ_FOREACH(e, l, node) { e->init(); } }
而type_init
出现在QEMU
设备代码中,用于设备的注册/初始化。所以module_call_init(MODULE_INIT_QOM)
就是拿来初始化QEMU
中的设备的。同样的,module_call_init(MODULE_INIT_LIBQOS)
用于初始化libqos
框架,把QTest
中的machine
、driver
、test
等都初始化了,具体可以查看调用了libqos_init(function)
函数的代码文件。
qos_set_machines_devices_available()
作用是将machines
和devices
的node->availabe
设为true
,默认创建node
的时候是false
。相当于启用设备,为后续的path walk
做准备。
接下来就是重点要分析的“路径遍历”了。首先看qos_graph_foreach_test_path
函数:
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 void qos_graph_foreach_test_path (QOSTestCallback fn) { QOSGraphNode *root = qos_graph_get_node(QOS_ROOT); qos_traverse_graph(root, fn); } static void qos_traverse_graph (QOSGraphNode *root, QOSTestCallback callback) { QOSGraphNode *v, *dest_node, *path; QOSStackElement *s_el; QOSGraphEdge *e, *next; QOSGraphEdgeList *list ; qos_push(root, NULL , NULL ); while (qos_node_tos > 0 ) { s_el = qos_tos(); v = s_el->node; if (v->visited) { qos_pop(); continue ; } v->visited = true ; list = get_edgelist(v->name); if (!list ) { qos_pop(); if (v->type == QNODE_TEST) { v->visited = false ; path = qos_reverse_path(s_el); callback(path, s_el->length); } } else { QSLIST_FOREACH_SAFE(e, list , edge_list, next) { dest_node = search_node(e->dest); if (!dest_node) { fprintf (stderr , "node %s in %s -> %s does not exist\n" , e->dest, v->name, e->dest); abort (); } if (!dest_node->visited && dest_node->available) { qos_push(dest_node, s_el, e); } } } } }
qos_traverse_graph
是路径遍历的算法函数,采用“栈”的方式来操作。定义了一个名为qos_node_stack
的栈元素数组,数组中的每个元素为一个叫QOSStackElement
的结构体,包含了node
、parent
以及两者间的edge
:
1 2 3 4 5 6 7 8 struct QOSStackElement { QOSGraphNode *node; QOSStackElement *parent; QOSGraphEdge *parent_edge; int length; }; static QOSStackElement qos_node_stack[QOS_PATH_MAX_ELEMENT_SIZE];
深度最长为50。
我作了一个节点树的图,来表明当前QEMU
中节点之间的联系:
我只列出了部分node
,主要做个效果,审计该函数的时候可以对照这个节点树来看,从上图可以找到一条完整的通路:
ROOT –> i386/pc –> i440FX-pcihost –> pci-bus-pc –> pci-bus –> virtio-scsi-pci –> virtio-scsi –> hotplug
这就是一条从machine
到test
的完整通路。当然,这只是其中一条,还有许多条test
路径。
同时,我也作了一个该函数利用栈来操作的栈空间示意图,如下:
一起结合着看,该函数的意图就显而易见了,就是深度优先遍历的算法。
算法看完了,再来看该函数处理路径的部分:
1 2 3 4 5 6 7 8 if (!list ) { qos_pop(); if (v->type == QNODE_TEST) { v->visited = false ; path = qos_reverse_path(s_el); callback(path, s_el->length); } } else {
当遍历到一条完整的machine
到test
路径时,就开始做处理了。qos_reverse_path(s_el)
函数简单说一下就是对node
结构体的path_edge
做操作,链接这条路径的各个父子节点,回溯出这条链路上的所有node
和edge
关系。callback
函数就是传入的walk_path
函数,参数为path
和s_el->length
,也就是遍历到的路径和路径的深度:
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 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 static void walk_path (QOSGraphNode *orig_path, int len) { QOSGraphNode *path; QOSGraphEdge *edge; QOSEdgeType etype = QEDGE_CONSUMED_BY; char **path_vec = g_new0(char *, (QOS_PATH_MAX_ELEMENT_SIZE * 2 )); int path_vec_size = 0 ; char *after_cmd, *before_cmd, *after_device; GString *after_device_str = g_string_new("" ); char *node_name = orig_path->name, *path_str; GString *cmd_line = g_string_new("" ); GString *cmd_line2 = g_string_new("" ); path = qos_graph_get_node(node_name); node_name = qos_graph_edge_get_dest(path->path_edge); path_vec[path_vec_size++] = node_name; path_vec[path_vec_size++] = qos_get_machine_type(node_name); for (;;) { path = qos_graph_get_node(node_name); if (!path->path_edge) { break ; } node_name = qos_graph_edge_get_dest(path->path_edge); if (path->command_line && etype == QEDGE_CONSUMED_BY) { g_string_append(cmd_line, path->command_line); g_string_append(cmd_line, after_device_str->str); g_string_truncate(after_device_str, 0 ); } path_vec[path_vec_size++] = qos_graph_edge_get_name(path->path_edge); after_cmd = qos_graph_edge_get_after_cmd_line(path->path_edge); after_device = qos_graph_edge_get_extra_device_opts(path->path_edge); before_cmd = qos_graph_edge_get_before_cmd_line(path->path_edge); edge = qos_graph_get_edge(path->name, node_name); etype = qos_graph_edge_get_type(edge); if (before_cmd) { g_string_append(cmd_line, before_cmd); } if (after_cmd) { g_string_append(cmd_line2, after_cmd); } if (after_device) { g_string_append(after_device_str, after_device); } } path_vec[path_vec_size++] = NULL ; g_string_append(cmd_line, after_device_str->str); g_string_free(after_device_str, true ); g_string_append(cmd_line, cmd_line2->str); g_string_free(cmd_line2, true ); path_str = g_strjoinv("/" , path_vec + 1 ); path_vec[1 ] = path_vec[0 ]; path_vec[0 ] = g_string_free(cmd_line, false ); if (path->u.test.subprocess) { gchar *subprocess_path = g_strdup_printf("/%s/%s/subprocess" , qtest_get_arch(), path_str); qtest_add_data_func(path_str, subprocess_path, subprocess_run_one_test); g_test_add_data_func(subprocess_path, path_vec, run_one_test); } else { qtest_add_data_func(path_str, path_vec, run_one_test); } g_free(path_str); }
path_str
指向一连串的字符串(例如”pc/i440FX-pcihost/...
“),path_vec
指向一个字符串数组(例如[0] = "i386/pc" [1] = "pc"...
)
后续就开始执行相应的test
函数run_one_test
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 static void run_one_test (const void *arg) { QOSGraphNode *test_node; QGuestAllocator *alloc = NULL ; void *obj; char **path = (char **) arg; GString *cmd_line = g_string_new(path[0 ]); void *test_arg; current_path = path; test_node = qos_graph_get_node(path[(g_strv_length(path) - 1 )]); test_arg = test_node->u.test.arg; if (test_node->u.test.before) { test_arg = test_node->u.test.before(cmd_line, test_arg); } restart_qemu_or_continue(cmd_line->str); g_string_free(cmd_line, true ); obj = qos_allocate_objects(global_qtest, &alloc); test_node->u.test.function(obj, test_arg, alloc); }
其中qos_allocate_objects
函数是颇为重要 的一部分,这部分就是测试函数最关键的一点–“对象”。之前所获取的都是一些node
节点和edge
关系,这只是一个很抽象代表的东西,并没有实例化,也就是说,没有实际设备上的结构体,例如一个设备所包含的一些功能或者元素属性。因此,我们需要实例化,来为后续测试函数做准备。先看看这个qos_allocate_objects
函数具体做了什么:
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 53 54 55 56 57 58 59 60 void *qos_allocate_objects (QTestState *qts, QGuestAllocator **p_alloc) { return allocate_objects(qts, current_path + 1 , p_alloc); } void *allocate_objects (QTestState *qts, char **path, QGuestAllocator **p_alloc) { int current = 0 ; QGuestAllocator *alloc; QOSGraphObject *parent = NULL ; QOSGraphEdge *edge; QOSGraphNode *node; void *edge_arg; void *obj; node = qos_graph_get_node(path[current]); g_assert(node->type == QNODE_MACHINE); obj = qos_machine_new(node, qts); qos_object_queue_destroy(obj); alloc = get_machine_allocator(obj); if (p_alloc) { *p_alloc = alloc; } for (;;) { if (node->type != QNODE_INTERFACE) { qos_object_start_hw(obj); parent = obj; } current++; edge = qos_graph_get_edge(path[current - 1 ], path[current]); node = qos_graph_get_node(path[current]); if (node->type == QNODE_TEST) { g_assert(qos_graph_edge_get_type(edge) == QEDGE_CONSUMED_BY); return obj; } switch (qos_graph_edge_get_type(edge)) { case QEDGE_PRODUCES: obj = parent->get_driver(parent, path[current]); break ; case QEDGE_CONSUMED_BY: edge_arg = qos_graph_edge_get_arg(edge); obj = qos_driver_new(node, obj, alloc, edge_arg); qos_object_queue_destroy(obj); break ; case QEDGE_CONTAINS: obj = parent->get_device(parent, path[current]); break ; } } }
标记1
处,拿i386/pc
下的架构来说,它的constructor
函数是x86_64_pc-machine.c
文件中的qos_create_machine_pc()
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 static void *qos_create_machine_pc (QTestState *qts) { QX86PCMachine *machine = g_new0(QX86PCMachine, 1 ); machine->obj.get_device = pc_get_device; machine->obj.get_driver = pc_get_driver; machine->obj.destructor = pc_destructor; pc_alloc_init(&machine->alloc, qts, ALLOC_NO_FLAGS); qos_create_i440FX_host(&machine->bridge, qts, &machine->alloc); return &machine->obj; } void pc_alloc_init (QGuestAllocator *s, QTestState *qts, QAllocOpts flags) { ram_size = qfw_cfg_get_u64(fw_cfg, FW_CFG_RAM_SIZE); alloc_init(s, flags, 1 << 20 , MIN(ram_size, 0xE0000000 ), PAGE_SIZE); }
标记2
处往下,就是循环遍历machine node
到test node
的过程,并在每次遍历的过程当中,都做obj
初始化分配的工作以及结构体初始化填充的工作,使得各个node
的实例化obj
也都能相互链接起来。
结构体上的链接关系如下:
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 struct QX86PCMachine { QOSGraphObject obj; QGuestAllocator alloc; i440FX_pcihost bridge; }; struct i440FX_pcihost { QOSGraphObject obj; QPCIBusPC pci; }; typedef struct QPCIBusPC { QOSGraphObject obj; QPCIBus bus; } QPCIBusPC; struct QPCIBus { uint8_t (*pio_readb)(QPCIBus *bus, uint32_t addr); uint16_t (*pio_readw)(QPCIBus *bus, uint32_t addr); uint32_t (*pio_readl)(QPCIBus *bus, uint32_t addr); uint64_t (*pio_readq)(QPCIBus *bus, uint32_t addr); void (*pio_writeb)(QPCIBus *bus, uint32_t addr, uint8_t value); void (*pio_writew)(QPCIBus *bus, uint32_t addr, uint16_t value); void (*pio_writel)(QPCIBus *bus, uint32_t addr, uint32_t value); void (*pio_writeq)(QPCIBus *bus, uint32_t addr, uint64_t value); void (*memread)(QPCIBus *bus, uint32_t addr, void *buf, size_t len); void (*memwrite)(QPCIBus *bus, uint32_t addr, const void *buf, size_t len); uint8_t (*config_readb)(QPCIBus *bus, int devfn, uint8_t offset); uint16_t (*config_readw)(QPCIBus *bus, int devfn, uint8_t offset); uint32_t (*config_readl)(QPCIBus *bus, int devfn, uint8_t offset); void (*config_writeb)(QPCIBus *bus, int devfn, uint8_t offset, uint8_t value); void (*config_writew)(QPCIBus *bus, int devfn, uint8_t offset, uint16_t value); void (*config_writel)(QPCIBus *bus, int devfn, uint8_t offset, uint32_t value); QTestState *qts; uint16_t pio_alloc_ptr; uint64_t mmio_alloc_ptr, mmio_limit; bool has_buggy_msi; };
allocate_objects
函数的之后部分,整个循环结束之后,得到的obj
就是test node
所使用的interface node
的obj
。
重新回到run_one_test
函数中来:
1 2 3 4 5 6 static void run_one_test (const void *arg) { obj = qos_allocate_objects(global_qtest, &alloc); test_node->u.test.function(obj, test_arg, alloc); }
最终就是执行相应的test function
就结束了。
0x05 QTest Summary 至此,整个qos-test.c
的QTest
细节就结束了,后续的话就是一些test
测试文件,这些留给读者自行阅读。大致概括一下QTest
的整体流程就是每找到machine
到test
的路径,就执行相应的test function
,相当于把所有编写的test
测试用例都执行了一遍。他不是一个持续性测试(持续性喂各种不同的数据进行测试)的过程,而是“一次性”的测试,因此局限性显而易见。
同时,编写的test
是基于前者interface
提供的接口进行的,所以测试情况会受interface
的接口限制。如果没有想要测试的设备的接口,那么完全就没有办法编写相应的test
。
0x06 Libfuzzer Test Code Analyze 先了解一下作者对fuzz target
所设计的结构体:
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 typedef struct FuzzTarget { const char *name; const char *description; GString *(*get_init_cmdline)(struct FuzzTarget *); void (*pre_vm_init)(void ); void (*pre_fuzz)(QTestState *); void (*fuzz)(QTestState *, const unsigned char *, size_t ); size_t (*crossover)(const uint8_t *data1, size_t size1, const uint8_t *data2, size_t size2, uint8_t *out, size_t max_out_size, unsigned int seed); void *opaque; } FuzzTarget;
集成libfuzzer
这块的代码存在于/tests/qtest/fuzz
目录下,主代码文件是fuzz.c
,先看前期init
的函数LLVMFuzzerInitialize
:
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 int LLVMFuzzerInitialize (int *argc, char ***argv, char ***envp) { module_call_init(MODULE_INIT_FUZZ_TARGET); qemu_init_exec_dir(**argv); target_name = strstr (**argv, "-target-" ); fuzz_qtest_set_serialize(serialize); fuzz_target = fuzz_get_target(target_name); if (!fuzz_target) { usage(**argv); } fuzz_qts = qtest_setup(); if (fuzz_target->pre_vm_init) { fuzz_target->pre_vm_init(); } GString *cmd_line = fuzz_target->get_init_cmdline(fuzz_target); g_string_append_printf(cmd_line, " %s -qtest /dev/null " , getenv("QTEST_LOG" ) ? "" : "-qtest-log none" ); wordexp_t result; wordexp(cmd_line->str, &result, 0 ); g_string_free(cmd_line, true ); qemu_init(result.we_wordc, result.we_wordv, NULL ); return 0 ; }
标记1
处和前面分析QTest
的时候提到的module_call_init(MODULE_INIT_QOM)
类似。实际上就是执行fuzz_target_init();
调用的函数,拿i440fx_fuzz.c
举例,其中最底部有这么一段代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 static void register_pci_fuzz_targets (void ) { fuzz_add_qos_target(&(FuzzTarget){ .name = "i440fx-qos-noreset-fuzz" , .description = "Fuzz the i440fx using raw qtest commands and " "rebooting after each run" , .fuzz = i440fx_fuzz_qos,}, "i440FX-pcihost" , &(QOSGraphTestOptions){} ); } fuzz_target_init(register_pci_fuzz_targets);
调用module_call_init
就是执行register_pci_fuzz_targets
函数,初始化编写的fuzz target
。当然,不止这一个文件调用了fuzz_target_init
函数。
标记2
处的作用是使得传输的QTest
指令显式表示,也就是能够从command
上看出传输的地址以及数据,但是这样会消耗部分资源,因此默认是关闭的。
标记3
的具体函数如下:
1 2 3 4 5 6 7 8 9 10 static FuzzTarget *fuzz_get_target (char * name) { QSLIST_FOREACH(tmp, fuzz_target_list, target_list) { if (strcmp (tmp->target->name, name) == 0 ) { return tmp->target; } } return NULL ; }
很明显就是取出对应name
的fuzz target
。
标记4
处具体函数如下:
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 static QTestState *qtest_setup (void ) { qtest_server_set_send_handler(&qtest_client_inproc_recv, &fuzz_qts); return qtest_inproc_init(&fuzz_qts, false , fuzz_arch, &qtest_server_inproc_recv); } void qtest_server_set_send_handler(void (*send)(void*, const char*), void *opaque) { qtest_server_send = send; qtest_server_send_opaque = opaque; } static void qtest_send (CharBackend *chr, const char *str) { qtest_server_send(qtest_server_send_opaque, str); } QTestState *qtest_inproc_init (QTestState **s, bool log , const char * arch, void (*send)(void*, const char*)) { qtest_client_set_rx_handler(qts, qtest_client_inproc_recv_line); qts->ops.external_send = send; qtest_client_set_tx_handler(qts, send_wrapper); gchar *bin_path = g_strconcat("/qemu-system-" , arch, NULL ); setenv("QTEST_QEMU_BINARY" , bin_path, 0 ); g_free(bin_path); return qts; }
基本就是一些初始化QTestState
结构体的工作,包括一些send
和recv
操作函数的赋值等等。
接下来就是正式开始执行fuzz
的流程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 int LLVMFuzzerTestOneInput (const unsigned char *Data, size_t Size) { static int pre_fuzz_done; if (!pre_fuzz_done && fuzz_target->pre_fuzz) { fuzz_target->pre_fuzz(fuzz_qts); pre_fuzz_done = true ; } fuzz_target->fuzz(fuzz_qts, Data, Size); return 0 ; }
其余的一些函数就比较易懂了,留给读者自行分析。接下来所要说的就是我觉得该作者在集成libfuzzer
过程中做的最棒的一点–设计了一个generic-fuzzer
,也就是一个通用fuzzer
,能够fuzz QEMU
中的任何设备。后续的分析请看(下)篇。