当前位置: 首页 > 科技观察

使用动态符号执行的代码覆盖率测试

时间:2023-03-13 02:47:46 科技观察

1.前言代码覆盖率测试主要用于漏洞研究领域。主要目的是用不同的输入覆盖程序代码的不同部分。如果输入导致程序崩溃,我们将检测崩溃是否可以被利用。代码覆盖率测试的方法有很多种,比如随机测试等等。但本文主要关注使用动态符号执行的代码覆盖率测试。覆盖代码并不意味着找到所有可能的错误。有些错误不会导致程序崩溃。然而,2017年才刚刚到来,勒索病毒就以惊人的速度爆发。本周我们发现了大量新变种,尤其是FSociety,它以一个非常著名的名字命名。我们还发现了一些解密工具,圣诞节相关的勒索软件,CryptoMix/CryptFile2分析,很多小型勒索软件。二、代码覆盖率和动态符号执行(DSE)与SSE(StaticSymbolicExecution)不同,DSE用于跟踪,只有在执行过程中到达这些分支时才会发现新分支。要到达另一条路径,我们必须找出从先前跟踪中找到的分支约束。然后我们重复直到到达所有分支。例如,让我们假设程序P有一个名为I的输入。I可以是模型M或随机种子R。执行P(I)可以返回一组PC约束。所有φi表示基本块,πi表示分支约束。模型Mi是约束πi的可靠解,M1=Solution(?π1∧π2)。为了发现所有路径,我们维护一个名为W的工作列表,它是一组M。在第一次迭代中,执行I=R,W=?和P(I)→PC。然后?π∈PC,W=W∪{Solution(π)},再次执行?M∈W,P(M)。当模型M被用作程序的输入时,它被从列表W中移除。重复直到W为空。使用符号执行的代码覆盖率测试既有优点也有缺点。它有助于混淆二进制文件。确实,使用符号覆盖可以检测到隐藏的、无法访问的代码,但它将是一个平面图。最糟糕的是,当你的表达式过于复杂时,它可能会超时或消耗大量内存(过去,我们的符号表达式在超时前消耗了将近450GB的RAM)。这种情况主要发生在分析非常大的二进制文件或包含复杂函数的混淆二进制文件时。3、使用Triton进行代码覆盖测试从v0.1build633版本开始,Triton集成了我们进行代码覆盖测试所需的一切。它可以让我们更好地处理和计算SMT2-lib表达的AST。下面我们将重点介绍代码覆盖率的设计和使用的算法。1.算法以下面的代码为例。01.char*serial="\x31\x3e\x3d\x26\x31";02.intcheck(char*ptr)03.{04.inti=0;05.while(i<5){06.if(((ptr[i]-1)^0x55)!=serial[i])07.return1;08.i++;09.}10.return0;11.}函数控制流程图如下所示。这是一个很好的例子,因为我们需要找到好的输入来覆盖所有基本块。可以看出,可控变量只有一个,位于地址rbp+var_18,指向argv[1]的指针。目标是通过计算约束和使用快照引擎来到达检查功能的所有底层块。例如,到达地址0x4005C3的基本块的约束是[rbp+var_4]>4,但我们不能直接控制这个变量。另一方面,地址0x4005B0处的跳转取决于用户输入,这个约束可以通过符号执行来解决。算法总结之前的思路是使用基于微软的Fuzzer算法(SAGE)。下图显示了包含约束的检查功能。开始和结束节点代表我们函数的开始(0x40056D)和函数的结束(0x4005C8)。在执行***之前,我们不知道任何分支约束。所以我们像上面那样做,我们注入一些随机种子来收集第一台PC并构建我们的W集。***执行P(I)的跟踪结果在下图中用蓝色表示。执行给我们第一个路径约束P(I)→(π0∧?π1)。根据第一条轨迹,我们知道找到了两个分支(π0∧?π1),还有两个没有找到。为了到达基本块φ3,我们计算第一个分支约束的否定条件。当且仅当Solution(?π0)是SAT时,我们将其添加到模型工作列表W中。同样达到φ4可以得到W=W∪{解(π0∧?(?π1))}。生成所有解决方案并将模型添加到工作列表中,我们执行工作列表中的每个模型。2.实现代码覆盖的条件是在跳转指令处可以预测下一条指令的地址。这是构造路径约束的必要条件。我们不能在分支指令之后放置回调,因为RIP寄存器已更改。因为Triton为所有寄存器创建语义表达式,所以RIP可以基于分支指令。我们第一次开发了SMTdeterminer来计算RIP,但是发现Pin提供的IARG_BRANCH_TARGET_ADDR和IARG_BRANCH_TAKEN获取下一个RIP值有点滞后。使用Pin计算下一个地址很简单,但SMT仲裁器对于检查指令的语义很有用。为了更好地演示决策,我们实现了访问者模式,将SMT的抽象语法树(AST)转换为Z3的抽象语法树。此设计可用于将SMTAST转换为任何其他表示。使用Z3API处理Z3的AST要简单得多。转换代码为src/smt2lib/z3AST.h和src/smt2lib/z3AST.cpp现在我们解释一下代码覆盖工具是如何工作的。假设输入来自命令行。首先,我们有:160.defrun(inputSeed,entryPoint,exitPoint,whitelist=[]):161....175.if__name__=='__main__':176.TritonExecution.run("bad!",0x400480,0x40061B,["main","check"])#crackme_xor在第176行,我们定义了输入种子bad!,它代表程序的第一个参数。然后我们给出我们将拍摄快照的代码覆盖率的起始地址。第三个参数将匹配最后一个块,这个地址我们将恢复快照。***,我们设置白名单,避免库函数、加密函数等。(rsi,IDREF.CPUSIZE.QWORD)#argv[0]pointer141.argv1_addr=getMemValue(rsi+8,IDREF.CPUSIZE.QWORD)#argv[1]pointer142.143.print"[+]Inmain()weset:"第144章v,v)148.setMemValue(k,IDREF.CPUSIZE.BYTE,v)149.convertMemToSymVar(k,IDREF.CPUSIZE.BYTE,“addr_%d”%k)150.151.foridx,byteinenumerate(TritonExecution.input.data):152.ifargv1_addr+idxnotinTritonExecution.input.dataAddr:#Notoverwritetheprevioussetting153.print"\t[0x%x]=%x%c"%(argv1_addr+idx,ord(byte),ord(byte))154.setMemValue(argv1_addr+idx,IDREF.CPUSIZE.BYTE,ord(byte))155.convertMemToSymVar(argv1_addr+idx,IDREF.CPUSIZE.BYTE,"addr_%d"%idx)接下来执行的代码是mainAnalysis回调函数,我们注入一些值进入进入在(第148、154行)中,我们可以通过符号变量(第149、155行)覆盖这些输入。所有选定的输入都存储在全局变量TritonExecution.input中。然后我们开始代码检测。58.ifinstruction.getAddress()==TritonExecution.entryPointandnotisSnapshotEnabled():59.print"[+]TakeSnapshot"60.takeSnapshot()61.return当我们在入口点时,我们拍摄快照,以便重新创建它使用新的输入执行代码检测。52.ifinstruction.getAddress()==TritonExecution.entryPoint+2:53.TritonExecution.myPC=[]#重置路径约束#Addthisinputtothetestedinput56.return我们重置路径约束(第53行),从工作列表中获取新输入。63.ifinstruction.isBranch()andinstruction.getRoutineName()inTritonExecution.whitelist:64.65.addr1=instruction.getAddress()+2#Addressnexttothisone66.addr2=instruction.getOperands()[0].getValue()#Addressintheinstructioncondition67.68.#[PCid,地址放样,地址未被占用]69.ifinstruction.isBranchTaken():70.TritonExecution.myPC.append([ripId,addr2,addr1])71.else:72.TritonExecution.myPC.append([ripId,addr1,addr2])73.74.return以上代码检测是否位于分支指令(如jnz、jle等)或白名单中的函数中。如果是,我们得到两个可能的地址(addr1和addr2)并通过isBranchTaken()计算有效地址(第69行)。然后我们将条件约束存储在RIP表达式中。81.ifinstruction.getAddress()==TritonExecution.exitPoint:82.print[+]Exitpoint"83.84.#SAGEalgorithm85.#http://research.microsoft.com/en-us/um/people/pg/public_psfiles/ndss2008.pdf86.forjinrange(TritonExecution.input.bound,len(TritonExecution.myPC)):87.expr=[]88.foriinrange(0,j):89.ripId=TritonExecution.myPC[i][0]90。symExp=getFullExpression(getSymExpr(ripId).getAst())91.addr=TritonExecution.myPC[i][1]92.expr.append(smt2lib.smtAssert(smt2lib.equal(symExp,smt2lib.bv(addr,64))))93.94.ripId=TritonExecution.myPC[j][0]95.symExp=getFullExpression(getSymExpr(ripId).getAst())96.addr=TritonExecution.myPC[j][2]97.expr.append(smt2lib.smtAssert(smt2lib.equal(symExp,smt2lib.bv(addr,64))))98.99.100.expr=smt2lib.compound(expr)101.model=getModel(expr)102.103.iflen(model)>0:104.newInput=TritonExecution.input105.newInput.setBound(j+1)106.107.fork,vinmodel.items():108.symVar=getSymVar(k)109.newInput.addDataAddress(symVar.getKindValue(),v)110。打印新输入#Ifthereisinputtotestintheworklist,wererestorethesnapshot122.iflen(TritonExecution.worklist)>0andisSnapshotEnabled():123.print"[+]Restoresnapshot"124.restoreSnapshot()125.126.return当我们在退出点时yes***一步84-120is圣人完成。简而言之,我们遍历路径约束列表,并为每个PC尝试获得满足否定的模型。如果不可靠的模型到达新的目标块,我们会将此模型添加到工作列表中。一旦所有模型都被插入到工作列表中,我们将恢复快照并重新执行每个模型。完整的代码可以在这里找到。我们例子的执行过程如下:4.总结虽然使用符号执行的代码覆盖是一个很好的方法,但它是一项复杂的任务。路径遍历意味着内存消耗,并且在某些情况下要评估的表达式过于复杂。目前determinator很慢,determinantexpression很慢。