1. 概述
本文主要是将之前调研的异常测试需求进行一个分类并抽象成不同的场景,然后针对每一个场景给出一些解决方案或者思路。目前大体分为4类:
网络异常,网络相关的异常情况,比如连接超时、接收/发送失败等;
内存异常,内存相关的异常情况,比如内存满、内存分配失败等;
磁盘异常,磁盘相关的异常情况,比如磁盘频繁坏掉、磁盘满等;
程序异常,程序逻辑相关的异常,这个根据不同的数据结构、设计和逻辑会有不同的需求,如函数的参数、返回值修改等。
异常测试的目的是为了测试到一些难以覆盖到的异常情况,如果把程序细分成逻辑单元的组合,我们的目的就是通过各种不同的途径(数据或者代码)来改变逻辑的走向以测试不同的异常情况。
2. 场景抽象及解决方案
2.1. 网络异常
2.1.1. 连接拒绝(connect refused)
在调用connect连接指定IP:PORT时被拒绝的情况。
2.1.1.1. 方案一 指定目的端口为一个没有进程监听的端口
将连接的目的IP指定为某一存在的IP,但是指定PORT为一个不处于listen状态的端口,此时connect会返回connect refused错误。
2.1.1.2. 方案二 Hook网络函数connect
连接被拒绝会返回ECONNREFUSED错误,可以通过hook connect函数,设置errno为ECONNREFUSED,然后编译成so通过LD_PRELOAD环境变量来达到hook的目的。示例步骤如下:
1. 编写hook.c实现connect函数:
#include
#include
#include
#include
#include
#include
extern int errno;
extern "C" {
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen){
srand(time(NULL));
int r = rand() % 100;
if (r < 50) {
//设置需要的errno
errno = ECONNREFUSED;
return -1;
} else {
int (*_connect)(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen) = NULL;
_connect = (int (*)(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen)) dlsym(RTLD_NEXT, "connect");
return _connect(sockfd, serv_addr, addrlen);
}
}
}
2. 编译成so。
g++ -shared -rdynamic -o hook.so -fPIC hook.c -ldl
3. 在程序执行前设置LD_REPLOAD环境变量
export LD_PRELOAD="./hook.so"
2.1.1.3. 方案三 iptables
拒绝发往目的地址的数据,并返回错误给发送方:
iptables -t filter -p tcp -A OUTPUT -d 目标IP --dport 目标端口 -j REJECT --reject-with tcp-reset
2.1.2. 连接超时
使用connect连接时返回timeout的情况。
2.1.3.1. 方案一 Hook网络函数connect
连接超时返回EINPROGRESS,可以采用LD_PRELOAD的hook方法实现connect函数,然后设置errno为EINPROGRESS并且返回-1,具体步骤参考2.1.1.2。
2.1.3.2. 方案二 iptables 丢包
Drop发往目的地址端口的数据(SYN)
iptables -t filter -p tcp -A OUTPUT -d 目标IP --dport 目标端口 -j DROP
或者Drop目的地址返回的数据(ACK)
iptables -t filter -p tcp -A INPUT -s 目标IP --sport 目标端口 -j DROP
2.1.3. 读超时
使用read/recv函数读取数据时超时的情况。
2.1.3.1. 方案一 Hook网络函数recv/read/select等
读超时需要分为blocking和non-blocking2种情况:
socket设置为阻塞时,如果设置了SO_ RCVTIMEO,则recv或者read在超过设定时间未读取到数据后会返回-1,并且设置EAGAIN的错误号,可以使用LD_REPLOAD hook读函数recv/read函数,置errno为EAGAIN并返回-1,具体参考2.1.1.2。
socket设置为非阻塞的情况,recv/read函数是立即返回的,超时的判断一般是通过select/poll/epoll等相关函数来查看,需要同时hook对应的函数,让其返回0,表示timeout。
非阻塞示例如下:
1. 编写hook.c实现recv和select的hook函数:
#include
#include
#include
#include
#include
#include
#include
ssize_t recv(int s, void *buf, size_t len, int flags)
{
errno = EAGAIN;
return -1;
}
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout){
if (NULL != readfds) {
return 0; //返回0表示超时
} else {
int (*_select)(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout) = NULL;
_select = (int (*)(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout))dlsym(RTLD_NEXT, "select");
return _select(n, readfds, writefds, exceptfds, timeout);
}
}
2. 编译成so。
g++ -shared -rdynamic -o hook.so -fPIC hook.c -ldl
3. 在程序执行前设置LD_REPLOAD环境变量
export LD_PRELOAD="./hook.so"
2.1.3.2. 方案二 iptables丢包
建立连接后,Drop目的地址返回的数据
iptables -t filter -p tcp -A INPUT -s 目标IP --sport 目标端口 -j DROP
2.1.3.3. 方案三 teeport限速
利用teeport的限速功能,在client-server类型的通信,如果要模拟client读取server端数据时超时,假设超时时间为5s,可以使用如下方法:
./teeport_2.py -l 50008 -r localhost:50007 --server=11 --stype=total --stime=5000
2.1.4. 写超时
使用write/send发送数据时超时的情况。
2.1.4.1. 方案一 Hook网络函数send/write
写超时也需要分为blocking和non-blocking2种情况:
socket设置为阻塞时,如果设置了SO_ SNDTIMEO,则send/write在超过设定时间未读取到数据后会返回-1,并且设置EAGAIN的错误号,可以使用LD_REPLOAD hook读函数send/write函数,置errno为EAGAIN并返回-1。
socket设置为非阻塞的情况,send/write函数是立即返回的,超时的判断一般是通过select/poll/epoll等相关函数来查看,需要同时hook对应的函数,让其返回0,表示timeout。
具体实现步骤参考2.1.3.1。
2.1.4.2. 方案二 iptables丢包
建立连接后,Drop源端口发出的数据包返回的数据
iptables -t filter -p tcp -A INPUT -s 目标IP --sport 目标端口 -j DROP
2.1.5. 读写过程中连接断开
在调用send/write/recv/read函数进行数据通信时,网络连接断开的情况。
2.1.5.1. 方案一Hook网络函数send/write/recv/read
读写过程中连接断开会返回ECONNRESET错误,可以通过Hook send/write/recv/read函数,返回-1,置errno为ECONNRESET,具体参考2.1.1.2的实现。
2.1.5.2. 方案二 iptables reject
建立连接后,使用iptables reject被测程序发往目标IP:Port的数据包。
iptables -t filter -p tcp -A OUTPUT -d 目标IP --dport 目标端口 -j REJECT --reject-with tcp-reset
2.1.5.3. 方案三 teeport中转断开
用teeport建立转发关系后,在通信过程中kill teeport。
2.1.6. 慢连接
限制网络数据的传输速度,模拟慢连接的情况。
2.1.6.1. 方案一 teeport限速
利用teeport的限速功能,在client-server类型的通信,如果要模拟client读取server端数据时超时,假设超时时间为5s,可以使用如下方法:
./teeport_2.py -l 50008 -r localhost:50007 --server=11 --stype=total --stime=5000 --client=11 --ctype=total --ctime=5000
2.1.6.2. 方案二 iptables限制数据包个数
通过iptables限制单位时间通过的数据包数,例如每分钟只能通过一个数据包:
iptables -A INPUT -p tcp --dport 目标端口 -m limit --limit 1/m --limit-burst 1 -j ACCEPT
iptables -A INPUT -p tcp --dport 目标端口 -j DROP
2.1.7. 异常数据包
模拟由于逻辑异常或者硬件异常导致的数据损坏和丢失。
2.1.7.1. 方案一 通过应用层fuzzing后转发
建立转发关系(类似teeport),在中间层对上游的数据进行修改或者丢弃后,转发给下游。
2.1.7.2. 方案二 Hook网络函数send/write
在上游发送方,使用LD_REPLOAD hook发送函数send或者write,对传进来的buffer中的数据进行修改或者丢弃后,再调用真实的send或write函数。
2.1.7.3. 方案二 Hook网络函数recv/read
在下游接收方,使用LD_REPLOAD hook接收函数recv或者read,先调用原始函数获取接收到的数据,然后修改其buffer参数的数据再返回。
2.2. 内存异常
2.2.1. 内存申请失败
OOM或者硬件异常导致的内存申请失败。
2.2.1.1. 方案一 Hook内存函数malloc
内存申请malloc或者new等都是通过调用malloc函数实现,可以直接hook malloc函数,然后让其return NULL表示申请失败。示例步骤如下:
1. 编写hook.c实现malloc函数,直接让其返回NULL:
#include
#include
#include
void *malloc(size_t size)
{
size_t *(*_malloc)(size_t size) = NULL;
_malloc = (size_t *(*)(size_t size)) dlsym(RTLD_NEXT, "malloc");
return NULL;
}
2. 编译成so。
g++ -shared -rdynamic -o hook.so -fPIC hook.c -ldl
3. 在程序执行前设置LD_REPLOAD环境变量
export LD_PRELOAD="./hook.so"
2.2.1.2. 方案二 使用GNU提供的Memory Allocation Hooks(适用于单测)
使用GNU提供的Memory Allocation Hooks,替换malloc,适用于单测(类似的,通过设置__free_hook变量,可以对free函数进行替换):
#include
static void *my_malloc_hook (size_t size, const void *caller){
return NULL; //直接返回NULL
}
TEST_F(test_armor_create_suite, test_armor_create__param_metanum_1)
{
void *(*old_malloc_hook)(size_t size, const void *caller) = __malloc_hook; //保存旧的Hook
__malloc_hook = my_malloc_hook; //安装自定义Hook
void* mem = malloc(100);
__malloc_hook = old_malloc_hook; //恢复原Hook
ASSERT_EQ((void*)NULL, mem);
}
2.3. 磁盘异常
2.3.1. 磁盘频繁坏掉
磁盘频繁坏掉,表现为经常打开、读或者写失败。
2.3.1.1. 方案一 Hook文件系统函数
文件的打开、读和写操作实际上都是通过调用系统函数open/read/write(或fopen/fread/fwrite)来进行操作的,所以可以考虑自己实现open/read/write(或fopen/fread/fwrite)函数,随机返回错误,然后编译成so通过LD_PRELOAD环境变量来达到hook的目的。示例步骤如下:
1. 编写hook.c实现open/read/write(或fopen/fread/fwrite)函数,如下示例实现50%概率read失败,其它函数类似:
#include
#include
#include
#include
#include
ssize_t read(int fd, void *buf, size_t count)
{
srand(time(NULL));
int r = rand() % 100;
if (r > 50) {
return -1;
} else {
ssize_t (*_read)(int fd, void *buf, size_t count) = NULL;
_read = (ssize_t (*)(int fd, void *buf, size_t count)) dlsym(RTLD_NEXT, "read");
//调用原函数
return _read(fd, buf, count);
}
}
2. 编译成so。
g++ -shared -rdynamic -o hook.so -fPIC hook.c -ldl
3. 在程序执行前设置LD_REPLOAD环境变量
export LD_PRELOAD="./hook.so"
2.3.1.2. 方案二 直接链接so
该方案和上面的方案一基本类似,同样是需要实现系统read/write(或fread/fwrite)等函数,不同的是需要程序重新编译并且把hook.so链接进入,这样就不需要在执行的时候设置LD_PRELOAD环境变量了,如:
g++ -g -Wall -o test test.cpp -L. hook.so
注意,程序拷贝到其它地方执行时需要把hook.so也拷贝过去,否则不会有效果,可以通过ldd查看hook.so是否存在。
2.3.1.3. 方案三 GDB修改
该方案的思路是gdb在指定读写函数的地方下断点,可以是系统的read/write等函数,也可以是业务层的读写封装接口,在函数返回之后修改其返回值为错误,让后面的逻辑接收到磁盘读写错误的信息。不过这种方式在每次读写操作时都需要断点修改,比较麻烦,这里就不具体介绍了。
2.3.2. 磁盘满
磁盘满表现为文件无法写入内容,write函数返回-1(fwrite错误通过ferror判断返回非0),并且errno == ENOSPC错误。
2.3.2.1. 方案一 Hook系统写函数write或fwrite
实现write或fwrite函数,取决于程序所使用的文件操作接口,如果不确定,可以2个函数同时实现,然后编译成so采用LD_PRELOAD来加载。示例:
1. 编写hook.c实现fwrite函数,write函数类似:
#include
#include
#include
#include
#include
#include
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)
{
srand(time(NULL));
int r = rand() % 100;
if (r > 50) {
//设置错误,用户可通过ferror判断
stream->_flags |= _IO_ERR_SEEN;
//设置错误码,perror会输出
errno = ENOSPC;
return 0;
} else {
size_t (*_fwrite)(const void *ptr, size_t size, size_t nmemb, FILE *stream) = NULL;
_fwrite = (size_t (*)(const void *ptr, size_t size, size_t nmemb, FILE *stream)) dlsym(RTLD_NEXT, "fwrite");
return _fwrite(ptr, size, nmemb, stream);
}
}
2. 编译成so。
g++ -shared -rdynamic -o hook.so -fPIC hook.c -ldl
3. 在程序执行前设置LD_REPLOAD环境变量
export LD_PRELOAD="./hook.so"
2.3.2.2. 方案二 直接链接so
参考2.3.1.2。
2.3.3. 文件损坏
文件损坏一般的表现是可以读成功,但是读取到的内容不正确。方案一以假乱真,采用的是hook系统读函数,修改其返回的buffer;方案二偷梁换柱,采用的是hook文件打开函数,修改其文件名参数指向自己修改过的数据文件,这样之后的读写操作都是在自己指定的数据文件中进行。
2.3.3.1. 方案一 Hook系统读函数read或fread
文件损坏后读取到的内容不正确,可以通过hook系统读函数read或fread,通过将实际读取到的buffer改写,或者随机写入一串数据进行fuzzing中,用户获取到的即是不正确的内容,但实际数据文件没有被破坏,仍可以下次正常读取。示例:
4. 编写hook.so实现fread函数,实现50%概率破坏文件,read函数类似:
#include
#include
#include
#include
#include
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)
{
size_t (*_fread)(void *ptr, size_t size, size_t nmemb, FILE *stream) = NULL;
_fread = (size_t (*)(void *ptr, size_t size, size_t nmemb, FILE *stream)) dlsym(RTLD_NEXT, "fread");
size_t ret = _fread(ptr, size, nmemb, stream);
srand(time(NULL));
int r = rand() % 100;
if (r > 50) {
snprintf((char *)ptr, ret, "data is broken", r);
}
return ret;
}
5. 编译成so。
g++ -shared -rdynamic -o hook.so -fPIC hook.c -ldl
6. 在程序执行前设置LD_REPLOAD环境变量
export LD_PRELOAD="./hook.so"
2.3.3.2. 方案二 Hook文件打开函数open或fopen
方案一是直接在读的过程中修改数据,而方案二是直接“替换”了原文件,文件的内容可以随意的构造,对于一个全局把控更方便些,一般来说可以拷贝一份原文件对其修改。示例:
1. 编写hook.so实现fopen函数,如果文件名是data.txt则替换为hook.txt。
#include
#include
#include
#include
#include
#include
FILE *fopen(const char *path, const char *mode)
{
FILE *(*_fopen)(const char *path, const char *mode) = NULL;
_fopen = (FILE *(*)(const char *path, const char *mode)) dlsym(RTLD_NEXT, "fopen");
if(strcmp(path, "data.txt") == 0) {
char *hook_filename = "hook.txt";
return _fopen(hook_filename, mode);
}
return _fopen(path, mode);
}
2. 编译成so。
g++ -shared -rdynamic -o hook.so -fPIC hook.c -ldl
3. 在程序执行前设置LD_REPLOAD环境变量
export LD_PRELOAD="./hook.so"
2.3.4. 数据fuzzing
数据fuzzing的目的是为了随机的改写数据文件来检测程序的稳定性,其解决方案和2.3.3节的“文件损坏”基本类似,可以直接参考。
2.4. 程序异常
2.4.1. 正常的逻辑错误返回
正常的逻辑错误返回,即本来是要返回正确的值,实际却返回了错误的值,改变了正常的逻辑走向,主要是为了让下游能走到异常的处理逻辑来测试不同的情况。示例图如下,正常的流程是A -> B -> C,D是异常处理逻辑,比较难构造数据走到,这时我们可以hook函数B,让其返回错误值,这样就可以走到D逻辑来测试。
17 #include
18
19 void A()
20 {
21 printf("this is A\n");
22 }
23 int B()
24 {
25 printf("this is B\n");
26 return 0;
27 }
28 void C()
29 {
30 printf("this is C\n");
31 }
32 void D()
33 {
34 printf("this is D\n");
35 }
36 int main()
37 {
38 A();
39 int ret = B();
40 if(ret == 0) {
41 C();
42 } else {
43 D();
44 }
45 return 0;
46 }
2.4.3.1. 方案一 Hook关键静态函数
根据上面的代码示例,为了要走到D函数的逻辑,需要让B函数返回非0,而正常情况下B很难或者不会走到D函数,如上函数B始终return 0,这时可以通过testdbg hook函数B,让其返回非0值来测试D函数的逻辑。示例:
1. 编写hook.cpp实现B函数的hook版本,50%概率返回-1,50%概率调用原函数:
#include
#include
#include
#include "hookmon.h"
int (* old_B)();
int B()
{
srand(time(NULL));
int r = rand() % 100;
if(r > 50) {
printf("this is hook B\n");
return -1;
} else {
return old_B();
}
}
void __attribute__ ((constructor)) hook_init(void)
{
attach_func("B", (void *)B, (void **)&old_B);
}
2. 编译成hook.so,需要依赖testdbg的include。
WORKROOT=../../../../../..
TESTDBG=$(WORKROOT)/svn/com-test/itest/tools/testdbg/output
hook.so : hook.cpp
g++ -shared -rdynamic -o $@ -fPIC $< -I$(TESTDBG)/include
3. 用testdbg进行启动执行,执行脚本如下:
#!/bin/sh
WORKROOT=../../../../../..
TESTDBG=$WORKROOT/svn/com-test/itest/tools/testdbg/output
$TESTDBG/bin/testdbg -l $TESTDBG/bin/hookmon.so -s ./hook.so ./test
2.4.3.2. 方案二 GDB修改
GDB修改有很多种方法,前期是程序需要用-g来编译,可以在函数B内(26行)下断点,然后让其return -1,也可以在40行下断点,if判断时修改ret的值为非0,不过这种方法需要每次都得修改,当然也可以将这些命令序列存成一个文本文件,然后通过gdb < cmds来执行。示例:
1. 将以下gdb命令保存到文本cmds中:
file test
b 40
r
set var ret=-1
c
2. gdb执行
gdb < cmds
2.4.2. 触发信号量处理函数
触发自定义的信号量处理函数一般来说可以直接产生相应的信号给程序。如果没有其它条件要求,只是希望触发函数可以采用方案一;如果需要在指定的时候发送信号量则需要在关键点hook进行逻辑判断再使用kill函数发送信号,参考方案二。
2.4.2.1. 方案一 命令行发送信号
命令行下直接通过kill –s signal pid来发送。示例:
1. 发送SIGIO信号给pid=22651的进程
kill -s SIGIO 22651
2.4.2.2. 方案二 程序发送信号
该方案是为了解决需要在指定的地方发送信号的问题,通过testdbg进行关键函数的hook,在函数开始前进行kill(0, signal)操作,然后直接调用原函数返回。示例:
1. 编写hook.cpp实现func函数的hook,先是kill发送SIGIO信号,再调用原函数:
#include
#include
#include
#include "hookmon.h"
int (* old_func)();
int func()
{
kill(0, SIGIO);
return old_func();
}
void __attribute__ ((constructor)) hook_init(void)
{
attach_func("func", (void *)func, (void **)&old_func);
}
2. 编译成hook.so,需要依赖testdbg的include。
WORKROOT=../../../../../..
TESTDBG=$(WORKROOT)/svn/com-test/itest/tools/testdbg/output
hook.so : hook.cpp
g++ -shared -rdynamic -o $@ -fPIC $< -I$(TESTDBG)/include
3. 用testdbg进行启动执行,执行脚本如下:
#!/bin/sh
WORKROOT=../../../../../..
TESTDBG=$WORKROOT/svn/com-test/itest/tools/testdbg/output
$TESTDBG/bin/testdbg -l $TESTDBG/bin/hookmon.so -s ./hook.so ./test
2.4.3. 时序问题
时序问题一般在多模块或者多线程方面有较多存在,测试中往往希望程序按照某种特定的顺序来执行,这种情况需要细化到具体场景,然后在关键的地方采用hook的方式修改其逻辑,或者在hook函数前sleep之类的以达到所想要的执行顺序,hook的方法参考2.4.1。
(作者:xuanbiao、linyi)