KLEE学习笔记

这是OSDI 2008年的一篇论文,至2020已被引用将近3000次的论文,决定学习一下这个工具。

先按照官网的tutorial来学一学。

  • Tutorial One:针对一个小程序
  • Tutorial Two:针对正则库
  • Tutorial Three:使用符号化的环境
  • Tutorial Four:针对GNU tools

Tutorial One

官网链接:http://klee.github.io/tutorials/testing-function/

首先有个小的函数:

int get_sign(int x) {
  if (x == 0)
    return 0;

  if (x < 0)
    return -1;
  else
    return 1;
}

可以看到这个函数有个参数x作为输入,我们用符号化的值代替这个参数x。为了用符号化的值,我们用klee_make_symbolic() (在klee/klee.h里定义的)这个函数来实现,这个函数有三个参数:

  • 变量的地址
  • 变量的大小
  • 变量的名字(it can be anything)

下面是一个简单的main函数,将变量a符号化,作为get_sign()函数的输入。

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

KLEE是在LLVM字节码上操作的,要用KLEE测试程序,就需要用clange -emit-llvm来先编译成字节码,比如下面的命令就生成了get_sign.bc

clang -I ../../include -emit-llvm -c -g -O0 -Xclang -disable-O0-optnone get_sign.c
  • -I:表示编译器能找到klee.h
  • -g:可以审查字节码的debug信息
  • -O0 -Xclang -disable-O0-optnone:注意,传递给KLEE的字节码不能被优化过,因为klee的开发者针对KLEE的特性做了另外的优化。在LLVM 5.0以后的版本,在编译KLEE的时候不能用-O0,因为这会防止KLEE做自己的优化。所以采用了这一长串的选项。

接下来,就用KLEE去跑字节码。

klee get_sign.bc

会看到下面的输出(LLVM 3.4):

KLEE: output directory = "klee-out-0"

KLEE: done: total instructions = 31
KLEE: done: completed paths = 3
KLEE: done: generated tests = 3

get_sign函数里只有三条路径,分别是x=0,x>0,x<0这三种。正如期望的那样,KLEE告诉我们找到了三条路径,并且为每一条路径生成了一个测试样例。KLEE执行的输出结果是一个文件夹,包含KLEE生成的测试样例。KLEE将输出的文件夹命名为klee-out-N,其中N是最小的可以用的数字。同时也会生成一个符号链接klee-last表示最近的一次输出。

$ ls klee-last/
assembly.ll      run.istats       test000002.ktest
info             run.stats        test000003.ktest
messages.txt     test000001.ktest warnings.txt

用ktest-tool工具查看生成的测试样例,会显示:

  • 程序唤起的参数
  • 路径上的符号化变量数目
  • 符号化变量的名字及size
  • 具体的测试样例的输入值
$ ktest-tool klee-last/test000001.ktest
ktest file : 'klee-last/test000001.ktest'
args       : ['get_sign.bc']
num objects: 1
object 0: name: 'a'
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: ....

$ ktest-tool klee-last/test000002.ktest
ktest file : 'klee-last/test000002.ktest'
args       : ['get_sign.bc']
num objects: 1
object 0: name: 'a'
object 0: size: 4
object 0: data: b'\x01\x01\x01\x01'
object 0: hex : 0x01010101
object 0: int : 16843009
object 0: uint: 16843009
object 0: text: ....

$ ktest-tool klee-last/test000003.ktest
ktest file : 'klee-last/test000003.ktest'
args       : ['get_sign.bc']
num objects: 1
object 0: name: 'a'
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: ....

当我们用KLEE生成了测试样例后,KLEE提供了一个方便的replay library,可以简单地用一个调用覆盖掉klee_make_symbolic。只要用动态库libkleeRuntest链接一下就可以了。

$ export LD_LIBRARY_PATH=path-to-klee-build-dir/lib/:$LD_LIBRARY_PATH
$ gcc -I ../../include -L path-to-klee-build-dir/lib/ get_sign.c -lkleeRuntest
$ KTEST_FILE=klee-last/test000001.ktest ./a.out
$ echo $?
0
$ KTEST_FILE=klee-last/test000002.ktest ./a.out
$ echo $?
1
$ KTEST_FILE=klee-last/test000003.ktest ./a.out
$ echo $?
255

Tutorial Two

第二个教程是针对一个正则库来测试。文件在examples/regexp下。

第一个步骤还是先生成字节码,

$ clang -I ../../include -emit-llvm -c -g -O0 -Xclang -disable-O0-optnone Regexp.c

