一、前言
代码覆盖测试主要用于漏洞研究领域。主要目的是使用不同的输入来覆盖程序代码的不同部分。如果输入导致程序崩溃,我们将检测崩溃是否可以使用。代码覆盖测试的 *** 有很多,如随机测试。但本文重点关注使用动态符号进行代码覆盖测试。覆盖代码并不意味着找到所有可能的缺陷。有些缺陷不会导致程序崩溃。然而,在2017年,勒索软件以惊人的速度爆发。本周我们发现了很多新的变化,特别是那些以著名的名字命名的FSociety。我们还发现了一些与圣诞节相关的勒索软件解密工具,CryptoMix/CryptFile2分析,大量的小勒索软件。
二、二。代码覆盖和动态符号执行(DSE)
不像SSE(静态符号执行),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为空。
符号执行的代码覆盖测试既有优缺点。这对混淆的二进制文件很有帮助。使用符号覆盖确实可以检测到隐藏的、不可达的代码,但它将是一个平面图。最糟糕的是,当你的表达式太复杂时,它可能会超时或巨大的内存消耗(在过去,我们的符号表达式在超时前消耗了近450 G的 RAM)。这种场景主要发生在分析超大二进制文件或者包含复杂功能的混淆的二进制文件。
三、使用Triton代码覆盖测试
从版本v0.1 build 633开始,Triton整合我们的代码覆盖测试所需的一切。它能使我们更好地处理和计算 *** T2-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]指针。目标是计算约束条件,使用快照引擎到达check函数的所有基础块。例如,到达地址0x4005C3基块的约束条件是[rbp var_4] > 4,但我们不能直接控制变量。另一方面,地址0x4005B0跳转取决于用户的输入,这种约束可以通过符号来解决。
总结以前想法的算法是基于微软的Fuzzer算法(SAGE),下图显示了包含约束条件的约束条件check函数。这个start和end节点表示我们的函数开始(0x40056D)以及函数的结尾(0x4005C8)
在***在执行之前,我们不知道任何分支约束。因此,根据上述情况,我们注入一些随机种子收集***个PC并构建我们的W *** 。***执行P(I)跟踪结果以下跟踪结果。
给了我们执行结果***条路径约束P(I) → (π0 ∧ ¬π1)。
基于***第二次跟踪,我们发现了两个分支(π0 ∧ ¬π1),还有两个没有发现。为了到达基础块。φ3,我们计算***个别分支约束的否定条件。Solution(¬π0) 是SAT,我们将它添加到模型工作列表W中。
同样到达 φ4 可以得到W = W ∪ {Solution(π0 ∧ ¬(¬π1))}。所有的解决方案都生成并添加到工作列表中,我们执行工作列表中的每个模型。
2. 实现
执行代码覆盖的一个条件是在跳转指令处预测下一个指令的地址。这是构建路径约束的必要条件。
我们不能在分支指令后放置回调,因为RIP寄存器已经改变了。Triton为所有寄存器创建语义表达式,因此可以在分支指令中确定RIP。
***第二,我们开发了一个 *** T用于计算的判定器RIP,但是我们发现了Pin用于获得下一个RIP值的IARG_BRANCH_TARGET_ADDR和IARG_BRANCH_TAKEN有点滞后Pin计算下一个地址很简单,但是 *** T用于检查指令语义的判定器非常有用。
我们实现了更好的演示判断visitor pattern来将 *** T抽象语法树(AST)转化为Z3抽象语法树。这个设计能够用于将 *** T AST转化为任何其他表达。
Z3的AST使用Z3 API更容易处理。转换代码是src/ *** t2lib/z3AST.h 和 src/ *** t2lib/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!,代表程序的***一个参数。然后我们给出代码覆盖的起始地址,我们会在这个地址照。第三个参数将匹配***一块,我们将在这个地址恢复快照。***,为避免库函数、加密函数等设置白名单。
134.defmainAnalysis(threadId):135.136.print"[ ]Inmain"137.rdi=getRegValue(IDREF.REG.RDI)#argc138.rsi=getRegValue(IDREF.REG.RSI)#argv139.140.argv0_addr=getMemValue(rsi,IDREF.CPUSIZE.QWORD)#argv[0]pointer141.argv1_addr=getMemValue(rsi 8,IDREF.CPUSIZE.QWORD)#argv[1]pointer142.143.print"[ ]Inmain()weset:"144.od=OrderedDict(sorted(TritonExecution.input.dataAddr.items()))145.146.fork,vinod.iteritems():147.print"\t[0x%x]=%x%c"%(k,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=[]#Resetthepathconstraint54.TritonExecutionTritonExecution.input=TritonExecution.worklist.pop()#Takethefirstinput55.TritonExecution.inputTested.append(TritonExecution.input)#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,addresstaken,addressnottaken]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( *** t2lib. *** tAssert( *** t2lib.equal(symExp, *** t2lib.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( *** t2lib. *** tAssert( *** t2lib.equal(symExp, *** t2lib.bv(addr,64))))98.99.100.expr= *** t2lib.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.printnewInput.dataAddr111.112.isPresent=False113.114.forinpinTritonExecution.worklist:115.ifinp.dataAddr==newInput.dataAddr:116.isPresent=True117.break118.ifnotisPresent:119.TritonExecution.worklist.append(newInput)120.121.#Ifthereisinputtotestintheworklist,werestorethesnapshot122.iflen(TritonExecution.worklist)>0andisSnapshotEnabled():123.print"[ ]Restoresnapshot"124.restoreSnapshot()125.126.return当我们在出口点时,是的***一步。行84-120是SAGE实现。简而言之,我们浏览每个路径约束列表PC,我们试图获得一个令人满意的模型。如果一个不可靠的模型达到了一个新的目标块,我们将该模型添加到工作列表中。一旦所有模型都插入工作列表,我们将恢复快照并重新执行每个模型。
这里可以找到所有代码。我们例子的执行过程如下:
四、总结
虽然代码覆盖是使用符号执行的好 *** ,但它是一项复杂的任务。路径遍历意味着内存消耗,在某些情况下计算的表达式过于复杂。目前,判断器非常慢,表达式非常慢。