载入一个二进制文件 Loading a Binary
在上一章,我们初次尝试了angr的载入设施——你加载了/bin/true
,并且在不加载共享资源库的同时再次加载了它。你也看到了像proj.loader
等angr能做的事情。现在,我们将深入这些接口的细节,并且了解我们能通过它们获取到的信息。
装载器 The Loader
让我们加载examples/fauxware/fauxware
并且仔细看看如何与装载器进行交互。
>>> import angr, monkeyhex
>>> proj = angr.Project('examples/fauxware/fauxware')
>>> proj.loader
<Loaded fauxware, maps [0x400000:0x5008000]>
已装载的对象 Loaded Objects
CLE装载器表示已装载并映射到单个内存空间的二进制对象的整体。每个二进制对象由能处理该对象文件类型(cle.Backend
的一个子类)的装载器后端载入。比如,cle.ELF
被用来装载ELF二进制文件。
在内存中也会有一些与任何已经装载的二进制文件无关的对象。比如,提供本地线程存储支持的对象和提供未解决符号的外部对象。
你可以使用loader.all_objects
获取CLE装载的对象的完整列表,也包括几种更加具体分类的列表:
# All loaded objects
>>> proj.loader.all_objects
[<ELF Object fauxware, maps [0x400000:0x60105f]>,
<ELF Object libc-2.23.so, maps [0x1000000:0x13c999f]>,
<ELF Object ld-2.23.so, maps [0x2000000:0x2227167]>,
<ELFTLSObject Object cle##tls, maps [0x3000000:0x3015010]>,
<ExternObject Object cle##externs, maps [0x4000000:0x4008000]>,
<KernelObject Object cle##kernel, maps [0x5000000:0x5008000]>]
# This is the "main" object, the one that you directly specified when loading the project
>>> proj.loader.main_object
<ELF Object fauxware, maps [0x400000:0x60105f]>
# This is a dictionary mapping from shared object name to object
>>> proj.loader.shared_objects
{ 'fauxware': <ELF Object fauxware, maps [0x400000:0x60105f]>,
'libc.so.6': <ELF Object libc-2.23.so, maps [0x1000000:0x13c999f]>,
'ld-linux-x86-64.so.2': <ELF Object ld-2.23.so, maps [0x2000000:0x2227167]> }
# Here's all the objects that were loaded from ELF files
# If this were a windows program we'd use all_pe_objects!
>>> proj.loader.all_elf_objects
[<ELF Object fauxware, maps [0x400000:0x60105f]>,
<ELF Object libc-2.23.so, maps [0x1000000:0x13c999f]>,
<ELF Object ld-2.23.so, maps [0x2000000:0x2227167]>]
# Here's the "externs object", which we use to provide addresses for unresolved imports and angr internals
>>> proj.loader.extern_object
<ExternObject Object cle##externs, maps [0x4000000:0x4008000]>
# This object is used to provide addresses for emulated syscalls
>>> proj.loader.kernel_object
<KernelObject Object cle##kernel, maps [0x5000000:0x5008000]>
# Finally, you can to get a reference to an object given an address in it
>>> proj.loader.find_object_containing(0x400000)
<ELF Object fauxware, maps [0x400000:0x60105f]>
你可以与这些对象直接交互来从它们中提取元数据:
>>> obj = proj.loader.main_object
# The entry point of the object
>>> obj.entry
0x400580
>>> obj.min_addr, obj.max_addr
(0x400000, 0x60105f)
# Retrieve this ELF's segments and sections
>>> obj.segments
<Regions: [<ELFSegment memsize=0xa74, filesize=0xa74, vaddr=0x400000, flags=0x5, offset=0x0>,
<ELFSegment memsize=0x238, filesize=0x228, vaddr=0x600e28, flags=0x6, offset=0xe28>]>
>>> obj.sections
<Regions: [<Unnamed | offset 0x0, vaddr 0x0, size 0x0>,
<.interp | offset 0x238, vaddr 0x400238, size 0x1c>,
<.note.ABI-tag | offset 0x254, vaddr 0x400254, size 0x20>,
...etc
# You can get an individual segment or section by an address it contains:
>>> obj.find_segment_containing(obj.entry)
<ELFSegment memsize=0xa74, filesize=0xa74, vaddr=0x400000, flags=0x5, offset=0x0>
>>> obj.find_section_containing(obj.entry)
<.text | offset 0x580, vaddr 0x400580, size 0x338>
# Get the address of the PLT stub for a symbol
>>> addr = obj.plt['strcmp']
>>> addr
0x400550
>>> obj.reverse_plt[addr]
'strcmp'
# Show the prelinked base of the object and the location it was actually mapped into memory by CLE
>>> obj.linked_base
0x400000
>>> obj.mapped_base
0x400000
符号和重定位 Symbols and Relocations
你也可以在使用CLE的同时使用符号。符号是可执行文件格式中的一个重要概念,它有效地将一个名称定位到一个地址上。
从CLE获取符号的最简单的方式是使用loader.find_symbol
,它会获取一个名字或者地址,然后返回一个符号对象。
>>> strcmp = proj.loader.find_symbol('strcmp')
>>> strcmp
<Symbol "strcmp" in libc.so.6 at 0x1089cd0>
符号最有用的属性是它的名字,所在的文件和它的地址,虽然有时候一个符号的“地址”有可能是模糊的。符号对象返回地址的方式有三种:
.rebased_addr
是它在全局地址空间中的地址。这个是打印输出的显示。.linked_addr
是它关于于二进制文件的预链接基址的相对地址。这种地址是readelf(1)
等报告的地址。.relative_addr
是关于对象基址的相对地址。这在字面上(特别是在Windows环境中)被理解为RVA(relative virtual address)相对虚拟地址。
>>> strcmp.name
'strcmp'
>>> strcmp.owner
<ELF Object libc-2.23.so, maps [0x1000000:0x13c999f]>
>>> strcmp.rebased_addr
0x1089cd0
>>> strcmp.linked_addr
0x89cd0
>>> strcmp.relative_addr
0x89cd0
除了提供调试信息,符号也用于支持动态链接。libc提供了strcmp的导出符号,主程序依赖它。如果我们要求CLE直接从main对象中返回一个strcmp符号,它会告诉我们这是一个导入符号。导入符号不包含与它们相关的有意义的地址,但是它们通过.reslovedby
提供指向解析它们的符号的引用。
>>> strcmp.is_export
True
>>> strcmp.is_import
False
# On Loader, the method is find_symbol because it performs a search operation to find the symbol.
# On an individual object, the method is get_symbol because there can only be one symbol with a given name.
>>> main_strcmp = proj.loader.main_object.get_symbol('strcmp')
>>> main_strcmp
<Symbol "strcmp" in fauxware (import)>
>>> main_strcmp.is_export
False
>>> main_strcmp.is_import
True
>>> main_strcmp.resolvedby
<Symbol "strcmp" in libc.so.6 at 0x1089cd0>
在内存中注册链接导入和导出的这种特定方式使用叫做重定位的另外一种概念。重定位是这样描述的,“当与一个导出符号配对[导入符号]时,请将导出的地址按照[格式]写入[位置]给出的地址。”我们可以通过obj.relocs
看到一个对象重定位的完整列表(作为Relocation
实例),或者使用obj.imports
仅仅通过符号名称将符号名称定位到重定位。没有相关的导出符号。
与重定位相关的导入符号可以通过.symbol
访问。重定位写入的地址可以通过任何符号地址标识来访问。也可以通过.owner
来获取请求重定位的对象的引用。
# Relocations don't have a good pretty-printing, so those addresses are python-internal, unrelated to our program
>>> proj.loader.shared_objects['libc.so.6'].imports
{'__libc_enable_secure': <cle.backends.elf.relocation.amd64.R_X86_64_GLOB_DAT at 0x7ff5c5fce780>,
'__tls_get_addr': <cle.backends.elf.relocation.amd64.R_X86_64_JUMP_SLOT at 0x7ff5c6018358>,
'_dl_argv': <cle.backends.elf.relocation.amd64.R_X86_64_GLOB_DAT at 0x7ff5c5fd2e48>,
'_dl_find_dso_for_object': <cle.backends.elf.relocation.amd64.R_X86_64_JUMP_SLOT at 0x7ff5c6018588>,
'_dl_starting_up': <cle.backends.elf.relocation.amd64.R_X86_64_GLOB_DAT at 0x7ff5c5fd2550>,
'_rtld_global': <cle.backends.elf.relocation.amd64.R_X86_64_GLOB_DAT at 0x7ff5c5fce4e0>,
'_rtld_global_ro': <cle.backends.elf.relocation.amd64.R_X86_64_GLOB_DAT at 0x7ff5c5fcea20>}
如果导入不能解析到任何导出,比如,有一个共享资源库无法找到时,CLE会自动更新导出对象(loder.extern_obj
)来声明它将符号提供为导出。
装载选项 Loading Options
如果你使用angr.Project
加载了一些东西,并且你想要向项目静默创建的cle.Loader
实例传递一个选项,你可以直接向项目构造器传递关键词参数,它会直接传递给CLE。如果你想要了解任何可以传递的可选项,你可以查阅CLE的API手册。我们这里就只快速介绍一些重要且常用的选项。
基本选项 Basic Options
我们已经讨论了auto_load_libs
——它允许或禁止CLE尝试自动解析共享资源库依赖,这个选项默认是打开的。此外,有一个相反的选项,except_missing_libs
,如果它设置为真,会导致只要二进制文件存在无法解析的共享资源库依赖,就会抛出异常。
我们可以我们可以向force_load_libs
传递一个字符串列表,并且任何列出的项目将会马上被当作一个没有被解析的共享资源库依赖。你也可以向skip_libs
传递一个字符串列表来阻止名称在列表中的任何资源库被解析为依赖。此外,你可以向ld_path
传递一个字符串列表(或者单个字符串)来提供在默认路径——包括装载的程序的目录,当前工作环境目录和系统资源库外,用于检索共享资源库的其他路径。
装载前选项 Pre-Binary Options
如果你想要具体进行一些只作用于某个特定的二进制文件对象的设置,CLE也会允许这样做。main_opts
和lib_opts
参数通过接收包含选项的词典来完成设置。main_opts
是一个从选项名称到选项取值的映射,而lib_opts
是一个从资源库名称到选项名到选项取值映射的映射。
每个后端的选项可能很不一样,但是以下是一些通用的选项:
backend
- 选择使用的后端,可以是一个类或者是一个名字base_addr
- 使用的基址entry_point
- 使用的入口地址arch
- 使用的架构名称
下面是一个例子:
>>> angr.Project('examples/fauxware/fauxware', main_opts={'backend': 'blob', 'arch': 'i386'}, lib_opts={'libc.so.6': {'backend': 'elf'}})
<Project examples/fauxware/fauxware>
后端 Backends
CLE目前拥有静态装载ELF,PE,CGC,Mach-O和ELF核心转储文件的后端,同时只能将文件装载到一个平坦的地址空间中。在大多数时候,CLE会自动检测出需要使用的正确的后端,所以除非你在处理一些比较奇怪的东西,没有必要指定你要使用的后端。
你可以像上述中的例子那样,通过在选项词典中添加键的方式,强制指定CLE使用某个特定的后端。有一些后端不能自动检测需要使用的架构,所以必须指定arch
的值。键不需要匹配任何架构的列表。angr会识别出你想要提供的任何支持的架构的常见标识。
使用下表中的名字来选择一个后端:
后端名称 | 描述 | 是否需要提供arch 参数 |
---|---|---|
elf | 基于PyELFTools的ELF文件静态装载器 | 否 |
pe | 基于PEFile的PE文件静态装载器 | 否 |
mach-o | Mach-O文件的静态装载器。不支持动态链接或基址重定位。 | 否 |
cgc | Cyber Grand Challenge文件的静态装载器 | 否 |
backedcgc | 允许指定内存和寄存器后端的CGC文件静态装载器 | 否 |
elfcore | ELF内核转储文件的静态装载器 | 否 |
blob | 将文件作为平坦的镜像装载进内存 | 是 |
符号化函数概要
在默认情况下,项目尝试使用被称为符号执行过程的符号化概要来替换对资源库函数的外部调用——实际上是仅使用python函数来模仿资源库函数对状态的影响。我们已经将一堆函数实现为符号执行过程。这些内置的过程可以通过angr.SIM_PROCEDURES
词典来使用。这个词典被分为两级,使用包名(libc,posix,win32,stubs)作为首个关键词,然后依照资源库函数的名称命名。执行一个符号执行过程比使用从你的系统中载入的真正的库函数要更容易追踪的多,虽然可能有潜在的不精确的风险。
当给定的函数没有这样的概要的时候:
- 如果
auto_load_libs
为True
(这是默认值),那么真正的资源库会被执行。根据实际的函数,这可能不是你想要的。比如,一些libc的函数分析起来非常复杂,并且可能为了尝试执行它们的路径需要非常大数量的状态。 - 如果
auto_load_libs
为False
,那么外部函数将是未解析的。项目将会将它们解析解析到一个通用的“残端”符号执行过程,它叫做ReturnUnconstrained
。它所做的和它的名字一样:每次被调用时,它返回一个独一无二的未约束的符号量。 - 如果
use_sim_procedures
(这个是angr.Project
的一个参数,不是cle.loader
的)为False
(它默认为True
),那么只有外部对象提供的符号被替换为符号执行过程,并且它们会被替换为残端ReturnUnconstrained
,只返回一个符号量。 - 可以使用
angr.Project
的exclude_sim_procedures_list
和exclude_sim_procedures_func
参数来指定特定的符号,来使其不被符号执行过程替换。 - 通过查阅
angr.Project._register_object
处的代码来获得确切的算法。
钩子 Hooking
angr使用一个python函数概要来替换资源库代码的机制被称为钩子,并且你也可以使用它!符号执行时,每一次步进angr检查当前地址是否被钩住。如果被钩住,执行钩子函数而不是执行当前地址的二进制代码。proj.hook(addr, hook)
这个API允许你实现这种钩子,其中的hook
是一个符号执行过程实例。你可以通过.is_hooked
,.unhook
和.hooked_by
来管理你的项目的钩子。
还有一个可选的API用来钩住一个地址从而让你指定你自己编写的函数用作钩子。你可以把proj.hook(addr)
用作一个函数修饰器。如果你这么做,你也可以指定一个可选的length
关键词,在你的钩子执行完以后让执行向前跳过几个字节。
>>> stub_func = angr.SIM_PROCEDURES['stubs']['ReturnUnconstrained'] # this is a CLASS
>>> proj.hook(0x10000, stub_func()) # hook with an instance of the class
>>> proj.is_hooked(0x10000) # these functions should be pretty self-explanitory
True
>>> proj.hooked_by(0x10000)
<ReturnUnconstrained>
>>> proj.unhook(0x10000)
>>> @proj.hook(0x20000, length=5)
... def my_hook(state):
... state.regs.rax = 1
>>> proj.is_hooked(0x20000)
True
此外,你可以使用proj.hook_symbol(name, hook)
——将符号的名字作为第一个参数提供——来钩住符号存活的地址。这样的一种非常重要的用法是扩展angr内置的符号执行过程资源库的行为。因为这些资源库函数只是一些类,你可以创建它们的子类,覆盖它们一部分的行为,并且在一个钩子中使用你自己的子类。
到目前为止还行!So far so good!
到现在为止,你应该已经在CLE装载器和angr项目的层面对如何控制你进行分析的环境有了很好的了解。你应该也理解了angr花费了很大的努力通过使用对函数影响提取概要的符号执行过程钩住复杂资源库函数来简化它的分析过程。
为了查看你可以干的有关CLE装载器和它的后端的所有事情,你可以查阅CLE的API手册。