angr入门之CLE
什么是angr,我该如何使用它?
angr 是一个支持多处理器架构的二进制分析工具包,它具有对二进制程序执行动态符号化执行(像MAyhem,KLEE等工具一样)以及多种静态分析的能力。如果想要学习如何使用它,你算是来对地方了!
Mayhem是一个自动化寻找二进制程序漏洞的系统。相关链接
我们已经尽力使angr的使用不那么费劲——我们的目标是创造一个对用户友好的二进制分析套件,它使用户只需要打开iPython并且输入几条的命令就能对一个二进制文件进行简要的分析。话虽如此,二进制分析是复杂的,这使得angr也很复杂。这个文档提供使用叙述性的解释和对angr及其设计的探索来帮助用户理清复杂的angr。
编程分析二进制文件需要克服许多困难,大致列举如下:
- 加载一个二进制文件到分析器中。
- 将二进制转化为中间表示形式(intermediate representation)
-
执行分析,可以是:
- 对二进制文件局部或整体的静态分析(比如依赖分析,程序切片)
- 对程序状态空间的符号化探索(比如“我们可以执行这个程序直到我们找到一个溢出吗?”)
- 上述两种分析在某些程度上的结合(比如“我们只执行程序中对内存写的程序片段来找到一个溢出。”)
angr提供应对上述挑战的组件。这个文档将会解释每个组件是如何工作的,以及如何使用它们来完成你的邪恶目的:)
加载二进制 - CLE和angr项目
以前,您只看到了angr的加载工具的例子 - 您加载了/bin/true
,然后在没有共享库的情况下再次加载它。你也看到proj.loader
它可以做的一些事情。现在,我们将深入探讨这些接口面的细微差别以及它们可以告诉您的事情。
我们简要提到了angr的二进制加载组件CLE。CLE代表“CLE Loads Everything”,负责获取二进制文件(以及它所依赖的任何库)并以易于使用的方式将其呈现给angr的其余部分。
装载机
让我们重新加载/bin/true
并深入了解如何与加载器进行交互。
1 >>> import angr, monkeyhex 2 >>> proj = angr.Project('/bin/true') 3 >>> proj.loader 4 <Loaded true, maps [0x400000:0x5008000]>
加载的对象
CLE loader(cle.Loader
)表示整个加载的二进制对象集合,加载并映射到单个内存空间。每个二进制对象都由一个可以处理其文件类型(cle.Backend
)的加载器后端加载。例如,cle.ELF
用于加载ELF二进制文件。
内存中的对象也不会与任何加载的二进制文件相对应。例如,用于提供线程本地存储支持的对象,以及用于提供未解析符号的externs对象。
您可以获得CLE已加载的对象的完整列表loader.all_objects
,以及几个更有针对性的分类:
1 # All loaded objects 2 >>> proj.loader.all_objects 3 [<ELF Object fauxware, maps [0x400000:0x60105f]>, 4 <ELF Object libc.so.6, maps [0x1000000:0x13c42bf]>, 5 <ELF Object ld-linux-x86-64.so.2, maps [0x2000000:0x22241c7]>, 6 <ELFTLSObject Object cle##tls, maps [0x3000000:0x300d010]>, 7 <KernelObject Object cle##kernel, maps [0x4000000:0x4008000]>, 8 <ExternObject Object cle##externs, maps [0x5000000:0x5008000]> 9 10 # This is the "main" object, the one that you directly specified when loading the project 11 >>> proj.loader.main_object 12 <ELF Object true, maps [0x400000:0x60105f]> 13 14 # This is a dictionary mapping from shared object name to object 15 >>> proj.loader.shared_objects 16 { 'libc.so.6': <ELF Object libc.so.6, maps [0x1000000:0x13c42bf]> 17 'ld-linux-x86-64.so.2': <ELF Object ld-linux-x86-64.so.2, maps [0x2000000:0x22241c7]>} 18 19 # Here's all the objects that were loaded from ELF files 20 # If this were a windows program we'd use all_pe_objects! 21 >>> proj.loader.all_elf_objects 22 [<ELF Object true, maps [0x400000:0x60105f]>, 23 <ELF Object libc.so.6, maps [0x1000000:0x13c42bf]>, 24 <ELF Object ld-linux-x86-64.so.2, maps [0x2000000:0x22241c7]>] 25 26 # Here's the "externs object", which we use to provide addresses for unresolved imports and angr internals 27 >>> proj.loader.extern_object 28 <ExternObject Object cle##externs, maps [0x5000000:0x5008000]> 29 30 # This object is used to provide addresses for emulated syscalls 31 >>> proj.loader.kernel_object 32 <KernelObject Object cle##kernel, maps [0x4000000:0x4008000]> 33 34 # Finally, you can to get a reference to an object given an address in it 35 >>> proj.loader.find_object_containing(0x400000) 36 <ELF Object true, maps [0x400000:0x60105f]>
您可以直接与这些对象进行交互以从中提取元数据:
1 >>> obj = proj.loader.main_object 2 3 # The entry point of the object 4 >>> obj.entry 5 0x400580 6 7 >>> obj.min_addr, obj.max_addr 8 (0x400000, 0x60105f) 9 10 # Retrieve this ELF's segments and sections 11 >>> obj.segments 12 <Regions: [<ELFSegment offset=0x0, flags=0x5, filesize=0xa74, vaddr=0x400000, memsize=0xa74>, 13 <ELFSegment offset=0xe28, flags=0x6, filesize=0x228, vaddr=0x600e28, memsize=0x238>]> 14 >>> obj.sections 15 <Regions: [<Unnamed | offset 0x0, vaddr 0x0, size 0x0>, 16 <.interp | offset 0x238, vaddr 0x400238, size 0x1c>, 17 <.note.ABI-tag | offset 0x254, vaddr 0x400254, size 0x20>, 18 ...etc 19 20 # You can get an individual segment or section by an address it contains: 21 >>> obj.find_segment_containing(obj.entry) 22 <ELFSegment offset=0x0, flags=0x5, filesize=0xa74, vaddr=0x400000, memsize=0xa74> 23 >>> obj.find_section_containing(obj.entry) 24 <.text | offset 0x580, vaddr 0x400580, size 0x338> 25 26 # Get the address of the PLT stub for a symbol 27 >>> addr = obj.plt['abort'] 28 >>> addr 29 0x400540 30 >>> obj.reverse_plt[addr] 31 'abort' 32 33 # Show the prelinked base of the object and the location it was actually mapped into memory by CLE 34 >>> obj.linked_base 35 0x400000 36 >>> obj.mapped_base 37 0x400000
符号和重新定位
您还可以在使用CLE时使用符号。符号是可执行格式世界中的基本概念,有效地将名称映射到地址。
从CLE获取符号的最简单方法是使用loader.find_symbol
,它接受名称或地址并返回Symbol对象。
1 >>> malloc = proj.loader.find_symbol('malloc') 2 >>> malloc 3 <Symbol "malloc" in libc.so.6 at 0x1054400>
符号上最有用的属性是其名称,所有者和地址,但符号的“地址”可能不明确。Symbol对象有三种报告其地址的方式:
.rebased_addr
是它在所有地址空间中的地址。这是打印输出中显示的内容。.linked_addr
是它相对于二进制的预链接基础的地址。.relative_addr
是它相对于对象库的地址。这在文献(特别是Windows文献)中称为RVA(相对虚拟地址)。
1 >>> malloc.name 2 'malloc' 3 4 >>> malloc.owner_obj 5 <ELF Object libc.so.6, maps [0x1000000:0x13c42bf]> 6 7 >>> malloc.rebased_addr 8 0x1054400 9 >>> malloc.linked_addr 10 0x54400 11 >>> malloc.relative_addr 12 0x54400
除了提供调试信息之外,符号还支持动态链接的概念。libc提供函数符号作为导出,主二进制文件依赖于它。如果我们要求CLE直接从主对象给我们一个函数符号,它会告诉我们这是一个导入符号。导入符号没有与之关联的有意义的地址,但它们确实提供了用于解析它们的符号的引用,如.resolvedby
。
>>> malloc.is_export True >>> malloc.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_malloc = proj.loader.main_object.get_symbol("malloc") >>> main_malloc <Symbol "malloc" in true (import)> >>> main_malloc.is_export False >>> main_malloc.is_import True >>> main_malloc.resolvedby <Symbol "malloc" in libc.so.6 at 0x1054400>
导入和导出之间的链接在内存中注册的具体方式由另一个名为relocations的概念处理。重定位也就是“当您使用导出符号匹配[import]时,请将导出地址写入[location],格式为[format]。” 我们可以看到对象(作为Relocation
实例)的完整重定位列表obj.relocs
,或者把符号名称到Relocation as的映射作为obj.imports
。没有相应的导出符号列表。
可以访问重定位的相应导入符号.symbol
。重定位将写入的地址,可用于Symbol的任何地址标识符访问,并且您也可以获取对请求重定位的对象的引用.owner_obj
。
# 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 {u'__libc_enable_secure': <cle.backends.relocations.generic.GenericJumpslotReloc at 0x4221fb0>, u'__tls_get_addr': <cle.backends.relocations.generic.GenericJumpslotReloc at 0x425d150>, u'_dl_argv': <cle.backends.relocations.generic.GenericJumpslotReloc at 0x4254d90>, u'_dl_find_dso_for_object': <cle.backends.relocations.generic.GenericJumpslotReloc at 0x425d130>, u'_dl_starting_up': <cle.backends.relocations.generic.GenericJumpslotReloc at 0x42548d0>, u'_rtld_global': <cle.backends.relocations.generic.GenericJumpslotReloc at 0x4221e70>, u'_rtld_global_ro': <cle.backends.relocations.generic.GenericJumpslotReloc at 0x4254210>}
如果导入无法解析为任何导出,例如,因为找不到共享库,CLE将自动更新externs对象(loader.extern_obj
)以声明它将符号作为导出。
加载选项
如果要加载某些内容angr.Project
并且想要将选项传递给cle.Loader
Project隐式创建的实例,则只需将关键字参数直接传递给Project构造函数,它就会传递给CLE。如果您想知道可能作为选项传递的所有内容,但我们将在此处介绍一些重要且经常使用的选项。
class cle.loader.Loader(main_binary,auto_load_libs = True,force_load_libs =(),skip_libs =(),main_opts = None,lib_opts = None,custom_ld_path =(),
use_system_libs = True,ignore_import_version_numbers = True,case_insensitive = False,rebase_granularity = 16777216,except_missing_libs = False,
aslr = False,page_size = 1,extern_size = 32768 ) 加载器加载所有对象并导出进程内存的抽象。你在这里看到的是一个带有加载和重新定位的二进制文件的地址空间。 参数: main_binary - 要加载的主二进制文件的路径,或者包含二进制文件的类文件对象。
以下参数是可选的。 参数: auto_load_libs - 是否自动加载加载对象所依赖的共享库。 force_load_libs - 要加载的库列表,无论加载的对象是否需要它们。 skip_libs - 永不加载的库列表,即使加载对象需要它们也是如此。 main_opts - 加载主二进制文件的选项字典。 lib_opts - 字典映射库名称到加载它们时要使用的选项的字典。 custom_ld_path - 我们可以在其中搜索共享库的路径列表。 use_system_libs - 是否搜索所请求库的系统加载路径。默认为True。 ignore_import_version_numbers - 文件名中具有不同版本号的库是否会被视为等效,例如libc.so.6和libc.so.0 case_insensitive - 如果将其设置为True,则无论基础文件系统的区分大小写如何,文件系统加载都将以区分大小写的方式完成。 rebase_granularity - 用于重新定位共享对象的对齐方式 except_missing_libs - 无法找到共享库时抛出异常。 aslr - 在符号地址空间中加载库。不要使用此选项。 page_size - 数据映射到内存的粒度。如果您在非分页环境中工作,请设置为1。 变量: memory(cle.memory.Clemory) - 程序的加载,重新定位和重定位的内存。 main_object - 表示主二进制文件的对象(即可执行文件)。 shared_objects - 将加载的库名称映射到表示它们的对象的字典。 all_objects - 包含加载的所有不同对象的表示的列表。 requested_names - 包含由某人标记为依赖项的所有不同共享库的名称的集合。 initial_load_objects - 由于初始加载请求而加载的所有对象的列表。
基本选项
我们已经讨论过auto_load_libs
- 它启用或禁用CLE尝试自动解析共享库依赖项,默认情况下处于启用状态。此外,还有相反的情况,except_missing_libs
如果设置为true,则只要二进制文件具有无法解析的共享库依赖项,就会引发异常。
您可以传递一个字符串列表,force_load_libs
列出的任何内容都将被视为一个未解析的共享库依赖项,或者您可以传递一个字符串列表skip_libs
以防止该名称的任何库被解析为依赖项。此外,您可以custom_ld_path
在任何默认值之前传递一个字符串列表(或单个字符串),它将用作共享库的附加搜索路径:与加载的程序相同的目录,当前工作目录和你的系统库。
额外选项
如果要指定仅适用于特定二进制对象的某些选项,CLE也会允许您这样做。参数main_ops和
lib_opts
通过选择词典来完成。main_opts
是从选项名称到选项值lib_opts
的映射,同时是从库名称到字典映射选项名称到选项值的映射。
您可以使用的选项从后端各不相同,但一些常见的选项是:
backend
- 使用哪个后端,作为类或名称custom_base_addr
- 要使用的基地址custom_entry_point
- 使用的入口点custom_arch
- 要使用的体系结构的名称
例:
ngr.Project(main_opts={'backend': 'ida', 'custom_arch': 'i386'}, lib_opts={'libc.so.6': {'backend': 'elf'}})
后端
CLE目前有后端静态加载ELF,PE,CGC,Mach-O和ELF核心转储文件,以及使用IDA加载二进制文件并将文件加载到平面地址空间。在大多数情况下,CLE会自动检测正确的后端,所以你不需要指定你正在使用哪个后端,除非你做了一些非常奇怪的事情。
如上所述,您可以通过在其选项字典中包含一个键来强制CLE使用特定的后端作为对象。某些后端无法自动检测要使用的架构,并且必须具有custom_arch
指定的架构。密钥不需要匹配任何体系结构列表; angr将确定您所指的架构的任何公共标识符。
要引用后端,请使用此表中的名称:
backend name | description | requires custom_arch ? |
---|---|---|
elf | 基于PyELFTools的ELF文件的静态加载程序 | no |
pe | 基于PEFile的PE文件静态加载器 | no |
mach-o | Mach-O文件的静态加载程序。不支持动态链接或变基。 | no |
cgc | Cyber Grand Challenge二进制文件的静态加载程序 | no |
backedcgc | GC二进制文件的静态加载程序,允许指定内存和寄存器 | no |
elfcore | 用于ELF核心转储的静态加载程序 | no |
ida | 启动IDA实例来解析文件 | yes |
blob | 将文件作为平面镜像加载到内存中 | yes |
符号函数摘要
默认情况下,Project尝试使用称为SimProcedures的符号摘要替换对库函数的外部调用- 实际上只是模仿库函数对状态的影响的python函数。我们已经实现了一大堆 SimProcedures 功能。这些内置过程在angr.SIM_PROCEDURES
字典中可用,它是两层的,首先在包名称(libc,posix,win32,stubs)上植入,然后在库函数的名称上植入。执行SimProcedure而不是从系统加载的实际库函数使得分析更容易处理,代价是一些潜在的不准确性。
使用angr时遇到了问题 此部分包含angr的用户/受害者经常遇到的陷阱列表。 SimProcedure不准确 为了使符号执行更容易处理,angr用Python编写的摘要替换了常见的库函数。我们将这些摘要称为SimProcedures。SimProcedures允许我们减轻路径爆炸,否则将通过strlen在符号字符串上运行引入路径爆炸。 不幸的是,我们的SimProcedures远非完美。如果angr显示意外行为,则可能是由错误/不完整的SimProcedure引起的。你可以做几件事: 禁用SimProcedure(您可以通过将选项传递给angr.Project类来排除特定的SimProcedures)。这有可能导致路径爆炸的缺点,除非您非常小心地将输入约束到相关函数。使用其他angr功能(例如Veritesting)可以部分减轻路径爆炸。 将SimProcedure替换为直接写入相关情况的内容。例如,我们的scanf实现并不完整,但是如果你只需要支持一个已知的格式字符串,你可以编写一个钩子来完成它。 修复SimProcedure。 不支持的系统调用 系统调用也实现为SimProcedures。不幸的是,我们尚未在angr中实现系统调用。对于不受支持的系统调用,有几种解决方法: 实施系统调用。TODO:记录这个过程 挂钩系统调用的呼叫站点(使用project.hook)以临时方式对状态进行必要的修改。 使用state.posix.queued_syscall_returns列表对syscall返回值进行排队。如果返回值排队,则不会执行系统调用,而是使用该值。此外,函数可以排队作为“返回值”,这将导致该函数应用于触发系统调用时的状态。 符号记忆模型 angr使用的默认记忆模型的灵感来自Mayhem。此内存模型支持有限的符号读取和写入。如果读取的内存索引是符号,并且此索引的可能值范围太宽,则索引将具体化为单个值。如果写入的内存索引完全是符号,则索引将具体化为单个值。这可以通过改变内存具体化策略来配置state.memory。 符号长度 SimProcedures,尤其是系统调用,例如read(),write()可能会遇到缓冲区长度符号化的情况。一般来说,处理得非常糟糕:在许多情况下,这个长度最终会在后来的执行步骤中被完全具体化或追溯性具体化。即使在不是这样的情况下,源文件或目标文件可能最终看起来有点“怪异”。
如果没有针对给定函数的摘要:
- 如果
auto_load_libs
是True
(这是默认值),则执行真正的库函数。根据实际功能,这可能是您想要的,也可能不是。例如,某些libc的函数分析非常复杂,并且很可能会导致尝试执行它们的路径的状态数量激增。 - 如果
auto_load_libs
是False
,则外部函数未解析,Project将其解析为通用的“存根”SimProcedure调用ReturnUnconstrained
。它的名字就是这样:它每次调用时都会返回一个唯一的无约束符号值。 - 如果
use_sim_procedures
(这是一个参数angr.Project
,而不是cle.Loader
)是False
(True
默认情况下),那么只有extern对象提供的符号将被SimProcedures替换,它们将被一个存根替换ReturnUnconstrained
,它只会返回一个符号值。 - 您可以指定要排除的特定符号,以使用以下参数替换为SimProcedures
angr.Project
:exclude_sim_procedures_list
和exclude_sim_procedures_func
。 - 查看
angr.Project._register_object
确切算法的代码。
Hooking
angr用python替换库代码的机制称为挂钩,你也可以这样做!在执行模拟时,每个步骤都会检查当前地址是否已挂钩,如果是,则运行挂钩而不是该地址处的二进制代码。该API,让你做到这一点是proj.hook(addr, hook)
,这里hook
是一个SimProcedure实例。您可以使用和管理项目的钩子.is_hooked
,希望不需要解释。.unhook
.hooked_by
有一个用于挂钩地址的备用API,通过使用proj.hook(addr)
函数装饰器,您可以指定自己的函数作为钩子使用。如果执行此操作,还可以选择指定length
关键字参数,以使执行在挂钩完成后向前跳转一些字节数。
1 >>> stub_func = angr.SIM_PROCEDURES['stubs']['ReturnUnconstrained'] # this is a CLASS 2 >>> proj.hook(0x10000, stub_func()) # hook with an instance of the class 3 4 >>> proj.is_hooked(0x10000) # these functions should be pretty self-explanitory 5 True 6 >>> proj.unhook(0x10000) 7 >>> proj.hooked_by(0x10000) 8 <ReturnUnconstrained> 9 10 >>> @proj.hook(0x20000, length=5) 11 ... def my_hook(state): 12 ... state.regs.rax = 1 13 14 >>> proj.is_hooked(0x20000) 15 True
此外,您可以使用proj.hook_symbol(name, hook)
符号名称作为第一个参数来挂钩符号所在的地址。一个非常重要的用法是扩展angr的内置库SimProcedures的行为。由于这些库函数只是类,因此可以对它们进行子类化,覆盖它们的行为,然后在钩子中使用子类。