4、angr的路径管理机制:SimState、SimulationManager、路径优先级与探索策略

路径爆炸,说白了就是状态太多,内存撑不住。我刚开始用angr的时候,经常跑着跑着就OOM了。后来才发现,问题出在路径管理上——你让angr怎么管这些状态,它就会怎么爆炸。

这一节,我们来聊聊angr的路径管理核心。说白了就三样东西:SimState(状态怎么存)、SimulationManager(状态怎么管)、探索策略(先跑哪个状态)。搞懂了这三样,你就能控制路径爆炸的节奏。

4.1 SimState:每个路径的“快照”

每个路径,本质上就是一个SimState对象。它记录了程序执行到某一点时的全部信息:寄存器、内存、符号约束、文件系统、网络状态……嗯,你可以把它理解成虚拟机的一个快照。

我个人习惯把SimState拆成三部分来看:

  • 寄存器与内存state.regsstate.mem,存的是具体数值或符号变量
  • 符号约束state.solver.constraints,记录了这个路径必须满足的条件
  • 插件系统:比如 state.fs(文件系统)、state.posix(标准输入输出)

举个例子,你创建一个初始状态:

import angr

proj = angr.Project('binary')
state = proj.factory.entry_state()

这个state就是程序入口处的快照。angr每遇到一个分支,就会复制一份当前状态,然后分别加上不同的约束。复制多了,路径就爆炸了。

核心要点:SimState是angr路径管理的最小单位。你管理的不是路径,而是一堆SimState。

4.2 SimulationManager:状态的“调度器”

有了状态,谁来管它们?SimulationManager(简称simgr)就是干这个的。

我刚开始用angr时,直接写了个循环手动管理状态,结果代码又臭又长。后来发现simgr已经封装好了——它维护了一个状态列表,并且提供了各种探索方法。

创建simgr很简单:

simgr = proj.factory.simulation_manager(state)

simgr内部维护了多个“状态桶”:

桶名含义说明
active活跃状态还能继续执行的状态
deadended死胡同执行到exit或无法继续的状态
errored出错状态执行过程中抛异常的状态
found目标状态满足你设定条件的状态

你想想看,每次simgr.step(),angr就会把active里的每个状态往前推一步。遇到分支,状态就分裂。遇到exit,状态就移到deadended

避坑指南:我曾经在循环里反复调用simgr.step(),结果状态数指数级增长。后来我改用simgr.run()并设置until条件,才控制住局面。

4.3 路径优先级:谁先跑,谁后跑

状态多了,先处理哪个?这就涉及到路径优先级了。angr默认是深度优先(DFS),也就是一条路走到黑,不行再回头。

但深度优先有个问题——如果路径很深,你可能永远跑不到其他分支。我遇到过这种情况:一个循环跑了1000次,angr就在那一个分支里死磕,其他分支全被晾着。

angr提供了几种优先级策略:

  • 深度优先(DFS):默认策略,优先探索最深的状态
  • 广度优先(BFS):优先探索最浅的状态,适合找最短路径
  • 符号执行优化:根据状态复杂度排序,比如约束少的优先

设置优先级其实很简单:

# 使用广度优先
simgr.use_technique(angr.exploration_techniques.DFS())  # 默认就是DFS
simgr.use_technique(angr.exploration_techniques.BFS())  # 改成BFS

嗯,这里要注意:use_technique可以叠加使用。比如你先用DFS跑一会儿,再切到BFS,或者混合使用。

4.4 探索策略:不止是DFS和BFS

angr内置了十几种探索策略。我挑几个常用的说说:

策略名作用适用场景
Explorer找满足条件的状态找指定地址、指定条件
LoopSeer检测循环,限制循环次数防止循环导致路径爆炸
LengthLimiter限制路径长度防止无限递归
Spiller把状态溢出到磁盘内存不够时用

举个例子,你想找程序里某个地址:

simgr.explore(find=0x4001024)

这行代码背后,angr自动用了Explorer策略。它会优先探索可能到达目标地址的路径,其他路径暂时放一放。

注意explore不是万能的。如果目标地址被大量分支包围,它依然可能爆炸。我建议配合LoopSeer一起用,先限制循环次数。

4.5 知识体系:一张图看懂路径管理

说了这么多,我们来画个图总结一下。下面这张SVG图展示了angr路径管理的核心逻辑:

angr路径管理核心逻辑 二进制程序 SimState(状态快照) 寄存器 · 内存 · 约束 · 插件 SimulationManager(状态调度器) active · deadended · errored · found 探索策略(路径优先级) DFS · BFS · Explorer · LoopSeer · Spiller 决定先跑哪个状态,控制路径爆炸

这张图其实就说明了一件事:路径管理 = 状态快照 + 调度器 + 优先级策略。你控制好这三个环节,路径爆炸就能被有效抑制。

4.6 实战建议

最后,我分享几个实战中的小技巧:

  • 别让状态无限增长:每次step()后检查len(simgr.active),超过阈值就暂停或丢弃
  • 善用Spiller:内存不够时,把不活跃的状态写到磁盘,需要时再读回来
  • 组合使用策略:比如LoopSeer + Explorer,既能防循环爆炸,又能快速找到目标
  • 手动干预:如果自动策略不理想,可以直接操作simgr.active列表,手动排序或删除状态

个人经验:我一般会在step()循环里加个计数器,每100步打印一次状态数。一旦发现状态数超过1000,就立刻启用SpillerLengthLimiter。这招帮我避免了好几次OOM。

路径管理,说白了就是跟状态数做斗争。你理解了SimState怎么存、SimulationManager怎么管、策略怎么选,就能在这场斗争中占据主动。下一节,我们会深入探讨如何用LoopSeerSpiller来具体解决路径爆炸问题。


公众号:蓝海资料掘金营,微信deep3321