美文网首页
符号执行(1) - 自动生成覆盖率用例之利器

符号执行(1) - 自动生成覆盖率用例之利器

作者: Jtag特工 | 来源:发表于2020-10-14 20:37 被阅读0次

符号执行(1) - 自动生成覆盖率用例之利器

对于安全性要求比较高的软件,为了防止出现安全漏洞,我们不得不花大量时间写更多的测试用例来提升覆盖率。尤其是高可靠性软件需要的修正条件判定覆盖MC/DC(Modified Condition/Decision Coverage),更是要多花不少心思。

全靠手工写,工作量太大,而且重复性工作不少。靠模糊测试命中的话效率又比较低。
那么,有没有什么办法可以将这些机械的工作做得自动化一点,机器能够帮我们设计一些测试用例呢?符号执行就是一种可用的利器。

什么是符号执行

为了避免有同学望文生义,我们先解释下符号执行的含义。符号执行是借助程序的形式化语义来分析代码的一种方法,具体地说,不考虑循环的情况下,符号执行就是求解霍尔逻辑的最弱前置条件。
这里面主要的工具除了霍尔逻辑的公理外,主要还会用到可满足性模理论SMT工具。后面我们讲符号执行工具klee时大家就会看到,相当多的步骤其实我们是在准备SMT工具。

有个简单的概念之后,我们迅速进入通过例子学习的阶段。对于跟安全性打交道不多的同学来说,完全不懂Hoare Logic, SMT, SAT这些概念不影响使用符号执行工具来帮我们找出一些测试用例。

通过例子学习klee符号执行

下面我们就以klee为例来讲解下如何在工作中使用符号执行来帮我们生成测试用例。

klee是用于C/C++的符号执行工具,也有达人研究中应用于Rust等语言的用法。通过后面的例子可以看到,只要能生成llvm byte code,应该都有办法来执行。

我们先写个待测函数,将百分制的分数映射成ABCD等级:

char testscore(int score)
{
    if (score > 100)
    {
        return 'E';
    }
    else if (score < 0)
    {
        return 'E';
    }
    else if (score >= 90)
    {
        return 'A';
    }
    else if (score >= 80)
    {
        return 'B';
    }
    else if (score >= 60)
    {
        return 'C';
    }
    else
    {
        return 'D';
    }
}

既然是符号执行,我们写测试用例时不给具体值,只给一个符号,然后让klee帮我们去找该测什么值。这通过klee_make_symbolic函数来实现,我们给上面的testscore写个main函数来调用:

int main()
{
    int score;
    klee_make_symbolic(&score, sizeof(score), "score");
    return (testscore(score));
}

我们用clang来编译它,生成llvm中间代码testscore.bc:

clang -emit-llvm -c testscore.c

然后我们就调用klee去自动执行上一步编译出的字节码:

klee testscore.bc

输出如下:

KLEE: output directory is "/workspace/xulun/github/libs/klee-out-3"
KLEE: Using STP solver backend

KLEE: done: total instructions = 61
KLEE: done: completed paths = 6
KLEE: done: generated tests = 6

以上说明,总共61条指令,klee为我们发现了6个分支,并生成了覆盖这6个分支的测试用例。

我们通过klee-stats工具来看下生成的用例的覆盖率:

----------------------------------------------------------------------------
|    Path     |  Instrs|  Time(s)|  ICov(%)|  BCov(%)|  ICount|  TSolver(%)|
----------------------------------------------------------------------------
|./klee-out-3/|      61|     0.05|   100.00|   100.00|      41|       96.17|
----------------------------------------------------------------------------

我们看到,语句和分支的覆盖率都是100%,干得不错。

下面我们用ktest-tool工具来看下生成的6个测试用例的值是什么:

ktest-tool ./klee-out-3/test000001.ktest
ktest file : './klee-out-3/test000001.ktest'
args       : ['testscore.bc']
num objects: 1
object 0: name: 'score'
object 0: size: 4
object 0: data: b'\xff\xff\xff\x7f'
object 0: hex : 0xffffff7f
object 0: int : 2147483647
object 0: uint: 2147483647
object 0: text: ....
[root@7a5293f64325 libs]# ktest-tool ./klee-out-3/test000002.ktest
ktest file : './klee-out-3/test000002.ktest'
args       : ['testscore.bc']
num objects: 1
object 0: name: 'score'
object 0: size: 4
object 0: data: b'\x00\x00\x00\x80'
object 0: hex : 0x00000080
object 0: int : -2147483648
object 0: uint: 2147483648
object 0: text: ....
[root@7a5293f64325 libs]# ktest-tool ./klee-out-3/test000003.ktest
ktest file : './klee-out-3/test000003.ktest'
args       : ['testscore.bc']
num objects: 1
object 0: name: 'score'
object 0: size: 4
object 0: data: b'\x00\x00\x00\x00'
object 0: hex : 0x00000000
object 0: int : 0
object 0: uint: 0
object 0: text: ....
[root@7a5293f64325 libs]# ktest-tool ./klee-out-3/test000004.ktest
ktest file : './klee-out-3/test000004.ktest'
args       : ['testscore.bc']
num objects: 1
object 0: name: 'score'
object 0: size: 4
object 0: data: b'Z\x00\x00\x00'
object 0: hex : 0x5a000000
object 0: int : 90
object 0: uint: 90
object 0: text: Z...
[root@7a5293f64325 libs]# ktest-tool ./klee-out-3/test000005.ktest
ktest file : './klee-out-3/test000005.ktest'
args       : ['testscore.bc']
num objects: 1
object 0: name: 'score'
object 0: size: 4
object 0: data: b'<\x00\x00\x00'
object 0: hex : 0x3c000000
object 0: int : 60
object 0: uint: 60
object 0: text: <...
[root@7a5293f64325 libs]# ktest-tool ./klee-out-3/test000006.ktest
ktest file : './klee-out-3/test000006.ktest'
args       : ['testscore.bc']
num objects: 1
object 0: name: 'score'
object 0: size: 4
object 0: data: b'P\x00\x00\x00'
object 0: hex : 0x50000000
object 0: int : 80
object 0: uint: 80
object 0: text: P...

