接上文:
- 《深入考察解释型语言背后隐藏的攻击面,Part 1(上)》
- 《深入考察解释型语言背后隐藏的攻击面,Part 1(下)》
- 《深入考察解释型语言背后隐藏的攻击面,Part 2(一)》
- 《深入考察解释型语言背后隐藏的攻击面,Part 2(二)》
在本文中,我们将深入地探讨,在通过外部函数接口(Foreign Function Interface,FFI)将基于C/C++的库“粘合”到解释语言的过程中,安全漏洞是如何产生的。
决定最终的漏洞利用策略
为了弄清楚如何在触发我们的堆内存覆盖之前对data_数组进行最佳定位,我们需要检查一下堆的状态。到目前为止,我们有两个感兴趣的目标:png_ptr结构体和运行时解析器正在使用的动态链接器数据。
如果我们检查png_ptr结构体数据所在的堆分块,我们就会发现,它是一个大小为0x530的main arena分块。
- Thread 1 "node" hit Breakpoint 2, 0x00007ffff40309b4 in png_read_row () from /home/anticomputer/node_modules/png-img/build/Release/png_img.node
- gef? i r rdi
- rdi 0x2722ef0 0x2722ef0
- gef? heap chunk $rdi
- Chunk(addr=0x2722ef0, size=0x530, flags=PREV_INUSE)
- Chunk size: 1328 (0x530)
- Usable size: 1320 (0x528)
- Previous chunk size: 25956 (0x6564)
- PREV_INUSE flag: On
- IS_MMAPPED flag: Off
- NON_MAIN_ARENA flag: Off
- gef?
此前,我们已经研究了png_ptr结构体本身,以及如何利用它来颠覆node进程,现在,让我们仔细考察_dl_fixup,以及在解析器代码中发生崩溃的具体原因。
当我们触发崩溃时,我们注意到:
- 0x00007ffff7de2fb2 in _dl_fixup (l=0x2722a10, reloc_arg=0x11d) at ../elf/dl-runtime.c:69
- 69 const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);
- gef? p *l
- $5 = {
- l_addr = 0x4141414141414141,
- ...
- l_info = {0x4141414141414141
- ...
- }
- gef? p l
- $6 = (struct link_map *) 0x2722a10
- gef?
这意味着,我们已经破坏了用于解析png-img库函数的linkmap。实际上,linkmap是一个数据结构,用于保存动态链接器执行运行时解析和重定位所需的所有信息。
下面,我们来看一下linkmap堆块和数据结构未被破坏之前的样子:
- gef? heap chunk 0x2722a10
- Chunk(addr=0x2722a10, size=0x4e0, flags=PREV_INUSE)
- Chunk size: 1248 (0x4e0)
- Usable size: 1240 (0x4d8)
- Previous chunk size: 39612548531313 (0x240703e24471)
- PREV_INUSE flag: On
- IS_MMAPPED flag: Off
- NON_MAIN_ARENA flag: Off
- gef? p *l
- $7 = {
- l_addr = 0x7ffff400f000,
- l_name = 0x2718010 "/home/anticomputer/node_modules/png-img/build/Release/png_img.node",
- l_ld = 0x7ffff4271c40,
- l_next = 0x0,
- l_prev = 0x7ffff7ffd9f0
- l_real = 0x2722a10,
- l_ns = 0x0,
- l_libname = 0x2722e88,
- l_info = {0x0, 0x7ffff4271c70, 0x7ffff4271d50, 0x7ffff4271d40, 0x0, 0x7ffff4271d00, 0x7ffff4271d10, 0x7ffff4271d80, 0x7ffff4271d90, 0x7ffff4271da0, 0x7ffff4271d20, 0x7ffff4271d30, 0x7ffff4271c90, 0x7ffff4271ca0, 0x7ffff4271c80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7ffff4271d60, 0x0, 0x0, 0x7ffff4271d70, 0x0, 0x7ffff4271cb0, 0x7ffff4271cd0, 0x7ffff4271cc0, 0x7ffff4271ce0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7ffff4271dc0, 0x7ffff4271db0, 0x0, 0x0, 0x0, 0x0, 0x7ffff4271de0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7ffff4271dd0, 0x0
- ...
- }
- gef?
当我们检查png_ptr chunk和linkmap chunk的地址和大小时,注意到它们所在的内存不仅是相邻的,并且是连续内存。png_ptr chunk位于地址0x2722ef0处,而大小为0x4e0的linkmap chunk则位于它之前的地址0x2722a10处。由于是连续内存,因此,两者之间不存在分块。
当从攻击者的角度评估堆状态时,我们总是同时考虑连续内存布局和逻辑内存布局(例如链表)。
因为linkmap和png_ptr的内存在我们开始影响目标node进程之前分配的,并且在漏洞利用过程中都处于使用状态,所以,我们似乎不太可能在这两个分块之间挪动我们的data_ chunk,以畅通无阻地破坏png_ptr数据。此外,我们貌似可以通过例如PNG文件大小来影响早期的堆状态,但这似乎无法得到可靠的结果。
这意味着我们将必须对linkmap进行破坏,以获取对node进程的控制权。
攻击运行时解析器
作为攻击者,我们经常要从系统代码中提炼出非预期的、但有用的行为。这里的挑战是:不要被那些我们不关心的东西所干扰,而要专注于那些在特定的漏洞利用场景中可资利用的行为。
那么,运行时解析器代码的哪些行为可能对攻击者有用呢?
要回答这个问题,我们必须了解运行时解析器是如何使用linkmap的。简而言之,它将从linkmap中抓取已加载的库的基地址,然后检查各种二进制段,以确定从库的基地址到要解析的函数起始地址的正确偏移量。一旦计算出这个偏移量,把它加到库的基地址上,用解析函数地址更新函数的GOT条目,然后跳转到解析函数的起始地址即可。
作为攻击者,我们从中提炼出以下有用的原语:向动态链接器的运行时解析器提供一个精心设计的linkmap,将它们加在一起,然后将执行流重定向到生成的地址处。加法的第一个操作数是直接从linkmap中得到的,而加法的第二个操作数可以通过linkmap中提供的指针从二进制段中获取。我们注意到,根据包含在某个解除引用的二进制段中的数据,在执行被重定向之前,解析的值将被写入一个内存位置。
实际上,通过破坏动态链接器来发动攻击并不是一个新主意,其中,所谓的“ret2dlresolve”攻击就是一种流行的方式,它可以在不知道libc本身在内存中的位置的情况下,将执行重定向到所需的libc函数中。在Nergal发布在Phrack上的“The advanced return-into-lib(c) exploits: PaX case study”一文中,就公开讨论过这个概念。
当PLT处于目标二进制文件的已知位置时,就像非PIE二进制文件的情况一样,ret2dlresolve攻击就是一个非常有吸引力的选择,它可以将执行重定向到任意库偏移处,而无需知道所需的目标库实际加载到内存的具体位置。这是因为解析器代码会替我们完成所有繁重的工作。
滥用运行时解析器的主流方法,通常会假设攻击者已经能够重定向进程的执行流,并通过PLT返回到解析器代码中,以便为_dl_runtime_resolve提供攻击者控制的参数。因此,这种方法被称为“ret2dlresolve”(即return to dl resolve的缩写形式)。他们的想法是,随后可以利用解析器与现有的或精心制作的linkmap数据和重定位数据的交互,推导出攻击者控制的偏移量,即到达内存中的现有指针值的偏移。例如,他们可以欺骗解析器,让解析器将攻击者控制的偏移量应用到一个已经建立的libc地址上,以便从那里偏移到一个任意的libc函数上,比如system(3)。在不知道libc基地址且无法直接返回libc的情况下,上面这种方法的一个变体是使用解析器逻辑来解析libc函数。
当然,这个技术还存在其他变体,例如在内存中的已知位置提供一个完全精心制作的linkmap,用相对寻址来伪造重定位和符号数据。这里的目标同样是滥用运行时解析器,从已知的内存位置偏移到攻击者想要转移执行的位置。
然而,虽然在我们的例子中,我们能够提供一个精心制作的linkmap,但我们并不能控制运行时解析器的参数。此外,我们也还没有掌握执行控制权,而是旨在“策反”运行时解析器,通过我们精心制作的linkmap数据,以绕过ASLR机制并实现执行重定向。由于堆的基地址是随机的,而且我们是通过PNG文件来攻击进程的,所以,我们没有办法泄露linkmap的位置,因此我们只能基于非PIE节点二进制文件来进行内存布局和内容假设。
为了更好地了解如何实现攻击者的目标,让我们来看看_dl_fixup的工作原理。在这里,所有的代码引用都来自glibc-2.27。
- elf/dl-runtime.c:
- #ifndef reloc_offset
- # define reloc_offset reloc_arg
- # define reloc_index reloc_arg / sizeof (PLTREL)
- #endif
- /* This function is called through a special trampoline from the PLT the
- first time each PLT entry is called. We must perform the relocation
- specified in the PLT of the given shared object, and return the resolved
- function address to the trampoline, which will restart the original call
- to that address. Future calls will bounce directly from the PLT to the
- function. */
- DL_FIXUP_VALUE_TYPE
- attribute_hidden __attribute ((noinline)) ARCH_FIXUP_ATTRIBUTE
- _dl_fixup (
- # ifdef ELF_MACHINE_RUNTIME_FIXUP_ARGS
- ELF_MACHINE_RUNTIME_FIXUP_ARGS,
- # endif
- struct link_map *l, ElfW(Word) reloc_arg)
- {
- const ElfW(Sym) *const symtab
- = (const void *) D_PTR (l, l_info[DT_SYMTAB]);
- const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);
- [1]
- const PLTREL *const reloc
- = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
- [2]
- const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
- const ElfW(Sym) *refsym = sym;
- [3]
- void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);
- lookup_t result;
- DL_FIXUP_VALUE_TYPE value;
- /* Sanity check that we're really looking at a PLT relocation. */
- assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
- /* Look up the target symbol. If the normal lookup rules are not
- used don't look in the global scope. */
- if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0)
- {
- const struct r_found_version *version = NULL;
- if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)
- {
- const ElfW(Half) *vernum =
- (const void *) D_PTR (l, l_info[VERSYMIDX (DT_VERSYM)]);
- ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff;
- version = &l->l_versions[ndx];
- if (version->hash == 0)
- version = NULL;
- }
- /* We need to keep the scope around so do some locking. This is
- not necessary for objects which cannot be unloaded or when
- we are not using any threads (yet). */
- int flags = DL_LOOKUP_ADD_DEPENDENCY;
- if (!RTLD_SINGLE_THREAD_P)
- {
- THREAD_GSCOPE_SET_FLAG ();
- flags |= DL_LOOKUP_GSCOPE_LOCK;
- }
- #ifdef RTLD_ENABLE_FOREIGN_CALL
- RTLD_ENABLE_FOREIGN_CALL;
- #endif
- result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,
- version, ELF_RTYPE_CLASS_PLT, flags, NULL);
- /* We are done with the global scope. */
- if (!RTLD_SINGLE_THREAD_P)
- THREAD_GSCOPE_RESET_FLAG ();
- #ifdef RTLD_FINALIZE_FOREIGN_CALL
- RTLD_FINALIZE_FOREIGN_CALL;
- #endif
- /* Currently result contains the base load address (or link map)
- of the object that defines sym. Now add in the symbol
- offset. */
- value = DL_FIXUP_MAKE_VALUE (result,
- sym ? (LOOKUP_VALUE_ADDRESS (result)
- + sym->st_value) : 0);
- }
- else
- {
- /* We already found the symbol. The module (and therefore its load
- address) is also known. */
- value = DL_FIXUP_MAKE_VALUE (l, l->l_addr + sym->st_value);
- result = l;
- }
- /* And now perhaps the relocation addend. */
- value = elf_machine_plt_value (l, reloc, value);
- if (sym != NULL
- && __builtin_expect (ELFW(ST_TYPE) (sym->st_info) == STT_GNU_IFUNC, 0))
- value = elf_ifunc_invoke (DL_FIXUP_VALUE_ADDR (value));
- /* Finally, fix up the plt itself. */
- if (__glibc_unlikely (GLRO(dl_bind_not)))
- return value;
- return elf_machine_fixup_plt (l, result, refsym, sym, reloc, rel_addr, value);
- }
复杂,但我们只需要关注以下几点:_dl_fixup是如何通过与我们控制的linkmap中的三个主要指针进行交互,来解析和重定位函数地址的,所有这些指针都是从linkmap的l_info数组中提取的:
- l_info[DT_SYMTAB],指向符号表的.dynamic条目的指针。
- l_info[DT_STRTAB],指向字符串表的.dynamic条目的指针。
- l_info[DT_JMPREL],指向PLT重定位记录数组的.dynamic条目的指针。
Elf二进制文件中的.dynamic段用于保存解析器需要获取的各个段的信息。在我们的例子中,.dynstr (STRTAB)、.dynsym (SYMTAB)和.rela.plt (JMPREL)段都是解析和重定位函数所需要的。
动态条目(Dynamic entry)的结构如下所示:
- typedef struct
- {
- Elf64_Sxword d_tag; /* Dynamic entry type */
- union
- {
- Elf64_Xword d_val; /* Integer value */
- Elf64_Addr d_ptr; /* Address value */
- } d_un;
- } Elf64_Dyn;
用于访问l_info条目的D_PTR宏定义为:
- /* All references to the value of l_info[DT_PLTGOT],
- l_info[DT_STRTAB], l_info[DT_SYMTAB], l_info[DT_RELA],
- l_info[DT_REL], l_info[DT_JMPREL], and l_info[VERSYMIDX (DT_VERSYM)]
- have to be accessed via the D_PTR macro. The macro is needed since for
- most architectures the entry is already relocated - but for some not
- and we need to relocate at access time. */
- #ifdef DL_RO_DYN_SECTION
- # define D_PTR(map, i) ((map)->i->d_un.d_ptr + (map)->l_addr)
- #else
- # define D_PTR(map, i) (map)->i->d_un.d_ptr
- #endif
请注意,在大多数情况下,D_PTR只是从.dynamic段条目中获取d_ptr字段,以检索相关段的运行时重定位地址。例如,const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);将按照提供的指向.dynstr (STRTAB)段的.dynamic条目的指针,在l_info数组的索引DT_STRTAB处,获取上述条目的d_ptr字段。
在指针方面,这里比较让人头疼,但我们只要记住一点就行了:我们并不是通过控制linkmap中的l_info数组来提供直接指向解析器所需要的各个段的指针,而是指向(假定的).dynamic条目的指针,这些条目在偏移量+8处应该包含一个指向相关段的指针。
前面,我们介绍了如何通过精心制作的linkmap数据为解析器提供伪造的二进制段,接下来,我们快速了解一下_dl_fixup中的实际解析和重定位逻辑。
在我们的测试平台上,重定位记录的定义如下所示:
- elf.h:
- typedef struct
- {
- Elf64_Addr r_offset; /* Address */
- Elf64_Xword r_info; /* Relocation type and symbol index */
- Elf64_Sxword r_addend; /* Addend */
- } Elf64_Rela;
在我们的测试平台上,这些符号的定义如下所示:
- elf.h:
- typedef struct
- {
- Elf64_Word st_name; /* Symbol name (string tbl index) */
- unsigned char st_info; /* Symbol type and binding */
- unsigned char st_other; /* Symbol visibility */
- Elf64_Section st_shndx; /* Section index */
- Elf64_Addr st_value; /* Symbol value */
- Elf64_Xword st_size; /* Symbol size */
- } Elf64_Sym;
我们再来回顾一下_dl_fixup的代码,注意在[1]处,_dl_fixup的reloc_arg参数重定位记录表的索引,来读取重定位记录。这个重定位记录提供了一个reloc->r_info字段,该字段通过宏分为高32位的符号表索引和低32位的重定位类型。
在[2]处,_dl_fixup利用reloc->r_info索引从符号表中获取相应的符号条目,在reloc->r_info处的ELF_MACHINE_JMP_SLOT类型断言和sym->st_other处的符号查找范围检查之前,实际的函数解析以一种非常简单的方式进行。首先,通过将linkmap中的l->l_addr字段和符号表项的sym->st_value字段相加来解析函数地址。然后将解析后的值写入rel_addr中,rel_addr是在[3]处计算出来的,也就是l->l_addr和reloc->r_offset相加的结果。
linkmap中的l->l_addr字段是用来存放加载库的基地址,任何解析的偏移值都会被加入其中。
综上所述,sym->st_value + l->l_addr是解析函数的地址,l->l_addr + reloc->r_offset是重定位目标,也就是GOT条目,将用解析函数地址进行更新。
所以,从我们攻击者的角度来看,既然我们控制了l->l_addr,以及指向符号表和重定位记录的.dynamic段的指针,我们就可以将执行重定向到对我们有利的地方。
小结
在本文中,我们将深入地探讨,在通过外部函数接口(Foreign Function Interface,FFI)将基于C/C++的库“粘合”到解释语言的过程中,安全漏洞是如何产生的。由于篇幅过长,我们将分为多篇进行介绍,更多精彩内容敬请期待!
本文翻译自:https://securitylab.github.com/research/now-you-c-me-part-two