如果有LLVM工具,可以运行llvm-nm来验证生成的文件。

$ llvm-nm Regexp.bc
                 U klee_make_symbolic
---------------- T main
---------------- T match
---------------- t matchhere
---------------- t matchstar

正常来说,在运行这个程序之前,我们需要链接它来创建一个可执行文件。然而,KLEE直接在LLVM字节码上运行。由于这个程序只有一个文件,所以没有比要去链接。对于有很多个输入的真实程序,llvm-link工具可以用来合并多个LLVM字节码为一个单一的模块,这样就可以用KLEE跑了。

下一步是用KLEE执行字节码。

$ klee --only-output-states-covering-new Regexp.bc
KLEE: output directory = "klee-out-1"
KLEE: ERROR: .../klee/examples/regexp/Regexp.c:23: memory error: out of bound pointer
KLEE: NOTE: now ignoring this error at this location
KLEE: ERROR: .../klee/examples/regexp/Regexp.c:25: memory error: out of bound pointer
KLEE: NOTE: now ignoring this error at this location
KLEE: done: total instructions = 6334861
KLEE: done: completed paths = 7692
KLEE: done: generated tests = 22

然而可能是klee版本的问题,我这边实际生成的测试样例要少7个,但错误个数是一样的。

一开始,KLEE打印出存储输出的文件夹(klee-out-1)。也可以指定输出的文件夹-output-dir=path

当KLEE开始跑的时候,它会打印重要的信息,比如当找到程序中的一个错误时。这个例子中,就找到了23和25行的错误。

最后,当KLEE结束执行时,就会打印关于运行时的一些数据。这里我们看到klee执行了600万的指令,探索了7692条路径,并且生成了22个测试样例。只生成22个测试样例是因为用了--only-output-states-covering-new选项,所以只会生成产生新状态的测试样例。如果关掉这个flag,就会产生6578个测试样例!并且KLEE不会为每条路径生成一个测试样例。当找到一个bug的时候,它会为第一个到达bug的状态生成一个测试样例。其他到达同一个位置的漏洞会被沉默的终止掉。如果你不在意重复的错误样例的话,可以用-emit-all-errors去为7692条路径生成测试样例。

需要注意的是,许多真实的程序是路径数目是有限的,而对KLEE来说有时候却不会终止。当然也可以用ctrl-C来终止运行,但也有选项来限制KLEE的运行时间和内存使用。

  • -max-time=
  • -max-forks=N:在N轮符号分支后终止分裂路径,然后把剩下的路径跑完。
  • -max-memory=N:限制内存消耗在N M以内。

当klee检测出程序的错误时,就会产生一个测试样例去展示错误,并且写一些额外的信息到文件testN.TYPE.err,N是测试样例的数目,TYPE是错误的类型,主要包含以下这几类:

  • ptr:存储或加载非法内存位置
  • free:double 或者非法的 free()
  • abort:程序调用了abort()
  • assert:断言失败的情况
  • div:除0错误
  • user:输入有关
  • exec:产生了一个问题让KLEE不能执行程序,比如一个未知的指令或者一个非法函数指针的调用,或者嵌入汇编。
  • model:KLEE不能保证很高的准确性,而且只探索程序部分的状态。例如,不支持malloc的大小的那个参数的符号化。

当KLEE检测错误的时候,会在控制台打印信息。对于所有程序的错误,KLEE会打印到.err文件中,比如下面这样。

Error: memory error: out of bound pointer
File: .../klee/examples/regexp/Regexp.c
Line: 23
Stack:
  #0 00000146 in matchhere (re=14816471, text=14815301) at .../klee/examples/regexp/Regexp.c:23
  #1 00000074 in matchstar (c, re=14816471, text=14815301) at .../klee/examples/regexp/Regexp.c:16
  #2 00000172 in matchhere (re=14816469, text=14815301) at .../klee/examples/regexp/Regexp.c:26
  #3 00000074 in matchstar (c, re=14816469, text=14815301) at .../klee/examples/regexp/Regexp.c:16
  #4 00000172 in matchhere (re=14816467, text=14815301) at .../klee/examples/regexp/Regexp.c:26
  #5 00000074 in matchstar (c, re=14816467, text=14815301) at .../klee/examples/regexp/Regexp.c:16
  #6 00000172 in matchhere (re=14816465, text=14815301) at .../klee/examples/regexp/Regexp.c:26
  #7 00000231 in matchhere (re=14816464, text=14815300) at .../klee/examples/regexp/Regexp.c:30
  #8 00000280 in match (re=14816464, text=14815296) at .../klee/examples/regexp/Regexp.c:38
  #9 00000327 in main () at .../klee/examples/regexp/Regexp.c:59
