Fuzz方法在SPDK iSCSI的应用实例

点击上方蓝字 关注我们

Fuzz方法在SPDK iSCSI的应用实例王海亮 | 英特尔SPDK测试工程师

Fuzz 简介

本文目的是介绍如何使用fuzz方法写出测试代码。首先了解一下fuzz的概念。

1

Fuzz Test 是什么

中文可翻译为模糊测试。就是用大量的测试用例一个一个试,尽可能多的找出有可能出问题的地方。第一个模糊测试工具,最初由Barton Miller于1989年在威斯康星大学开发。模糊测试是一种软件测试技术,它是一种安全测试。

在模糊测试中,用随机坏数据(也称做 fuzz)攻击一个程序,然后等待观察哪里遭到了破坏。模糊测试的技巧在于,它是不符合逻辑的。自动模糊测试不去猜测哪个数据会导致破坏(就像人工测试员那样),而是将尽可能多的杂乱数据投入程序中。经过这个测试验证过的失败模式通常对程序员来说是个彻底的震憾,因为任何按逻辑思考的人可能都不会想到这种失败。

2

为什么要进行模糊测试?

  • 可能发现最严重的安全故障或缺陷。

  • 当与Black Box测试、Beta测试和其他调试方法一起使用时,模糊测试可以提供更有效的结果。

  • 模糊测试用于检查软件的漏洞。这是非常划算的测试技术。

  • 模糊测试是黑盒测试技术之一。模糊测试是黑客用来发现系统漏洞的最常见方法之一。 

3

模糊测试能检测到的错误类型

  • 断言失败和内存泄漏

    此方法广泛用于大型应用程序,其中的错误会影响内存的安全性,这是一个严重的漏洞。

  • 输入无效

    在模糊测试中,模糊器用于生成无效输入,用于测试错误处理例程,这对于不控制其输入的软件很重要。简单的模糊测试可以被称为自动化负面测试(Negative Testing)的一种方法。

  • 正确的错误

    模糊测试也可用于检测某些类型的“正确性”错误。如数据库损坏,搜索结果不佳等。

4

模糊测试实现过程

1) 找到一份待测试的可执行文件代码;

2) 生成大量的测试用例(Fuzzed数据)找到输入点,然后把随机数据丢进去;

3) 执行文件;

4) 观察破坏了什么;

5) 记录缺陷。

5

模糊测试的优缺点

好处

  • 模糊测试改进了软件安全测试。

  • 在模糊测试中发现的错误有时很严重,包括崩溃、内存泄漏、未处理的异常等。

  • 如果由于时间和资源的限制,测试人员可能没有注意到的一些错误,那么在模糊测试中会发现。

缺点

  • 仅靠模糊测试无法保证整体安全。

  • 模糊测试在处理不会导致程序崩溃的安全威胁方面效果较差,例如某些病毒、蠕虫、特洛伊木马等。

  • 模糊测试只能检测简单的故障或威胁。

  • 要有效地执行,需要大量时间。

  • 使用随机输入设置边界值条件是非常有问题的,但现在使用基于用户输入的确定性算法,大多数测试人员解决了这个问题。


SPDK iSCSI应用实例

1

被测对象

确定需要进行fuzz测试的文件代码。这里以spdk的lib/iscsi.c为被测对象。我们编写的fuzz app代码为spdk/test/app/fuzz/iscsi_fuzz/iscsi_fuzz.c。

2

了解iSCSI流程

iSCSI结构简介

iSCSI使用Client/Server模型。Target端即磁盘阵列或其他装有磁盘的主机。通过iSCSI Target工具将磁盘空间映射到网络上,initiator端就可以寻找发现并使用该磁盘。

Figure 1 给出了iSCSI结构里不同部分之间的关系。Figure 2 给出了iSCSI中数据传输的简单流程。

Figure 1:iSCSI结构

Figure 2:数据传输的简单流程

PDU(Protocol Data Units)是iSCSI交换数据最基本的单位,格式见Figure 3。

Figure 3:iSCSI PDU的格式

其中,基本报文头BHS(Basic Header Segment)的格式见Figure 4。

Figure 4:BHS的格式

