本文架构
- 1. 动机
- 2.KLEE简介
- 3.KLEE的代码工程结构
- 4. 从KLEE主函数入手
- main函数
- step1: 初始化
- step2:加载.bc文件
- 进行符号执行
- 读取测试用例
- 输出日志信息
1. 动机
最近准备对KLEE进行修改使其符合我的需要,因此免不了需要对源码进行修改。读懂源码是对在其进行修改之前的必经之路。但其工程量巨大,该如何下手呢?
于是我将阅读源码过程中所得分享出来,希望能为同样学习KLEE的同行之人提供一些参考。内容可能写的存在纰漏,还请大家及时指出,不吝赐教。
程序分析-klee工具分析
符号执行, KLEE 与 LLVM
2.KLEE简介
KLEE(可读作“克利”)是一个基于符号执行的自动化测试工具,旨在帮助发现软件中的错误和漏洞。该工具可以用于分析 C/C++ 程序,并生成能够触发程序错误路径的测试用例。KLEE的主要目标是执行程序的所有可能路径,而不仅仅是具体输入下的一条路径。通过符号执行技术,它能够在未提供具体输入的情况下模拟程序执行的各种路径,从而发现隐藏在程序中的潜在漏洞。
KLEE的核心思想是将程序中的输入视为符号(symbol),而不是具体的数值。这意味着在执行过程中,KLEE不会给定实际输入值,而是用符号代替输入,从而创建了一种执行路径的符号表示。然后,KLEE使用约束求解器来解决程序中各种条件语句的约束,以确定是否存在输入,能够导致程序执行到不同的路径上。
KLEE的工作流程可以分为几个主要步骤:
符号执行
: KLEE通过对程序进行符号执行,以符号形式代表程序的输入和执行路径。在执行过程中,它记录了执行路径上的每个条件分支以及相应的约束条件。路径探索
: KLEE使用路径探索技术来导航程序的不同执行路径。它会尝试通过不同的路径执行程序,并在执行过程中收集约束条件。约束求解
: 在执行过程中,KLEE将收集到的约束条件传递给约束求解器,以确定是否存在一组输入能够满足这些约束条件。如果找到了满足条件的输入,那么就找到了一条可以导致特定程序路径执行的输入序列。测试用例生成
: 当约束求解器找到满足条件的输入时,KLEE会生成相应的测试用例,并将其用于进一步的测试和验证。
KLEE的强大之处在于它能够自动化地发现程序中的潜在错误,例如内存泄漏、空指针解引用、整数溢出等。通过覆盖程序的多个执行路径,KLEE可以提高测试覆盖率,从而增加程序的健壮性和可靠性。
3.KLEE的代码工程结构
从【Github】KLEE: Symbolic Execution Engine下载KLEE的源代码。
git clone https://github.com/klee/klee.git
使用你电脑中的IDE(本文使用的是jetbrain公司的CLion)打开后可以看到其工程结构如下所示:
以弄懂其大致流程为目标导向,我们只需要关注其include
和lib
文件夹。
其中:
include
文件夹包含了KLEE的所有接口信息,你可以在该文件下获取大部分KLEE的数据结构信息。lib
文件夹包含了KLEE核心内容的具体实现,即我们需要阅读的大部分源码。将该文件夹展开还能进一步获取信息:
文件夹名 | 存放内容说明 |
---|---|
lib/Basic | Low level support for both klee and kleaver which should be independent of LLVM. |
lib/Support | Higher level support, but only used by klee. This can use LLVM facilities. |
lib/Expr | The core kleaver expression library. |
lib/Solver | The kleaver solver library. |
lib/Module | klee facilities for working with LLVM modules, including the shadow module/instruction structures we use during execution. |
lib/Core | The core symbolic virtual machine. |
其中Core
是我们最关心的内容,里面包含了对符号执行模拟器(Executor
)、符号执行树(ExecutionTree
)以及符号执行状态(ExecutionState
)等关键概念的定义(.h
)和实现(.cpp
)。
|-- AddressSpace.cpp
|-- AddressSpace.h
|-- CallPathManager.cpp
|-- CallPathManager.h
|-- Context.cpp
|-- Context.h
|-- CoreStats.cpp
|-- CoreStats.h
|-- ExecutionState.cpp
|-- ExecutionState.h
|-- Executor.cpp
|-- Executor.h
|-- ExecutorUtil.cpp
|-- ExternalDispatcher.cpp
|-- ExternalDispatcher.h
|-- GetElementPtrTypeIterator.h
|-- ImpliedValue.cpp
|-- ImpliedValue.h
|-- Memory.cpp
|-- Memory.h
|-- MemoryManager.cpp
|-- MemoryManager.h
|-- MergeHandler.cpp
|-- MergeHandler.h
|-- PTree.cpp
|-- PTree.h
|-- Searcher.cpp
|-- Searcher.h
|-- SeedInfo.cpp
|-- SeedInfo.h
|-- SpecialFunctionHandler.cpp
|-- SpecialFunctionHandler.h
|-- StatsTracker.cpp
|-- StatsTracker.h
|-- TimingSolver.cpp
|-- TimingSolver.h
|-- UserSearcher.cpp
|-- UserSearcher.h
但大家也可以看见,内容过多,虽然去除头文件但依然还有十几个文件。回想当初学习C语言时,面对繁多的函数定义,我们要了解一个程序的执行流程(基本执行思路)时,都是从主函数入手的。这给我们提供了一个有益的思路:从主函数开始我们的阅读!
当然,也有的文章直接从core下的文件入手,这里给供大家参考吧~
【安全客Blog】KLEE 源码阅读笔记
4. 从KLEE主函数入手
KLEE的主函数并不在我们上述的重要工程目录中。它位于tool/klee/main.cpp
中我们点开这个文件后可以发现:(这TM也有近两千行代码~)。一点开Structure
,五花八门的函数定义和结构体定义。
但!没关系,我们关注的只有main
函数!(大概位于1200行左右,由于我添加了注释所以跟原始的可能有偏差)
main函数
让我们看看主函数主要做了什么。
step1: 初始化
- step1: 初始化加载各种初始环境
- step2: 调用
parseArguments()
分析你在命令行中对KLEE做出的配置。 - step3: 设置异常处理函数
SetInterruptFunction()
KCommandLine::KeepOnlyCategories({&ChecksCat, ... &ExecTreeCat});llvm::InitializeNativeTarget();//初始化环境parseArguments(argc, argv);//分析传入的命令行参数
..//设置异常处理函数// 会报一些错sys::SetInterruptFunction(interrupt_handle);
step2:加载.bc文件
现在开始加载.bc
字节码文件进行符号分析
int main(int argc, char **argv, char **envp) // Load the bytecode...// 加载字节码std::string errorMsg;LLVMContext ctx;
...std::vector<std::unique_ptr<llvm::Module>> loadedModules;/*** loadFile 需要解析bc文件,bc文件是一个整体运行单元*/if (!klee::loadFile(InputFile, ctx, loadedModules, errorMsg)) {klee_error("error loading program '%s': %s", InputFile.c_str(),errorMsg.c_str());}// linkModule 将bc文件中的module合并为一个整体的modulestd::unique_ptr<llvm::Module> M(klee::linkModules(loadedModules,"" /* link all modules together */,errorMsg));if (!M) {//链接错误的话,报错klee_error("error loading program '%s': %s", InputFile.c_str(),errorMsg.c_str());}//链接完成,返回mainModulellvm::Module *mainModule = M.get();
...// 将这个module作为第一个项// Push the module as the first entryloadedModules.emplace_back(std::move(M));std::string LibraryDir = KleeHandler::getRunTimeLibraryPath(argv[0]);//配置module基本信息Interpreter::ModuleOptions Opts(LibraryDir.c_str(), EntryPoint, opt_suffix,/*Optimize=*/OptimizeModule,/*CheckDivZero=*/CheckDivZero,/*CheckOvershift=*/CheckOvershift);// 遍历已经加载完毕的modules,以找到主函数mainfor (auto &module : loadedModules) {mainFn = module->getFunction("main");if (mainFn)break;}// 找到入口点if (EntryPoint.empty())klee_error("entry-point cannot be empty");// 找到入口函数for (auto &module : loadedModules) {entryFn = module->getFunction(EntryPoint);if (entryFn)break;}if (!entryFn)klee_error("Entry function '%s' not found in module.", EntryPoint.c_str());//如果设置了POSIX运行时环境if (WithPOSIXRuntime) {...}// 如果设置了UBSan运行时if (WithUBSanRuntime) {...}// 如果设置了libcxxif (Libcxx) {...}
// genswitch (Libc) {..}// 检查是否成功加载字节码库for (const auto &library : LinkLibraries) {if (!klee::loadFile(library, mainModule->getContext(), loadedModules,errorMsg))klee_error("error loading bitcode library '%s': %s", library.c_str(),errorMsg.c_str());}int pArgc;char **pArgv;char **pEnvp;//如果设置了Environ 环境if (Environ != "") {std::vector<std::string> items;// 打开环境配置信息std::ifstream f(Environ.c_str());if (!f.good())klee_error("unable to open --environ file: %s", Environ.c_str());//读取环境配置文件while (!f.eof()) {std::string line;std::getline(f, line);line = strip(line);if (!line.empty())items.push_back(line);}//end of the while(!f.eif())f.close();//关闭文件pEnvp = new char *[items.size()+1];unsigned i=0;for (; i != items.size(); ++i)pEnvp[i] = strdup(items[i].c_str());pEnvp[i] = 0;} else {pEnvp = envp;}pArgc = InputArgv.size() + 1;pArgv = new char *[pArgc];for (unsigned i=0; i<InputArgv.size()+1; i++) {std::string &arg = (i==0 ? InputFile : InputArgv[i-1]);unsigned size = arg.size() + 1;char *pArg = new char[size];std::copy(arg.begin(), arg.end(), pArg);pArg[size - 1] = 0;pArgv[i] = pArg;}//end of for
进行符号执行
Interpreter是进行符号执行的重要部件
Interpreter::InterpreterOptions IOpts;IOpts.MakeConcreteSymbolic = MakeConcreteSymbolic;KleeHandler *handler = new KleeHandler(pArgc, pArgv);Interpreter *interpreter =theInterpreter = Interpreter::create(ctx, IOpts, handler);// 条件为假则终止程序继续执行assert(interpreter);handler->setInterpreter(interpreter);//输出详细信息(info)for (int i = 0; i < argc; i++)//逐个输出你刚才设置的命令行参数handler->getInfoStream() << argv[i] << (i + 1 < argc ? " " : "\n");handler->getInfoStream() << "PID: " << getpid() << "\n";// 最终的module// Get the desired main function. klee_main initializes uClibc// locale and other data and then calls main.auto finalModule = interpreter->setModule(loadedModules, Opts);entryFn = finalModule->getFunction(EntryPoint);if (!entryFn)klee_error("Entry function '%s' not found in module.", EntryPoint.c_str());externalsAndGlobalsCheck(finalModule);//重放路径std::vector<bool> replayPath;if (!ReplayPathFile.empty()) {//加载重放路径文件KleeHandler::loadPathFile(ReplayPathFile, replayPath);interpreter->setReplayPath(&replayPath);}
// 这一部分也是打印到日志info里面// 开始时间auto startTime = std::time(nullptr);{ // output clock info and start timestd::stringstream startInfo;startInfo << time::getClockInfo()<< "Started: "<< std::put_time(std::localtime(&startTime), "%Y-%m-%d %H:%M:%S") << '\n';handler->getInfoStream() << startInfo.str();handler->getInfoStream().flush();// 输出到info文件中}
读取测试用例
// 读取用例文件if (!ReplayKTestDir.empty() || !ReplayKTestFile.empty()) {assert(SeedOutFile.empty());assert(SeedOutDir.empty());std::vector<std::string> kTestFiles = ReplayKTestFile;for (std::vector<std::string>::iteratorit = ReplayKTestDir.begin(), ie = ReplayKTestDir.end();it != ie; ++it)KleeHandler::getKTestFilesInDir(*it, kTestFiles);std::vector<KTest*> kTests;for (std::vector<std::string>::iteratorit = kTestFiles.begin(), ie = kTestFiles.end();it != ie; ++it) {KTest *out = kTest_fromFile(it->c_str());if (out) {kTests.push_back(out);} else {klee_warning("unable to open: %s\n", (*it).c_str());}}if (RunInDir != "") {int res = chdir(RunInDir.c_str());if (res < 0) {klee_error("Unable to change directory to: %s - %s", RunInDir.c_str(),sys::StrError(errno).c_str());}}unsigned i=0;for (std::vector<KTest*>::iteratorit = kTests.begin(), ie = kTests.end();it != ie; ++it) {KTest *out = *it;interpreter->setReplayKTest(out);llvm::errs() << "KLEE: replaying: " << *it << " (" << kTest_numBytes(out)<< " bytes)"<< " (" << ++i << "/" << kTestFiles.size() << ")\n";// XXX should put envp in .ktest ?interpreter->runFunctionAsMain(entryFn, out->numArgs, out->args, pEnvp);if (interrupted) break;}//end of forinterpreter->setReplayKTest(0);//当测试用例不为空while (!kTests.empty()) {kTest_free(kTests.back());kTests.pop_back();}}//当replay路径为空时,从SeedOutFile中读取用例生成种子else {std::vector<KTest *> seeds;for (std::vector<std::string>::iteratorit = SeedOutFile.begin(), ie = SeedOutFile.end();it != ie; ++it) {KTest *out = kTest_fromFile(it->c_str());if (!out) {klee_error("unable to open: %s\n", (*it).c_str());}seeds.push_back(out);}//输入测试用例for (std::vector<std::string>::iteratorit = SeedOutDir.begin(), ie = SeedOutDir.end();it != ie; ++it) {std::vector<std::string> kTestFiles;KleeHandler::getKTestFilesInDir(*it, kTestFiles);for (std::vector<std::string>::iteratorit2 = kTestFiles.begin(), ie = kTestFiles.end();it2 != ie; ++it2) {//从文件中读取用例KTest *out = kTest_fromFile(it2->c_str());if (!out) {klee_error("unable to open: %s\n", (*it2).c_str());}// 将out加入用例队列seeds.push_back(out);}// kTest是一种记录程序执行路径的文件格式,文件包含了程序// 执行结束时的状态信息,如寄存器值、内存的内容等if (kTestFiles.empty()) {klee_error("seeds directory is empty: %s\n", (*it).c_str());}}// 如果存在测试用例,开始使用其进行测试if (!seeds.empty()) {klee_message("KLEE: using %lu seeds\n", seeds.size());interpreter->useSeeds(&seeds);}// end of ifif (RunInDir != "") {int res = chdir(RunInDir.c_str());if (res < 0) {klee_error("Unable to change directory to: %s - %s",RunInDir.c_str(),sys::StrError(errno).c_str());}}// end of ifinterpreter->runFunctionAsMain(entryFn, pArgc, pArgv, pEnvp);
// 释放种子while (!seeds.empty()) {// 释放最后一个队列中的最后一个用例kTest_free(seeds.back());seeds.pop_back();}}
输出日志信息
// 结束时间,计算测试执行时间。输出日志信息auto endTime = std::time(nullptr);{ // output end and elapsed timestd::uint32_t h;std::uint8_t m, s;std::tie(h,m,s) = time::seconds(endTime - startTime).toHMS();std::stringstream endInfo;endInfo << "Finished: "<< std::put_time(std::localtime(&endTime), "%Y-%m-%d %H:%M:%S") << '\n'<< "Elapsed: "<< std::setfill('0') << std::setw(2) << h<< ':'<< std::setfill('0') << std::setw(2) << +m<< ':'<< std::setfill('0') << std::setw(2) << +s<< '\n';handler->getInfoStream() << endInfo.str();handler->getInfoStream().flush();}// 释放所有参数// Free all the args.for (unsigned i=0; i<InputArgv.size()+1; i++)//释放数组delete[] pArgv[i];delete[] pArgv;delete interpreter;// 获取统计信息uint64_t queries =*theStatisticManager->getStatisticByName("SolverQueries");
...uint64_t forks =*theStatisticManager->getStatisticByName("Forks");handler->getInfoStream()<< "KLEE: done: explored paths = " << 1 + forks << "\n";// Write some extra information in the info file which users won't// necessarily care about or understand.?// 查询的相关信息,这的结果体现在info文件中if (queries)handler->getInfoStream()<< "KLEE: done: avg. constructs per query = "<< queryConstructs / queries << "\n";handler->getInfoStream()<< "KLEE: done: total queries = " << queries << "\n"<< "KLEE: done: valid queries = " << queriesValid << "\n"<< "KLEE: done: invalid queries = " << queriesInvalid << "\n"<< "KLEE: done: query cex = " << queryCounterexamples << "\n";// 一些统计的信息std::stringstream stats;stats << '\n'<< "KLEE: done: total instructions = " << instructions << '\n'<< "KLEE: done: completed paths = " << handler->getNumPathsCompleted()<< '\n'<< "KLEE: done: partially completed paths = "<< handler->getNumPathsExplored() - handler->getNumPathsCompleted()<< '\n'<< "KLEE: done: generated tests = " << handler->getNumTestCases()<< '\n';return 0;
}
这一部分也就对应我们测试后生成的info文件中的内容
几个月前学习时写的草稿,若有错误和不足,还望不吝赐教。