angr源码分析——BackwardSlice

BackwardSlice 程序反向切片,从给出的某一个目标点,获取所有到达该目标点的路径。angr的这个功能似乎并不完善,里面可以自己进行定制。在进行程序切片前,我们需要提供一个控制流图CFG。此外,为了确定程序反向的起始点,还需要提供一个target。下面是angr文档中给出的一个例子:

>>> import angr
# Load the project
>>> b = angr.Project("examples/fauxware/fauxware", load_options={"auto_load_libs": Fal
se})
# Generate a CFG first. In order to generate data dependence graph afterwards,
# you’ll have to keep all input states by specifying keep_stat=True. Feel free
# to provide more parameters (for example, context_sensitivity_level)for CFG
# recovery based on your needs.
>>> cfg = b.analyses.CFGAccurate(context_sensitivity_level=2, keep_state=True)
# Generate the control dependence graph
>>> cdg = b.analyses.CDG(cfg)
# Build the data dependence graph. It might take a while. Be patient!
>>> ddg = b.analyses.DDG(cfg)
# See where we wanna go... let’s go to the exit() call, which is modeled as a
# SimProcedure.
>>> target_func = cfg.kb.functions.function(name="exit")
# We need the CFGNode instance
>>> target_node = cfg.get_any_node(target_func.addr)
# Let’s get a BackwardSlice out of them!
# `targets` is a list of objects, where each one is either a CodeLocation
# object, or a tuple of CFGNode instance and a statement ID. Setting statement
# ID to -1 means the very beginning of that CFGNode. A SimProcedure does not
# have any statement, so you should always specify -1 for it.
>>> bs = b.analyses.BackwardSlice(cfg, cdg=cdg, ddg=ddg, targets=[ (target_node, -1) ]
) #
Here is our awesome program slice!
>>> print bs

先加载一个二进制文件,然后执行CFGAccurate算法,获取一个cfg;

执行CDG获取程序的控制依赖图CDG,执行DDG获取程序的数据依赖图DDG;

获取目标点targets,即,反向切片的起始点,最后生成的切片的结束点;

传入cfg, cdg,ddg以及targets,执行反向切片。

其中cdg与ddg是可选的,也就是可以不提供控制依赖图和数据依赖图。因为某些程序的数据依赖图ddg是生成不出来的。

下面我们跟以往一样,先了解BackwardSlice的API,然后探究它的算法。

classangr.analyses.backward_slice.BackwardSlice(cfg, cdg, ddg, targets=None, cfg_node=None, stmt_id=None, control_flow_slice=False, same_function=False, no_construct=False)

根据提供的控制流图(CFG),控制依赖图(CDG)和数据依赖图(DDG),从特定语句创建后向切片。
数据依赖关系图可以是基于CFG的,也可以是基于值集分析的。 基于CFG的DDG生成速度要快得多,但它仅在生成CFG时反映这些状态,而且既不健全也不准确。 基于VSA的DDG(称为VSA_DDG)基于静态分析,可提供更好的结果。

参数介绍:

cfg - 控制流图。
cdg - 控制依赖图。
ddg - 数据依赖图。
targets - 指定后向切片目标的“targets”列表。 每个目标可以是(cfg_node(节点),stmt_idx(目标语句id,可以设置为-1))或CodeLocation实例形式的元组。
cfg_node - 弃用。 要达到的目标CFGNode。 它应该存在于CFG中。
stmt_id - 弃用。 要达成的目标声明。
control_flow_slice - True / False,表示我们是否应该仅基于CFG进行切片。 有时,在获取DDG很困难或不可能时,您可以在CFG上创建一个切片。 那么,如果你甚至没有CFG,那么......
no_construct - 仅用于测试和调试以轻松创建BackwardSlice对象。

参数非常少,也很好理解。总之,如果你想获得一个准确的程序切片就提供参数:cfg,cdg,ddg,targets,但是耗时比较长。如果你想快速的获得一个程序切片,就是提供参数:cfg,targets,control_flow_slice=True, 速度快但是可能是不可靠的。

所以后向切片才是分析程序的难点,也是我们主要要解决的问题。

下面分析源码:

构造的主要逻辑的在函数_construct里

self._construct(self._targets, control_flow_slice=control_flow_slice)

 def _construct(self, targets, control_flow_slice=False):
        """
        Construct a dependency graph based on given parameters.
        :param targets:             A list of tuples like (CFGNode, statement ID)
        :param control_flow_slice:  Is the backward slicing only depends on CFG or not.
        """

        if control_flow_slice:
            simruns = [ r for r, _ in targets ]
            self._construct_control_flow_slice(simruns)

        else:
            self._construct_default(targets)

它的功能是根据给定的参数构造一个关系依赖图。

如果仅依赖于cfg则调用construct_control_flow_slice函数传入simruns参数。否则调用_construct_default函数,传入targets。

我们主要看_construct_default函数吧,因为construct_control_flow_slice不准确呀。

_construct_default函数从特定块中的特定语句创建后向片。 这是通过向后遍历CFG来完成的,并根据最初提供的依赖图(CDG和DDG)标记所有受污染的语句。 当我们到达入口点时,或者当没有未解决的依赖关系时,遍历就会终止。参数targets:像(cfg_node,stmt_idx)这样的元组列表,其中cfg_node是CFGNode实例,其中后向片开始,并且它必须包含在CFG和CDG中。 stmt_idx是后向片开始处的目标语句的ID。