接下来,我们要用大量的fuzz输入用例,来模拟填充PDU的主要数据结构,这里重点是填充BHS,以及其中的操作码(opcode)。各个操作码的意义见Figure5。

代码里对BHS操作码的定义:

1.  enum iscsi_op {  

2.      /* Initiator opcodes */  

3.      ISCSI_OP_NOPOUT         = 0x00,  

4.      ISCSI_OP_SCSI           = 0x01,  

5.      ISCSI_OP_TASK           = 0x02,  

6.      ISCSI_OP_LOGIN          = 0x03,  

7.      ISCSI_OP_TEXT           = 0x04,  

8.      ISCSI_OP_SCSI_DATAOUT   = 0x05,  

9.      ISCSI_OP_LOGOUT         = 0x06,  

10.    ISCSI_OP_SNACK          = 0x10,  

11.    ISCSI_OP_VENDOR_1C      = 0x1c,  

12.    ISCSI_OP_VENDOR_1D      = 0x1d,  

13.    ISCSI_OP_VENDOR_1E      = 0x1e,  

14.  

15.    /* Target opcodes */  

16.    ISCSI_OP_NOPIN          = 0x20,  

17.    ISCSI_OP_SCSI_RSP       = 0x21,  

18.    ISCSI_OP_TASK_RSP       = 0x22,  

19.    ISCSI_OP_LOGIN_RSP      = 0x23,  

20.    ISCSI_OP_TEXT_RSP       = 0x24,  

21.    ISCSI_OP_SCSI_DATAIN    = 0x25,  

22.    ISCSI_OP_LOGOUT_RSP     = 0x26,  

23.    ISCSI_OP_R2T            = 0x31,  

24.    ISCSI_OP_ASYNC          = 0x32,  

25.    ISCSI_OP_VENDOR_3C      = 0x3c,  

26.    ISCSI_OP_VENDOR_3D      = 0x3d,  

27.    ISCSI_OP_VENDOR_3E      = 0x3e,  

28.    ISCSI_OP_REJECT         = 0x3f,  

29.};  

Figure 5:操作码的意义

3

生成fuzz数据

iSCSI主要的数据处理流程包括iscsi_pdu_hdr_handle( )和iscsi_pdu_payload_handle( )这两个函数。

iscsi_pdu_hdr_handle( )代码如下:

1.  static int  

2.  iscsi_pdu_hdr_handle(struct spdk_iscsi_conn *conn, struct spdk_iscsi_pdu *pdu)  