通过读取test000001.ktest到test000006.ktest这6个文件,我们发现,系统帮我们找到的score值分别为:2147483647,-2147483648,0,90,60,80。最大正值,最小负值和0都被考虑到了,还有代码中区分不同分支的60,80,90都被自动找到了。

两个参数的例子

单找一个参数不过瘾,我们再来试试两个参数的。
先来个最简单的,求两个数的最大值吧:

#include "klee/klee.h"

int max2(int a, int b){
    if(a>b){
        return a;
    }else{
        return b;
    }
}

int main()
{
    int a,b;
    klee_make_symbolic(&a, sizeof(a), "a");
    klee_make_symbolic(&b, sizeof(b), "b");
    return (max2(a,b));
}

老办法,编译成bc字节码:

clang -emit-llvm -c max.c

然后运行klee max.bc:

klee max.bc
KLEE: output directory is "/workspace/xulun/github/libs/klee-out-4"
KLEE: Using STP solver backend

KLEE: done: total instructions = 32
KLEE: done: completed paths = 2
KLEE: done: generated tests = 2

这个只有两个分支,所以klee给我们也就找到两个。
再用klee-stats看下覆盖率:

# klee-stats ./klee-out-4/
----------------------------------------------------------------------------
|    Path     |  Instrs|  Time(s)|  ICov(%)|  BCov(%)|  ICount|  TSolver(%)|
----------------------------------------------------------------------------
|./klee-out-4/|      32|     0.00|   100.00|   100.00|      29|       65.62|
----------------------------------------------------------------------------

带循环的例子

下面我们再挑战个复杂点的例子,带循环结构的例子。
我们以辗转相除法求最大公约数为例子吧:

#include "klee/klee.h"

short gcd(short a, short b){
    short a0 = a;
    short b0 = b;
    short c0 = 0;
    if(a<=0 || b<=0){
        return 0;
    }

    if(a<b){
        a0 = b;
        b0 = a;
    }

    for(;;){
        c0 = a0 % b0;
        if(c0==0){
            return b0;
        }else{
            a0 = b0;
            b0 = c0;
        }
    }

    return 1;
}

int main()
{
    short a,b;
    klee_make_symbolic(&a, sizeof(a), "a");
    klee_make_symbolic(&b, sizeof(b), "b");
    return (gcd(a,b));
}

大家看到,这个例子我们没用int类型,而是使用的short,这样是因为int需要运行的时间较长,光short类型klee就为我们发现了44个case,大约会占满一个CPU核几分钟左右。

klee gcd2.bc
KLEE: output directory is "/workspace/xulun/github/libs/klee-out-5"
KLEE: Using STP solver backend

KLEE: done: total instructions = 1431
KLEE: done: completed paths = 44
KLEE: done: generated tests = 44

我们看下coverage:

# klee-stats ./klee-out-5/
----------------------------------------------------------------------------
|    Path     |  Instrs|  Time(s)|  ICov(%)|  BCov(%)|  ICount|  TSolver(%)|
----------------------------------------------------------------------------
|./klee-out-5/|    1431|   212.26|    98.75|    90.00|      80|       99.99|
----------------------------------------------------------------------------

因为这个是测试最大公约数,klee能够帮助我们生成例子能帮我们省不少事。
因为a和b都是0的case已经可以cover到a<=0 || b<=0这一分支,所以klee除了第一个case是0,0之外,后面全是正的有效例子。
第一个是0,0:

ktest-tool ./klee-out-6/test000001.ktest
ktest file : './klee-out-6/test000001.ktest'
args       : ['gcd3.bc']
num objects: 2
object 0: name: 'a'
object 0: size: 2
object 0: data: b'\x00\x00'
object 0: hex : 0x0000
object 0: int : 0
object 0: uint: 0
object 0: text: ..
object 1: name: 'b'
object 1: size: 2
object 1: data: b'\x00\x00'
object 1: hex : 0x0000
object 1: int : 0
object 1: uint: 0
object 1: text: ..