Info:
  address: 14816471
  next: object at 14816624 of size 4
  prev: object at 14816464 of size 7

对于内存错误,KLEE会展示其非法地址,以及那个地址在堆上的前后对象。在这个例子中,我们可以看到发生漏洞的地址离前一个对象只有1个字节的距离。

KLEE找到内存错误的原因不是因为正则库里有漏洞,而是在KLEE的测试驱动产生了问题。实际问题是我们将正则表达式的缓冲区完全符号化了,但是匹配的函数希望其为一个空的终止字符串。最简单的解决方法是在符号化后,将\0放在buffer的末尾。这样我们的main函数就变成这样了。

int main() {
  // The input regular expression.
  char re[SIZE];

  // Make the input symbolic.
  klee_make_symbolic(re, sizeof re, "re");
  re[SIZE - 1] = '\0';

  // Try to match against a constant string "hello".
  match(re, "hello");

  return 0;
}

使一个缓冲区符号化只要初始化关于符号变量那部分的内容。但我们仍然可以修改内存。如果你重新编译并且在测试程序上运行klee,内存错误。

另一个方式是用klee_assume函数。klee_assume函数以一个条件表达式为输入,并且会假设这个表达式在当前路径上为真。有种强行加约束条件的感觉。具体写法如下:

int main() {
  // The input regular expression.
  char re[SIZE];

  // Make the input symbolic.
  klee_make_symbolic(re, sizeof re, "re");
  klee_assume(re[SIZE - 1] == '\0');

  // Try to match against a constant string "hello".
  match(re, "hello");

  return 0;
}

在这个例子中,两种写法都可以,但klee_assume更灵活。

  • 通过显式地定义约束条件,会强制测试样例有\0。某些情况下,如果想手工检查所有测试样例,还是展示其所有的值比较方便。
  • klee_assume可以用于编码更复杂的约束条件。比如klee_assume(re[0]!='^')就可以让KLEE探索第一个字节不是^的状态。

当要用klee_assume要表示多个条件的时候,记住,逻辑表达式如&&和||可能会被编译成代码。就会导致klee到达klee_assume函数前切换到分支进程。所以这个时候就用&和|来代替前面两个。

Tutorial Three

这一节主要介绍如何使用符号化的环境。KLEE提供了若干符号化环境的选项。新用户一开始接触可能会比较懵逼。这个教程主要介绍一些基础的关于符号化环境的例子,包括-sym-arg-sym-files

-sym-arg Usage

-sym-arg <MIN> <MAX> <N>
  • MIN:符号化参数个数的最小值
  • MAX:符号化参数个数的最大值
  • N:符号化参数的长度

比如下面这个检查密码的程序

#include <stdio.h>

int check_password(char *buf) {
  if (buf[0] == 'h' && buf[1] == 'e' &&
      buf[2] == 'l' && buf[3] == 'l' &&
      buf[4] == 'o')
    return 1;
  return 0;
}

int main(int argc, char **argv) {
  if (argc < 2)
     return 1;
  
  if (check_password(argv[1])) {
    printf("Password found!\n");
    return 0;
  }

  return 1;
}

如果直接用klee测试,会发现只生成了一条路径和一个测试样例。

$ clang -c -g -emit-llvm password.c
$ klee -posix-runtime password.bc
KLEE: WARNING: undefined reference to function: printf
KLEE: WARNING ONCE: Alignment of memory from call "malloc" is not modelled. Using alignment of 8.
KLEE: WARNING ONCE: calling external: syscall(4, 94863385474752, 94863386124288) at /tmp/klee_src/runtime/POSIX/fd.c:528 12
KLEE: WARNING ONCE: calling __klee_posix_wrapped_main with extra arguments.

KLEE: done: total instructions = 1273
KLEE: done: completed paths = 1
KLEE: done: generated tests = 1

如果加了-sym-arg 5的参数的话,会生成63条路径,并且打印了password found,说明其中一条的路径找到了密码。

$ klee -posix-runtime password.bc -sym-arg 5
KLEE: NOTE: Using POSIX model: /tmp/klee_build60stp_z3/Debug+Asserts/lib/libkleeRuntimePOSIX.bca
KLEE: output directory is "/home/klee/examples/sym-env/klee-out-1"
KLEE: Using STP solver backend
warning: Linking two modules of different target triples: password.bc' is 'x86_64-unknown-linux-gnu' whereas 'klee_init_env.bc' is 'x86_64-pc-linux-gnu'