3.  {  

4.    

5.      if (opcode == ISCSI_OP_LOGIN) {  

6.          return iscsi_pdu_hdr_op_login(conn, pdu);  

7.      }  

8.    

9.      switch (opcode) {  

10.    case ISCSI_OP_NOPOUT:  

11.        rc = iscsi_pdu_hdr_op_nopout(conn, pdu);  

12.    case ISCSI_OP_SCSI:  

13.        rc = iscsi_pdu_hdr_op_scsi(conn, pdu);  

14.    case ISCSI_OP_TASK:  

15.        rc = iscsi_pdu_hdr_op_task(conn, pdu);  

16.    case ISCSI_OP_TEXT:  

17.        rc = iscsi_pdu_hdr_op_text(conn, pdu);  

18.    case ISCSI_OP_LOGOUT:  

19.        rc = iscsi_pdu_hdr_op_logout(conn, pdu);  

20.    case ISCSI_OP_SCSI_DATAOUT:  

21.        rc = iscsi_pdu_hdr_op_data(conn, pdu);  

22.    case ISCSI_OP_SNACK:  

23.        rc = iscsi_pdu_hdr_op_snack(conn, pdu);  

24.    default:  

25.        return iscsi_reject(conn, pdu, ISCSI_REASON_PROTOCOL_ERROR);  

26.    }  

Fuzz的任务,就是来填充包含opcode的整个BHS结构体。

1.  struct iscsi_bhs {  

2.      uint8_t opcode      : 6

3.      uint8_t immediate   : 1;  

4.      uint8_t reserved    : 1;  

5.      uint8_t flags;  

6.      uint8_t rsv[2];  

7.      uint8_t total_ahs_len;  

8.      uint8_t data_segment_len[3];  

9.      uint64_t lun;  

10.    uint32_t itt;  

11.    uint32_t ttt;  

12.    uint32_t stat_sn;  

13.    uint32_t exp_stat_sn;  

14.    uint32_t max_stat_sn;  

15.    uint8_t res3[12];  

16. };  

Request消息,根据自身BHS的opcode来进入iscsi_pdu_hdr_handle( )不同的处理分支。

填充BHS的函数是prep_iscsi_pdu_bhs_opcode_cmd(),

1.  static void  

2. prep_iscsi_pdu_bhs_opcode_cmd(struct fuzz_iscsi_dev_ctx *dev_ctx, struct fuzz_iscsi_io_ctx *io_ctx)  

3.  {  

4.      io_ctx->iov_ctx.iov_req.iov_len = sizeof(struct iscsi_bhs);  

5.      fuzz_fill_random_bytes((char *)io_ctx->req.bhs, sizeof(struct iscsi_bhs),  

6.                     &dev_ctx->random_seed);  

7.  }  

其中函数fuzz_fill_random_bytes( ),用来生成随机数据,填充BHS各个字段。

1.  static void  

2.  fuzz_fill_random_bytes(char *character_repr, size_t len, unsigned int *rand_seed)  

3.  {  

4.      size_t i;  

5.    

6.      for (i = 0; i < len; i++) {  

7.          character_repr[i] = rand_r(rand_seed) % UINT8_MAX;  //生成随机数据

8.      }  

9.  } 

不仅要填充bhs,还需要填充适量的PDU的数据结构。

1.  struct spdk_iscsi_pdu {  

2.      struct iscsi_bhs bhs;  

3.      struct spdk_mobj *mobj;  

4.      bool is_rejected;  

5.      uint8_t *data_buf;  

6.      uint8_t *data;  

7.      uint8_t header_digest[ISCSI_DIGEST_LEN];  

8.      uint8_t data_digest[ISCSI_DIGEST_LEN];  

9.      size_t data_segment_len;  

10.......  

因为包处理是以PDU为对象的,缺少参数会导致意外的错误,第一步验证条件就被挡住了。所以,为了能深入尽可能多的分支,需要填充一些基本的PDU参数。

例如这样,

1.  req_pdu->writev_offset = 0;  

2.  req_pdu->hdigest_valid_bytes = 0;  

3.  req_pdu->ahs_valid_bytes = 0;  

4.  req_pdu->data_buf_len = 0;  

同样是为了能深入尽可能多的分支,还需要重新指定一些基本的BHS参数。并且,需要对login这一特殊的的PDU进行单独地处理。

1.  if (opcode == ISCSI_OP_LOGIN) {  

2.      return iscsi_pdu_hdr_op_login(conn, pdu);  

3.  }  

4.  req_pdu->bhs.immediate = 1;  

5.  req_pdu->bhs.reserved = 0;  

6.  req_pdu->bhs_valid_bytes = ISCSI_BHS_LEN;  

7.  req_pdu->bhs.total_ahs_len = 0;  

8.  req_pdu->bhs.stat_sn = 0;  

4

执行结果

最开始的时候,initiator端发送的第一个包是login request,用以跟target端建立connection。成功后,从第二个开始就可以都是随机包了。如果没有建立connection的话,target端不会处理任何一个来自initiator的PDU包。

Received payload_handle response opcode from Target is 0x23.(这是target回过来的第一个response包,LOGIN_RSP)

无效包的处理,target端返回REJECT包。

例如:发出请求0x1e(无效包),返回响应0x3f(REJECT包)

Random request bhs.opcode of Initiator is 0x1e.(随机生成的无效包)

Dumping this request bhs contents now.

"bhs": {

  "opcode": 30, (0x1e十进制)

  "immediate": 1,

  "reserved": 0,

  "total_ahs_len":0,

 "data_segment_len": "AAAA",

  "itt":1658750280,

  "exp_stat_sn":4117834500

}

Sent an invalid opcode PDU.(这是一个非法包)

Received rejected hdr_handle response opcode(0x3f) from Target.

Received payload_handle response opcode from Target is 0x3f.(REJECT包)

有效包的处理,target端返回相应的response包。

例如:发出请求0x2(TASK),返回响应0x24(TASK_RSP)

Random request bhs.opcode of Initiator is 0x4.(随机生成的TASK包)

Dumping this request bhs contents now.

"bhs": {

  "opcode": 4,

  "immediate": 1,

  "reserved": 0,

  "total_ahs_len":0,

 "data_segment_len": "AAAA",

  "itt": 0,

  "exp_stat_sn":4138086897

}

Sent a valid opcode PDU.(这是一个合法包)

Received hdr_handle response opcode from Target is 0x24. (target返回正确的response)

Received payload_handle response opcode from Target is 0x24.

 

可以看出,结果是符合预期的。这里测试时间设定30秒。完成时,fuzz app模拟的initiator端一共向target端发送了17447个合法随机包,161259个非法随机包。

Fuzzing completed. Shutting down the fuzz application.

device 0x1efc200 stats: Sent 17447 valid opcode PDUs, 161259invalid opcode PDUs.

具体执行参数,请参考shell脚本spdk/test/iscsi_tgt/fuzz/fuzz.sh。

5

通过fuzz发现的一个issue

例如在iscsi_reject()函数里,要把pdu->ahs指向的数据拷贝到data + data_len的地址。原先的代码没有考虑到多条路径过来的验证越界问题。即如果(4 * total_ahs_len)大于ISCSI_AHS_LEN时,源数据长度超过目标缓冲区长度,返回地址乱了,会导致Segmentation Fault的错误。

Fuzz随机生成了一个比较狂野的bhs.total_ahs_len的值,超过了ISCSI_AHS_LEN的范围,暴露了这个问题。代码见Figure 6,绿底是修改后的。

Figure 6:lib/iscsi.c

写在最后,如果仅仅用fuzz来模拟bhs->opcode,作用会非常有限。Iscsi.c代码里iscsi_pdu_hdr_handle( )函数的switch (opcode)部分代码已经对各种opcode做了处理。但是用fuzz来模拟填充各种BHS结构体的字段内容,就能进入更多的其他代码分支,就能发现更多潜在的问题。同样地,可以再扩大fuzz的范围,来模拟填充更多PDU结构体的字段,这样会覆盖更多的代码分支。

转载须知

DPDK与SPDK开源社区公众号文章转载声明

推荐阅读

SPDK发布v20.04版本

重磅消息! SPDK, PMDK and VTune™ Profiler US Virtual Forum官宣啦

你“在看”我吗?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
SPDK(存储性能开发套件)官方文档中文版。 第一章 简介 1 1.1.什么是SPDK? 1 1.2.入门 1 1.3. Vagrant开发环境 3 1.4.更新日志(略) 6 第二章 概念 6 2.1. 用户空间驱动程序** 6 2.2. 来自用户空间的DMA** 7 2.3. 消息传递和并发** 9 2.4. NAND Flash SSD内部 13 2.5. 将I / O提交到NVMe设备** 15 2.5.1 NVMe规范 15 2.5.2 SPDK NVMe驱动程序I / O路径 15 2.6. 使用Vhost-user进行虚拟化I / O. 16 2.6.1 介绍 16 2.6.2 QEMU 17 2.6.3 设备初始化 18 2.6.4 I / O路径 19 2.6.5 SPDK优化 20 2.7. SPDK目录结构概述 20 2.8. SPDK移植指南 22 第三章 用户指南 22 3.1. 系统配置用户指南 22 3.1.1 IOMMU配置 22 3.2. SPDK应用程序概述 23 3.2.1 配置SPDK应用程序 23 3.3. iSCSI Target 26 3.3.1. iSCSI Target入门指南 26 3.3.2. 通过配置文件配置iSCSI Target 27 3.3.3. 通过RPC方法配置iSCSI Target 28 3.3.4. 配置iSCSI启动器 29 3.3.5. rpc配置示例*** 30 3.3.6. iSCSI 热插拔 32 3.4. NVMe over Fabrics Target 32 3.5. Vhost Target(略) 37 3.6 块设备用户指南 38 3.6.1 bdev介绍 38 3.6.2 通用RPC命令 38 3.6.3 Ceph RBD 39 3.6.4 压缩虚拟Bdev模块 40 3.6.5 加密虚拟Bdev模块 41 3.6.6 延迟vbdev模块 41 3.6.7 GPT(GUID分区表) 42 3.6.8 iSCSI bdev 43 3.6.9 Linux AIO bdev 43 3.6.10 OCF虚拟bdev 43 3.6.11 Malloc bdev 44 3.6.12 NULL bdev 44 3.6.13 NVMe bdev 44 3.6.14 逻辑卷Lvol 45 3.6.15 RAID 46 3.6.16 Passthru 46 3.6.17 Pmem 46 3.6.18 Virtio Block 47 3.6.19 Virtio SCSI 47 3.7 BlobFS(Blobstore文件系统) 48 3.7.1 RocksDB集成 48 3.7.2 FUSE插件 49 3.8 JSON-RPC方法(略) 49 第四章 程序员指南 49 4.1. Blobstore程序员指南 49 4.1.1 介绍 50 4.1.2 运作理论 50 4.1.3 设计注意事项 52 4.1.4 例子 54 4.1.5配置 54 4.1.6 组件细节 54 4.2. 块设备层编程指南 56 4.3 编写自定义块设备模块 58 4.3.1 介绍 58 4.3.2 创建一个新模块 59 4.3.3创建虚拟Bdev 60 4.4 NVMe over Fabrics目标编程指南 61 4.4.1 介绍 61 4.4.2 原语结构体 61 4.4.3 基础函数 62 4.4.4访问控制 62 4.4.5发现子系统 62 4.4.6 传输 63 4.4.7选择线程模型 63 4.4.8 跨CPU核心扩展 63 4.4.9 零拷贝支持 63 4.4.10 RDMA 63 4.5 Flash传输层 64 4.5.1 术语 64 4.5.2 使用方法 67 4.6 GDB宏用户指南 69 4.6.1 介绍 69 4.6.2 加载gdb宏 71 4.6.3 使用gdb数据目录 72 4.6.4 使用.gdbinit加载宏 72 4.6.5 为什么我们需要显式调用spdk_load_macros 72 4.6.6 以上可用的宏总结 73 4.6.7 添加新宏 73 4.7 SPDK “Reduce”块压缩算法 73 4.7.1 介绍 73 4.7.2 例子 74 4.8 通知库 78 第五章 基本信息 79 5.1 事件框架 79 5.1.1 事件框架设计注意事项 80 5.1.2 SPDK事件框架组件 80 5.1.3 应用框架 80 5.2 逻辑卷 81 5.2.1 术语 81 5.2.2 配置逻辑卷 84 5.3 矢量数据包处理(略) 86 第六章 杂项 86 6.1 介绍 86 6.2 NVMe的P2P API 86 6.3 确定设备支持 87 6.4 P2P问题 87 第七章 驱动程序 88 7.1 NVMe驱动程序*** 88 7.1.1 介绍 88 7.1.2 例子 88 7.1.3 公共接口 89 7.1.4 NVMe驱动程序设计 89 7.1.5 NVMe over Fabrics主机支持 91 7.1.6 NVMe多进程 91 7.1.7 NVMe Hotplug 92 7.2 I/OAT驱动程序 93 7.2.1 公共接口 93 7.2.2 关键功能 93 7.3 Virtio驱动程序 93 7.3.1 介绍 93 7.3.2 2MB大页面 93 第八章 工具 94 8.1 SPDK CLI 94 8.1.1 安装所需的依赖项 94 8.1.2 运行SPDK应用程序实例 94 8.1.3 运行SPDK CLI 94 8.1.4 可选 - 创建Python虚拟环境 94 8.2 nvme-CLI 95 8.2.1 nvme-cli with SPDK入门指南 95 8.2.2 使用场景 95 第九章 性能测试报告(略) 96 第十章NVMe-oF Target跟踪点*** 96 10.1 介绍 96 10.2 启用跟踪点 97 10.3 捕获事件的快照 97 10.4 捕获足够的跟踪事件 98 10.5 添加新的跟踪点 99
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值