全部44次的值如下:

轮数 a b
1 0 0
2 1 0
3 1 1
4 1 3
5 2 3
6 2559 2
7 5 7
8 31317 65
9 4102 16405
10 20482 12289
11 6573 8204
12 32601 143
13 2589 6038
14 32511 4192
15 3783 16384
16 20610 11838
17 21524 28799
18 22774 1444
19 24704 31937
20 24587 16044
21 25961 26699
22 19805 17422
23 23516 25899
24 13077 7540
25 18653 28171
26 32577 20000
27 19932 32107
28 32622 26737
29 19290 26657
30 27009 16386
31 19074 29917
32 23467 32422
33 28266 17443
34 19584 31685
35 31505 19452
36 29779 18406
37 15413 24939
38 25829 15969
39 28655 17709
40 20041 32428
41 15841 25631
42 25633 15842
43 17711 28657
44 28657 17711

数组和字符串的例子

我们来个字符串的例子,因为不是测库函数,所以没有调用库函数。其实klee_make_symbolic本来就是为数组设计的,我们只要把数组大小传给第2个参数就可以了:

#include "klee/klee.h"

int check_pass(char* a){
    if(a==NULL){
        return -1;
    }else if(a[0]=='\0'){
        return -2;
    }else{
        if(a[0]=='C' && a[1]=='+' && a[2]=='+'){
            return 1;
        }else{
            return 0;
        }
    }
}

#define LEN 3

int main()
{
    char secret[LEN];
    klee_make_symbolic(&secret, LEN, "secret");
    return (check_pass(secret));
}

klee为我们找到5个分支,分别是:0开头的,非0非C开头的,C开头的, C+开头的, C++ 5种:

[root@7a5293f64325 libs]# ktest-tool ./klee-out-7/test000001.ktest
ktest file : './klee-out-7/test000001.ktest'
args       : ['strc.bc']
num objects: 1
object 0: name: 'secret'
object 0: size: 3
object 0: data: b'\x00\x00\x00'
object 0: hex : 0x000000
object 0: text: ...
[root@7a5293f64325 libs]# ktest-tool ./klee-out-7/test000002.ktest
ktest file : './klee-out-7/test000002.ktest'
args       : ['strc.bc']
num objects: 1
object 0: name: 'secret'
object 0: size: 3
object 0: data: b'\x01\xff\xff'
object 0: hex : 0x01ffff
object 0: text: ...
[root@7a5293f64325 libs]# ktest-tool ./klee-out-7/test000003.ktest
ktest file : './klee-out-7/test000003.ktest'
args       : ['strc.bc']
num objects: 1
object 0: name: 'secret'
object 0: size: 3
object 0: data: b'C\x00\xff'
object 0: hex : 0x4300ff
object 0: text: C..
[root@7a5293f64325 libs]# ktest-tool ./klee-out-7/test000004.ktest
ktest file : './klee-out-7/test000004.ktest'
args       : ['strc.bc']
num objects: 1
object 0: name: 'secret'
object 0: size: 3
object 0: data: b'C+\x00'
object 0: hex : 0x432b00
object 0: text: C+.
[root@7a5293f64325 libs]# ktest-tool ./klee-out-7/test000005.ktest
ktest file : './klee-out-7/test000005.ktest'
args       : ['strc.bc']
num objects: 1
object 0: name: 'secret'
object 0: size: 3
object 0: data: b'C++'
object 0: hex : 0x432b2b
object 0: text: C++

编译安装klee

展示了klee的能力之后,很多同学跃跃欲试想一试身手了。建议初试身手时在ubuntu Linux上尝试。

klee因为其复杂性,依赖比较多。除了llvm,cmake之类的通用依赖之外,我们还需要为其编译SAT求解器minisat和SMT求解器stp。

为了让文章简单,下面的步骤都取了极简的步骤。后面我们还会编译更复杂依赖的klee。

下载编译minisat

minisat是一种SAT-布尔可满足性理论求解器。后面会介绍SAT的原理,包括DPLL, CDCL方法等。

git clone https://github.com/stp/minisat.git
cd minisat
mkdir build
cd build
cmake ..
sudo make install

下载编译stp

STP是基于minisat的SMT-可满足性模理论求解器。

git clone https://github.com/stp/stp.git
cd stp
mkdir build
cd build
cmake ..
make
sudo make install

下载编译klee

下面是极简步骤,后面我们还会增加库和加上测试。

git clone https://github.com/klee/klee.git
mkdir build
cd build
cmake -DENABLE_UNIT_TESTS=OFF -DENABLE_SYSTEM_TESTS=OFF ..
make
sudo make install

好了,现在klee命令可用了,大家就可以用自己的代码做实验了:)

相关文章

网友评论

      本文标题:符号执行(1) - 自动生成覆盖率用例之利器

      本文链接:https://www.haomeiwen.com/subject/bmuwpktx.html