KLEE: WARNING: undefined reference to function: printf
KLEE: WARNING ONCE: Alignment of memory from call "malloc" is not modelled. Using alignment of 8.
KLEE: WARNING ONCE: calling external: syscall(4, 94673550841560, 94673551491216) at /tmp/klee_src/runtime/POSIX/fd.c:528 12
KLEE: WARNING ONCE: calling __klee_posix_wrapped_main with extra arguments.
KLEE: WARNING ONCE: calling external: printf(94673551004128) at password.c:17 5
Password found!

KLEE: done: total instructions = 25545
KLEE: done: completed paths = 63
KLEE: done: generated tests = 63

-sym-files Usage

-sym-files <NUM> <N>
  • NUM:创建NUM个符号文件
  • N:创建文件的大小为N

现在还是以一个密码检查器为例来说明,文件名为password_file.c.,它从用户指定的文件中读取一个字符串,并检查它是否与硬编码的密码匹配。如果没有指定文件名,或者在打开文件时出现错误,它将从标准输入中读取字符串。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

int check_password(int fd) {
  char buf[5];
  if (read(fd, buf, 5) != -1) {
    if (buf[0] == 'h' && buf[1] == 'e' &&
	buf[2] == 'l' && buf[3] == 'l' &&
	buf[4] == 'o')
      return 1;
  }
  return 0;
}

int main(int argc, char **argv) {
  int fd;

  if (argc >= 2) {
    if ((fd = open(argv[1], O_RDONLY)) != -1) {
      if (check_password(fd)) {
        printf("Password found in %s\n", argv[1]);
        close(fd);
        return 0;
      }
      close(fd);
      return 1;
    }
  }

  if (check_password(0)) {
    printf("Password found in standard input\n");
    return 0;
  }

  return 1;
}

先用klee普通运行一下,发现要等很久,而且只发现了一条路径。

$ clang -c -g -emit-llvm password_file.c 
$ klee -posix-runtime password_file.bc 
...
KLEE: done: total instructions = 1398
KLEE: done: completed paths = 1
KLEE: done: generated tests = 1

加上sym-file参数后,一下就找到了password。(10表示10bytes)这行命令的意思是生成了10bytes符号化的标准输入。

$ klee -posix-runtime password_file.bc -sym-stdin 10
...
Password found in standard input

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

同样,klee还能模拟生成一个文件A来符号执行,最后结果和上面也是一样的。这行命令的意思是生成了一个10bytes符号文件A。

$ klee -posix-runtime password_file.bc A -sym-files 1 10
...
Password found in A

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

Tutorial Four

这一节的教程是如何针对GNU 工具进行测试。在做这一小节之前,先按这个文档做好实验配置。教程总共分为八个步骤,整体上的感觉和前面差不多,侧重点在于如何对一个大规模的多文件的开源软件做测试(我自己在docker上做了,除了可视化的没成功,其他都行,推测是应该还需要在docker上安装其他组件才可以可视化):

  • Build coreutils with gcov
  • Install WLLVM
  • Build Coreutils with LLVM
  • Using KLEE as an interpreter
  • Introducing symbolic data to an application
  • Visualizing KLEE’s progress with kcachegrind
  • Replaying KLEE generated test cases
  • Using zcov to analyze coverage

Build coreutils with gcov

在我们开始用LLVM build程序之前,先用gcov支持来构建一个版本的coreutils。我们将用这个来获取KLEE产生的测试样例的覆盖率。在coreutils目录里,我们在子目录(obj-gcov)里做些build。

coreutils-6.11$ mkdir obj-gcov
coreutils-6.11$ cd obj-gcov
obj-gcov$ ../configure --disable-nls CFLAGS="-g -fprofile-arcs -ftest-coverage"
... verify that configure worked ...
obj-gcov$ make
obj-gcov$ make -C src arch hostname
... verify that make worked ...
  • –disable-nls:这个可以去除C库里额外的初始化信息,klee测试的时候不需要这个信息。即使这些生成的不是KLEE会在上面运行的文件,我们也用同样的编译选项,来让klee产生的测试样例在未插桩的二进制上也能正常运行。

接下来就有src文件夹了,比如:

obj-gcov$ cd src
src$ ls -l ls echo cat
-rwxrwxr-x 1 klee klee 150632 Nov 21 21:58 cat
-rwxrwxr-x 1 klee klee 135984 Nov 21 21:58 echo
-rwxrwxr-x 1 klee klee 390552 Nov 21 21:58 ls
src$ ./cat --version
cat (GNU coreutils) 6.11
Copyright (C) 2008 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Written by Torbjorn Granlund and Richard M. Stallman.

