上篇文章分析angr.Project类时说到,angr工具第一步就是将二进制文件加载到cle.loader类里。(CLE means Loading Everything)。loader类加载所有的对象并导出一个进程内存的抽象。生成该程序已加载和准备运行的地址空间。
构造函数参数:
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)
:param main_binary: 要加载主要二进制文件的路径,或者一个带有二进制文件的对象。
以下参数是可选的.
:param auto_load_libs: 是否自动加载已加载对象所依赖的共享库.
:param force_load_libs: 无论加载对象是否需要,都加载的库列表.
:param skip_libs: 即使加载对象需要也不会加载的库列表.
:param main_opts: 一个字典,加载主二进制文件的选项.
:param lib_opts: 一个字典,将库名称映射到加载它们时要使用的选项字典.
:param custom_ld_path: 用于搜索共享库的路径列表.
:param use_system_libs: 是否搜索所请求库的系统加载路径,默认为true.
:param ignore_import_version_numbers: 文件名中具有不同版本号的库是否被认为是等效的,例如libc.so.6和libc.so.0。
:param case_insensitive: 如果将其设置为True,则无论底层文件系统的区分大小写如何,文件系统加载都将以不区分大小写的方式进行。
:param rebase_granularity: 用于重新分配共享对象的对齐方式。
:param except_missing_libs: 当找不到共享库时抛出异常.
:param aslr: 在符号地址空间中加载库。 不要使用这个选项(???)。
:param page_size: 数据映射到内存的粒度。 如果您在非分页环境中工作,请将其设置为1。
一些变量解释:
:ivar memory: 程序加载,重定位的内存。
:vartype memory: 类型为cle.memory.Clemory类
:ivar main_object: 代表了主要的二进制文件对象
:ivar shared_objects: 一个字典,映射了名名称和它们代表的对象.
:ivar all_objects: 一个列表,代表了所有被加载的不同对象.
:ivar requested_names: 一个集合,包含了所有没标记为某个部分的依赖的不同的共享库.
:ivar initial_load_objects: 一个列表,代表所有初始加载请求结果的所有对象.
当引用字典选项时,它需要一个包含零个或多个以下key的字典:
- backend(后端):“elf”,“pe”,“mach-o”,“ida”,“blob”:后端使用的加载器
- custom_arch:用于二进制文件的archinfo.Arch对象
- custom_base_addr:重新绑定对象的地址
- custom_entry_point:用于对象的入口点
更多的key在每个后端的基础上定义。
执行流程:
第一步:参数初始化。在构造函数中,主要就是初始化各个参数,这部分在后面有些参数需要格外关注时可以再仔细分析。
第二步:加载对象初始化。构造函数的最后一行代码为
self.initial_load_objects = self._internal_load(main_binary, *force_load_libs)
这里调用了函数_internal_load(),跟进去看对象初始化的逻辑。
这个函数的参数为传入要加载的文件或库。如果由于任何原因无法加载其中的任何一个,则会退出。
它将返回一个成功加载的所有对象的列表,如果其中它们任何被加载过,那么返回的列表会比你提供的列表小。
下面这段函数看一下:
for main_spec in args:
if self.find_object(main_spec, extra_objects=objects) is not None:
l.info("Skipping load request %s - already loaded", main_spec)
continue
l.info("loading %s...", main_spec)
main_obj = self._load_object_isolated(main_spec)
objects.append(main_obj)
dependencies.extend(main_obj.deps)
if self.main_object is None:
self.main_object = main_obj
self.memory = Clemory(self.main_object.arch, root=True)
main_spec 就是传入的二进制文件或库。循环中,先判断的是这个对象是否加载过,若加载过则不再加载。加载一个文件时调用的函数为 _load_object_isolated()。这个函数给定一个依赖的部分说明,它将会把加载对象作为一个后端实例返回。它不会触及任何加载器全局数据。再跟进这个函数看下。
# STEP 1: identify file
if isinstance(spec, Backend):
return spec
elif hasattr(spec, 'read') and hasattr(spec, 'seek'):
full_spec = spec
elif type(spec) in (bytes, unicode):
full_spec = self._search_load_path(spec) # this is allowed to cheat and do partial static loading
l.debug("... using full path %s", full_spec)
else:
raise CLEError("Bad library specification: %s" % spec)
# STEP 2: collect options
if self.main_object is None:
options = self._main_opts
else:
for ident in self._possible_idents(full_spec): # also allowed to cheat
if ident in self._lib_opts:
options = self._lib_opts[ident]
break
else:
options = {}
# STEP 3: identify backend
backend_spec = options.pop('backend', None)
backend_cls = self._backend_resolver(backend_spec)
if backend_cls is None:
backend_cls = self._static_backend(full_spec)
if backend_cls is None:
raise CLECompatibilityError("Unable to find a loader backend for %s. Perhaps try the 'blob' loader?" % spec)
# STEP 4: LOAD!
l.debug("... loading with %s", backend_cls)
return backend_cls(full_spec, is_main_bin=self.main_object is None, loader=self, **options) #它就是main_oject 调用的是backend的init函数
第一步:识别文件。确认这是一个二进制文件,并调用_search_load_path()函数获取一个完整的文件路径。这部分是允许欺骗并做部分静态加载的。
第二步:收集选项options。在这一步,调用了_possible_idents()函数,对刚刚获得full_spec进行识别,它遍历所有可能用于描述给定spec的识别符。
第三步:识别后端。首先从options中获取backend,并初始化一个backend_cls变量,如果该变量为空,则调用_static_backend()函数获取backend_cls实例。
也就是说,真正对二进制文件识别的函数为_static_backend(),再次跟进看其逻辑。
def _static_backend(self, spec):
"""
Returns the correct loader for the file at `spec`.
Returns None if it's a blob or some unknown type.
TODO: Implement some binwalk-like thing to carve up blobs automatically
"""
try:
return self._backend_resolver(self._lib_opts[spec]['backend'])
except KeyError:
pass
with stream_or_path(spec) as stream:
for rear in ALL_BACKENDS.values():
if rear.is_default and rear.is_compatible(stream):
return rear
return None
该函数根据spec返回二进制文件的正确加载器。如果是blob或者其他未知类型则返回none。其中代码中允许我们实现能够识别和分割blob类型二进制文件的代码。(这个重点哎!)
识别后端利用的是特征匹配吧,首先加载刚刚遍历提取得到的spec,然后与ALL_BACKENDS的值进行匹配,及rear变量,调用rear.is_compatible()函数。所以我们要去看一下ALL_BACKENDS都是些什么,is_compatible()函数的匹配逻辑。而且我们的目标是要解