4、加载二进制文件:angr.Project()参数详解,加载ELF/PE文件

好,咱们进入正题。这一章我打算聊聊 angr 的入口——Project()。说白了,它就是 angr 认识一个二进制文件的起点。你给它一个文件,它帮你把里面的代码、数据、符号、段信息全拆开,然后装进自己的内存模型里。

我刚开始用 angr 的时候,觉得这玩意儿不就是传个路径嘛,有啥好讲的?后来踩了几个坑才发现,Project() 的参数配置,直接决定了后续分析能不能跑通。嗯,咱们一个一个来看。

4.1 核心参数:auto_load_libs

这个参数,我个人认为是新手最容易忽略的坑。它的默认值是 True,意思是 angr 会自动加载目标文件依赖的所有共享库。听起来很贴心对吧?但实际跑起来,你会发现它慢得离谱。

为什么会这样?因为 angr 会把 libc.so、libstdc++.so 这些庞然大物全部加载进来,然后对每个库里的每个函数都做符号执行。你想想看,一个简单的 printf 调用,angr 会尝试模拟 libc 里几百行代码的逻辑。这谁顶得住?

警告: 如果你只是做漏洞挖掘,比如找缓冲区溢出、格式化字符串这类问题,强烈建议把 auto_load_libs 设为 False。angr 会用一个叫 SimProcedure 的轻量级模拟函数来代替真实的库函数,速度能快上几十倍。
# 推荐的做法
proj = angr.Project('./target_binary', auto_load_libs=False)

我曾经在一个 CTF 题目里,忘了关这个选项,结果跑了半小时还没出结果。关了之后,30 秒就找到了路径。嗯,血的教训。

4.2 核心参数:main_opts 与 lib_opts

这两个参数是用来定制加载行为的。我习惯把它们理解成「加载选项字典」。

  • main_opts:针对主二进制文件的选项
  • lib_opts:针对依赖库的选项,是个字典,key 是库名,value 是选项字典

最常用的场景是:你想指定二进制文件的基地址(base address)。比如有些固件分析,或者 PIE 关闭的 ELF 文件,你可能需要手动设置加载基址。

proj = angr.Project(
    './target_binary',
    main_opts={'base_addr': 0x400000},
    auto_load_libs=False
)

对于 PE 文件,基址通常由 Windows 加载器决定,但 angr 也允许你覆盖。我记得有一次分析一个加了壳的 PE,默认基址跟实际运行时的基址对不上,导致符号解析全乱套。手动指定 base_addr 后,问题就解决了。

4.3 核心参数:use_sim_procedures

这个参数默认是 True。它控制 angr 是否用 SimProcedure 来替换外部函数。如果你设了 auto_load_libs=False,那这个参数基本就是必须为 True 的,否则外部函数调用会直接报错。

但有一种情况你需要把它设为 False:当你希望 angr 真的去执行外部库的代码,而不是用模拟函数替代。比如你在分析一个跟加密库交互的程序,想看看真实的加密流程。不过这种情况很少见,因为性能代价太大了。

小技巧: 如果你不确定某个外部函数有没有对应的 SimProcedure,可以用 angr.SIM_PROCEDURES 查看。这个字典里存了所有内置的模拟函数。

4.4 加载 ELF 文件 vs PE 文件

angr 对 ELF 和 PE 的支持都挺成熟的,但底层实现有区别。我简单列个表,方便你对比:

特性 ELF PE
加载器 CLE (CLE Loads Everything) 的 ELF 后端 CLE 的 PE 后端
基址处理 支持 PIE,默认随机基址 默认基址 0x400000,可覆盖
导入表 通过 .dynsym 和 .rela.plt 解析 通过 IMAGE_IMPORT_DESCRIPTOR 解析
导出表 通过 .dynsym 解析 通过 IMAGE_EXPORT_DIRECTORY 解析
资源节 支持 .rsrc 节解析
异常处理 不支持(通常不需要) 支持 SEH (结构化异常处理)

我个人经验是,ELF 文件的分析体验更流畅,因为 Linux 下的工具链更成熟。PE 文件偶尔会遇到一些奇怪的问题,比如某些加壳 PE 的导入表被破坏了,angr 解析不出来。这时候你可能需要先用其他工具(比如 pefile)把导入表修复一下。

4.5 其他实用参数

除了上面几个,还有几个参数我偶尔会用:

  • force_load_libs:强制加载某些库,即使它们不在依赖列表里。适合分析插件或动态加载的场景。
  • skip_libs:跳过某些库的加载。跟 auto_load_libs=False 不同,这个可以精确控制跳过哪些库。
  • custom_ld_path:指定动态链接器的路径。如果你在分析嵌入式固件,可能需要这个。
  • arch:手动指定架构。大部分情况下 angr 能自动识别,但遇到一些冷门架构(比如 msp430)时,需要手动指定。
proj = angr.Project(
    './firmware.bin',
    arch='msp430',
    main_opts={'base_addr': 0x8000},
    auto_load_libs=False
)

4.6 加载后的检查

加载完二进制文件后,我建议你马上做几个检查,确认 angr 理解得对不对:

  1. 检查入口点proj.entry 应该指向你期望的入口地址。
  2. 检查段信息proj.loader.main_object.segments 可以列出所有段。
  3. 检查符号表proj.loader.main_object.symbols 可以列出所有符号。
  4. 检查函数列表proj.kb.functions 可以查看 angr 识别出了哪些函数。

如果这些检查结果跟你预期的不一样,那多半是加载参数没配对。我曾经遇到过一个情况,入口点指向了一个奇怪的地址,后来发现是二进制文件被 strip 了,angr 把某个数据段当成了代码段。嗯,这时候就需要手动指定 main_opts 里的 entry_point 了。

4.7 知识体系图

下面这张图,我把 Project() 的核心逻辑画出来了。你可以看到,加载过程分为几个阶段:文件识别、架构检测、段加载、符号解析、库加载。每个阶段都有对应的参数可以干预。

angr.Project() 加载流程 传入文件路径 文件格式识别 (ELF/PE/Mach-O) 架构检测 (arch 参数可覆盖) 段加载 (main_opts/base_addr) 符号解析 + 库加载 (auto_load_libs/lib_opts) 关键参数 • auto_load_libs • main_opts • lib_opts • use_sim_procedures • force_load_libs • skip_libs • custom_ld_path • arch • base_addr • entry_point

核心要点:

  • auto_load_libs=False 是性能优化的第一步,除非你明确需要分析库代码。
  • main_optslib_opts 是定制加载行为的主要手段,尤其是基址设置。
  • 加载后一定要做检查,确认入口点、段、符号都正确。
  • ELF 和 PE 的加载细节不同,但接口是统一的,你不需要关心底层实现。

好了,这一章的内容就到这里。记住,Project() 是 angr 的入口,配好参数,后面的分析才能顺风顺水。下一章我们会聊如何用 angr 做符号执行,到时候你会看到,加载阶段的配置直接影响符号执行的效率和准确性。


专注资料整理