如果stmt_idx 等于 -1 ,那么会调用_handle_control_dependence生成一个新的taints(taint是污点的意思,要是学习过污点分析就应该能理解,也就是目标点的意思)。
_handle_control_dependence函数是基于控制依赖图,挑选出所有指向目标点的taints。

    def _handle_control_dependence(self, target_node):
        """
        Based on control dependence graph, pick all exits (statements) that lead to the target.
        :param target_node: A CFGNode instance.
        :returns:           A set of new tainted code locations.
        """

        new_taints = set()

        # 查询CDG并找出所有控制流转换以达到此目标,原来从cdg中查询某个点的前向控制点的函数是这个!!
        cdg_guardians = self._cdg.get_guardians(target_node) 
        if not cdg_guardians:
            # this block is directly reachable from the entry point #如果没有,则说明不可达
            pass

        else:
            # 对于CDG的每个前向点,找到正确的出口,并从这些出口继续切片
            for predecessor in cdg_guardians:
                exits = self._find_exits(predecessor, target_node)

                for stmt_idx, target_addresses in exits.iteritems():
                    if stmt_idx is not None:
                        # 如果是一个出口语句,就将它标记为picked
                        self._pick_statement(predecessor.addr,
                                             self._normalize_stmt_idx(predecessor.addr, stmt_idx)
                                             )
                        # 如果是默认语句,我们也应该选择其他条件出口语句
                        if stmt_idx == 'default':
                            conditional_exits = self._conditional_exits(predecessor.addr)
                            for conditional_exit_stmt_id in conditional_exits:
                                cl = CodeLocation(predecessor.addr,
                                                  self._normalize_stmt_idx(predecessor.addr, conditional_exit_stmt_id)
                                                  )
                                new_taints.add(cl)

                                self._pick_statement(predecessor.addr,
                                                     self._normalize_stmt_idx(predecessor.addr, conditional_exit_stmt_id)
                                                     )

                    if target_addresses is not None:

                        if stmt_idx is not None:
                            # If it's an exit statement, we create a new tainted code location
                            cl = CodeLocation(predecessor.addr,
                                              self._normalize_stmt_idx(predecessor.addr, stmt_idx)
                                              )
                            new_taints.add(cl)#加入新的污点

                        # 标记这些exit为picked
                        for target_address in target_addresses:
                            self._pick_exit(predecessor.addr, stmt_idx, target_address)

                    # On CFG, pick default exits of all nodes between predecessor and our target node
                    # Usually this is not required if basic blocks strictly end at control flow transitions. But this is
                    # not always the case for some architectures
                    all_simple_paths = list(networkx.all_simple_paths(self._cfg.graph, predecessor, target_node, cutoff=3))

                    previous_node = None
                    for path in all_simple_paths:
                        for node in path:
                            self._pick_statement(node.addr, self._normalize_stmt_idx(node.addr, 'default'))
                            if previous_node is not None:
                                self._pick_exit(previous_node.addr, 'default', node.addr)

        return new_taints

这一部分复杂又重要啊,挖挖挖个坑。

获取到指向targets的taints后。对于每个taints,我们先将它标记为picked,然后标记为accessed。

然后从数据依赖图中挑选出它的数据依赖。函数是:_ddg.predecessors,然后再将新发现的前向数据依赖点添加到taints中。
然后再调用控制依赖图挑选出它的控制依赖,再寻找新的taints。
就这样一级一级找上去,把所有的指向targets的taint都找到,并加入到taints中。
获取控制依赖的前向点的函数为 _handle_control_dependence > _cdg.get_guardians
获取数据依赖的前向点的函数为 _ddg.predecessors 

有的时候无法得到ddg或cdg,除了无法处理数据依赖外,也与给出的targets不在cfg中有关。

下面我们看一下,当只有控制流图CFG时,angr是如何得到backward slice的。

在这个过程中调用的是函数_construct_control_flow_slice

    def _construct_control_flow_slice(self, simruns):
        """
        Build a slice of the program without considering the effect of data dependencies.
        This is an incorrect hack, but it should work fine with small programs.
        :param simruns: A list of SimRun targets. You probably wanna get it from the CFG somehow. It must exist in the
                        CFG.
        """

        # TODO: Support context-sensitivity!

        if self._cfg is None:
            l.error('Please build CFG first.')

        cfg = self._cfg.graph
        for simrun in simruns:
            if simrun not in cfg:
                l.error('SimRun instance %s is not in the CFG.', simrun)

        stack = [ ]
        for simrun in simruns:
            stack.append(simrun)

        self.runs_in_slice = networkx.DiGraph()
        self.cfg_nodes_in_slice = networkx.DiGraph()

        self.chosen_statements = { }
        while stack:
            # Pop one out
            block = stack.pop()
            if block.addr not in self.chosen_statements:
                self.chosen_statements[block.addr] = True
                # Get all predecessors of that block
                predecessors = cfg.predecessors(block)
                for pred in predecessors:
                    stack.append(pred)
                    self.cfg_nodes_in_slice.add_edge(pred, block)
                    self.runs_in_slice.add_edge(pred.addr, block.addr)

它利用的是targets的SimRuns。SimRuns具体是什么我还没有搞清楚,但是目前可以把它当做targets所在的block。

首先把targets的simrun加入一个栈中。然后依次从栈弹出block,如果block没有被添加,则加入到字典chosen_statements中,并获取这个block的前向点集合predecessors,再加入到stack里,同时构建以node为节点的图cfg_nodes_in_slice,和地址为节点的图runs_in_slice。

到这为止,程序就切片完成啦!是不是很简单?

实际上No!还有很多地方需要解决。

例如,如何改进CFG的跳转恢复。CDG与DDG的恢复算法。



你可能感兴趣的:(漏洞挖掘,物联网)