另外,这些可执行文件应该用gcov支持来build,所以如果你运行其中的一个,它就会在当前目录生成一个.gcda文件。我们可以用gcov工具来生成一个可读性不错的覆盖率信息文件。比如:

src$ rm -f *.gcda # Get rid of any stale gcov files
src$ ./echo**

src$ ls -l echo.gcda
-rw-rw-r-- 1 klee klee 896 Nov 21 22:00 echo.gcda
src$ gcov echo
File '../../src/echo.c'
Lines executed:24.27% of 103
Creating 'echo.c.gcov'

File '../../src/system.h'
Lines executed:0.00% of 3
Creating 'system.h.gcov'

默认情况下,gcov会显示程序执行了多少行数。

Install WLLVM

用KLEE测试真实程序的一个最难的地方在于要先将程序编译为llvm 字节码文件而不是一个二进制。而一个软件build系统可能用ar,libtool和ld等工具,就很难理解生成的LLVM字节码文件。

对于GNU工具,我们用 whole-program-llvm (WLLVM)这个工具。这个工具能够从一个未修改的C/C++源码中生成整个程序的LLVM字节码文件。WLLVM包含4个python的可执行文件:

  • WLLVM:c编译器
  • wllvm++:c++编译器
  • extrac-bc:从build好的工程中提取字节码
  • wllvm-sanity-checker:检测配置疏忽

用pip安装wllvm

pip install --upgrade wllvm

为了成功执行wllvm,我们设置一下环境变量LLVM_COMPILER

$ export LLVM_COMPILER=clang

为了让下次开机不用再输上面的命令,我们可以将上面的命令加到.bashrc里面

Build Coreutils with LLVM

接下来建一个子文件夹,让我们比较好访问二进制和LLVM字节码,例如:

coreutils-6.11$ mkdir obj-llvm
coreutils-6.11$ cd obj-llvm
obj-llvm$ CC=wllvm ../configure --disable-nls CFLAGS="-g -O1 -Xclang -disable-llvm-passes -D__NO_STRING_INLINES  -D_FORTIFY_SOURCE=0 -U__OPTIMIZE__"
... verify that configure worked ...
obj-llvm$ make
obj-llvm$ make -C src arch hostname
... verify that make worked ...
  • 没加-fprofile-arcs -ftest-coverage:不在二进制添加gcov的插桩

  • 添加-O1 -Xclang -disable-llvm-passes:这有点像-O0,但是在LLVM5.0和后续版本中,用-O0编译会防止KLEE做自己的优化,所以用-O1选项,但是禁用了所有的优化。

  • -D__NO_STRING_INLINES -D_FORTIFY_SOURCE=0 -U__OPTIMIZE__
    

    这也是很重要的选项,因为在LLVM的后面版本里,clang会用更安全的库函数,比如将fprintf替换成__fprintf_chk。而KLEE没有对这种函数建模,就会将其视为外部函数,导致预期以外的结果。

虽然也可使用这种选项-O0 -Xclang -disable-O0-optnone,但由于后面我们还要做优化,所以更好的选择是-O1 -Xclang -disable-llvm-passes。因为-O1版本产生字节码更适合优化,所以我们更喜欢用这个。

如果一切正常的话,我们就有GNU coreutils的可执行文件了,如下:

obj-llvm$ cd src
src$ ls -l ls echo cat
-rwxrwxr-x 1 klee klee 105448 Nov 21 12:03 cat
-rwxrwxr-x 1 klee klee  95424 Nov 21 12:03 echo
-rwxrwxr-x 1 klee klee 289624 Nov 21 12:03 ls
src$ ./cat --version
cat (GNU coreutils) 6.11
Copyright (C) 2008 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Written by Torbjorn Granlund and Richard M. Stallman.

你可能会注意到,与LLVM字节码文件相比,我们获得了可执行文件,这是因为WLLVM工作流程有两步。首先,WLLVM唤起标准编译器,然而对于每个对象文件,会唤起一个字节码编译器来生成LLVM字节码。WLLVM将生成的字节码的位置放到对象文件的一个段里。当对象文件链接的时候,位置信息会被连接起来来节省所有文件的位置。在build完成之后,可以用wllvm的extract-bc来读取特定位的内容,然后链接所有的字节码到一个程序的字节码里去。

我们可以用extract-bc来获取程序的字节码,:

