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 真的去执行外部库的代码,而不是用模拟函数替代。比如你在分析一个跟加密库交互的程序,想看看真实的加密流程。不过这种情况很少见,因为性能代价太大了。
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 理解得对不对:
- 检查入口点:
proj.entry应该指向你期望的入口地址。 - 检查段信息:
proj.loader.main_object.segments可以列出所有段。 - 检查符号表:
proj.loader.main_object.symbols可以列出所有符号。 - 检查函数列表:
proj.kb.functions可以查看 angr 识别出了哪些函数。
如果这些检查结果跟你预期的不一样,那多半是加载参数没配对。我曾经遇到过一个情况,入口点指向了一个奇怪的地址,后来发现是二进制文件被 strip 了,angr 把某个数据段当成了代码段。嗯,这时候就需要手动指定 main_opts 里的 entry_point 了。
4.7 知识体系图
下面这张图,我把 Project() 的核心逻辑画出来了。你可以看到,加载过程分为几个阶段:文件识别、架构检测、段加载、符号解析、库加载。每个阶段都有对应的参数可以干预。
核心要点:
auto_load_libs=False是性能优化的第一步,除非你明确需要分析库代码。main_opts和lib_opts是定制加载行为的主要手段,尤其是基址设置。- 加载后一定要做检查,确认入口点、段、符号都正确。
- ELF 和 PE 的加载细节不同,但接口是统一的,你不需要关心底层实现。
好了,这一章的内容就到这里。记住,Project() 是 angr 的入口,配好参数,后面的分析才能顺风顺水。下一章我们会聊如何用 angr 做符号执行,到时候你会看到,加载阶段的配置直接影响符号执行的效率和准确性。