点击上方蓝字 关注我们
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官宣啦
你“在看”我吗?