src$ find . -executable -type f | xargs -I '{}' extract-bc '{}'
src$ ls -l ls.bc
-rw-rw-r-- 1 klee klee 543052 Nov 21 12:03 ls.bc

Using KLEE as an interpreter

后面就和前面的几个教程比较相像了。本质上,KLEE就是LLVM字节码的解释器。现在我们用klee去运行cat可执行文件,需要加上uclibc和POSIX这两个运行支持的选项。

src$ klee --libc=uclibc --posix-runtime ./cat.bc --version
KLEE: NOTE: Using klee-uclibc : /usr/local/lib/klee/runtime/klee-uclibc.bca
KLEE: NOTE: Using model: /usr/local/lib/klee/runtime/libkleeRuntimePOSIX.bca
KLEE: output directory is "/home/klee/coreutils-6.11/obj-llvm/src/./klee-out-0"
Using STP solver backend
KLEE: WARNING ONCE: function "vasnprintf" has inline asm
KLEE: WARNING: undefined reference to function: __ctype_b_loc
KLEE: WARNING: undefined reference to function: klee_posix_prefer_cex
KLEE: WARNING: executable has module level assembly (ignoring)
KLEE: WARNING ONCE: calling external: syscall(16, 0, 21505, 42637408)
KLEE: WARNING ONCE: calling __user_main with extra arguments.
KLEE: WARNING ONCE: calling external: getpagesize()
KLEE: WARNING ONCE: calling external: vprintf(43649760, 51466656)
cat (GNU coreutils) 6.11

License GPLv3+: GNU GPL version 3 or later
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Written by Torbjorn Granlund and Richard M. Stallman.
Copyright (C) 2008 Free Software Foundation, Inc.
KLEE: WARNING ONCE: calling close_stdout with extra arguments.

KLEE: done: total instructions = 28988
KLEE: done: completed paths = 1
KLEE: done: generated tests = 1

这次我们获得了很多输出!来看看这行命令做了什么。klee就是运行klee测试,cat.bc就是目标的字节码文件,–version是应用程序传递的参数。

klee --libc=uclibc --posix-runtime ./cat.bc --version

如果我们运行一个普通的应用,就会和C库链接起来,但这个例子里,KLEE直接运行LLVM字节码。为了处理程序可能调用的外部函数,klee修改了C库,名为uclibc,来在klee中使用(有种angr重写函数那种感觉了)。

类似的,一个普通的应用程序可能会用到像write这样的底层调用,C库里也会直接用这种。所以为了更好的理解程序,klee提供了POSIX这个选项,来保证能够正常测试程序。

还有一些无伤大雅的警告,包括以下这些:

  • undefined reference to function: ___ctype_b_loc
  • executable has module level assembly (ignoring)
  • calling __user_main with extra arguments
  • calling external: getpagesize()

Introducing symbolic data to an application

前面我们展示了,KLEE可以正常解释程序,但KLEE的真正的目的是用符号化的输入来穷尽程序的执行路径。接下来,以echo举例来说明如何用klee测试echo。

当用uclibc和POSIX时,klee会用特别的函数来替代程序的main函数,这个函数改变了应用程序的正常命令行处理,特别是支持符号参数的构造。比如,传递一个–help参数。

src$ klee --libc=uclibc --posix-runtime ./echo.bc --help
...

usage: (klee_init_env) [options] [program arguments]
  -sym-arg <N>              - Replace by a symbolic argument with length N
  -sym-args <MIN> <MAX> <N> - Replace by at least MIN arguments and at most
                              MAX arguments, each with maximum length N
  -sym-files <NUM> <N>      - Make NUM symbolic files ('A', 'B', 'C', etc.),
                              each with size N
  -sym-stdin <N>            - Make stdin symbolic with size N.
  -sym-stdout               - Make stdout symbolic.
  -max-fail <N>             - Allow up to N injected failures
  -fd-fail                  - Shortcut for '-max-fail 1'
...

再让我们用一个3个字符长度的符号变量去运行echo

