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中的任何设备。后续的分析请看(下)篇。