静态二进制分析中,对于程序控制流图CFG的计算是很基础且重要的一步,很多的分析是要建立在CFG的基础上。angr作为二进制分析工具,当然提供了CFG功能,下面我们就来探索下要如何使用angr计算CFG,以及其中的坑。

angr中的CFG分为2种:CFGFast和CFGAccurate。两者的区别在于前者计算的东西更少,从而也就更快。一般情况下CFGFast就够了,但在研究中若要依靠CFG进一步分析的话可能就需要CFGAccurate了,更精准当然也就更慢,需在两者间加以权衡。

我们就以一个简单的例子开始吧。

(1) 计算CFGFast

import angr
binary = 'func_call'
b = angr.Project(binary,load_options={'auto_load_libs':False})
cfg = b.analyses.CFG()

说明:angr中CFG()是CFGFast()的子类,也就是在CFGFast()基础上的一个包装。

这里我们通过angr-utils将cfg绘画出来,看起来更直观:

这里我们主要关注0x4005bc->0x4005ff0x4005bc->0x4005ee两条边,也就是源码中从func()函数有2次返回至main()函数的边。由于计算CFGFast()时不会考虑函数调用的上下文关系(在angr中称为context_sensitivity_level),导致单纯从CFG来看我们发现从0x4005f5节点可以走到0x4005ff,从0x4005e4节点也可以走到0x4005ff,而源码表示的含义是若要走到0x4005ff节点,那就必须经过0x4005f5,而从0x4005e4节点只能走到0x4005ee。因此,CFGFast丢失了上下文调用关系,从而导致在后向切片时的不准确性,也就是说当要对0x4005ff进行切片时,0x4005f50x4005e4都会在切片结果内,而实际上只有0x4005f5在我们希望的切片范围中。所以,为了保留这种上下文调用关系,angr还提供了一个API - CFGAccurate()。

(2) 计算CFGAccurate

import angr
from angrutils import *
binary = 'func_call'
b = angr.Project(binary,load_options={'auto_load_libs':False})
main = b.loader.main_bin.get_symbol("main")
s = b.factory.blank_state(addr=main.addr)
cfg = b.analyses.CFGAccurate(fail_fast=True, starts=[main.addr], initial_state=s)
plot_cfg(cfg, "cfgaccurate", asminst=True, remove_imports=True, remove_path_terminator=True)

CFGAccurate()默认的context_sensitivity_level参数=1

在上图中我们可以看出,有2个相同的func()函数节点,分别从0x4005e4和0x4005f5两个节点可到达。也就是说它把两次与func()函数的调用关系单独计算,从而将这两条路径分离开,从而对于0x4005ff节点进行后向切片时,只有0x4005f5会在切片范围内,表示从0x4005f5->0x4005ff是可达的,而从0x4005e4出发则是不能到达目标节点的,符合实际情况。

因此总结来说,angr计算CFG的过程就是:模拟执行每个基本块,并判断该基本块下一步会走向哪个基本块,从而建立cfg的边关系。然而存在一些困难:一个基本块在不同的上下文中会有不同的表现形式。举例来说,一个函数返回时的基本块(即上面的func()函数),对于不同的调用者而言该基本块的下一跳的地址是不一样的。因此上下文调用关系的保留情况对于CFG构建至关重要。angr中即是通过context_sensitivity_level参数来确定在调用栈中保留多少个函数,用一个官网的例子来解释下这个参数的概念吧:

这里对于puts函数而言,有4个调用链:main->alpha->putsmain->alpha->error->putsmain->beta->putsmain->beta->error->puts。在这种情况下,angr可以对每条调用链都分别处理,但在实际程序中往往不可行,因此angr提供context_sensitivity_level参数用于设置在调用栈中保存的函数调用者caller的个数。举例说明,对于puts函数而言:

context_sensitivity_level=1:当从alpha调用puts时,puts函数便会返回到alpha;当从beta调用puts时,puts便会返回到beta;对应上面的例子,当从main1调用func时会返回到main1,从main2处调用func的话会返回到main2。

context_sensitivity_level=0:CFG仅仅表示当从puts函数返回时,会返回到alpha, beta, error这三个函数;对应上面的例子,不管从哪里调用func函数最终返回时会到任何调用func函数的下一基本块,即同时返回到main1和main2。

上面就是context_sensitivity_level的概念和应用场景分析,当然该值越大计算的CFG越准确,但这也会指数级地增加分析的时间,到底是要速度还是要精度就要具体情况具体分析了。

results matching ""

    No results matching ""