src$ klee --libc=uclibc --posix-runtime ./echo.bc --sym-arg 3
KLEE: NOTE: Using klee-uclibc : /usr/local/lib/klee/runtime/klee-uclibc.bca
KLEE: NOTE: Using model: /usr/local/lib/klee/runtime/libkleeRuntimePOSIX.bca
KLEE: output directory is "/home/klee/coreutils-6.11/obj-llvm/src/./klee-out-1"
Using STP solver backend
KLEE: WARNING ONCE: function "vasnprintf" has inline asm
KLEE: WARNING: undefined reference to function: __ctype_b_loc
KLEE: WARNING: undefined reference to function: klee_posix_prefer_cex
KLEE: WARNING: executable has module level assembly (ignoring)
KLEE: WARNING ONCE: calling external: syscall(16, 0, 21505, 39407520)
KLEE: WARNING ONCE: calling __user_main with extra arguments.
..
KLEE: WARNING: calling close_stdout with extra arguments.
...
KLEE: WARNING ONCE: calling external: printf(42797984, 41639952)
..
KLEE: WARNING ONCE: calling external: vprintf(41640400, 52740448)
..
Echo the STRING(s) to standard output.

  -n             do not output the trailing newline
  -e             enable interpretation of backslash escapes
  -E             disable interpretation of backslash escapes (default)
      --help     display this help and exit
      --version  output version information and exit
Usage: ./echo.bc [OPTION]... [STRING]...
echo (GNU coreutils) 6.11
Copyright (C) 2008 Free Software Foundation, Inc.
If -e is in effect, the following sequences are recognized:

  \0NNN   the character whose ASCII code is NNN (octal)
  \\     backslash
  \a     alert (BEL)
  \b     backspace

License GPLv3+: GNU GPL version 3 or later
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

  \c     suppress trailing newline
  \f     form feed
  \n     new line
  \r     carriage return
  \t     horizontal tab
  \v     vertical tab

NOTE: your shell may have its own version of echo, which usually supersedes
the version described here.  Please refer to your shell's documentation
for details about the options it supports.

Report bugs to <bug-coreutils@gnu.org>.
Written by FIXME unknown.

KLEE: done: total instructions = 64546
KLEE: done: completed paths = 25
KLEE: done: generated tests = 25

结果有点有趣,klee在程序中发现了25条执行路径。所有路径的输出是混合的,但是你可以发现除了打印各种随机的字符,一些基本块的文本段也打印出来了。你也许还会发现在这个例子中,选项–v,–h都探索过。我们可以通过运行klee-stats来查看总体的数据。

src$ klee-stats klee-last
------------------------------------------------------------------------
|  Path   |  Instrs|  Time(s)|  ICov(%)|  BCov(%)|  ICount|  TSolver(%)|
------------------------------------------------------------------------
|klee-last|   64546|     0.15|    22.07|    14.14|   19943|       62.97|
------------------------------------------------------------------------
  • ICov:覆盖的LLVM的指令数
  • BCov:覆盖的分支数

为啥覆盖率这么低?原因在于这些数据是在字节码上计算的,其中包括了很多不会执行到的库的代码。我们可以用–optimize选项来解决这个问题。这会让klee在执行前先在LLVM 优化的pass上优化一下,比如移掉一些死区代码。当针对大型程序的时候,最好用这个选项。下面是开启了–optimize的结果

src$ klee --optimize --libc=uclibc --posix-runtime ./echo.bc --sym-arg 3
...
KLEE: done: total instructions = 33991
KLEE: done: completed paths = 25
KLEE: done: generated tests = 25
src$ klee-stats klee-last
------------------------------------------------------------------------
|  Path   |  Instrs|  Time(s)|  ICov(%)|  BCov(%)|  ICount|  TSolver(%)|
------------------------------------------------------------------------
|klee-last|   33991|     0.13|    30.16|    21.91|    8339|       80.66|
------------------------------------------------------------------------

这下指令的覆盖率提高了6%,而且你会发现运行地更快了并且执行了更少指令。大部分剩余的代码仍然是库函数,因为优化器还无法无法完全剔除这部分代码。我们可以用可视化工具kcachegrind来验证这点。

Visualizing KLEE’s progress with kcachegrind

安装kcahcegrind

sudo apt-get install kachegrind

运行

src$ kcachegrind klee-last/run.istats

然后会看到

在这里插入图片描述

docker中暂时出现不了这个可视化界面,以后用到再研究这个。

Replaying KLEE generated test cases

如果我们看下klee-last文件夹里,会发现有25个测试样例。

src$ ls klee-last
assembly.ll	  test000004.ktest  test000012.ktest  test000020.ktest
info		  test000005.ktest  test000013.ktest  test000021.ktest
messages.txt	  test000006.ktest  test000014.ktest  test000022.ktest
run.istats	  test000007.ktest  test000015.ktest  test000023.ktest
run.stats	  test000008.ktest  test000016.ktest  test000024.ktest
test000001.ktest  test000009.ktest  test000017.ktest  test000025.ktest
test000002.ktest  test000010.ktest  test000018.ktest  warnings.txt
test000003.ktest  test000011.ktest  test000019.ktest

