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中。有的时候无法得到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的恢复算法。