这些文件里包含实际的值可以用来重现路径。我们可以用ktest-tool来查看每个文件的内容。

$ ktest-tool klee-last/test000001.ktest
ktest file : 'klee-last/test000001.ktest'
args       : ['./echo.bc', '--sym-arg', '3']
num objects: 2
object    0: name: 'arg0'
object    0: size: 4
object    0: data: '\x00\x00\x00\x00'
object    1: name: 'model_version'
object    1: size: 4
object    1: data: '\x01\x00\x00\x00'

在这个例子里,测试样例说明了“\x00\x00\x00\x00”会被作为第一个参数。通常来说,不需要直接看这些ktest文件。klee有一个klee-replay工具可以读取.ktest文件,然后唤起原生应用,然后自动地传递数据来复现klee执行的路径。

要看它是如何工作的,我们先回到一开始build好的原生二进制文件目录下:

src$ cd ..
obj-llvm$ cd ..
coreutils-6.11$ cd obj-gcov
obj-gcov$ cd src
src$ ls -l echo
-rwxrwxr-x 1 klee klee 135984 Nov 21 21:58 echo

要用klee-replay工具,我们只要告诉二进制运行并使用哪个ktest文件。如下:

src$ klee-replay ./echo ../../obj-llvm/src/klee-last/test000001.ktest
klee-replay: TEST CASE: ../../obj-llvm/src/klee-last/test000001.ktest
klee-replay: ARGS: "./echo" ""

klee-replay: EXIT STATUS: NORMAL (0 seconds)

前两行和最后一行来自klee-replay工具本身。前两行列举了要运行的测试样例和要传递给应用程序里具体的参数值。

当然也可跑一个测试样例集合。

src$ rm -f *.gcda # Get rid of any stale gcov files
src$ klee-replay ./echo ../../obj-llvm/src/klee-last/*.ktest
klee-replay: TEST CASE: ../../obj-llvm/src/klee-last/test000001.ktest
klee-replay: ARGS: "./echo" "@@@"
@@@
klee-replay: EXIT STATUS: NORMAL (0 seconds)
_..._
klee-replay: TEST CASE: ../../obj-llvm/src/klee-last/test000022.ktest
klee-replay: ARGS: "./echo" "--v"
echo (GNU coreutils) 6.11
Copyright (C) 2008 Free Software Foundation, Inc.
_..._

src$ gcov echo
File '../../src/echo.c'
Lines executed:52.43% of 103
Creating 'echo.c.gcov'

File '../../src/system.h'
Lines executed:100.00% of 3
Creating 'system.h.gcov'

echo.c里代码的覆盖行数要远高于klee-stats的数据,这是因为gcov只统计了一个文件的覆盖率,而没有统计整个应用程序的。用kcachegrind,我们可以看gcov生成的覆盖率文件来看那些行数被覆盖了。这是一部分的输出结果:

在这里插入图片描述

左边表示这行代码执行的次数,-表示这行没有可以执行的。#####表示没有覆盖这行。

在测试更复杂的程序之前,我们先尽可能让echo.c的覆盖率高一点。前面覆盖率低的原因在于我们没有让足够多的数据符号化,为echo提供两个参数,应该足够覆盖整个程序了。我们可以用-sym-args来覆盖更多的选项。现在切换为obj-llvm/src目录:

src$ klee --only-output-states-covering-new --optimize --libc=uclibc --posix-runtime ./echo.bc --sym-args 0 2 4
...
KLEE: done: total instructions = 7611521
KLEE: done: completed paths = 10179
KLEE: done: generated tests = 57
  • –sym-args 0 2 4 表示传递0-2个参数,长度为4
  • –only-output-states-covering-new:表示覆盖到新的路径时才会生成样例

如果我们现在再去运行生成的测试样例,会发现这次覆盖率不错。

src$ rm -f *.gcda # Get rid of any stale gcov files
src$ klee-replay ./echo ../../obj-llvm/src/klee-last/*.ktest
klee-replay: TEST CASE: ../../obj-llvm/src/klee-last/test000001.ktest
klee-replay: ARGS: "./echo"

...

src$ gcov echo
File '../../src/echo.c'
Lines executed:97.09% of 103
Creating 'echo.c.gcov'

File '../../src/system.h'
Lines executed:100.00% of 3
Creating 'system.h.gcov'

为啥没有到达100%呢,我猜测是覆盖率计算把不可执行代码也加入到未覆盖的行数里去了。

Using zcov to analyze coverage

要可视化覆盖率的结果可以用这个工具:zcov

  • 7
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 10
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

破落之实

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值